"use client"; import { useMemo, useState } from "react"; import { ImageIcon, Loader2Icon, PencilIcon, PlusIcon, RefreshCwIcon, SaveIcon, Trash2Icon, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import type { CollectionPhoto, PhotoCollection, } from "@/types/admin/collection"; type CollectionForm = { id: string | null; title: string; slug: string; description: string; coverPhotoId: string | null; isPublished: boolean; photoIds: string[]; }; type ApiResponse = { ok: boolean; data: T; error?: string; }; const emptyForm: CollectionForm = { id: null, title: "", slug: "", description: "", coverPhotoId: null, isPublished: false, photoIds: [], }; function getPhotoUrl(photo: CollectionPhoto) { const urls = photo.urls as Record | undefined; return photo.oss_url ?? urls?.small ?? urls?.thumb ?? urls?.regular ?? ""; } function getPhotoLabel(photo: CollectionPhoto) { return ( photo.alt_description || photo.description || photo.unsplash_id || "Picked photo" ); } function getFormFromCollection(collection: PhotoCollection): CollectionForm { return { id: collection.id, title: collection.title, slug: collection.slug, description: collection.description ?? "", coverPhotoId: collection.cover_photo_id, isPublished: collection.is_published, photoIds: collection.photos.map((photo) => photo.id), }; } export function CollectionsManager({ initialCollections, initialPickedPhotos, }: { initialCollections: PhotoCollection[]; initialPickedPhotos: CollectionPhoto[]; }) { const [collections, setCollections] = useState(initialCollections); const [pickedPhotos, setPickedPhotos] = useState(initialPickedPhotos); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [refreshing, setRefreshing] = useState(false); const [deletingId, setDeletingId] = useState(null); const [error, setError] = useState(null); const selectedPhotoIds = useMemo( () => new Set(form.photoIds), [form.photoIds], ); const selectedPhotos = useMemo( () => form.photoIds .map((photoId) => pickedPhotos.find((photo) => photo.id === photoId), ) .filter((photo): photo is CollectionPhoto => Boolean(photo)), [form.photoIds, pickedPhotos], ); const updateForm = (patch: Partial) => { setForm((current) => ({ ...current, ...patch })); }; const startNew = () => { setError(null); setForm(emptyForm); }; const togglePhoto = (photoId: string) => { setForm((current) => { const exists = current.photoIds.includes(photoId); const photoIds = exists ? current.photoIds.filter((id) => id !== photoId) : [...current.photoIds, photoId]; const coverPhotoId = current.coverPhotoId && photoIds.includes(current.coverPhotoId) ? current.coverPhotoId : (photoIds[0] ?? null); return { ...current, photoIds, coverPhotoId, }; }); }; const saveCollection = async () => { setSaving(true); setError(null); try { const res = await fetch( form.id ? `/api/admin/photo-collections/${form.id}` : "/api/admin/photo-collections", { method: form.id ? "PATCH" : "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ title: form.title, slug: form.slug || null, description: form.description || null, coverPhotoId: form.coverPhotoId, isPublished: form.isPublished, photoIds: form.photoIds, }), }, ); const json = (await res.json()) as ApiResponse; if (!json.ok) { throw new Error(json.error || "Failed to save collection."); } setCollections((current) => { const exists = current.some( (collection) => collection.id === json.data.id, ); if (exists) { return current.map((collection) => collection.id === json.data.id ? json.data : collection, ); } return [json.data, ...current]; }); setForm(getFormFromCollection(json.data)); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error."); } finally { setSaving(false); } }; const deleteCollection = async (collection: PhotoCollection) => { if (!confirm(`Delete "${collection.title}"?`)) { return; } setDeletingId(collection.id); setError(null); try { const res = await fetch( `/api/admin/photo-collections/${collection.id}`, { method: "DELETE", }, ); const json = (await res.json()) as ApiResponse; if (!json.ok) { throw new Error(json.error || "Failed to delete collection."); } setCollections((current) => current.filter((item) => item.id !== collection.id), ); if (form.id === collection.id) { setForm(emptyForm); } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error."); } finally { setDeletingId(null); } }; const refreshPickedPhotos = async () => { setRefreshing(true); setError(null); try { const res = await fetch( "/api/admin/gallery-photos?picked=true&per_page=100", ); const json = (await res.json()) as ApiResponse; if (!json.ok) { throw new Error(json.error || "Failed to refresh photos."); } setPickedPhotos(json.data); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error."); } finally { setRefreshing(false); } }; return (

Collections

Title Photos Status Actions {collections.map((collection) => (
{collection.cover_photo ? ( // eslint-disable-next-line @next/next/no-img-element {collection.title} ) : ( )}
{collection.title}
{collection.slug}
{collection.photos.length} {collection.is_published ? "Published" : "Draft"}
))} {collections.length === 0 && ( No collections yet. )}