diff --git a/packages/react/src/components/header/searchbar/components/SearchBar.tsx b/packages/react/src/components/header/searchbar/components/SearchBar.tsx index 79ce2097a..3e3053102 100644 --- a/packages/react/src/components/header/searchbar/components/SearchBar.tsx +++ b/packages/react/src/components/header/searchbar/components/SearchBar.tsx @@ -1,5 +1,5 @@ import { CommandList, Command as CommandPrimitive } from "cmdk"; -import { Command, CommandGroup } from "@/shadcn/ui/command"; +import { Command, CommandGroup, CommandShortcut } from "@/shadcn/ui/command"; import { cn } from "@/lib/utils"; import { queryAtom, @@ -30,28 +30,6 @@ export function SearchBar({ const { search, updateSearch, autocomplete } = useSearchboxAutocomplete(); const navigate = useNavigate(); - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - const input = inputRef.current; - if (input) { - if (e.key === "Delete" || e.key === "Backspace") { - if (input.value === "") { - setQuery((prev) => { - const newSelected = [...prev]; - newSelected.pop(); - return newSelected; - }); - } - } - // This is not a default behaviour of the field - if (e.key === "Escape") { - input.blur(); - } - } - }, - [setQuery], - ); - const handleItemSelect = useCallback( (item: QueryItem) => { if (item.incomplete) { @@ -96,6 +74,33 @@ export function SearchBar({ } }, [navigate, query]); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const input = inputRef.current; + if (input) { + if (e.key === "Delete" || e.key === "Backspace") { + if (input.value === "") { + setQuery((prev) => { + const newSelected = [...prev]; + newSelected.pop(); + return newSelected; + }); + } + } + // This is not a default behaviour of the field + if (e.key === "Escape") { + input.blur(); + } + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + doSearch(); + } + } + }, + [doSearch, setQuery], + ); + return (
-
+
+
+ + Shift⏎ + + +
+
diff --git a/packages/react/src/components/header/searchbar/helper.ts b/packages/react/src/components/header/searchbar/helper.ts index bfacadfb3..b75fc49b7 100644 --- a/packages/react/src/components/header/searchbar/helper.ts +++ b/packages/react/src/components/header/searchbar/helper.ts @@ -128,7 +128,7 @@ export function getQueryModelFromQuery( } } else return; //ignore. }); - return vqm; + return sanitizeQueryModel(vqm); } async function gen2array(gen: AsyncIterable): Promise { diff --git a/packages/react/src/components/video/MainVideoListing.tsx b/packages/react/src/components/video/MainVideoListing.tsx index 5e8bef190..2b0df5a61 100644 --- a/packages/react/src/components/video/MainVideoListing.tsx +++ b/packages/react/src/components/video/MainVideoListing.tsx @@ -13,6 +13,7 @@ interface MainVideoListingProps { isLoading?: boolean; hasNextPage?: boolean; isFetchingNextPage?: boolean; + nonVirtual?: boolean; } export function MainVideoListing({ @@ -23,6 +24,7 @@ export function MainVideoListing({ hasNextPage, isFetchingNextPage, isLoading, + nonVirtual, }: MainVideoListingProps) { const listClassName = useMemo( () => @@ -40,6 +42,43 @@ export function MainVideoListing({ [size, className], ); + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
+ ); + } + + // If nonVirtual is true, render a simple grid + if (nonVirtual) { + return ( +
+ {videos?.map((video, idx) => ( + + ))} + {isFetchingNextPage && hasNextPage && ( +
+ +
+ )} +
+ ); + } // const Footer = () => // (isLoading || isFetchingNextPage) && ( //
@@ -52,19 +91,15 @@ export function MainVideoListing({ return ( - isLoading ? ( - - ) : ( - - ) - } + itemContent={(idx, video) => ( + + )} endReached={async () => { if (hasNextPage && !isFetchingNextPage && !isLoading) { await fetchNextPage?.(); diff --git a/packages/react/src/lib/utils.ts b/packages/react/src/lib/utils.ts index 6e96089f1..d3af2a3d4 100644 --- a/packages/react/src/lib/utils.ts +++ b/packages/react/src/lib/utils.ts @@ -170,3 +170,74 @@ export function omitNullish(obj: T): OmitNullish { return result; } + +/** + * Generates an array of page numbers for pagination navigation. + * The array includes the current page and surrounding pages, with ellipsis (-1) + * where pages are skipped. Always includes first and last pages. + * + * Example outputs: + * - For 3 total pages: [1, 2, 3] + * - For current page 1 of 10: [1, 2, 3, -1, 10] + * - For current page 5 of 10: [1, -1, 4, 5, 6, -1, 10] + * - For current page 10 of 10: [1, -1, 8, 9, 10] + * + * @param currentPage - The currently active page number (1-based) + * @param totalPages - The total number of pages available + * @param maxPagesToShow - Maximum number of page numbers to display (default: 5) + * @returns Array of page numbers, with -1 representing ellipsis + * @throws Error if currentPage or totalPages are less than 1 + */ +export function generatePageNumbers( + currentPage: number, + totalPages: number, + maxPagesToShow: number = 5, +): number[] { + // Input validation + if (currentPage < 1 || totalPages < 1) { + throw new Error("Current page and total pages must be greater than 0"); + } + if (currentPage > totalPages) { + throw new Error("Current page cannot be greater than total pages"); + } + + const pages: number[] = []; + + // Case 1: Show all pages if total is less than or equal to max + if (totalPages <= maxPagesToShow) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + return pages; + } + + // Case 2: Need to show selective pages with possible ellipsis + // Always show first page + pages.push(1); + + // Add leading ellipsis if current page is far from start + if (currentPage > 3) { + pages.push(-1); + } + + // Show pages around current page + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + for (let i = start; i <= end; i++) { + pages.push(i); + } + + // Add trailing ellipsis if current page is far from end + if (currentPage < totalPages - 2) { + pages.push(-1); + } + + // Always show last page + if (pages[pages.length - 1] !== totalPages) { + pages.push(totalPages); + } + + return pages; +} + +export default generatePageNumbers; diff --git a/packages/react/src/routes/search.tsx b/packages/react/src/routes/search.tsx index 1f77eed4f..d1c1ca968 100644 --- a/packages/react/src/routes/search.tsx +++ b/packages/react/src/routes/search.tsx @@ -21,14 +21,17 @@ import { import { Pagination, PaginationContent, + PaginationEllipsis, PaginationItem, PaginationLink, + PaginationNext, + PaginationPrevious, } from "@/shadcn/ui/pagination"; -import { cn } from "@/lib/utils"; +import generatePageNumbers, { cn } from "@/lib/utils"; import { SearchBar } from "@/components/header/searchbar/components/SearchBar"; import { type SearchTotalHits } from "@elastic/elasticsearch/lib/api/types"; -const ITEMS_PER_PAGE = 24; +const ITEMS_PER_PAGE = 25; function elasticSearchTotalToValue(total?: number | SearchTotalHits) { if (total === undefined) { @@ -52,7 +55,6 @@ export default function Search() { nextSize, setNextSize, } = useVideoCardSizes(["list", "md", "lg"]); - const [searchInput, setSearchInput] = useState(""); // Calculate offset based on current page const offset = useMemo( @@ -79,7 +81,7 @@ export default function Search() { ); const videos = useMemo( - () => data?.hits.hits.map((hit) => hit._source) ?? [], + () => data?.hits.hits.map((hit) => hit._source!) ?? [], [data?.hits.hits], ); @@ -95,46 +97,16 @@ export default function Search() { }; const handlePageChange = (targetPage: number) => { + console.log("page changed", targetPage); setCurrentPage(targetPage); window.scrollTo({ top: 0, behavior: "smooth" }); }; // Generate array of page numbers to display - const pageNumbers = useMemo(() => { - const pages: number[] = []; - const maxPagesToShow = 5; - - if (totalPages <= maxPagesToShow) { - // Show all pages if total is less than max - for (let i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - // Always show first page - pages.push(1); - - if (currentPage > 3) { - pages.push(-1); // Add ellipsis - } - - // Show pages around current page - const start = Math.max(2, currentPage - 1); - const end = Math.min(totalPages - 1, currentPage + 1); - - for (let i = start; i <= end; i++) { - pages.push(i); - } - - if (currentPage < totalPages - 2) { - pages.push(-1); // Add ellipsis - } - - // Always show last page - pages.push(totalPages); - } - - return pages; - }, [currentPage, totalPages]); + const pageNumbers = useMemo( + () => (totalPages > 0 ? generatePageNumbers(currentPage, totalPages) : []), + [currentPage, totalPages], + ); if (status === "error") { return
{t("component.apiError.title")}
; @@ -206,6 +178,7 @@ export default function Search() { videos={videos} size={cardSize} className="mb-4" + nonVirtual /> {/* Pagination */} @@ -214,21 +187,16 @@ export default function Search() { {/* Previous Page Button */} - + /> - {/* Page Numbers */} {pageNumbers.map((pageNum, idx) => ( - + {pageNum === -1 ? ( - ... + ) : ( handlePageChange(pageNum)} @@ -239,21 +207,18 @@ export default function Search() { )} ))} - {/* Next Page Button */} - + isActive={currentPage <= totalPages} + /> )} + + {/* {
{JSON.stringify(pageNumbers, null, 2)}
} */} )}
diff --git a/packages/react/src/services/search.service.ts b/packages/react/src/services/search.service.ts index bea2f0743..02f49a923 100644 --- a/packages/react/src/services/search.service.ts +++ b/packages/react/src/services/search.service.ts @@ -31,7 +31,7 @@ export function useSearch( const newQ = { ...queryContainer, offset: offset ?? 0, - limit: 24, + limit: 25, }; return await client.post, typeof newQ>( "/api/v3/search/videoSearch", diff --git a/packages/react/src/shadcn/ui/pagination.tsx b/packages/react/src/shadcn/ui/pagination.tsx index d111226ae..a6e43ea8c 100644 --- a/packages/react/src/shadcn/ui/pagination.tsx +++ b/packages/react/src/shadcn/ui/pagination.tsx @@ -54,9 +54,11 @@ const PaginationLink = ({ aria-current={isActive ? "page" : undefined} className={cn( buttonVariants({ - variant: isActive ? "outline" : "ghost", + variant: isActive ? "ghost-primary" : "ghost", size, }), + "cursor-pointer", + isActive ? "bg-primaryA-4 hover:bg-primaryA-5" : "", className )} {...props} @@ -64,34 +66,21 @@ const PaginationLink = ({ ) PaginationLink.displayName = "PaginationLink" -const PaginationPrevious = ({ - className, - ...props -}: React.ComponentProps) => ( - - - Previous +const PaginationPrevious = ( + props: React.ComponentProps +) => ( + + {/* */} +
+ {/* Previous */}
) PaginationPrevious.displayName = "PaginationPrevious" -const PaginationNext = ({ - className, - ...props -}: React.ComponentProps) => ( - - Next - +const PaginationNext = (props: React.ComponentProps) => ( + + {/* Next */} +
) PaginationNext.displayName = "PaginationNext"