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+ }
0 commit comments