Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4ba8416
fetched API
AgnesSj01 Dec 16, 2025
2ccd53f
Added the movie API. Added descriptive text for the movie, rating and…
AgnesSj01 Dec 17, 2025
579203e
added tailwind to project
daniellauding Dec 17, 2025
59dfe20
did something
AgnesSj01 Dec 17, 2025
3e1d26c
Merge branch 'main' of https://github.com/AgnesSj01/js-project-movies
AgnesSj01 Dec 17, 2025
a91cf30
Tailwind
AgnesSj01 Dec 17, 2025
3a53ffa
added tailwind to project
daniellauding Dec 17, 2025
6fd3d22
added slider for movies
daniellauding Dec 17, 2025
defbf2a
Worked on page two --- Styled with Tailwind for the title, rating, b…
AgnesSj01 Dec 17, 2025
b6b5347
made the second page responsive
AgnesSj01 Dec 18, 2025
f5b0293
Fix Netifly SPA routing
AgnesSj01 Dec 18, 2025
f51d8ce
fixed for lighthouse
AgnesSj01 Dec 18, 2025
8b1d3fe
Try again with lighthouse
AgnesSj01 Dec 18, 2025
45b89f1
Minor adjustments to optimize Lighthouse performance
AgnesSj01 Dec 18, 2025
76d31c1
Lighthouse work
AgnesSj01 Dec 18, 2025
dc9f915
added loader for movie slider
daniellauding Dec 18, 2025
8057a88
Merge branch 'main' of https://github.com/AgnesSj01/js-project-movies
daniellauding Dec 18, 2025
ab0a470
Centered content at 768 px screen size
AgnesSj01 Dec 18, 2025
1906c50
Centered content at 768 px screen size
AgnesSj01 Dec 18, 2025
11b1e70
added error and loading
daniellauding Dec 18, 2025
b5a0bbf
Merge branch 'main' of https://github.com/AgnesSj01/js-project-movies
daniellauding Dec 18, 2025
a82f3fd
Add Not Found page and handle invalid movie ID
AgnesSj01 Dec 18, 2025
47253da
Implemented stretch goal: show genres and production companies
AgnesSj01 Dec 18, 2025
f601274
Add genres and production companies
AgnesSj01 Dec 18, 2025
32bdcb0
Implemented stretch goals: genres, companies, backnavigation
AgnesSj01 Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# Movies
# Movies

- What
- How
- ?

## Todo

- []
- []
- []
- []
- []

## Timeplan

- Wednesday 16/12
--- 09:00 – Quick sync
--- 13:00 – Continue working
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Movies</title>
<meta name="description" content="Browse and discover popular movies with ratings, reviews, and details." />
</head>
<body>
<div id="root"></div>
Expand Down
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-router": "^7.10.1",
"react-router-dom": "^7.10.1"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4.1.18",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.23",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^6.2.0"
}
}
91 changes: 91 additions & 0 deletions pages/CompanyMovies.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Link, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import BackButton from "../src/components/BackButtons";
import { useLocation } from "react-router-dom";

const API_KEY = "f4e07b8c3bee08478eb1ddafeed7e326";
const IMG_URL = "https://image.tmdb.org/t/p/w342";

const CompanyMovies = () => {
const { companyId } = useParams();
const [company, setCompany] = useState(null);
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const location = useLocation();
const from = location.state?.from || "/";

useEffect(() => {
const run = async () => {
try {
setLoading(true);
setError(false);

// 1) Company details
const companyRes = await fetch(
`https://api.themoviedb.org/3/company/${companyId}?api_key=${API_KEY}`
);
if (!companyRes.ok) {
setError(true);
return;
}
const companyData = await companyRes.json();
setCompany(companyData);

// 2) Movies by company (discover)
const moviesRes = await fetch(
`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&language=en-US&with_companies=${companyId}&sort_by=popularity.desc`
);
if (!moviesRes.ok) {
setError(true);
return;
}
const moviesData = await moviesRes.json();
setMovies(moviesData.results ?? []);
} catch {
setError(true);
} finally {
setLoading(false);
}
};

run();
}, [companyId]);

if (loading) return <p className="p-6">Loading...</p>;
if (error) return <p className="p-6">Something went wrong.</p>;

return (
<main className="min-h-screen p-6">
<div className="px-4 sm:px-8 md:px-8 lg:px-16 pt-4 mb-2">
<BackButton to={from} label="Back to movie" />

<h1 className="text-2xl font-bold mb-3">
{company?.name ? `Company: ${company.name}` : `Company ${companyId}`}
</h1>
</div>

<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 px-4 sm:px-8 md:px-8 lg:px-16">
{movies.map((m) => (
<Link key={m.id} to={`/movies/${m.id}`} className="block">
{m.poster_path ? (
<img
src={`${IMG_URL}${m.poster_path}`}
alt={m.title}
className="w-full aspect-[2/3] object-cover rounded-lg"
loading="lazy"
/>
) : (
<div className="w-full aspect-[2/3] bg-gray-200 rounded-lg flex items-center justify-center text-sm">
No image
</div>
)}
<p className="mt-2 text-sm font-semibold">{m.title}</p>
</Link>
))}
</div>
</main>
);
};

export default CompanyMovies;
92 changes: 92 additions & 0 deletions pages/GenreMovies.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Link, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import BackButton from "../src/components/BackButtons";
import { useLocation } from "react-router-dom";

const API_KEY = "f4e07b8c3bee08478eb1ddafeed7e326";
const IMG_URL = "https://image.tmdb.org/t/p/w342";

const GenreMovies = () => {
const { genreId } = useParams();
const [movies, setMovies] = useState([]);
const [genreName, setGenreName] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const location = useLocation();
const from = location.state?.from || "/";

useEffect(() => {
const fetchGenreMovies = async () => {
try {
setLoading(true);
setError(false);

// 1) Hämta genre-namn
const genreRes = await fetch(
`https://api.themoviedb.org/3/genre/movie/list?api_key=${API_KEY}&language=en-US`
);
const genreData = await genreRes.json();
const found = genreData.genres?.find(
(g) => String(g.id) === String(genreId)
);
setGenreName(found?.name ?? "");

// 2) Hämta filmer i genren
const res = await fetch(
`https://api.themoviedb.org/3/discover/movie?api_key=${API_KEY}&language=en-US&with_genres=${genreId}&sort_by=popularity.desc`
);

if (!res.ok) {
setError(true);
return;
}

const data = await res.json();
setMovies(data.results ?? []);
} catch (e) {
setError(true);
} finally {
setLoading(false);
}
};

fetchGenreMovies();
}, [genreId]);

if (loading) return <p className="p-6">Loading...</p>;
if (error) return <p className="p-6">Something went wrong.</p>;

return (
<main className="min-h-screen p-6">
<div className="px-4 sm:px-8 md:px-8 lg:px-16 pt-4 mb-2">
<BackButton to={from} label="Back to movie" />

<h1 className="text-2xl font-bold mb-3">
{genreName ? `Genre: ${genreName}` : `Genre ${genreId}`}
</h1>
</div>

<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 px-4 sm:px-8 md:px-8 lg:px-16">
{movies.map((m) => (
<Link key={m.id} to={`/movies/${m.id}`} className="block">
{m.poster_path ? (
<img
src={`${IMG_URL}${m.poster_path}`}
alt={m.title}
className="w-full aspect-[2/3] object-cover rounded-lg"
loading="lazy"
/>
) : (
<div className="w-full aspect-[2/3] bg-gray-200 rounded-lg flex items-center justify-center text-sm">
No image
</div>
)}
<p className="mt-2 text-sm font-semibold">{m.title}</p>
</Link>
))}
</div>
</main>
);
};

export default GenreMovies;
45 changes: 45 additions & 0 deletions pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
import MovieSlider from "../src/components/MovieSlider";

const Home = () => {
const [movies, setMovies] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {

const API_KEY = "f4e07b8c3bee08478eb1ddafeed7e326";
const API_URL = `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}&language=en-US&page=1`;

const fetchMovies = async () => {
try {
const [response] = await Promise.all([
await fetch(API_URL),
new Promise(resolve => setTimeout(resolve, 3000))
]);
const result = await response.json();
const movies = result.results;

setMovies(movies);
} catch (err) {
setError(err.message);
console.error(err);
} finally {
setLoading(false);
}
};

fetchMovies();
}, []);

if (loading) return <div className="text-white bg-rgba(0,0,0,0.3) h-screen w-screen flex justify-center align-center items-center"><span className="typewriter">Loading...</span></div>;
if (error) return <div className="text-red-500 bg-rgba(0,0,0,0.3) h-screen w-screen flex justify-center align-center items-center"><span className="typewriter">{error}</span></div>;

return (
<main className="bg-black">
<MovieSlider movies={movies} />
</main>
);
};

export default Home;
Loading