- {skuGroups?.map(edge => {
- const group = edge?.node;
- if (!group?.id) return null;
-
- const href = getProductHref?.(group.id);
- return (
-
- );
- })}
+
+ {enablePagination && (
+
+
+
+ {t.storefront.itemsPerPage}
+
+
+
+
+ )}
+
+
+ {skuGroups?.map(edge => {
+ const group = edge?.node;
+ if (!group?.id) return null;
+
+ return (
+
+ );
+ })}
+
+
+ {enablePagination && totalPages > 1 && (
+
+
+
+ {
+ e.preventDefault();
+ if (currentPage > 1) handlePageChange(currentPage - 1);
+ }}
+ aria-disabled={currentPage === 1}
+ className={
+ currentPage === 1 ? 'pointer-events-none opacity-50' : ''
+ }
+ />
+
+
+ {getPageNumbers().map((page, index) =>
+ page === 'ellipsis' ? (
+
+
+
+ ) : (
+
+ {
+ e.preventDefault();
+ handlePageChange(page);
+ }}
+ isActive={currentPage === page}
+ >
+ {page}
+
+
+ )
+ )}
+
+
+ {
+ e.preventDefault();
+ if (currentPage < totalPages)
+ handlePageChange(currentPage + 1);
+ }}
+ aria-disabled={currentPage === totalPages}
+ className={
+ currentPage === totalPages
+ ? 'pointer-events-none opacity-50'
+ : ''
+ }
+ />
+
+
+
+ )}
);
}
diff --git a/packages/react/src/components/storefront/product-search.tsx b/packages/react/src/components/storefront/product-search.tsx
new file mode 100644
index 00000000..ed9b8e90
--- /dev/null
+++ b/packages/react/src/components/storefront/product-search.tsx
@@ -0,0 +1,158 @@
+'use client';
+
+import { Search, X } from 'lucide-react';
+import React, { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { Button } from '@/components/ui/button';
+import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { useGoDaddyContext } from '@/godaddy-provider';
+
+interface ProductSearchProps {
+ showButton?: boolean;
+ onSearch?: (query: string) => void;
+}
+
+interface SearchFormValues {
+ query: string;
+}
+
+export function ProductSearch({
+ showButton = false,
+ onSearch,
+}: ProductSearchProps) {
+ const { t } = useGoDaddyContext();
+ const form = useForm
({
+ defaultValues: {
+ query: '',
+ },
+ });
+
+ const [hasUrlQuery, setHasUrlQuery] = React.useState(false);
+
+ // Sync form with URL and track if URL has query param
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const updateFromUrl = () => {
+ const params = new URLSearchParams(window.location.search);
+ const q = params.get('q') || '';
+ setHasUrlQuery(!!q);
+ if (q) {
+ form.setValue('query', q);
+ }
+ };
+
+ // Initial sync
+ updateFromUrl();
+
+ // Listen for URL changes
+ const handleUrlChange = () => {
+ updateFromUrl();
+ };
+
+ window.addEventListener('popstate', handleUrlChange);
+ window.addEventListener('urlchange', handleUrlChange);
+
+ return () => {
+ window.removeEventListener('popstate', handleUrlChange);
+ window.removeEventListener('urlchange', handleUrlChange);
+ };
+ }, [form]);
+
+ const handleSubmit = (values: SearchFormValues) => {
+ if (!values.query.trim()) {
+ return;
+ }
+
+ if (onSearch) {
+ onSearch(values.query);
+ } else {
+ // Update URL with query parameter, preserving existing params
+ const url = new URL(window.location.href);
+ url.searchParams.set('q', values.query);
+ window.history.pushState({}, '', url.toString());
+
+ // Dispatch custom event to notify components of URL change
+ window.dispatchEvent(new CustomEvent('urlchange'));
+ setHasUrlQuery(true);
+ }
+ };
+
+ const handleClear = () => {
+ form.setValue('query', '');
+
+ if (onSearch) {
+ onSearch('');
+ } else {
+ // Remove query parameter from URL
+ const url = new URL(window.location.href);
+ url.searchParams.delete('q');
+ window.history.pushState({}, '', url.toString());
+
+ // Dispatch custom event to notify components of URL change
+ window.dispatchEvent(new CustomEvent('urlchange'));
+ setHasUrlQuery(false);
+ }
+ };
+
+ // Determine which icon to show inside the input
+ const showClearIcon = hasUrlQuery;
+ const showSearchIcon = !hasUrlQuery && !showButton;
+ const inputPadding = showClearIcon || showSearchIcon ? 'pr-10' : '';
+
+ return (
+
+
+ );
+}
+
+export type { ProductSearchProps };
diff --git a/packages/react/src/components/ui/pagination.tsx b/packages/react/src/components/ui/pagination.tsx
new file mode 100644
index 00000000..f011c127
--- /dev/null
+++ b/packages/react/src/components/ui/pagination.tsx
@@ -0,0 +1,135 @@
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ MoreHorizontalIcon,
+} from 'lucide-react';
+import * as React from 'react';
+import { Button, buttonVariants } from '@/components/ui/button';
+import { useGoDaddyContext } from '@/godaddy-provider';
+import { cn } from '@/lib/utils';
+
+function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
+ const { t } = useGoDaddyContext();
+
+ return (
+
+ );
+}
+
+function PaginationContent({
+ className,
+ ...props
+}: React.ComponentProps<'ul'>) {
+ return (
+
+ );
+}
+
+function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
+ return ;
+}
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick, 'size'> &
+ React.ComponentProps<'a'>;
+
+function PaginationLink({
+ className,
+ isActive,
+ size = 'icon',
+ ...props
+}: PaginationLinkProps) {
+ return (
+
+ );
+}
+
+function PaginationPrevious({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { t } = useGoDaddyContext();
+
+ return (
+
+
+ {t.ui.pagination.previous}
+
+ );
+}
+
+function PaginationNext({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { t } = useGoDaddyContext();
+
+ return (
+
+ {t.ui.pagination.next}
+
+
+ );
+}
+
+function PaginationEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ const { t } = useGoDaddyContext();
+
+ return (
+
+
+ {t.ui.pagination.morePages}
+
+ );
+}
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationLink,
+ PaginationItem,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
+};
diff --git a/packages/react/src/lib/godaddy/catalog-storefront-queries.ts b/packages/react/src/lib/godaddy/catalog-storefront-queries.ts
index eab1ed43..66158673 100644
--- a/packages/react/src/lib/godaddy/catalog-storefront-queries.ts
+++ b/packages/react/src/lib/godaddy/catalog-storefront-queries.ts
@@ -1,8 +1,8 @@
import { graphql } from '@/lib/gql/gql-catalog-storefront.tada.ts';
export const SkuGroupsQuery = graphql(`
- query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter) {
- skuGroups(first: $first, after: $after, id: $id) {
+ query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter, $listId: ListIdFilter, $label: LabelFilter) {
+ skuGroups(first: $first, after: $after, id: $id, listId: $listId, label: $label) {
edges {
cursor
node {
@@ -74,6 +74,7 @@ export const SkuGroupsQuery = graphql(`
startCursor
endCursor
}
+ totalCount
}
}
`);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 50480f3c..d8f052a1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -66,8 +66,8 @@ importers:
specifier: ^0.475.0
version: 0.475.0(react@19.2.0)
next:
- specifier: 16.0.1
- version: 16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ specifier: 16.0.7
+ version: 16.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react:
specifier: 19.2.0
version: 19.2.0
@@ -309,7 +309,7 @@ importers:
specifier: ^1.1.2
version: 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-slot':
- specifier: ^1.1.2
+ specifier: ^1.2.3
version: 1.2.3(@types/react@19.2.2)(react@19.2.0)
'@radix-ui/react-switch':
specifier: ^1.1.3
@@ -1474,53 +1474,53 @@ packages:
'@napi-rs/wasm-runtime@1.0.7':
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
- '@next/env@16.0.1':
- resolution: {integrity: sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==}
+ '@next/env@16.0.7':
+ resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==}
- '@next/swc-darwin-arm64@16.0.1':
- resolution: {integrity: sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==}
+ '@next/swc-darwin-arm64@16.0.7':
+ resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.0.1':
- resolution: {integrity: sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==}
+ '@next/swc-darwin-x64@16.0.7':
+ resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.0.1':
- resolution: {integrity: sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==}
+ '@next/swc-linux-arm64-gnu@16.0.7':
+ resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@16.0.1':
- resolution: {integrity: sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==}
+ '@next/swc-linux-arm64-musl@16.0.7':
+ resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@16.0.1':
- resolution: {integrity: sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==}
+ '@next/swc-linux-x64-gnu@16.0.7':
+ resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@16.0.1':
- resolution: {integrity: sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==}
+ '@next/swc-linux-x64-musl@16.0.7':
+ resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@16.0.1':
- resolution: {integrity: sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==}
+ '@next/swc-win32-arm64-msvc@16.0.7':
+ resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.0.1':
- resolution: {integrity: sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==}
+ '@next/swc-win32-x64-msvc@16.0.7':
+ resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -4420,8 +4420,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
- next@16.0.1:
- resolution: {integrity: sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==}
+ next@16.0.7:
+ resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -6518,30 +6518,30 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
- '@next/env@16.0.1': {}
+ '@next/env@16.0.7': {}
- '@next/swc-darwin-arm64@16.0.1':
+ '@next/swc-darwin-arm64@16.0.7':
optional: true
- '@next/swc-darwin-x64@16.0.1':
+ '@next/swc-darwin-x64@16.0.7':
optional: true
- '@next/swc-linux-arm64-gnu@16.0.1':
+ '@next/swc-linux-arm64-gnu@16.0.7':
optional: true
- '@next/swc-linux-arm64-musl@16.0.1':
+ '@next/swc-linux-arm64-musl@16.0.7':
optional: true
- '@next/swc-linux-x64-gnu@16.0.1':
+ '@next/swc-linux-x64-gnu@16.0.7':
optional: true
- '@next/swc-linux-x64-musl@16.0.1':
+ '@next/swc-linux-x64-musl@16.0.7':
optional: true
- '@next/swc-win32-arm64-msvc@16.0.1':
+ '@next/swc-win32-arm64-msvc@16.0.7':
optional: true
- '@next/swc-win32-x64-msvc@16.0.1':
+ '@next/swc-win32-x64-msvc@16.0.7':
optional: true
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
@@ -9638,9 +9638,9 @@ snapshots:
natural-compare@1.4.0: {}
- next@16.0.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ next@16.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
- '@next/env': 16.0.1
+ '@next/env': 16.0.7
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001737
postcss: 8.4.31
@@ -9648,14 +9648,14 @@ snapshots:
react-dom: 19.2.0(react@19.2.0)
styled-jsx: 5.1.6(react@19.2.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.0.1
- '@next/swc-darwin-x64': 16.0.1
- '@next/swc-linux-arm64-gnu': 16.0.1
- '@next/swc-linux-arm64-musl': 16.0.1
- '@next/swc-linux-x64-gnu': 16.0.1
- '@next/swc-linux-x64-musl': 16.0.1
- '@next/swc-win32-arm64-msvc': 16.0.1
- '@next/swc-win32-x64-msvc': 16.0.1
+ '@next/swc-darwin-arm64': 16.0.7
+ '@next/swc-darwin-x64': 16.0.7
+ '@next/swc-linux-arm64-gnu': 16.0.7
+ '@next/swc-linux-arm64-musl': 16.0.7
+ '@next/swc-linux-x64-gnu': 16.0.7
+ '@next/swc-linux-x64-musl': 16.0.7
+ '@next/swc-win32-arm64-msvc': 16.0.7
+ '@next/swc-win32-x64-msvc': 16.0.7
sharp: 0.34.4
transitivePeerDependencies:
- '@babel/core'