"use client"; import { useState, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Trash2, Globe, Lock, Link2, X, Check, ListChecks } from "lucide-react"; import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from "sonner"; import { getPublicUrl } from "@/lib/storage"; import { Badge } from "@/components/ui/badge"; import { Clock, Users } from "lucide-react"; import Link from "next/link"; import { cn } from "@/lib/utils"; type Recipe = { id: string; title: string; description: string | null; baseServings: number; prepMins: number | null; cookMins: number | null; difficulty: "easy" | "medium" | "hard" | null; visibility: "private" | "unlisted" | "public"; tags: string[]; updatedAt: Date; photos?: Array<{ storageKey: string; isCover: boolean }>; }; const DIFFICULTY_COLOR = { easy: "default", medium: "secondary", hard: "destructive" } as const; const VISIBILITY_ICON = { private: Lock, unlisted: Link2, public: Globe }; function SelectableRecipeCard({ recipe, selected, selectMode, onToggle, }: { recipe: Recipe; selected: boolean; selectMode: boolean; onToggle: (id: string) => void; }) { const t = useTranslations("recipe"); const cover = recipe.photos?.find((p) => p.isCover) ?? recipe.photos?.[0]; const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); const VisibilityIcon = VISIBILITY_ICON[recipe.visibility]; const inner = (
{/* Cover image */}
{cover ? ( {recipe.title} ) : (
🍽️
)} {/* Selection overlay tint */} {selectMode && selected && (
)} {/* Checkbox */} {selectMode ? (
{selected && }
) : null}
{/* Body */}

{recipe.title}

{recipe.description && (

{recipe.description}

)} {recipe.tags.length > 0 && (
{recipe.tags.slice(0, 3).map((tag) => ( {tag} ))} {recipe.tags.length > 3 && ( +{recipe.tags.length - 3} )}
)}
{/* Footer */}
{recipe.baseServings} {totalMins > 0 && ( {t("total", { mins: totalMins })} )} {recipe.difficulty && ( {t(`difficulty.${recipe.difficulty}`)} )}
); if (selectMode) { return (
onToggle(recipe.id)} role="checkbox" aria-checked={selected} className="h-full" > {inner}
); } return ( {inner} ); } export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] }) { const t = useTranslations("recipe"); const tCommon = useTranslations("common"); const [recipes, setRecipes] = useState(initialRecipes); const [selectMode, setSelectMode] = useState(false); const [selected, setSelected] = useState>(new Set()); const [busy, setBusy] = useState(false); const toggleSelect = useCallback((id: string) => { setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); }, []); const toggleAll = () => { setSelected((prev) => prev.size === recipes.length ? new Set() : new Set(recipes.map((r) => r.id)) ); }; const exitSelect = () => { setSelectMode(false); setSelected(new Set()); }; async function bulkDelete() { if (!confirm(t("bulkDeleteConfirm", { count: selected.size }))) return; setBusy(true); try { const res = await fetch("/api/v1/recipes/bulk", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: [...selected] }), }); if (!res.ok) throw new Error("Delete failed"); setRecipes((prev) => prev.filter((r) => !selected.has(r.id))); toast.success(t("bulkDeleted", { count: selected.size })); exitSelect(); } catch { toast.error(t("bulkDeleteFailed")); } finally { setBusy(false); } } async function bulkSetVisibility(visibility: "private" | "unlisted" | "public") { setBusy(true); try { const res = await fetch("/api/v1/recipes/bulk", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: [...selected], visibility }), }); if (!res.ok) throw new Error("Update failed"); setRecipes((prev) => prev.map((r) => selected.has(r.id) ? { ...r, visibility } : r) ); toast.success(t("bulkVisibility", { count: selected.size, visibility })); exitSelect(); } catch { toast.error(t("bulkUpdateFailed")); } finally { setBusy(false); } } if (recipes.length === 0) return null; const allSelected = selected.size === recipes.length; return (
{/* Toolbar */}
{selectMode ? (
{selected.size > 0 ? t("selectedCount", { count: selected.size }) : t("clickToSelect")}
) : (
)}
{/* Grid */}
{recipes.map((recipe) => ( ))}
{/* Floating bulk action bar */} {selectMode && selected.size > 0 && (
{t("selectedCount", { count: selected.size })}
{t("visibility")} { void bulkSetVisibility("public"); }}> {t("makePublic")} { void bulkSetVisibility("unlisted"); }}> {t("makeUnlisted")} { void bulkSetVisibility("private"); }}> {t("makePrivate")}
)}
); }