"use client"; import { useState, useCallback, useMemo } from "react"; import Link from "next/link"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { ListChecks, X, FolderInput, FolderMinus, Check, GripVertical, Search } from "lucide-react"; import { DndContext, type DragEndEvent, PointerSensor, useSensor, useSensors, closestCenter, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { RecipeGridCard, type GridCardRecipe } from "@/components/recipe/recipe-grid-card"; import { AddToCollectionDialog } from "@/components/recipe/add-to-collection-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { cn } from "@/lib/utils"; function SortableRecipeCard({ recipe, selectMode, selected, onToggle, dragDisabled, }: { recipe: GridCardRecipe; selectMode: boolean; selected: boolean; onToggle: () => void; dragDisabled: boolean; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: recipe.id, disabled: dragDisabled, }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
{selectMode && (
{selected && }
)} {!selectMode && !dragDisabled && ( )}
{selectMode ? ( ) : ( )}
); } export function CollectionRecipesGrid({ collectionId, recipes: initialRecipes }: { collectionId: string; recipes: GridCardRecipe[] }) { const t = useTranslations("collections"); const tCommon = useTranslations("common"); const [recipes, setRecipes] = useState(initialRecipes); const [selectMode, setSelectMode] = useState(false); const [selected, setSelected] = useState>(new Set()); const [moveOpen, setMoveOpen] = useState(false); const [removeConfirmOpen, setRemoveConfirmOpen] = useState(false); const [busy, setBusy] = useState(false); const [search, setSearch] = useState(""); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } })); const toggleSelect = useCallback((id: string) => { setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }, []); const exitSelect = () => { setSelectMode(false); setSelected(new Set()); }; async function removeFromCollection() { setBusy(true); try { const res = await fetch(`/api/v1/collections/${collectionId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ removeRecipeIds: [...selected] }), }); if (!res.ok) throw new Error(); setRecipes((prev) => prev.filter((r) => !selected.has(r.id))); toast.success(t("removedFromCollection", { count: selected.size })); exitSelect(); } catch { toast.error(t("removeFromCollectionFailed")); } finally { setBusy(false); } } async function persistOrder(ordered: GridCardRecipe[]) { try { const res = await fetch(`/api/v1/collections/${collectionId}/reorder`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ recipeIds: ordered.map((r) => r.id) }), }); if (!res.ok) throw new Error(); } catch { toast.error(t("reorderFailed")); } } function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = recipes.findIndex((r) => r.id === active.id); const newIndex = recipes.findIndex((r) => r.id === over.id); if (oldIndex === -1 || newIndex === -1) return; const reordered = arrayMove(recipes, oldIndex, newIndex); setRecipes(reordered); void persistOrder(reordered); } const filtered = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return recipes; return recipes.filter((r) => r.title.toLowerCase().includes(q)); }, [recipes, search]); const searchActive = search.trim().length > 0; if (recipes.length === 0) return null; return (
setSearch(e.target.value)} placeholder={t("searchRecipesPlaceholder")} className="pl-9" />
{filtered.length === 0 ? (

{t("noRecipeSearchResults")}

) : ( r.id)} strategy={verticalListSortingStrategy}>
{filtered.map((recipe) => ( toggleSelect(recipe.id)} dragDisabled={selectMode || searchActive} /> ))}
)} {selectMode && selected.size > 0 && (
{selected.size}
)} { setRecipes((prev) => prev.filter((r) => !selected.has(r.id))); exitSelect(); }} /> {t("removeFromCollectionConfirmTitle", { count: selected.size })} {t("removeFromCollectionConfirmDescription")} {tCommon("cancel")} { setRemoveConfirmOpen(false); void removeFromCollection(); }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t("removeFromCollection")}
); }