'use client' import * as React from 'react' import type { CarouselApi } from '@admin/components/ui/carousel' import type { AdminMedia } from '@admin/types' import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core' import { MediaGalleryDialog } from '@admin/components/shared/media/media-gallery-dialog' import { Card, CardContent, CardFooter } from '@admin/components/ui/card' import { Carousel, CarouselContent, CarouselItem } from '@admin/components/ui/carousel' import { Skeleton } from '@admin/components/ui/skeleton' import { useMediaByIds } from '@admin/hooks/use-media' import { closestCenter, DndContext, DragOverlay, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' import { arrayMove, horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable' import { Plus } from 'lucide-react' import { MediaField } from './media-field' import { MediaFieldEmptyState } from './media-field-empty-state' import { SortableGalleryItem } from './sortable-gallery-item' interface GalleryFieldProps { value?: string[] onChange: (value: string[]) => void disabled?: boolean className?: string maxItems?: number } export function GalleryField({ value = [], onChange, disabled, className, maxItems }: GalleryFieldProps) { const [open, setOpen] = React.useState(false) const [activeId, setActiveId] = React.useState(null) const { data: mediaItems } = useMediaByIds(value) const carouselRootRef = React.useRef(null) const carouselApiRef = React.useRef(undefined) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor) ) function handleDragStart(event: DragStartEvent) { setActiveId(String(event.active.id)) } function handleDragEnd(event: DragEndEvent) { setActiveId(null) const { active, over } = event if (!over || active.id === over.id) return const oldIndex = value.indexOf(active.id as string) const newIndex = value.indexOf(over.id as string) if (oldIndex !== -1 && newIndex !== -1) { onChange(arrayMove(value, oldIndex, newIndex)) } } function handleDragCancel() { setActiveId(null) } function handleSelect(selected: AdminMedia | AdminMedia[]) { const items = Array.isArray(selected) ? selected : [selected] const newIds = items.map((m) => m.id).filter((id) => !value.includes(id)) const merged = [...value, ...newIds] onChange(maxItems ? merged.slice(0, maxItems) : merged) } function handleRemove(id: string) { onChange(value.filter((v) => v !== id)) } function handleReplace(currentId: string, nextId: string) { if (!nextId) { handleRemove(currentId) return } if (nextId === currentId) return onChange( value.flatMap((id) => { if (id !== currentId) return [id] return value.includes(nextId) ? [] : [nextId] }) ) } function handleCarouselApi(api: CarouselApi) { carouselApiRef.current = api } const hasSelectedMedia = value.length > 0 React.useEffect(() => { if (!hasSelectedMedia) return const carouselRoot = carouselRootRef.current if (!carouselRoot) return function handleWheel(event: WheelEvent) { const api = carouselApiRef.current if (!api) return const isHorizontalIntent = Math.abs(event.deltaX) >= Math.abs(event.deltaY) const delta = isHorizontalIntent ? event.deltaX : event.deltaY if (Math.abs(delta) < 1) return const engine = api.internalEngine() const target = engine.limit.constrain(engine.target.get() - delta) const distance = target - engine.target.get() if (Math.abs(distance) < 0.5) { if (isHorizontalIntent) { event.preventDefault() } return } event.preventDefault() const scrollBody = engine.scrollBody const setScrollFriction = scrollBody.useFriction const setScrollDuration = scrollBody.useDuration setScrollFriction(0) setScrollDuration(0) engine.scrollTo.distance(distance, false) } carouselRoot.addEventListener('wheel', handleWheel, { passive: false }) return () => { carouselRoot.removeEventListener('wheel', handleWheel) } }, [hasSelectedMedia]) const mediaMap = React.useMemo(() => { const map = new Map() if (mediaItems) { for (let i = 0; i < value.length; i++) { const m = mediaItems[i] if (m) map.set(m.id, m) } } return map }, [mediaItems, value]) const canAdd = !maxItems || value.length < maxItems const activeItem = activeId ? mediaMap.get(activeId) : undefined return (
{hasSelectedMedia ? (
{ const target = event.target return !( target instanceof Element && target.closest( '[data-gallery-drag-handle], [data-media-field-action], button, a' ) ) } }} setApi={handleCarouselApi} className="w-full" > {canAdd && !disabled ? ( setOpen(true)}>
Add Images
) : null} {value.map((id) => { const item = mediaMap.get(id) return ( {item ? ( handleReplace(id, nextId)} onRemove={() => handleRemove(id)} disabled={disabled} isRemovable className="**:data-media-field-preview:aspect-square! [&_[data-media-field-preview]_img]:object-cover!" footerMode="filename" /> ) : (
)}
) })}
{activeId ? (
{activeItem ? ( handleReplace(activeId, nextId)} onRemove={() => handleRemove(activeId)} disabled isRemovable className="**:data-media-field-preview:aspect-square! [&_[data-media-field-preview]_img]:object-cover!" footerMode="filename" /> ) : (
)}
) : null}
) : null} {!hasSelectedMedia ? ( ) : null} {hasSelectedMedia ? ( ) : null}
) }