Skip to content

Commit ab87638

Browse files
Search front end
1 parent 0992799 commit ab87638

7 files changed

Lines changed: 595 additions & 28 deletions

File tree

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
'use client';
2+
3+
/**
4+
* Search Movie Modal Component
5+
*
6+
* Modal for searching movies across multiple fields using MongoDB Search.
7+
* Supports plot, fullplot, directors, writers, cast fields with search operator options.
8+
*/
9+
10+
import { useState } from 'react';
11+
import { Movie } from '../../types/movie';
12+
import styles from '../EditMovieForm/EditMovieForm.module.css';
13+
14+
interface SearchMovieModalProps {
15+
onSearch: (searchParams: SearchParams) => void;
16+
onCancel: () => void;
17+
isLoading?: boolean;
18+
searchResults?: Movie[];
19+
resultCount?: number;
20+
}
21+
22+
export interface SearchParams {
23+
plot?: string;
24+
fullplot?: string;
25+
directors?: string;
26+
writers?: string;
27+
cast?: string;
28+
limit?: number;
29+
skip?: number;
30+
search_operator?: 'must' | 'should' | 'mustNot' | 'filter';
31+
}
32+
33+
interface SearchFormData {
34+
plot: string;
35+
fullplot: string;
36+
directors: string;
37+
writers: string;
38+
cast: string;
39+
limit: string;
40+
search_operator: 'must' | 'should' | 'mustNot' | 'filter';
41+
}
42+
43+
const getInitialFormData = (): SearchFormData => ({
44+
plot: '',
45+
fullplot: '',
46+
directors: '',
47+
writers: '',
48+
cast: '',
49+
limit: '20',
50+
search_operator: 'must',
51+
});
52+
53+
export default function SearchMovieModal({
54+
onSearch,
55+
onCancel,
56+
isLoading = false,
57+
searchResults = [],
58+
resultCount = 0
59+
}: SearchMovieModalProps) {
60+
const [formData, setFormData] = useState<SearchFormData>(getInitialFormData());
61+
const [errors, setErrors] = useState<Record<string, string>>({});
62+
63+
const validateForm = () => {
64+
const newErrors: Record<string, string> = {};
65+
66+
// Check if at least one search field has a value
67+
const hasSearchInput = formData.plot.trim() ||
68+
formData.fullplot.trim() ||
69+
formData.directors.trim() ||
70+
formData.writers.trim() ||
71+
formData.cast.trim();
72+
73+
if (!hasSearchInput) {
74+
newErrors.general = 'Please enter search terms in at least one field';
75+
}
76+
77+
// Validate limit
78+
const limitNum = parseInt(formData.limit);
79+
if (!limitNum || limitNum < 1 || limitNum > 100) {
80+
newErrors.limit = 'Limit must be between 1 and 100';
81+
}
82+
83+
setErrors(newErrors);
84+
return Object.keys(newErrors).length === 0;
85+
};
86+
87+
const handleSubmit = (e: React.FormEvent) => {
88+
e.preventDefault();
89+
90+
if (!validateForm()) {
91+
return;
92+
}
93+
94+
// Build search parameters, only including non-empty fields
95+
const searchParams: SearchParams = {
96+
search_operator: formData.search_operator,
97+
limit: parseInt(formData.limit),
98+
skip: 0, // Always start from beginning for new search
99+
};
100+
101+
if (formData.plot.trim()) {
102+
searchParams.plot = formData.plot.trim();
103+
}
104+
if (formData.fullplot.trim()) {
105+
searchParams.fullplot = formData.fullplot.trim();
106+
}
107+
if (formData.directors.trim()) {
108+
searchParams.directors = formData.directors.trim();
109+
}
110+
if (formData.writers.trim()) {
111+
searchParams.writers = formData.writers.trim();
112+
}
113+
if (formData.cast.trim()) {
114+
searchParams.cast = formData.cast.trim();
115+
}
116+
117+
onSearch(searchParams);
118+
};
119+
120+
const handleInputChange = (field: string, value: string) => {
121+
setFormData(prev => ({ ...prev, [field]: value }));
122+
123+
// Clear errors when user starts typing
124+
if (errors[field]) {
125+
setErrors(prev => ({ ...prev, [field]: '' }));
126+
}
127+
if (errors.general) {
128+
setErrors(prev => ({ ...prev, general: '' }));
129+
}
130+
};
131+
132+
const handleClear = () => {
133+
setFormData(getInitialFormData());
134+
setErrors({});
135+
};
136+
137+
const searchOperatorOptions = [
138+
{ value: 'must', label: 'Must match all fields (AND)', description: 'All specified fields must match' },
139+
{ value: 'should', label: 'Should match any field (OR)', description: 'At least one field should match' },
140+
{ value: 'mustNot', label: 'Must not match', description: 'Results must NOT contain these terms' },
141+
{ value: 'filter', label: 'Filter results', description: 'Filter results by these criteria' },
142+
];
143+
144+
return (
145+
<div className={styles.formContainer}>
146+
<h2 className={styles.formTitle}>Search Movies</h2>
147+
<p className={styles.batchDescription}>
148+
Search across movie plots, directors, writers, and cast. Use MongoDB Search with fuzzy matching for names.
149+
</p>
150+
151+
{errors.general && (
152+
<div className={styles.generalError}>
153+
{errors.general}
154+
</div>
155+
)}
156+
157+
{/* Show results count if we have results */}
158+
{searchResults.length > 0 && (
159+
<div className={styles.batchDescription} style={{ backgroundColor: '#d1f2eb', borderColor: '#b7e5d1' }}>
160+
Found {resultCount} movie{resultCount !== 1 ? 's' : ''} matching your search criteria
161+
</div>
162+
)}
163+
164+
<form onSubmit={handleSubmit} className={styles.form}>
165+
{/* Search Fields */}
166+
<div className={styles.formGrid}>
167+
{/* Plot Search */}
168+
<div className={styles.formGroup}>
169+
<label htmlFor="plot" className={styles.label}>
170+
Plot Keywords
171+
</label>
172+
<input
173+
type="text"
174+
id="plot"
175+
value={formData.plot}
176+
onChange={(e) => handleInputChange('plot', e.target.value)}
177+
className={`${styles.input} ${errors.plot ? styles.inputError : ''}`}
178+
disabled={isLoading}
179+
placeholder="Search in plot summaries"
180+
/>
181+
{errors.plot && <span className={styles.error}>{errors.plot}</span>}
182+
</div>
183+
184+
{/* Full Plot Search */}
185+
<div className={styles.formGroup}>
186+
<label htmlFor="fullplot" className={styles.label}>
187+
Full Plot Keywords
188+
</label>
189+
<input
190+
type="text"
191+
id="fullplot"
192+
value={formData.fullplot}
193+
onChange={(e) => handleInputChange('fullplot', e.target.value)}
194+
className={`${styles.input} ${errors.fullplot ? styles.inputError : ''}`}
195+
disabled={isLoading}
196+
placeholder="Search in full plot descriptions"
197+
/>
198+
{errors.fullplot && <span className={styles.error}>{errors.fullplot}</span>}
199+
</div>
200+
201+
{/* Directors Search */}
202+
<div className={styles.formGroup}>
203+
<label htmlFor="directors" className={styles.label}>
204+
Directors
205+
</label>
206+
<input
207+
type="text"
208+
id="directors"
209+
value={formData.directors}
210+
onChange={(e) => handleInputChange('directors', e.target.value)}
211+
className={`${styles.input} ${errors.directors ? styles.inputError : ''}`}
212+
disabled={isLoading}
213+
placeholder="Director names (fuzzy search enabled)"
214+
/>
215+
{errors.directors && <span className={styles.error}>{errors.directors}</span>}
216+
</div>
217+
218+
{/* Writers Search */}
219+
<div className={styles.formGroup}>
220+
<label htmlFor="writers" className={styles.label}>
221+
Writers
222+
</label>
223+
<input
224+
type="text"
225+
id="writers"
226+
value={formData.writers}
227+
onChange={(e) => handleInputChange('writers', e.target.value)}
228+
className={`${styles.input} ${errors.writers ? styles.inputError : ''}`}
229+
disabled={isLoading}
230+
placeholder="Writer names (fuzzy search enabled)"
231+
/>
232+
{errors.writers && <span className={styles.error}>{errors.writers}</span>}
233+
</div>
234+
235+
{/* Cast Search */}
236+
<div className={styles.formGroup}>
237+
<label htmlFor="cast" className={styles.label}>
238+
Cast
239+
</label>
240+
<input
241+
type="text"
242+
id="cast"
243+
value={formData.cast}
244+
onChange={(e) => handleInputChange('cast', e.target.value)}
245+
className={`${styles.input} ${errors.cast ? styles.inputError : ''}`}
246+
disabled={isLoading}
247+
placeholder="Actor names (fuzzy search enabled)"
248+
/>
249+
{errors.cast && <span className={styles.error}>{errors.cast}</span>}
250+
</div>
251+
252+
{/* Limit */}
253+
<div className={styles.formGroup}>
254+
<label htmlFor="limit" className={styles.label}>
255+
Max Results
256+
</label>
257+
<input
258+
type="number"
259+
id="limit"
260+
value={formData.limit}
261+
onChange={(e) => handleInputChange('limit', e.target.value)}
262+
className={`${styles.input} ${errors.limit ? styles.inputError : ''}`}
263+
disabled={isLoading}
264+
min="1"
265+
max="100"
266+
/>
267+
{errors.limit && <span className={styles.error}>{errors.limit}</span>}
268+
</div>
269+
</div>
270+
271+
{/* Search Operator */}
272+
<div className={styles.formGroup}>
273+
<label htmlFor="search_operator" className={styles.label}>
274+
Search Logic
275+
</label>
276+
<select
277+
id="search_operator"
278+
value={formData.search_operator}
279+
onChange={(e) => handleInputChange('search_operator', e.target.value)}
280+
className={styles.input}
281+
disabled={isLoading}
282+
>
283+
{searchOperatorOptions.map((option) => (
284+
<option key={option.value} value={option.value}>
285+
{option.label}
286+
</option>
287+
))}
288+
</select>
289+
<small style={{ color: '#6c757d', fontSize: '0.875rem', marginTop: '0.25rem' }}>
290+
{searchOperatorOptions.find(opt => opt.value === formData.search_operator)?.description}
291+
</small>
292+
</div>
293+
294+
{/* Form Actions */}
295+
<div className={styles.formActions}>
296+
<button
297+
type="button"
298+
onClick={handleClear}
299+
className={`${styles.button} ${styles.cancelButton}`}
300+
disabled={isLoading}
301+
style={{ backgroundColor: '#6c757d', borderColor: '#6c757d' }}
302+
>
303+
Clear
304+
</button>
305+
<button
306+
type="button"
307+
onClick={onCancel}
308+
className={`${styles.button} ${styles.cancelButton}`}
309+
disabled={isLoading}
310+
>
311+
Close
312+
</button>
313+
<button
314+
type="submit"
315+
className={`${styles.button} ${styles.saveButton}`}
316+
disabled={isLoading}
317+
>
318+
{isLoading ? 'Searching...' : 'Search Movies'}
319+
</button>
320+
</div>
321+
</form>
322+
</div>
323+
);
324+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './SearchMovieModal';
2+
export type { SearchParams } from './SearchMovieModal';

client/app/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { default as ActionButtons } from './ActionButtons';
66
export { default as EditMovieForm } from './EditMovieForm';
77
export { default as AddMovieForm } from './AddMovieForm';
88
export { default as BatchEditMovieForm } from './BatchEditMovieForm';
9+
export { default as SearchMovieModal } from './SearchMovieModal';
910
export {
1011
Skeleton,
1112
MovieCardSkeleton,

0 commit comments

Comments
 (0)