Skip to content

Commit 788d96a

Browse files
feat: implement front-end scaffolding (#3)
1 parent 57a8f52 commit 788d96a

28 files changed

Lines changed: 974 additions & 2 deletions

client/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Client Environment Variables
2+
# Copy this file to .env.local and update the values as needed
3+
4+
# URL of the Express API server
5+
# Default: http://localhost:3001 (for local development)
6+
API_URL=http://localhost:3001

client/.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
package-lock.json
13+
14+
# testing
15+
/coverage
16+
17+
# next.js
18+
/.next/
19+
/out/
20+
21+
# production
22+
/build
23+
24+
# misc
25+
.DS_Store
26+
*.pem
27+
28+
# debug
29+
npm-debug.log*
30+
yarn-debug.log*
31+
yarn-error.log*
32+
.pnpm-debug.log*
33+
34+
# env files (can opt-in for committing if needed)
35+
.env*
36+
!.env.example
37+
38+
# vercel
39+
.vercel
40+
41+
# typescript
42+
*.tsbuildinfo
43+
next-env.d.ts
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Movies Page Styles
3+
*
4+
* CSS Module for the movies page component.
5+
* Provides responsive grid layout and movie card styling.
6+
*/
7+
8+
.moviesGrid {
9+
display: grid;
10+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
11+
gap: 24px;
12+
width: 100%;
13+
max-width: 1200px;
14+
}
15+
16+
.movieCard {
17+
border: 1px solid #ddd;
18+
border-radius: 8px;
19+
padding: 16px;
20+
background: white;
21+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
22+
transition: transform 0.2s ease, box-shadow 0.2s ease;
23+
}
24+
25+
.movieCard:hover {
26+
transform: translateY(-2px);
27+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
28+
}
29+
30+
.moviePoster {
31+
position: relative;
32+
width: 100%;
33+
height: 300px;
34+
margin-bottom: 16px;
35+
background: #f5f5f5;
36+
border-radius: 4px;
37+
overflow: hidden;
38+
}
39+
40+
.moviePoster img {
41+
border-radius: 4px;
42+
}
43+
44+
.posterPlaceholder {
45+
display: flex;
46+
align-items: center;
47+
justify-content: center;
48+
width: 100%;
49+
height: 100%;
50+
color: #666;
51+
font-size: 14px;
52+
text-align: center;
53+
background: #f5f5f5;
54+
border-radius: 4px;
55+
}
56+
57+
.movieInfo {
58+
margin-bottom: 16px;
59+
}
60+
61+
.movieTitle {
62+
margin: 0 0 8px 0;
63+
font-size: 18px;
64+
font-weight: 600;
65+
line-height: 1.3;
66+
color: #333;
67+
}
68+
69+
.movieYear {
70+
margin: 0 0 4px 0;
71+
color: #666;
72+
font-size: 14px;
73+
}
74+
75+
.movieRating {
76+
margin: 0 0 4px 0;
77+
color: #666;
78+
font-size: 14px;
79+
}
80+
81+
.movieGenres {
82+
margin: 0;
83+
color: #888;
84+
font-size: 12px;
85+
font-style: italic;
86+
}
87+
88+
.detailsButton {
89+
width: 100%;
90+
background: #0066cc;
91+
color: white;
92+
border: none;
93+
padding: 12px 16px;
94+
border-radius: 4px;
95+
font-size: 14px;
96+
font-weight: 500;
97+
cursor: pointer;
98+
transition: background-color 0.2s ease;
99+
}
100+
101+
.detailsButton:hover {
102+
background: #0052a3;
103+
}
104+
105+
.noMovies {
106+
text-align: center;
107+
padding: 40px;
108+
color: #666;
109+
}
110+
111+
.pageTitle {
112+
margin: 0 0 16px 0;
113+
font-size: 32px;
114+
color: #333;
115+
}
116+
117+
.movieCount {
118+
margin: 0 0 32px 0;
119+
color: #666;
120+
font-size: 16px;
121+
}
122+
123+
/* Responsive Design */
124+
@media (max-width: 768px) {
125+
.moviesGrid {
126+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
127+
gap: 16px;
128+
}
129+
130+
.moviePoster {
131+
height: 240px;
132+
}
133+
134+
.pageTitle {
135+
font-size: 28px;
136+
}
137+
}
138+
139+
@media (max-width: 480px) {
140+
.moviesGrid {
141+
grid-template-columns: 1fr;
142+
gap: 16px;
143+
}
144+
145+
.movieCard {
146+
padding: 12px;
147+
}
148+
149+
.moviePoster {
150+
height: 200px;
151+
}
152+
153+
.pageTitle {
154+
font-size: 24px;
155+
}
156+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client';
2+
3+
import Image from 'next/image';
4+
import movieStyles from "./MovieCard.module.css";
5+
import { MovieCardProps } from "../../types/movie";
6+
7+
/**
8+
* Movie Card Client Component
9+
*
10+
* This component handles the interactive parts of the movie card,
11+
* such as image error handling, while the parent remains a Server Component.
12+
*/
13+
export default function MovieCard({ movie }: MovieCardProps) {
14+
const handleImageError = () => {
15+
// This will be handled by the Image component's onError prop
16+
console.warn(`Failed to load poster for: ${movie.title}`);
17+
};
18+
19+
return (
20+
<div className={movieStyles.movieCard}>
21+
<div className={movieStyles.moviePoster}>
22+
{movie.poster ? (
23+
<Image
24+
src={movie.poster}
25+
alt={`${movie.title} poster`}
26+
fill
27+
sizes="(max-width: 480px) 100vw, (max-width: 768px) 50vw, 280px"
28+
style={{ objectFit: 'cover' }}
29+
onError={handleImageError}
30+
placeholder="blur"
31+
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R7Dh5zq6esmOk2cWkgaWKJZoSGEa5qKUlPP45++P//Z"
32+
/>
33+
) : (
34+
<div className={movieStyles.posterPlaceholder}>
35+
No Poster Available
36+
</div>
37+
)}
38+
</div>
39+
40+
<div className={movieStyles.movieInfo}>
41+
<h3 className={movieStyles.movieTitle}>{movie.title}</h3>
42+
{movie.year && (
43+
<p className={movieStyles.movieYear}>({movie.year})</p>
44+
)}
45+
{movie.imdb?.rating && (
46+
<p className={movieStyles.movieRating}>{movie.imdb.rating}/10</p>
47+
)}
48+
{movie.genres && movie.genres.length > 0 && (
49+
<p className={movieStyles.movieGenres}>
50+
{movie.genres.slice(0, 3).join(', ')}
51+
</p>
52+
)}
53+
</div>
54+
55+
<button className={movieStyles.detailsButton} type="button">
56+
Get Details
57+
</button>
58+
</div>
59+
);
60+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './MovieCard';

client/app/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Components
2+
export { default as MovieCard } from './MovieCard';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Error component for displaying error states
3+
*/
4+
5+
interface ErrorDisplayProps {
6+
message?: string;
7+
onRetry?: () => void;
8+
}
9+
10+
export default function ErrorDisplay({
11+
message = "Something went wrong",
12+
onRetry
13+
}: ErrorDisplayProps) {
14+
return (
15+
<div className="error-display">
16+
<h2>Error</h2>
17+
<p>{message}</p>
18+
{onRetry && (
19+
<button onClick={onRetry} type="button">
20+
Try Again
21+
</button>
22+
)}
23+
24+
<style jsx>{`
25+
.error-display {
26+
text-align: center;
27+
padding: 2rem;
28+
border: 1px solid #fee2e2;
29+
border-radius: 8px;
30+
background-color: #fef2f2;
31+
color: #991b1b;
32+
}
33+
34+
.error-display h2 {
35+
margin: 0 0 1rem 0;
36+
color: #dc2626;
37+
}
38+
39+
.error-display button {
40+
margin-top: 1rem;
41+
padding: 0.5rem 1rem;
42+
background: #dc2626;
43+
color: white;
44+
border: none;
45+
border-radius: 4px;
46+
cursor: pointer;
47+
}
48+
49+
.error-display button:hover {
50+
background: #b91c1c;
51+
}
52+
`}</style>
53+
</div>
54+
);
55+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client';
2+
3+
/**
4+
* Loading spinner component
5+
*/
6+
7+
interface LoadingSpinnerProps {
8+
size?: 'small' | 'medium' | 'large';
9+
message?: string;
10+
}
11+
12+
export default function LoadingSpinner({
13+
size = 'medium',
14+
message = 'Loading...'
15+
}: LoadingSpinnerProps) {
16+
const sizeClasses = {
17+
small: 'w-4 h-4',
18+
medium: 'w-8 h-8',
19+
large: 'w-12 h-12'
20+
};
21+
22+
return (
23+
<div className="loading-container">
24+
<div className={`loading-spinner ${sizeClasses[size]}`}></div>
25+
{message && <p className="loading-message">{message}</p>}
26+
27+
<style jsx>{`
28+
.loading-container {
29+
display: flex;
30+
flex-direction: column;
31+
align-items: center;
32+
gap: 1rem;
33+
padding: 2rem;
34+
}
35+
36+
.loading-spinner {
37+
border: 2px solid #f3f3f3;
38+
border-top: 2px solid #3498db;
39+
border-radius: 50%;
40+
animation: spin 1s linear infinite;
41+
}
42+
43+
.w-4 { width: 1rem; height: 1rem; }
44+
.w-8 { width: 2rem; height: 2rem; }
45+
.w-12 { width: 3rem; height: 3rem; }
46+
47+
.loading-message {
48+
color: #666;
49+
margin: 0;
50+
}
51+
52+
@keyframes spin {
53+
0% { transform: rotate(0deg); }
54+
100% { transform: rotate(360deg); }
55+
}
56+
`}</style>
57+
</div>
58+
);
59+
}

0 commit comments

Comments
 (0)