diff --git a/.changeset/major-turtles-shout.md b/.changeset/major-turtles-shout.md new file mode 100644 index 00000000..00d6a4d5 --- /dev/null +++ b/.changeset/major-turtles-shout.md @@ -0,0 +1,9 @@ +--- +"@godaddy/localizations": patch +"@godaddy/react": patch +--- + +- Add filtering by `productIds` and `categoryIds` to ProductGrid +- Add `productId` prop to ProductCard for single product rendering +- Add pagination support to ProductGrid with `enablePagination` prop +- Add translations for pagination controls \ No newline at end of file diff --git a/examples/nextjs/app/store/products.tsx b/examples/nextjs/app/store/products.tsx index 48a01459..dadce850 100644 --- a/examples/nextjs/app/store/products.tsx +++ b/examples/nextjs/app/store/products.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ProductGrid } from '@godaddy/react'; +import { ProductGrid, ProductSearch } from '@godaddy/react'; import { useCart } from './layout'; export default function ProductsPage() { @@ -8,7 +8,11 @@ export default function ProductsPage() { return (
+
+ +
`/store/product/${sku}`} onAddToCartSuccess={openCart} /> diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index ba69a659..7dec0734 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -15,7 +15,7 @@ "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.76.1", "lucide-react": "^0.475.0", - "next": "16.0.1", + "next": "16.0.7", "react": "19.2.0", "react-dom": "19.2.0", "zod": "^3.24.1" diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index 185dc2a9..9c186fa8 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -384,5 +384,8 @@ export const deDe = { remove: 'Entfernen', removing: 'Wird entfernt...', checkout: 'Zur Kasse', + itemsPerPage: 'Artikel pro Seite:', + search: 'Suchen', + searchPlaceholder: 'Produkte suchen...', }, }; diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index b53fda8c..8e3baf6f 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -361,5 +361,8 @@ export const enIe = { remove: 'Remove', removing: 'Removing...', checkout: 'Checkout', + itemsPerPage: 'Items per page:', + search: 'Search', + searchPlaceholder: 'Search products...', }, }; diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index c7a7f8f1..2458daa0 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -361,5 +361,8 @@ export const enUs = { remove: 'Remove', removing: 'Removing...', checkout: 'Checkout', + itemsPerPage: 'Items per page:', + search: 'Search', + searchPlaceholder: 'Search products...', }, }; diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index 1386d8cc..c55877dd 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -367,5 +367,8 @@ export const esAr = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index 62512931..100aa97b 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -369,5 +369,8 @@ export const esCl = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index 29ac90fc..5c34644b 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -367,5 +367,8 @@ export const esCo = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 5b5780ad..a2220308 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -372,5 +372,8 @@ export const esEs = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index c03d4cbd..c484e103 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -368,5 +368,8 @@ export const esMx = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index 36ba50cf..ccae27f8 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -367,5 +367,8 @@ export const esPe = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index 1a937cd4..da390efe 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -367,5 +367,8 @@ export const esUs = { remove: 'Eliminar', removing: 'Eliminando...', checkout: 'Pagar', + itemsPerPage: 'Artículos por página:', + search: 'Buscar', + searchPlaceholder: 'Buscar productos...', }, }; diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index a0ffb957..26edeb97 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -384,5 +384,8 @@ export const frCa = { remove: 'Supprimer', removing: 'Suppression...', checkout: 'Commander', + itemsPerPage: 'Articles par page :', + search: 'Rechercher', + searchPlaceholder: 'Rechercher des produits...', }, }; diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index da188eb8..13abdf5f 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -385,5 +385,8 @@ export const frFr = { remove: 'Supprimer', removing: 'Suppression...', checkout: 'Commander', + itemsPerPage: 'Articles par page :', + search: 'Rechercher', + searchPlaceholder: 'Rechercher des produits...', }, }; diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index 9bb8fe49..280aac48 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -360,5 +360,8 @@ export const idId = { remove: 'Hapus', removing: 'Menghapus...', checkout: 'Checkout', + itemsPerPage: 'Item per halaman:', + search: 'Cari', + searchPlaceholder: 'Cari produk...', }, }; diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index 41ee362d..20fb884a 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -384,5 +384,8 @@ export const itIt = { remove: 'Rimuovi', removing: 'Rimozione...', checkout: 'Acquista', + itemsPerPage: 'Articoli per pagina:', + search: 'Cerca', + searchPlaceholder: 'Cerca prodotti...', }, }; diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index a439ed70..64a36964 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -365,5 +365,8 @@ export const ptBr = { remove: 'Remover', removing: 'Removendo...', checkout: 'Finalizar compra', + itemsPerPage: 'Itens por página:', + search: 'Pesquisar', + searchPlaceholder: 'Pesquisar produtos...', }, }; diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index 21a13abe..2b320271 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -369,5 +369,8 @@ export const qaPs = { remove: '[Remove]', removing: '[Removing...]', checkout: '[Checkout]', + itemsPerPage: '[Items per page:]', + search: '[Search]', + searchPlaceholder: '[Search products...]', }, }; diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index ad43f753..14cfdfa1 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -360,5 +360,8 @@ export const trTr = { remove: 'Kaldır', removing: 'Kaldırılıyor...', checkout: 'Ödeme yap', + itemsPerPage: 'Sayfa başına öğe:', + search: 'Ara', + searchPlaceholder: 'Ürün ara...', }, }; diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index 5ecd5377..436c37ca 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -361,5 +361,8 @@ export const viVn = { remove: 'Xóa', removing: 'Đang xóa...', checkout: 'Thanh toán', + itemsPerPage: 'Mục trên mỗi trang:', + search: 'Tìm kiếm', + searchPlaceholder: 'Tìm kiếm sản phẩm...', }, }; diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index b09702a8..6cf7c1d9 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -348,5 +348,8 @@ export const zhCn = { remove: '删除', removing: '正在删除...', checkout: '结账', + itemsPerPage: '每页项目数:', + search: '搜索', + searchPlaceholder: '搜索产品...', }, }; diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 0283ee41..1aaa6d12 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -348,5 +348,8 @@ export const zhSg = { remove: '删除', removing: '正在删除...', checkout: '结账', + itemsPerPage: '每页项目数:', + search: '搜索', + searchPlaceholder: '搜索产品...', }, }; diff --git a/packages/react/biome.json b/packages/react/biome.json index 1867c26d..a15c0d49 100644 --- a/packages/react/biome.json +++ b/packages/react/biome.json @@ -2,7 +2,11 @@ "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", "extends": ["biome-config-godaddy/biome.json"], "files": { - "includes": ["**/*", "!!**/src/globals.css"] + "includes": [ + "**/*", + "!!**/src/globals.css", + "!!**/src/globals-tailwind.css" + ] }, "linter": { "rules": { diff --git a/packages/react/package.json b/packages/react/package.json index 1142d9a5..e40e918c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -67,7 +67,7 @@ "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toggle": "^1.1.2", diff --git a/packages/react/src/components/storefront/index.ts b/packages/react/src/components/storefront/index.ts index 5441be69..02bebd62 100644 --- a/packages/react/src/components/storefront/index.ts +++ b/packages/react/src/components/storefront/index.ts @@ -4,3 +4,4 @@ export * from './cart-totals.tsx'; export * from './product-card'; export * from './product-details.tsx'; export * from './product-grid'; +export * from './product-search'; diff --git a/packages/react/src/components/storefront/product-card.tsx b/packages/react/src/components/storefront/product-card.tsx index 107d6442..94b662fb 100644 --- a/packages/react/src/components/storefront/product-card.tsx +++ b/packages/react/src/components/storefront/product-card.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; import { ChevronRight, Loader2, ShoppingBag } from 'lucide-react'; import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; import { useAddToCart } from '@/components/storefront/hooks/use-add-to-cart'; @@ -7,24 +8,83 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { RouterLink } from '@/components/ui/link'; +import { Skeleton } from '@/components/ui/skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; +import { getSkuGroup } from '@/lib/godaddy/godaddy'; import { SKUGroup } from '@/types.ts'; interface ProductCardProps { - product: SKUGroup; + product?: SKUGroup; + productId?: string; + storeId?: string; + clientId?: string; href?: string; + getProductHref?: (productId: string) => string; onAddToCartSuccess?: () => void; onAddToCartError?: (error: Error) => void; } export function ProductCard({ - product, - href, + product: productProp, + productId, + storeId: storeIdProp, + clientId: clientIdProp, + href: hrefProp, + getProductHref, onAddToCartSuccess, onAddToCartError, }: ProductCardProps) { + // All hooks must be called at the top, before any conditional returns + const context = useGoDaddyContext(); + const { t } = context; const formatCurrency = useFormatCurrency(); - const { t } = useGoDaddyContext(); + const storeId = storeIdProp || context.storeId; + const clientId = clientIdProp || context.clientId; + + // Fetch product by ID if productId is provided + const { data: fetchedProductData, isLoading } = useQuery({ + queryKey: ['sku-group', productId, storeId, clientId], + queryFn: () => + getSkuGroup({ id: productId! }, storeId!, clientId!, context.apiHost), + enabled: !!productId && !!storeId && !!clientId && !productProp, + }); + + // Use shared add to cart hook + const { addToCart, isLoading: isAddingToCart } = useAddToCart({ + onSuccess: onAddToCartSuccess, + onError: onAddToCartError, + }); + + // Use fetched product or prop product + const product = productProp || fetchedProductData?.skuGroup; + + // Compute href with priority: explicit href > getProductHref > no link + const resolvedProductId = product?.id || productId; + const href = + hrefProp || + (getProductHref && resolvedProductId + ? getProductHref(resolvedProductId) + : undefined); + + // Show loading skeleton while fetching + if (isLoading || !product) { + return ( + +
+ +
+
+ + + +
+ + +
+
+
+ ); + } const title = product?.label || product?.name || t.storefront.product; const description = product?.description || ''; const priceMin = product?.priceRange?.min || 0; @@ -51,12 +111,6 @@ export function ProductCard({ const isFirstSkuInStock = availableInventory > 0; const hasMultipleSkus = (product?.skus?.edges?.length || 0) > 1; - // Use shared add to cart hook - const { addToCart, isLoading: isAddingToCart } = useAddToCart({ - onSuccess: onAddToCartSuccess, - onError: onAddToCartError, - }); - const handleAddToCart = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); diff --git a/packages/react/src/components/storefront/product-grid.tsx b/packages/react/src/components/storefront/product-grid.tsx index d73607f0..2e3b6017 100644 --- a/packages/react/src/components/storefront/product-grid.tsx +++ b/packages/react/src/components/storefront/product-grid.tsx @@ -1,15 +1,37 @@ 'use client'; import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Skeleton } from '@/components/ui/skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; import { getSkuGroups } from '@/lib/godaddy/godaddy'; +import type { SkuGroupsInput } from '@/types'; import { ProductCard } from './product-card'; interface ProductGridProps { storeId?: string; clientId?: string; - first?: number; + /** Filter products by specific product IDs */ + productIds?: string[]; + /** Filter products by category IDs (maps to listId in GraphQL) */ + categoryIds?: string[]; + enablePagination?: boolean; getProductHref?: (productId: string) => string; onAddToCartSuccess?: () => void; onAddToCartError?: (error: Error) => void; @@ -34,7 +56,9 @@ function ProductGridSkeleton({ count = 6 }: { count?: number }) { export function ProductGrid({ storeId: storeIdProp, clientId: clientIdProp, - first = 100, + productIds, + categoryIds, + enablePagination = false, getProductHref, onAddToCartSuccess, onAddToCartError, @@ -44,15 +68,74 @@ export function ProductGrid({ const storeId = storeIdProp || context.storeId; const clientId = clientIdProp || context.clientId; + const [perPage, setPerPage] = useState(12); + const [pageCursors, setPageCursors] = useState<(string | null)[]>([null]); + const [currentPageIndex, setCurrentPageIndex] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + + const first = enablePagination ? perPage : 100; + const after = pageCursors[currentPageIndex] || undefined; + + // Get search query from URL and update state when it changes + useEffect(() => { + if (typeof window === 'undefined') return; + + const updateSearchQuery = () => { + const params = new URLSearchParams(window.location.search); + const q = params.get('q') || ''; + setSearchQuery(q); + }; + + // Set initial value + updateSearchQuery(); + + // Listen for URL changes (both browser navigation and custom events) + const handleUrlChange = () => { + updateSearchQuery(); + }; + + // Handle browser back/forward navigation + window.addEventListener('popstate', handleUrlChange); + // Handle custom URL change events (from ProductSearch) + window.addEventListener('urlchange', handleUrlChange); + + return () => { + window.removeEventListener('popstate', handleUrlChange); + window.removeEventListener('urlchange', handleUrlChange); + }; + }, []); + + // Reset pagination when search query changes + useEffect(() => { + setCurrentPageIndex(0); + setPageCursors([null]); + }, [searchQuery]); + + // Build filter object for GraphQL query + // Map categoryIds to listId (GraphQL uses 'listId' for categories) + // Only apply search filter if no productIds or categoryIds are provided + const hasExplicitFilters = + (productIds && productIds.length > 0) || + (categoryIds && categoryIds.length > 0); + + const filters: SkuGroupsInput = { + first, + after, + ...(productIds && productIds.length > 0 && { id: { in: productIds } }), + ...(categoryIds && + categoryIds.length > 0 && { listId: { in: categoryIds } }), + ...(!hasExplicitFilters && + searchQuery && { label: { contains: searchQuery } }), + }; + const { data, isLoading, error } = useQuery({ - queryKey: ['sku-groups', { storeId, clientId, first }], - queryFn: () => - getSkuGroups({ first }, storeId!, clientId!, context?.apiHost), + queryKey: ['sku-groups', { storeId, clientId, ...filters }], + queryFn: () => getSkuGroups(filters, storeId!, clientId!, context?.apiHost), enabled: !!storeId && !!clientId, }); if (isLoading || !data) { - return ; + return ; } if (error) { @@ -64,24 +147,165 @@ export function ProductGrid({ } const skuGroups = data?.skuGroups?.edges; + const pageInfo = data?.skuGroups?.pageInfo; + const totalCount = data?.skuGroups?.totalCount || 0; + const totalPages = Math.ceil(totalCount / perPage); + const currentPage = currentPageIndex + 1; + + if ( + pageInfo?.endCursor && + pageInfo?.hasNextPage && + !pageCursors[currentPageIndex + 1] + ) { + setPageCursors([...pageCursors, pageInfo.endCursor]); + } + + const handlePageChange = (page: number) => { + setCurrentPageIndex(page - 1); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handlePerPageChange = (value: string) => { + setPerPage(Number(value)); + setCurrentPageIndex(0); + setPageCursors([null]); // Reset cursors when changing per page + }; + + const getPageNumbers = () => { + const pages: (number | 'ellipsis')[] = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + // Show all pages if total is less than max + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + pages.push(1); + + if (currentPage > 3) { + pages.push('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('ellipsis'); + } + + // Always show last page + pages.push(totalPages); + } + + return pages; + }; return ( -
- {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 ( +
+ + ( + + +
+ + {showClearIcon && ( + + )} + {showSearchIcon && ( + + )} +
+
+
+ )} + /> + {showButton && ( + + )} + + + ); +} + +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 ( +