Skip to content

Commit b745584

Browse files
feat: batch delete
1 parent 4c06fd9 commit b745584

7 files changed

Lines changed: 411 additions & 23 deletions

File tree

client/app/components/MovieCard/MovieCard.module.css

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,40 @@
1919
padding: 16px;
2020
background: white;
2121
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
22-
transition: transform 0.2s ease, box-shadow 0.2s ease;
22+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
23+
position: relative;
2324
}
2425

2526
.movieCard:hover {
2627
transform: translateY(-2px);
2728
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
2829
}
2930

31+
.movieCard.selected {
32+
border-color: #0070f3;
33+
box-shadow: 0 2px 4px rgba(0, 112, 243, 0.2);
34+
}
35+
36+
.selectionCheckbox {
37+
position: absolute;
38+
top: 12px;
39+
right: 12px;
40+
z-index: 2;
41+
display: flex;
42+
align-items: center;
43+
gap: 4px;
44+
background: rgba(255, 255, 255, 0.9);
45+
padding: 4px 8px;
46+
border-radius: 4px;
47+
font-size: 12px;
48+
}
49+
50+
.checkbox {
51+
width: 16px;
52+
height: 16px;
53+
cursor: pointer;
54+
}
55+
3056
.moviePoster {
3157
position: relative;
3258
width: 100%;

client/app/components/MovieCard/MovieCard.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,49 @@
33
import Image from 'next/image';
44
import Link from 'next/link';
55
import movieStyles from "./MovieCard.module.css";
6-
import { MovieCardProps } from "../../types/movie";
6+
import { Movie } from "../../types/movie";
77
import { ROUTES } from "../../lib/constants";
88

99
/**
1010
* Movie Card Client Component
1111
*
1212
* This component handles the interactive parts of the movie card,
13-
* such as image error handling, while the parent remains a Server Component.
13+
* such as image error handling and selection checkbox.
1414
*/
15-
export default function MovieCard({ movie }: MovieCardProps) {
15+
16+
interface MovieCardProps {
17+
movie: Movie;
18+
isSelected?: boolean;
19+
onSelectionChange?: (movieId: string, isSelected: boolean) => void;
20+
showCheckbox?: boolean;
21+
}
22+
23+
export default function MovieCard({ movie, isSelected = false, onSelectionChange, showCheckbox = false }: MovieCardProps) {
1624
const handleImageError = () => {
1725
// This will be handled by the Image component's onError prop
1826
console.warn(`Failed to load poster for: ${movie.title}`);
1927
};
2028

29+
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30+
if (onSelectionChange) {
31+
onSelectionChange(movie._id, e.target.checked);
32+
}
33+
};
34+
2135
return (
22-
<div className={movieStyles.movieCard}>
36+
<div className={`${movieStyles.movieCard} ${isSelected ? movieStyles.selected : ''}`}>
37+
{showCheckbox && (
38+
<div className={movieStyles.selectionCheckbox}>
39+
<input
40+
type="checkbox"
41+
id={`select-${movie._id}`}
42+
checked={isSelected}
43+
onChange={handleCheckboxChange}
44+
className={movieStyles.checkbox}
45+
/>
46+
</div>
47+
)}
48+
2349
<div className={movieStyles.moviePoster}>
2450
{movie.poster ? (
2551
<Image

client/app/lib/api.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,54 @@ export async function createMoviesBatch(moviesData: Omit<Movie, '_id'>[]): Promi
261261
error: 'Network error occurred while creating movies'
262262
};
263263
}
264+
}
265+
266+
/**
267+
* Delete multiple movies in a batch operation
268+
*/
269+
export async function deleteMoviesBatch(movieIds: string[]): Promise<{ success: boolean; error?: string; deletedCount?: number }> {
270+
try {
271+
// Create filter to match the movie IDs
272+
// Note: The server will handle ObjectId conversion
273+
const filter = {
274+
_id: {
275+
$in: movieIds
276+
}
277+
};
278+
279+
const response = await fetch(`${API_BASE_URL}/api/movies`, {
280+
method: 'DELETE',
281+
headers: {
282+
'Content-Type': 'application/json',
283+
},
284+
body: JSON.stringify({ filter }),
285+
});
286+
287+
const result = await response.json();
288+
289+
if (!response.ok) {
290+
return {
291+
success: false,
292+
error: result.error || `Failed to delete movies: ${response.status}`
293+
};
294+
}
295+
296+
if (!result.success) {
297+
return {
298+
success: false,
299+
error: result.error || 'API returned error response'
300+
};
301+
}
302+
303+
return {
304+
success: true,
305+
deletedCount: result.data.deletedCount
306+
};
307+
} catch (error) {
308+
console.error('Error deleting movies batch:', error);
309+
return {
310+
success: false,
311+
error: 'Network error occurred while deleting movies'
312+
};
313+
}
264314
}

client/app/movies/movies.module.css

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,148 @@
1414
gap: 16px;
1515
}
1616

17+
.headerActions {
18+
display: flex;
19+
align-items: center;
20+
gap: 16px;
21+
flex-wrap: wrap;
22+
}
23+
24+
.selectionControls {
25+
display: flex;
26+
align-items: center;
27+
gap: 12px;
28+
flex-wrap: wrap;
29+
}
30+
31+
.selectAllButton {
32+
padding: 0.5rem 1rem;
33+
background: #6c757d;
34+
color: white;
35+
border: none;
36+
border-radius: 6px;
37+
font-size: 0.875rem;
38+
font-weight: 500;
39+
cursor: pointer;
40+
transition: all 0.2s ease;
41+
}
42+
43+
.selectAllButton:hover {
44+
background: #5a6268;
45+
transform: translateY(-1px);
46+
}
47+
48+
.batchDeleteButton {
49+
padding: 0.5rem 1rem;
50+
background: #dc3545;
51+
color: white;
52+
border: none;
53+
border-radius: 6px;
54+
font-size: 0.875rem;
55+
font-weight: 500;
56+
cursor: pointer;
57+
transition: all 0.2s ease;
58+
}
59+
60+
.batchDeleteButton:hover:not(:disabled) {
61+
background: #c82333;
62+
transform: translateY(-1px);
63+
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
64+
}
65+
66+
.batchDeleteButton:disabled {
67+
opacity: 0.6;
68+
cursor: not-allowed;
69+
transform: none;
70+
}
71+
72+
.confirmationOverlay {
73+
position: fixed;
74+
top: 0;
75+
left: 0;
76+
right: 0;
77+
bottom: 0;
78+
background: rgba(0, 0, 0, 0.5);
79+
display: flex;
80+
align-items: center;
81+
justify-content: center;
82+
z-index: 1000;
83+
padding: 20px;
84+
}
85+
86+
.confirmationDialog {
87+
background: white;
88+
border-radius: 12px;
89+
padding: 2rem;
90+
max-width: 400px;
91+
width: 100%;
92+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
93+
}
94+
95+
.confirmationTitle {
96+
margin: 0 0 1rem 0;
97+
font-size: 1.25rem;
98+
font-weight: 600;
99+
color: #333;
100+
text-align: center;
101+
}
102+
103+
.confirmationMessage {
104+
margin: 0 0 1.5rem 0;
105+
color: #666;
106+
text-align: center;
107+
line-height: 1.5;
108+
}
109+
110+
.confirmationActions {
111+
display: flex;
112+
gap: 12px;
113+
justify-content: center;
114+
}
115+
116+
.cancelButton {
117+
padding: 0.75rem 1.5rem;
118+
background: #6c757d;
119+
color: white;
120+
border: none;
121+
border-radius: 8px;
122+
font-size: 1rem;
123+
font-weight: 500;
124+
cursor: pointer;
125+
transition: all 0.2s ease;
126+
min-width: 100px;
127+
}
128+
129+
.cancelButton:hover {
130+
background: #5a6268;
131+
transform: translateY(-1px);
132+
}
133+
134+
.confirmDeleteButton {
135+
padding: 0.75rem 1.5rem;
136+
background: #dc3545;
137+
color: white;
138+
border: none;
139+
border-radius: 8px;
140+
font-size: 1rem;
141+
font-weight: 500;
142+
cursor: pointer;
143+
transition: all 0.2s ease;
144+
min-width: 100px;
145+
}
146+
147+
.confirmDeleteButton:hover:not(:disabled) {
148+
background: #c82333;
149+
transform: translateY(-1px);
150+
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
151+
}
152+
153+
.confirmDeleteButton:disabled {
154+
opacity: 0.6;
155+
cursor: not-allowed;
156+
transform: none;
157+
}
158+
17159
.pageTitle {
18160
margin: 0;
19161
font-size: 32px;
@@ -93,6 +235,15 @@
93235
gap: 12px;
94236
}
95237

238+
.headerActions {
239+
flex-direction: column;
240+
gap: 12px;
241+
}
242+
243+
.selectionControls {
244+
justify-content: center;
245+
}
246+
96247
.pageTitle {
97248
font-size: 28px;
98249
text-align: center;
@@ -102,6 +253,20 @@
102253
width: 100%;
103254
padding: 0.875rem 1rem;
104255
}
256+
257+
.confirmationDialog {
258+
margin: 20px;
259+
padding: 1.5rem;
260+
}
261+
262+
.confirmationActions {
263+
flex-direction: column;
264+
}
265+
266+
.cancelButton,
267+
.confirmDeleteButton {
268+
width: 100%;
269+
}
105270
}
106271

107272
@media (max-width: 480px) {
@@ -113,4 +278,15 @@
113278
padding: 0.75rem 1rem;
114279
font-size: 0.9rem;
115280
}
281+
282+
.selectionControls {
283+
flex-direction: column;
284+
width: 100%;
285+
}
286+
287+
.selectAllButton,
288+
.batchDeleteButton {
289+
width: 100%;
290+
padding: 0.75rem 1rem;
291+
}
116292
}

0 commit comments

Comments
 (0)