Skip to content

Commit 7a30644

Browse files
add pagination to search results
1 parent e43c88e commit 7a30644

2 files changed

Lines changed: 225 additions & 58 deletions

File tree

client/app/movies/movies.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@
148148
text-align: center;
149149
}
150150

151+
.searchInfo {
152+
margin: 2rem 0;
153+
padding: 1rem;
154+
background: #f8f9fa;
155+
border: 1px solid #e9ecef;
156+
border-radius: 8px;
157+
text-align: center;
158+
font-size: 0.9rem;
159+
color: #666;
160+
}
161+
151162
.selectAllButton {
152163
padding: 0.5rem 1rem;
153164
background: #6c757d;

client/app/movies/page.tsx

Lines changed: 214 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ import { Movie } from "../types/movie";
1111
import { APP_CONFIG, ROUTES } from "../lib/constants";
1212
import 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+
*/
1425
export 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

Comments
 (0)