diff --git a/app/search/page.json b/app/search/page.json index c883d01..200e9b9 100644 --- a/app/search/page.json +++ b/app/search/page.json @@ -18,7 +18,8 @@ "type": "search-results", "props": { "id": "search-results", - "title": "Search" + "title": "Search", + "placeholder": "Search products" } }, { diff --git a/components/shopify/collections.tsx b/components/shopify/collections.tsx index 1b73d09..17fd873 100644 --- a/components/shopify/collections.tsx +++ b/components/shopify/collections.tsx @@ -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 = ({ 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 = ({ ))} + + {hasNextPage && ( +
+ +
+ )} ); diff --git a/components/shopify/search-results.editor.tsx b/components/shopify/search-results.editor.tsx index 81bda55..04fa004 100644 --- a/components/shopify/search-results.editor.tsx +++ b/components/shopify/search-results.editor.tsx @@ -4,6 +4,7 @@ import SearchResults from '@/components/shopify/search-results'; export type SearchResultsBlockProps = { title?: string; + placeholder?: string; }; const searchResultsEditor: ComponentConfig = { @@ -12,9 +13,11 @@ const searchResultsEditor: ComponentConfig = { category: 'commerce', defaultProps: { title: 'Search', + placeholder: 'Search products', }, fields: { title: { label: 'Title', type: 'text', contentEditable: true }, + placeholder: { label: 'Search placeholder', type: 'text' }, }, render: (props) => , }; diff --git a/components/shopify/search-results.tsx b/components/shopify/search-results.tsx index 91bcbe7..3ca7aa6 100644 --- a/components/shopify/search-results.tsx +++ b/components/shopify/search-results.tsx @@ -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 = ({ title = 'Search' }) => { +const SearchResults: React.FC = ({ + 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([]); const [filters, setFilters] = useState([]); @@ -53,6 +61,24 @@ const SearchResults: React.FC = ({ 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 = ({ title = 'Search' }) => { {title} -
+
+
+ + 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 && ( + + )} +
+
+ +
setFiltersOpen(true)} diff --git a/graphql/collections.ts b/graphql/collections.ts index acb6db7..090ab30 100644 --- a/graphql/collections.ts +++ b/graphql/collections.ts @@ -2,9 +2,13 @@ import { gql } from '@shopify/hydrogen'; import { ProductFragment } from '@/graphql/products'; export const GET_COLLECTIONS_QUERY = gql(` - query GetCollections($first: Int!, $country: CountryCode, $language: LanguageCode) - @inContext(country: $country, language: $language) { - collections(first: $first) { + query GetCollections( + $first: Int! + $after: String + $country: CountryCode + $language: LanguageCode + ) @inContext(country: $country, language: $language) { + collections(first: $first, after: $after) { edges { node { id diff --git a/hooks/use-shopify-collections.ts b/hooks/use-shopify-collections.ts index a5acb73..c0ce2ba 100644 --- a/hooks/use-shopify-collections.ts +++ b/hooks/use-shopify-collections.ts @@ -3,16 +3,19 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { getCollections, + getCollectionsPage, getCollectionProducts, } from '@/services/shopify/shop'; export { getCollections, + getCollectionsPage, getCollectionProducts, getCollectionProductsPage, } from '@/services/shopify/shop'; export type { Collection, + CollectionsPage, CollectionWithProducts, CollectionSortKey, CollectionProductsPage, @@ -36,14 +39,19 @@ interface UseCollectionProductsOptions { export function useCollections(first = 50) { const [collections, setCollections] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); + const [cursor, setCursor] = useState(null); + const [hasNextPage, setHasNextPage] = useState(false); const fetchCollections = useCallback(async () => { try { setLoading(true); setError(null); - const data = await getCollections(first); - setCollections(data); + const page = await getCollectionsPage(first); + setCollections(page.collections); + setCursor(page.endCursor); + setHasNextPage(page.hasNextPage); } catch (err) { console.error('Error fetching collections:', err); setError(err instanceof Error ? err.message : 'Failed to load collections'); @@ -56,7 +64,35 @@ export function useCollections(first = 50) { fetchCollections(); }, [fetchCollections]); - return { collections, loading, error, refetch: fetchCollections }; + const loadMore = useCallback(async () => { + if (loadingMore || !hasNextPage) return; + + try { + setLoadingMore(true); + const page = await getCollectionsPage(first, cursor); + + setCollections((prev) => { + const seen = new Set(prev.map((collection) => collection.id)); + return [...prev, ...page.collections.filter((c) => !seen.has(c.id))]; + }); + setCursor(page.endCursor); + setHasNextPage(page.hasNextPage); + } catch (err) { + console.error('Failed to load more collections:', err); + } finally { + setLoadingMore(false); + } + }, [first, cursor, hasNextPage, loadingMore]); + + return { + collections, + loading, + loadingMore, + error, + hasNextPage, + loadMore, + refetch: fetchCollections, + }; } // Fetch once on demand for menus that may never open. diff --git a/package.json b/package.json index 79218c2..b3cd916 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@reacteditor/plugin-shopify": "0.0.2", "@reacteditor/plugin-tailwind-cdn": "0.0.4", "@remixicon/react": "^4.9.0", - "@shopify/hydrogen": "0.0.0-preview-116d5d7-20260730141607", + "@shopify/hydrogen": "2026.10.0-preview.1", "@shopify/storefront-api-client": "^1.0.0", "ai": "^7", "class-variance-authority": "0.7.1", diff --git a/services/shopify/shop.ts b/services/shopify/shop.ts index 2a47e8d..0a38be1 100644 --- a/services/shopify/shop.ts +++ b/services/shopify/shop.ts @@ -218,13 +218,36 @@ export interface ProductFilterFacet { }>; } +export interface CollectionsPage { + collections: Collection[]; + hasNextPage: boolean; + endCursor: string | null; +} + export async function getCollections(first = 50): Promise { + const page = await getCollectionsPage(first); + + return page.collections; +} + +export async function getCollectionsPage( + first = 50, + after: string | null = null +): Promise { const data = unwrapStorefrontResult( - await storefront.graphql(GET_COLLECTIONS_QUERY, { variables: { first } }), + await storefront.graphql(GET_COLLECTIONS_QUERY, { + variables: { first, after }, + }), 'GetCollections' ); - return data.collections.edges.map((edge) => edge.node); + const { edges, pageInfo } = data.collections; + + return { + collections: edges.map((edge) => edge.node), + hasNextPage: Boolean(pageInfo?.hasNextPage), + endCursor: pageInfo?.endCursor ?? null, + }; } export async function getCollectionProducts( diff --git a/yarn.lock b/yarn.lock index 24c58e9..d22837c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2404,22 +2404,25 @@ __metadata: languageName: node linkType: hard -"@shopify/hydrogen@npm:0.0.0-preview-116d5d7-20260730141607": - version: 0.0.0-preview-116d5d7-20260730141607 - resolution: "@shopify/hydrogen@npm:0.0.0-preview-116d5d7-20260730141607" +"@shopify/hydrogen@npm:2026.10.0-preview.1": + version: 2026.10.0-preview.1 + resolution: "@shopify/hydrogen@npm:2026.10.0-preview.1" dependencies: gql.tada: "npm:1.9.2" peerDependencies: react: ^18.0.0 || ^19.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.5.0 peerDependenciesMeta: react: optional: true + vite: + optional: true vue: optional: true bin: hydrogen: bin/hydrogen.mjs - checksum: 10c0/b12e230d174a910d8abb7ab18ecb99357dce82d2125054b30af6dcbecf280c6fea40c8ca5fd67d8931e1079fd73e587e3ea1484f50c294f4a79ad714c9664ecd + checksum: 10c0/3fdc23d9ac756634bf5a84b6fec1adad5f1e11c2a9bb901fe0928235ddeae0b50048fe5c1594a0baa0cfe9477b69067c63d9a0903ef49a2b4ee4aa7b1be0d18c languageName: node linkType: hard @@ -5828,7 +5831,7 @@ __metadata: "@reacteditor/plugin-shopify": "npm:0.0.2" "@reacteditor/plugin-tailwind-cdn": "npm:0.0.4" "@remixicon/react": "npm:^4.9.0" - "@shopify/hydrogen": "npm:0.0.0-preview-116d5d7-20260730141607" + "@shopify/hydrogen": "npm:2026.10.0-preview.1" "@shopify/storefront-api-client": "npm:^1.0.0" "@tailwindcss/postcss": "npm:^4" "@types/hast": "npm:^3.0.5"