@@ -11,6 +11,17 @@ import { Movie } from "../types/movie";
1111import { APP_CONFIG , ROUTES } from "../lib/constants" ;
1212import type { SearchParams } from "../components/SearchMovieModal" ;
1313
14+ /**
15+ * Movies Page Component
16+ *
17+ * Main page for browsing movies with the following features:
18+ * - Regular movie browsing with URL-based pagination
19+ * - MongoDB text search with server-side pagination
20+ * - Vector search with client-side pagination
21+ * - CRUD operations (create, update, delete movies)
22+ * - Batch operations on selected movies
23+ *
24+ */
1425export default function Movies ( ) {
1526 const searchParams = useSearchParams ( ) ;
1627 const router = useRouter ( ) ;
@@ -31,6 +42,9 @@ export default function Movies() {
3142 const [ error , setError ] = useState < string | null > ( null ) ;
3243 const [ successMessage , setSuccessMessage ] = useState < string | null > ( null ) ;
3344 const [ searchResults , setSearchResults ] = useState < Movie [ ] > ( [ ] ) ;
45+ const [ allVectorSearchResults , setAllVectorSearchResults ] = useState < Movie [ ] > ( [ ] ) ;
46+ const [ vectorSearchPage , setVectorSearchPage ] = useState ( 1 ) ;
47+ const [ vectorSearchPageSize , setVectorSearchPageSize ] = useState ( 20 ) ;
3448 const [ searchHasNextPage , setSearchHasNextPage ] = useState ( false ) ;
3549 const [ searchHasPrevPage , setSearchHasPrevPage ] = useState ( false ) ;
3650 const [ searchTotalCount , setSearchTotalCount ] = useState ( 0 ) ;
@@ -225,22 +239,33 @@ export default function Movies() {
225239 let result ;
226240
227241 if ( searchParams . searchType === 'vector-search' ) {
228- // Handle Vector Search
242+ // Vector Search: Fetch all results and implement client-side pagination
229243 const vectorSearchParams = {
230244 q : searchParams . q ! ,
231- limit : searchParams . limit || 10 ,
245+ limit : searchParams . limit || 50 , // Get more results for better pagination experience
232246 } ;
233247 result = await vectorSearchMovies ( vectorSearchParams ) ;
234248
235249 if ( result . success ) {
236- setSearchResults ( result . movies || [ ] ) ;
237- setSearchHasNextPage ( false ) ; // Vector search doesn't support pagination
250+ const allResults = result . movies || [ ] ;
251+ const pageSize = searchParams . limit || 20 ;
252+ setAllVectorSearchResults ( allResults ) ;
253+ setVectorSearchPage ( 1 ) ;
254+ setVectorSearchPageSize ( pageSize ) ;
255+
256+ // Calculate first page of results for immediate display
257+ const firstPageResults = allResults . slice ( 0 , pageSize ) ;
258+ setSearchResults ( firstPageResults ) ;
259+
260+ // Set pagination state based on total results
261+ const totalPages = Math . ceil ( allResults . length / pageSize ) ;
262+ setSearchHasNextPage ( totalPages > 1 ) ;
238263 setSearchHasPrevPage ( false ) ;
239- setSearchTotalCount ( result . movies ?. length || 0 ) ;
264+ setSearchTotalCount ( allResults . length ) ;
240265 }
241266 } else {
242- // Handle MongoDB Search
243- const searchSkip = 0 ; // For new searches, start from page 1
267+ // MongoDB Search
268+ const searchSkip = 0 ; // Always start from page 1 for new searches
244269 const searchLimitToUse = searchParams . limit || 20 ;
245270
246271 const searchParamsWithPagination = {
@@ -267,15 +292,23 @@ export default function Movies() {
267292 setShowSearchModal ( false ) ;
268293 setSelectedMovies ( new Set ( ) ) ; // Clear selection when switching to search mode
269294
270- const totalCount = searchParams . searchType === 'vector-search'
271- ? result . movies ?. length || 0
272- : ( result as any ) . totalCount || 0 ;
295+ if ( searchParams . searchType === 'vector-search' ) {
296+ const returnedCount = result . movies ?. length || 0 ;
273297
274- if ( totalCount === 0 ) {
275- setSuccessMessage ( 'Search completed, but no movies matched your criteria. Try different search terms.' ) ;
298+ if ( returnedCount === 0 ) {
299+ setSuccessMessage ( 'Vector search completed, but no movies matched your query. Try different search terms.' ) ;
300+ } else {
301+ setSuccessMessage ( `Found ${ returnedCount } results using vector search.` ) ;
302+ }
276303 } else {
277- const searchTypeLabel = searchParams . searchType === 'vector-search' ? 'vector search' : 'text search' ;
278- setSuccessMessage ( `Found ${ totalCount } movies using ${ searchTypeLabel } .` ) ;
304+ // MongoDB text search success message
305+ const totalCount = ( result as any ) . totalCount || 0 ;
306+
307+ if ( totalCount === 0 ) {
308+ setSuccessMessage ( 'Search completed, but no movies matched your criteria. Try different search terms.' ) ;
309+ } else {
310+ setSuccessMessage ( `Found ${ totalCount } results using MongoDB search.` ) ;
311+ }
279312 }
280313 } else {
281314 setError ( result . error || 'Failed to search movies' ) ;
@@ -287,9 +320,15 @@ export default function Movies() {
287320 setIsSearching ( false ) ;
288321 } ;
289322
323+ /**
324+ * Clears search mode and returns to regular movie browsing
325+ * Resets all search-related state including vector search pagination
326+ */
290327 const handleClearSearch = ( ) => {
291328 setIsSearchMode ( false ) ;
292329 setSearchResults ( [ ] ) ;
330+ setAllVectorSearchResults ( [ ] ) ;
331+ setVectorSearchPage ( 1 ) ;
293332 setSearchHasNextPage ( false ) ;
294333 setSearchHasPrevPage ( false ) ;
295334 setSearchTotalCount ( 0 ) ;
@@ -304,14 +343,68 @@ export default function Movies() {
304343 // Get the movies to display based on current mode
305344 const displayMovies = isSearchMode ? searchResults : movies ;
306345
346+ /**
347+ * Helper function for vector search client-side pagination
348+ * Calculates pagination data for the current vector search results
349+ */
350+ const getVectorSearchPageData = ( ) => {
351+ if ( ! isSearchMode || currentSearchParams ?. searchType !== 'vector-search' ) {
352+ return { paginatedResults : [ ] , hasNext : false , hasPrev : false , totalPages : 0 } ;
353+ }
354+
355+ const startIndex = ( vectorSearchPage - 1 ) * vectorSearchPageSize ;
356+ const endIndex = startIndex + vectorSearchPageSize ;
357+ const paginatedResults = allVectorSearchResults . slice ( startIndex , endIndex ) ;
358+ const totalPages = Math . ceil ( allVectorSearchResults . length / vectorSearchPageSize ) ;
359+
360+ return {
361+ paginatedResults,
362+ hasNext : vectorSearchPage < totalPages ,
363+ hasPrev : vectorSearchPage > 1 ,
364+ totalPages
365+ } ;
366+ } ;
367+
368+ /**
369+ * Handles page navigation for vector search results (client-side pagination)
370+ * Slices the cached results and updates the display
371+ */
372+ const handleVectorSearchPageChange = ( newPage : number ) => {
373+ if ( ! isSearchMode || currentSearchParams ?. searchType !== 'vector-search' ) return ;
374+
375+ const totalPages = Math . ceil ( allVectorSearchResults . length / vectorSearchPageSize ) ;
376+ if ( newPage < 1 || newPage > totalPages ) return ;
377+
378+ setVectorSearchPage ( newPage ) ;
379+
380+ // Update the displayed results based on the new page
381+ const startIndex = ( newPage - 1 ) * vectorSearchPageSize ;
382+ const endIndex = startIndex + vectorSearchPageSize ;
383+ const paginatedResults = allVectorSearchResults . slice ( startIndex , endIndex ) ;
384+
385+ setSearchResults ( paginatedResults ) ;
386+ setSearchHasNextPage ( newPage < totalPages ) ;
387+ setSearchHasPrevPage ( newPage > 1 ) ;
388+
389+ // Clear selection and scroll to top for better UX
390+ setSelectedMovies ( new Set ( ) ) ;
391+ window . scrollTo ( { top : 0 , behavior : 'smooth' } ) ;
392+ } ;
393+
307394 const handleSearchPageChange = async ( newPage : number ) => {
308395 if ( ! currentSearchParams ) return ;
309396
310- // Vector search doesn't support pagination
397+ // This function handles MongoDB search pagination (server-side)
398+ // Vector search uses handleVectorSearchPageChange for client-side pagination
311399 if ( currentSearchParams . searchType === 'vector-search' ) {
312400 return ;
313401 }
314402
403+ // Validate page number and prevent invalid navigation
404+ if ( newPage < 1 ) return ;
405+ if ( isSearching ) return ;
406+ if ( newPage > searchPage && ! searchHasNextPage ) return ;
407+
315408 setIsSearching ( true ) ;
316409 setError ( null ) ;
317410
@@ -331,10 +424,17 @@ export default function Movies() {
331424 setSearchHasPrevPage ( result . hasPrevPage || false ) ;
332425 setSearchTotalCount ( result . totalCount || 0 ) ;
333426 setSearchPage ( newPage ) ;
427+
428+ // Clear any previously selected movies when changing pages
429+ setSelectedMovies ( new Set ( ) ) ;
430+
431+ // Scroll to top to show new results
432+ window . scrollTo ( { top : 0 , behavior : 'smooth' } ) ;
334433 } else {
335434 setError ( result . error || 'Failed to load search results' ) ;
336435 }
337436 } catch ( error ) {
437+ console . error ( 'Search pagination error:' , error ) ;
338438 setError ( 'Failed to load search results' ) ;
339439 }
340440
@@ -502,52 +602,108 @@ export default function Movies() {
502602 </ div >
503603
504604 { /* Show pagination based on current mode */ }
505- { isSearchMode && currentSearchParams ?. searchType === 'mongodb-search' ? (
506- < nav className = { movieStyles . pagination } aria-label = "Search results pagination" >
507- < div className = { movieStyles . paginationContainer } >
508- { /* Previous Button */ }
509- { searchHasPrevPage ? (
510- < button
511- onClick = { ( ) => handleSearchPageChange ( searchPage - 1 ) }
512- className = { movieStyles . pageButton }
513- disabled = { isSearching }
514- aria-label = "Go to previous page"
515- >
516- ← Previous
517- </ button >
518- ) : (
519- < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
520- ← Previous
521- </ span >
522- ) }
523-
524- { /* Current Page Info */ }
525- < div className = { movieStyles . pageInfo } >
526- Page { searchPage }
605+ { isSearchMode ? (
606+ currentSearchParams ?. searchType === 'mongodb-search' ? (
607+ < nav className = { movieStyles . pagination } aria-label = "Search results pagination" >
608+ < div className = { movieStyles . paginationContainer } >
609+ { /* Previous Button */ }
610+ { searchHasPrevPage && ! isSearching ? (
611+ < button
612+ onClick = { ( ) => handleSearchPageChange ( searchPage - 1 ) }
613+ className = { movieStyles . pageButton }
614+ disabled = { isSearching }
615+ aria-label = "Go to previous page"
616+ >
617+ ← Previous
618+ </ button >
619+ ) : (
620+ < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
621+ ← Previous
622+ </ span >
623+ ) }
624+
625+ { /* Current Page Info */ }
626+ < div className = { movieStyles . pageInfo } >
627+ Page { searchPage } { searchTotalCount > 0 ? `of ${ Math . ceil ( searchTotalCount / searchLimit ) } ` : '' }
628+ </ div >
629+
630+ { /* Next Button */ }
631+ { searchHasNextPage && ! isSearching ? (
632+ < button
633+ onClick = { ( ) => handleSearchPageChange ( searchPage + 1 ) }
634+ className = { movieStyles . pageButton }
635+ disabled = { isSearching }
636+ aria-label = "Go to next page"
637+ >
638+ Next →
639+ </ button >
640+ ) : (
641+ < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
642+ Next →
643+ </ span >
644+ ) }
527645 </ div >
528646
529- { /* Next Button */ }
530- { searchHasNextPage ? (
531- < button
532- onClick = { ( ) => handleSearchPageChange ( searchPage + 1 ) }
533- className = { movieStyles . pageButton }
534- disabled = { isSearching }
535- aria-label = "Go to next page"
536- >
537- Next →
538- </ button >
647+ { /* Additional Info */ }
648+ < div className = { movieStyles . additionalInfo } >
649+ { searchLimit } movies per page • { searchTotalCount } total results
650+ </ div >
651+ </ nav >
652+ ) : (
653+ /* Vector search results with client-side pagination */
654+ ( ( ) => {
655+ const { hasNext, hasPrev, totalPages } = getVectorSearchPageData ( ) ;
656+ return totalPages > 1 ? (
657+ < nav className = { movieStyles . pagination } aria-label = "Vector search results pagination" >
658+ < div className = { movieStyles . paginationContainer } >
659+ { /* Previous Button */ }
660+ { hasPrev ? (
661+ < button
662+ onClick = { ( ) => handleVectorSearchPageChange ( vectorSearchPage - 1 ) }
663+ className = { movieStyles . pageButton }
664+ aria-label = "Go to previous page"
665+ >
666+ ← Previous
667+ </ button >
668+ ) : (
669+ < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
670+ ← Previous
671+ </ span >
672+ ) }
673+
674+ { /* Current Page Info */ }
675+ < div className = { movieStyles . pageInfo } >
676+ Page { vectorSearchPage } of { totalPages }
677+ </ div >
678+
679+ { /* Next Button */ }
680+ { hasNext ? (
681+ < button
682+ onClick = { ( ) => handleVectorSearchPageChange ( vectorSearchPage + 1 ) }
683+ className = { movieStyles . pageButton }
684+ aria-label = "Go to next page"
685+ >
686+ Next →
687+ </ button >
688+ ) : (
689+ < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
690+ Next →
691+ </ span >
692+ ) }
693+ </ div >
694+
695+ { /* Additional Info */ }
696+ < div className = { movieStyles . additionalInfo } >
697+ { vectorSearchPageSize } movies per page • { allVectorSearchResults . length } total results
698+ </ div >
699+ </ nav >
539700 ) : (
540- < span className = { `${ movieStyles . pageButton } ${ movieStyles . disabled } ` } >
541- Next →
542- </ span >
543- ) }
544- </ div >
545-
546- { /* Additional Info */ }
547- < div className = { movieStyles . additionalInfo } >
548- { searchLimit } movies per page
549- </ div >
550- </ nav >
701+ < div className = { movieStyles . searchInfo } >
702+ Showing { allVectorSearchResults . length } results (vector search)
703+ </ div >
704+ ) ;
705+ } ) ( )
706+ )
551707 ) : (
552708 < Pagination
553709 currentPage = { page }
0 commit comments