Skip to content

Commit 961ce9d

Browse files
feat: batch edit
1 parent b745584 commit 961ce9d

8 files changed

Lines changed: 554 additions & 17 deletions

File tree

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
'use client';
2+
3+
/**
4+
* Batch Edit Movie Form Component
5+
*
6+
* Form for editing multiple movies at once with validation
7+
*/
8+
9+
import { useState } from 'react';
10+
import { Movie } from '../../types/movie';
11+
import styles from '../EditMovieForm/EditMovieForm.module.css';
12+
13+
interface BatchEditMovieFormProps {
14+
selectedCount: number;
15+
onSave: (updateData: Partial<Movie>) => void;
16+
onCancel: () => void;
17+
isLoading?: boolean;
18+
}
19+
20+
export default function BatchEditMovieForm({
21+
selectedCount,
22+
onSave,
23+
onCancel,
24+
isLoading = false
25+
}: BatchEditMovieFormProps) {
26+
const [formData, setFormData] = useState({
27+
title: '',
28+
year: '',
29+
plot: '',
30+
runtime: '',
31+
rated: '',
32+
genres: '',
33+
directors: '',
34+
writers: '',
35+
cast: '',
36+
countries: '',
37+
languages: '',
38+
poster: '',
39+
});
40+
41+
const [errors, setErrors] = useState<Record<string, string>>({});
42+
43+
const validateForm = () => {
44+
const newErrors: Record<string, string> = {};
45+
46+
// For batch updates, we only validate if fields have values
47+
// Empty fields will be ignored in the update
48+
49+
if (formData.year && (parseInt(formData.year) < 1800 || parseInt(formData.year) > new Date().getFullYear() + 5)) {
50+
newErrors.year = 'Please enter a valid year';
51+
}
52+
53+
if (formData.runtime && (parseInt(formData.runtime) < 1 || parseInt(formData.runtime) > 1000)) {
54+
newErrors.runtime = 'Please enter a valid runtime in minutes';
55+
}
56+
57+
setErrors(newErrors);
58+
return Object.keys(newErrors).length === 0;
59+
};
60+
61+
const handleSubmit = (e: React.FormEvent) => {
62+
e.preventDefault();
63+
64+
if (!validateForm()) {
65+
return;
66+
}
67+
68+
// Only include fields that have values
69+
const updateData: Partial<Movie> = {};
70+
71+
if (formData.title.trim()) {
72+
updateData.title = formData.title.trim();
73+
}
74+
if (formData.year) {
75+
updateData.year = parseInt(formData.year);
76+
}
77+
if (formData.plot.trim()) {
78+
updateData.plot = formData.plot.trim();
79+
}
80+
if (formData.runtime) {
81+
updateData.runtime = parseInt(formData.runtime);
82+
}
83+
if (formData.rated.trim()) {
84+
updateData.rated = formData.rated.trim();
85+
}
86+
if (formData.genres.trim()) {
87+
updateData.genres = formData.genres.split(',').map(g => g.trim()).filter(g => g);
88+
}
89+
if (formData.directors.trim()) {
90+
updateData.directors = formData.directors.split(',').map(d => d.trim()).filter(d => d);
91+
}
92+
if (formData.writers.trim()) {
93+
updateData.writers = formData.writers.split(',').map(w => w.trim()).filter(w => w);
94+
}
95+
if (formData.cast.trim()) {
96+
updateData.cast = formData.cast.split(',').map(c => c.trim()).filter(c => c);
97+
}
98+
if (formData.countries.trim()) {
99+
updateData.countries = formData.countries.split(',').map(c => c.trim()).filter(c => c);
100+
}
101+
if (formData.languages.trim()) {
102+
updateData.languages = formData.languages.split(',').map(l => l.trim()).filter(l => l);
103+
}
104+
if (formData.poster.trim()) {
105+
updateData.poster = formData.poster.trim();
106+
}
107+
108+
// Check if there's actually something to update
109+
if (Object.keys(updateData).length === 0) {
110+
setErrors({ general: 'Please fill in at least one field to update' });
111+
return;
112+
}
113+
114+
onSave(updateData);
115+
};
116+
117+
const handleInputChange = (field: string, value: string) => {
118+
setFormData(prev => ({ ...prev, [field]: value }));
119+
// Clear error when user starts typing
120+
if (errors[field]) {
121+
setErrors(prev => ({ ...prev, [field]: '' }));
122+
}
123+
// Clear general error
124+
if (errors.general) {
125+
setErrors(prev => ({ ...prev, general: '' }));
126+
}
127+
};
128+
129+
return (
130+
<div className={styles.formContainer}>
131+
<h2 className={styles.formTitle}>Batch Edit {selectedCount} Movies</h2>
132+
<p className={styles.batchDescription}>
133+
Only fill in the fields you want to update. Empty fields will be left unchanged on all selected movies.
134+
</p>
135+
136+
{errors.general && (
137+
<div className={styles.generalError}>
138+
{errors.general}
139+
</div>
140+
)}
141+
142+
<form onSubmit={handleSubmit} className={styles.form}>
143+
<div className={styles.formGrid}>
144+
{/* Title */}
145+
<div className={styles.formGroup}>
146+
<label htmlFor="title" className={styles.label}>
147+
Title
148+
</label>
149+
<input
150+
type="text"
151+
id="title"
152+
value={formData.title}
153+
onChange={(e) => handleInputChange('title', e.target.value)}
154+
className={`${styles.input} ${errors.title ? styles.inputError : ''}`}
155+
disabled={isLoading}
156+
placeholder="Leave empty to keep existing titles"
157+
/>
158+
{errors.title && <span className={styles.error}>{errors.title}</span>}
159+
</div>
160+
161+
{/* Year */}
162+
<div className={styles.formGroup}>
163+
<label htmlFor="year" className={styles.label}>
164+
Year
165+
</label>
166+
<input
167+
type="number"
168+
id="year"
169+
value={formData.year}
170+
onChange={(e) => handleInputChange('year', e.target.value)}
171+
className={`${styles.input} ${errors.year ? styles.inputError : ''}`}
172+
disabled={isLoading}
173+
min="1800"
174+
max={new Date().getFullYear() + 5}
175+
placeholder="Leave empty to keep existing years"
176+
/>
177+
{errors.year && <span className={styles.error}>{errors.year}</span>}
178+
</div>
179+
180+
{/* Runtime */}
181+
<div className={styles.formGroup}>
182+
<label htmlFor="runtime" className={styles.label}>
183+
Runtime (minutes)
184+
</label>
185+
<input
186+
type="number"
187+
id="runtime"
188+
value={formData.runtime}
189+
onChange={(e) => handleInputChange('runtime', e.target.value)}
190+
className={`${styles.input} ${errors.runtime ? styles.inputError : ''}`}
191+
disabled={isLoading}
192+
min="1"
193+
max="1000"
194+
placeholder="Leave empty to keep existing runtimes"
195+
/>
196+
{errors.runtime && <span className={styles.error}>{errors.runtime}</span>}
197+
</div>
198+
199+
{/* Rated */}
200+
<div className={styles.formGroup}>
201+
<label htmlFor="rated" className={styles.label}>
202+
Rating
203+
</label>
204+
<input
205+
type="text"
206+
id="rated"
207+
value={formData.rated}
208+
onChange={(e) => handleInputChange('rated', e.target.value)}
209+
className={styles.input}
210+
disabled={isLoading}
211+
placeholder="e.g., PG-13, R, G"
212+
/>
213+
</div>
214+
215+
{/* Poster URL */}
216+
<div className={styles.formGroup}>
217+
<label htmlFor="poster" className={styles.label}>
218+
Poster URL
219+
</label>
220+
<input
221+
type="url"
222+
id="poster"
223+
value={formData.poster}
224+
onChange={(e) => handleInputChange('poster', e.target.value)}
225+
className={styles.input}
226+
disabled={isLoading}
227+
placeholder="https://..."
228+
/>
229+
</div>
230+
</div>
231+
232+
{/* Plot */}
233+
<div className={styles.formGroup}>
234+
<label htmlFor="plot" className={styles.label}>
235+
Plot
236+
</label>
237+
<textarea
238+
id="plot"
239+
value={formData.plot}
240+
onChange={(e) => handleInputChange('plot', e.target.value)}
241+
className={styles.textarea}
242+
disabled={isLoading}
243+
rows={4}
244+
placeholder="Leave empty to keep existing plots..."
245+
/>
246+
</div>
247+
248+
{/* Lists (comma-separated) */}
249+
<div className={styles.listFields}>
250+
<div className={styles.formGroup}>
251+
<label htmlFor="genres" className={styles.label}>
252+
Genres (comma-separated)
253+
</label>
254+
<input
255+
type="text"
256+
id="genres"
257+
value={formData.genres}
258+
onChange={(e) => handleInputChange('genres', e.target.value)}
259+
className={styles.input}
260+
disabled={isLoading}
261+
placeholder="Action, Drama, Comedy"
262+
/>
263+
</div>
264+
265+
<div className={styles.formGroup}>
266+
<label htmlFor="directors" className={styles.label}>
267+
Directors (comma-separated)
268+
</label>
269+
<input
270+
type="text"
271+
id="directors"
272+
value={formData.directors}
273+
onChange={(e) => handleInputChange('directors', e.target.value)}
274+
className={styles.input}
275+
disabled={isLoading}
276+
placeholder="Director 1, Director 2"
277+
/>
278+
</div>
279+
280+
<div className={styles.formGroup}>
281+
<label htmlFor="writers" className={styles.label}>
282+
Writers (comma-separated)
283+
</label>
284+
<input
285+
type="text"
286+
id="writers"
287+
value={formData.writers}
288+
onChange={(e) => handleInputChange('writers', e.target.value)}
289+
className={styles.input}
290+
disabled={isLoading}
291+
placeholder="Writer 1, Writer 2"
292+
/>
293+
</div>
294+
295+
<div className={styles.formGroup}>
296+
<label htmlFor="cast" className={styles.label}>
297+
Cast (comma-separated)
298+
</label>
299+
<input
300+
type="text"
301+
id="cast"
302+
value={formData.cast}
303+
onChange={(e) => handleInputChange('cast', e.target.value)}
304+
className={styles.input}
305+
disabled={isLoading}
306+
placeholder="Actor 1, Actor 2, Actor 3"
307+
/>
308+
</div>
309+
310+
<div className={styles.formGroup}>
311+
<label htmlFor="countries" className={styles.label}>
312+
Countries (comma-separated)
313+
</label>
314+
<input
315+
type="text"
316+
id="countries"
317+
value={formData.countries}
318+
onChange={(e) => handleInputChange('countries', e.target.value)}
319+
className={styles.input}
320+
disabled={isLoading}
321+
placeholder="USA, UK, France"
322+
/>
323+
</div>
324+
325+
<div className={styles.formGroup}>
326+
<label htmlFor="languages" className={styles.label}>
327+
Languages (comma-separated)
328+
</label>
329+
<input
330+
type="text"
331+
id="languages"
332+
value={formData.languages}
333+
onChange={(e) => handleInputChange('languages', e.target.value)}
334+
className={styles.input}
335+
disabled={isLoading}
336+
placeholder="English, Spanish, French"
337+
/>
338+
</div>
339+
</div>
340+
341+
{/* Form Actions */}
342+
<div className={styles.formActions}>
343+
<button
344+
type="button"
345+
onClick={onCancel}
346+
className={`${styles.button} ${styles.cancelButton}`}
347+
disabled={isLoading}
348+
>
349+
Cancel
350+
</button>
351+
<button
352+
type="submit"
353+
className={`${styles.button} ${styles.saveButton}`}
354+
disabled={isLoading}
355+
>
356+
{isLoading ? 'Updating...' : `Update ${selectedCount} Movies`}
357+
</button>
358+
</div>
359+
</form>
360+
</div>
361+
);
362+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './BatchEditMovieForm';

client/app/components/EditMovieForm/EditMovieForm.module.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@
2121
text-align: center;
2222
}
2323

24+
.batchDescription {
25+
background-color: #f8f9fa;
26+
border: 1px solid #e9ecef;
27+
border-radius: 8px;
28+
padding: 1rem;
29+
margin-bottom: 1.5rem;
30+
color: #495057;
31+
font-size: 0.9rem;
32+
line-height: 1.4;
33+
text-align: center;
34+
}
35+
36+
.generalError {
37+
background-color: #f8d7da;
38+
border: 1px solid #f5c6cb;
39+
color: #721c24;
40+
padding: 0.75rem 1rem;
41+
border-radius: 8px;
42+
margin-bottom: 1rem;
43+
font-size: 0.9rem;
44+
text-align: center;
45+
}
46+
2447
.form {
2548
width: 100%;
2649
}

0 commit comments

Comments
 (0)