Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ package-lock.json
*.njsproj
*.sln
*.sw?

# Environmental variables
.env
.env.*

25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Movies
# Movie Browser (React + React Router)

A beginner React project built in a team by **Arta** and **Iris**.

We use **The Movie Database (TMDB)** API to fetch movies, show them in a list, and open a **movie detail page** when you click a movie.

## What we are learning
- Building a multi-page app using **React Router**
- Using URL parameters (like a **movie id**) to load dynamic content
- Using APIs with **useState** + **useEffect**
- Making a responsive and accessible UI

## Features
- Movie list page (example: popular movies)
- Movie detail page (dynamic route)
- Responsive design (mobile to desktop)
- Accessibility improvements (alt text, contrast, semantic HTML)

## Tech stack
- React
- React Router
- TMDB API
- Vite
- CSS (styled-components)
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-router-dom": "^7.11.0",
"styled-components": "^6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
23 changes: 20 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import MovieDetail from "./pages/MovieDetail.jsx";
import Movies from "./pages/Movies.jsx";
import NotFound from "./pages/NotFound.jsx";
import GlobalStyle from "./styling/GlobalStyle.jsx";

export const App = () => {
return (
<h1>Movies</h1>
)
}
<BrowserRouter>
<>
<GlobalStyle />
<Routes>
<Route path="/" element={<Movies />} />
<Route path="/movies/:id" element={<MovieDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</>
</BrowserRouter>
);
};

export default App;
105 changes: 105 additions & 0 deletions src/components/movie-card.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import styled from "styled-components";

export default function MovieCard({ title, release_date, poster_path }) {
const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500";

return (
<Card tabIndex={0} aria-label={`${title} released on ${release_date}`}>
<h1>{title}</h1>
<h3>Released {release_date}</h3>
<img src={`${IMAGE_BASE_URL}${poster_path}`} alt={title} />
</Card>
)
}

const Card = styled.article`
position: relative;
width: 100%;
aspect-ratio: 2 / 3;
overflow: hidden;
background: #111;
margin: 0;
padding: 0;
cursor: pointer;

img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
object-fit: cover;
z-index: 0;
transform: scale(1);
transition: transform 180ms ease;
}

h1, h3 {
position: absolute;
left: 12px;
right: 12px;
margin: 0;
color: #fff;
z-index: 2;
text-align: left;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.9);

opacity: 0;
transform: translateY(10px);
transition: opacity 180ms ease, transform 180ms ease;
}

h1 {
bottom: 22px;
transform: translateY(-60%);
font-size: 30px;
font-weight: 900;
line-height: 1.1;

display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

h3 {
bottom: 22px;
transform: translateY(20%);
font-size: 20px;
font-weight: 400;
}

&::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.35) 35%,
rgba(0, 0, 0, 0) 65%
);
opacity: 0;
transition: opacity 180ms ease;
z-index: 1;
}

&:hover::after {opacity: 1; }
&:hover h1 {opacity: 1; transform: translateY(-60%); }
&:hover h3 {opacity: 1; transform: translateY(20%); }
&:hover img { transform: scale(1.03); }

/*keyboard accessibility*/
&:focus-visible {
outline: 3px solid #fff;
outline-offset:-3px;
}

&:focus-visible::after {opacity: 1; }
&:focus-visible h1 { opacity: 1; transform: translateY(-60%); }
&:focus-visible h3 { opacity: 1; transform: translateY(20%); }

@media (prefers-reduced-motion: reduce) {
img, h1, h3, &::after { transition: none; }
}
`;
Loading