refactor shopify storefront
This commit is contained in:
@@ -1,190 +1,287 @@
|
||||
"use client"
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
interface SelectContextType {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
}
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
const SelectContext = createContext<SelectContextType | undefined>(undefined);
|
||||
|
||||
function useSelect() {
|
||||
const context = useContext(SelectContext);
|
||||
if (!context) {
|
||||
throw new Error('Select components must be used within a Select');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
value?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
value: controlledValue,
|
||||
onValueChange,
|
||||
children,
|
||||
}: SelectProps) {
|
||||
const [internalValue, setInternalValue] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const value = isControlled ? controlledValue : internalValue;
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (!isControlled) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
onValueChange?.(newValue);
|
||||
setOpen(false);
|
||||
},
|
||||
[isControlled, onValueChange]
|
||||
);
|
||||
|
||||
// Handle clicking outside to close the menu
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<SelectContext.Provider
|
||||
value={{ open, setOpen, value, setValue: handleValueChange }}
|
||||
>
|
||||
<div ref={containerRef} data-slot="select" className="relative">
|
||||
{children}
|
||||
</div>
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
interface SelectTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
placeholder = 'Select...',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
}: SelectTriggerProps) {
|
||||
const { open, setOpen, value } = useSelect();
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
<button
|
||||
ref={triggerRef}
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*="text-"])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children || <span className="text-muted-foreground">{placeholder}</span>}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn(
|
||||
'size-4 opacity-50 transition-transform',
|
||||
open && 'rotate-180'
|
||||
)}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectValueProps {
|
||||
children?: React.ReactNode;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
children,
|
||||
placeholder = 'Select...',
|
||||
}: SelectValueProps) {
|
||||
const { value } = useSelect();
|
||||
|
||||
return (
|
||||
<span data-slot="select-value">{children || value || placeholder}</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SelectContent({ className, children, ...props }: SelectContentProps) {
|
||||
const { open } = useSelect();
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground absolute z-50 min-w-[8rem] rounded-md border border-border shadow-md overflow-hidden top-full mt-2 left-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="p-1 overflow-y-auto max-h-60">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
value,
|
||||
children,
|
||||
disabled = false,
|
||||
className,
|
||||
...props
|
||||
}: SelectItemProps) {
|
||||
const { value: selectedValue, setValue } = useSelect();
|
||||
const isSelected = selectedValue === value;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="select-item"
|
||||
onClick={() => !disabled && setValue(value)}
|
||||
className={cn(
|
||||
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*="text-"])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none transition-colors',
|
||||
!disabled &&
|
||||
'hover:bg-accent hover:text-accent-foreground cursor-pointer',
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
isSelected && 'bg-accent text-accent-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isSelected && (
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-4"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SelectGroup({ className, children, ...props }: SelectGroupProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="select-group"
|
||||
className={cn('overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectLabelProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SelectLabel({ className, children, ...props }: SelectLabelProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="select-label"
|
||||
className={cn(
|
||||
'text-muted-foreground px-2 py-1.5 text-xs font-semibold',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
interface SelectSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
function SelectSeparator({ className, ...props }: SelectSeparatorProps) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
<div
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
className={cn(
|
||||
'bg-border pointer-events-none -mx-1 my-1 h-px',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user