import { useState } from 'react'; import { Link, useSearchParams } from 'react-router'; import { Folder, FolderOpen, Pencil, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; import type { CollectionSummary } from '../lib/api'; import { useCollectionsManager } from './useCollectionsManager'; import type { CollectionsManager } from './useCollectionsManager'; /** * The collection list: a filter, a create button, and one row per collection. * * Rows rather than a grid of covers. A grid of thumbnails is a browsing surface and this panel is a * selector — it lives in a narrow column beside the gallery, where two cards per row means four * collections visible and a lot of scrolling to find the one you meant. A row is a name, a count and * its two actions. * * Selecting one is a LINK, writing `?collection=`. The Gallery panel reads that param and shows * the images; nothing here fetches them. That is the whole reason the detail view left this file. */ export const Collections = () => { const manager = useCollectionsManager(); const [searchParams] = useSearchParams(); const selectedId = searchParams.get('collection'); const [creating, setCreating] = useState(false); const [renaming, setRenaming] = useState(null); const [deleting, setDeleting] = useState(null); return (
manager.setQuery(ev.target.value)} />
{manager.error &&

{manager.error}

}
{manager.loading ? (

Loading…

) : manager.collections.length === 0 ? (

{manager.total === 0 ? ( <> No collections yet. Create one, or pass ?collection=name when generating and it will be made for you. ) : ( 'Nothing matches that filter.' )}

) : (
    {manager.collections.map((collection) => ( setRenaming(collection)} onDelete={() => setDeleting(collection)} /> ))}
)}
setCreating(false)} onSubmit={(name) => manager.create(name)} /> setRenaming(null)} onSubmit={(name) => manager.rename(renaming!.id, name)} /> setDeleting(null)} />
); }; type RowProps = { collection: CollectionSummary; selected: boolean; searchParams: URLSearchParams; onRename: () => void; onDelete: () => void; }; const Row = ({ collection, selected, searchParams, onRename, onDelete }: RowProps) => { const to = (() => { const next = new URLSearchParams(searchParams); next.set('collection', collection.id); // The lightbox selection belongs to whatever was being looked at before; carrying it into another // collection opens an image that is not in the list you just switched to. next.delete('selected'); return { search: `?${next.toString()}` }; })(); const Icon = selected ? FolderOpen : Folder; return (
  • {collection.name} {collection.item_count} {/* Visible on hover, and on keyboard focus — `opacity-0` alone would hide a button you can still tab to, which is a control nobody can see but everybody can press. */}
  • ); }; type NameDialogProps = { open: boolean; title: string; description?: string; initial?: string; confirmLabel: string; onClose: () => void; onSubmit: (name: string) => Promise; }; /** Create and rename are the same dialog: one text field, one verb. */ const NameDialog = ({ open, title, description, initial = '', confirmLabel, onClose, onSubmit }: NameDialogProps) => { const [name, setName] = useState(initial); const [busy, setBusy] = useState(false); const submit = async () => { if (!name.trim() || busy) return; setBusy(true); const ok = await onSubmit(name); setBusy(false); if (ok) onClose(); }; return ( { if (next) setName(initial); else onClose(); }} > {title} {description && {description}} {/* A form, so Enter submits. A dialog whose only field ignores Enter reads as broken. */}
    { ev.preventDefault(); void submit(); }} > setName(ev.target.value)} placeholder="Name" />
    ); }; type DeleteDialogProps = { collection: CollectionSummary | null; manager: CollectionsManager; onClose: () => void }; /** * Deleting a collection asks one further question, because there are two different intentions behind * it and the wrong guess is unrecoverable: the folder was a mistake (keep the images), or the work in * it was (take them too). Unchecked by default — the recoverable answer is the one you get by * pressing Enter without reading. */ const DeleteDialog = ({ collection, manager, onClose }: DeleteDialogProps) => { const [alsoImages, setAlsoImages] = useState(false); const [busy, setBusy] = useState(false); const count = collection?.item_count ?? 0; const confirm = async () => { if (!collection || busy) return; setBusy(true); const ok = await manager.remove(collection.id, alsoImages); setBusy(false); if (ok) onClose(); }; return ( { if (next) setAlsoImages(false); else onClose(); }} > Delete “{collection?.name}”? {count === 0 ? 'This collection is empty.' : `It holds ${count} image${count === 1 ? '' : 's'}. They stay in the gallery unless you say otherwise.`} {count > 0 && ( )} ); };