"use client";
import { useState, useCallback } from "react";
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";
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 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 ? (
})
) : (
🍽️
)}
{/* Selection overlay tint */}
{selectMode && selected && (
)}
{/* Checkbox */}
{selectMode ? (
{selected && }
) : null}
{/* Body */}
{recipe.title}
{recipe.description && (
{recipe.description}
)}
{/* Footer */}
{recipe.baseServings}
{totalMins > 0 && (
{totalMins}m
)}
{recipe.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 [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(`Delete ${selected.size} recipe${selected.size !== 1 ? "s" : ""}? This cannot be undone.`)) 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(`${selected.size} recipe${selected.size !== 1 ? "s" : ""} deleted`);
exitSelect();
} catch {
toast.error("Delete failed");
} 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(`${selected.size} recipe${selected.size !== 1 ? "s" : ""} set to ${visibility}`);
exitSelect();
} catch {
toast.error("Update failed");
} finally {
setBusy(false);
}
}
if (recipes.length === 0) return null;
const allSelected = selected.size === recipes.length;
return (
{/* Toolbar */}
{selectMode ? (
{selected.size > 0
? `${selected.size} selected`
: "Click cards to select"}
) : (
)}
{/* Grid */}
{recipes.map((recipe) => (
))}
{/* Floating bulk action bar */}
{selectMode && selected.size > 0 && (
{selected.size} selected
Visibility
{ void bulkSetVisibility("public"); }}>
Make public
{ void bulkSetVisibility("unlisted"); }}>
Make unlisted
{ void bulkSetVisibility("private"); }}>
Make private
)}
);
}