Add collections pagination and a search field on the search page

Paginate the collections grid with a "See more" button backed by a new
getCollectionsPage cursor API, add an editable search input to the
search-results block, and bump @shopify/hydrogen to 2026.10.0-preview.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqFkSaLYRPZ18dsMbzjiP4
This commit is contained in:
Rami Bitar
2026-08-23 11:01:48 -04:00
co-authored by Claude Opus 5
parent 9ed1cc60de
commit 3bc61066f0
9 changed files with 166 additions and 19 deletions
+25 -1
View File
@@ -4,6 +4,7 @@ import React from 'react';
import { useCollections } from '@/hooks/use-shopify-collections';
import CollectionCard from './collection-card';
import { Button } from '@/components/ui/button';
import { Loader } from '@/components/ui/loader';
interface CollectionsProps {
title?: string;
@@ -32,7 +33,15 @@ const Collections: React.FC<CollectionsProps> = ({
title = 'Our Collections',
subtitle = 'Discover our carefully crafted worlds',
}) => {
const { collections, loading, error, refetch } = useCollections(12);
const {
collections,
loading,
loadingMore,
error,
hasNextPage,
loadMore,
refetch,
} = useCollections(12);
if (loading) {
return (
@@ -92,6 +101,21 @@ const Collections: React.FC<CollectionsProps> = ({
<CollectionCard key={collection.id} collection={collection} />
))}
</div>
{hasNextPage && (
<div className="mt-16 flex justify-center">
<Button
onClick={loadMore}
disabled={loadingMore}
variant="outline"
size="lg"
className="font-normal text-muted-foreground hover:text-foreground"
>
{loadingMore && <Loader size={16} />}
{loadingMore ? 'Loading' : 'See more'}
</Button>
</div>
)}
</div>
</div>
);
@@ -4,6 +4,7 @@ import SearchResults from '@/components/shopify/search-results';
export type SearchResultsBlockProps = {
title?: string;
placeholder?: string;
};
const searchResultsEditor: ComponentConfig<SearchResultsBlockProps> = {
@@ -12,9 +13,11 @@ const searchResultsEditor: ComponentConfig<SearchResultsBlockProps> = {
category: 'commerce',
defaultProps: {
title: 'Search',
placeholder: 'Search products',
},
fields: {
title: { label: 'Title', type: 'text', contentEditable: true },
placeholder: { label: 'Search placeholder', type: 'text' },
},
render: (props) => <SearchResults {...props} />,
};
+56 -3
View File
@@ -1,12 +1,14 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import ProductCard from './product-card';
import ProductFilters from './product-filters';
import ProductToolbar from './product-toolbar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Loader } from '@/components/ui/loader';
import { RiSearchLine, RiCloseLine } from '@remixicon/react';
import {
searchProducts,
type SearchFilter,
@@ -30,11 +32,17 @@ const SORT_OPTIONS: SortOption[] = [
interface SearchResultsProps {
title?: string;
placeholder?: string;
}
const SearchResults: React.FC<SearchResultsProps> = ({ title = 'Search' }) => {
const SearchResults: React.FC<SearchResultsProps> = ({
title = 'Search',
placeholder = 'Search products',
}) => {
const router = useRouter();
const searchParams = useSearchParams();
const query = searchParams.get('q') ?? '';
const [term, setTerm] = useState(query);
const [products, setProducts] = useState<Product[]>([]);
const [filters, setFilters] = useState<SearchFilter[]>([]);
@@ -53,6 +61,24 @@ const SearchResults: React.FC<SearchResultsProps> = ({ title = 'Search' }) => {
// Stabilize the effect dependency by filter contents.
const activeKey = useMemo(() => activeFilters.join('|'), [activeFilters]);
// Keep the field in step with the URL, which also changes from the header
// search dialog and from back/forward navigation.
useEffect(() => {
setTerm(query);
}, [query]);
const submitSearch = (event: React.FormEvent) => {
event.preventDefault();
const next = term.trim();
if (next === query) return;
router.push(next ? `/search?q=${encodeURIComponent(next)}` : '/search');
};
const clearSearch = () => {
setTerm('');
if (query) router.push('/search');
};
useEffect(() => {
let cancelled = false;
@@ -126,7 +152,34 @@ const SearchResults: React.FC<SearchResultsProps> = ({ title = 'Search' }) => {
{title}
</h1>
<div className="mt-6">
<form onSubmit={submitSearch} className="mt-6 max-w-xl">
<div className="relative">
<RiSearchLine
size={18}
className="pointer-events-none absolute left-0 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
type="text"
value={term}
onChange={(event) => setTerm(event.target.value)}
placeholder={placeholder}
aria-label="Search products"
className="h-11 rounded-none border-0 border-b border-border bg-transparent pl-7 pr-8 shadow-none focus-visible:border-foreground focus-visible:ring-0 dark:bg-transparent"
/>
{term && (
<button
type="button"
onClick={clearSearch}
aria-label="Clear search"
className="absolute right-0 top-1/2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground"
>
<RiCloseLine size={18} />
</button>
)}
</div>
</form>
<div className="mt-8">
<ProductToolbar
totalCount={totalCount}
onOpenFilters={() => setFiltersOpen(true)}