Template
Add product search with autocomplete, filters, and sort
- Header: search button opening a command-palette dialog with debounced product suggestions, a term chip, and View All - Command: shadcn-shaped primitives built on the project's own Dialog so no cmdk/radix dependency is introduced - /search: results grid with item count, sort (relevance, price asc/desc), and cursor-based load more - Filters: driven by the API's productFilters facets — colour swatches, labelled lists with counts, and a price range — round-tripped through each facet's raw input string so no filter shape is hardcoded Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jWbNNJLksC1QG8z8845FX
This commit is contained in:
co-authored by
Claude Opus 5
parent
e04f1e0405
commit
cc901b9ce8
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { RiSearchLine, RiCloseLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// A command palette in the shadcn shape, implemented on this project's own
|
||||
// Dialog rather than cmdk so it stays dependency-free. Filtering is left to the
|
||||
// caller, which suits async sources like the Storefront API.
|
||||
|
||||
function Command({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-lg bg-popover text-popover-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CommandDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
children,
|
||||
className,
|
||||
}: CommandDialogProps) {
|
||||
// Dialog portals straight into document.body, so hold off until mounted or
|
||||
// prerendering this component's page throws "document is not defined".
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
React.useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
// DialogContent merges with clsx, so its own `p-6`/`gap-4` need the
|
||||
// important flag to lose to these.
|
||||
className={cn(
|
||||
'top-24 max-w-xl translate-y-0 overflow-hidden p-0! gap-0!',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Command>{children}</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface CommandInputProps
|
||||
extends Omit<React.ComponentProps<'input'>, 'onChange'> {
|
||||
onValueChange?: (value: string) => void;
|
||||
onClear?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
value,
|
||||
onValueChange,
|
||||
onClear,
|
||||
onClose,
|
||||
...props
|
||||
}: CommandInputProps) {
|
||||
const hasValue = Boolean(String(value ?? '').length);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-12 items-center gap-2 px-3"
|
||||
>
|
||||
<RiSearchLine size={18} className="shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
data-slot="command-input"
|
||||
value={value}
|
||||
onChange={(event) => onValueChange?.(event.target.value)}
|
||||
className={cn(
|
||||
'flex h-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{hasValue && onClear && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="font-normal text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
{onClose && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Close search"
|
||||
>
|
||||
<RiCloseLine size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-list"
|
||||
className={cn('max-h-80 overflow-y-auto overflow-x-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-empty"
|
||||
className={cn('px-4 py-8 text-center text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CommandGroupProps extends React.ComponentProps<'div'> {
|
||||
heading?: string;
|
||||
}
|
||||
|
||||
function CommandGroup({ className, heading, children, ...props }: CommandGroupProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-group"
|
||||
className={cn('px-2 py-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{heading && (
|
||||
<div className="px-2 pb-2 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
{heading}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-item"
|
||||
role="option"
|
||||
className={cn(
|
||||
'flex cursor-pointer select-none items-center gap-3 rounded-md px-2 py-2 text-sm outline-none hover:bg-accent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-separator"
|
||||
className={cn('h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandSeparator,
|
||||
};
|
||||
Reference in New Issue
Block a user