119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
"use client";
|
|
import { useState, useTransition } from "react";
|
|
import Link from "next/link";
|
|
import { FolderHeart, X, Plus } from "lucide-react";
|
|
import { addImageToCollection, removeImageFromCollection, createCollection } from "@/app/actions/collections";
|
|
|
|
interface PickedCollection { id: number; name: string; slug?: string }
|
|
|
|
export function CollectionPicker({
|
|
imageId,
|
|
current,
|
|
available,
|
|
}: {
|
|
imageId: number;
|
|
current: PickedCollection[];
|
|
available: PickedCollection[];
|
|
}) {
|
|
const [picked, setPicked] = useState(current);
|
|
const [open, setOpen] = useState(false);
|
|
const [draft, setDraft] = useState("");
|
|
const [pool, setPool] = useState(available);
|
|
const [, start] = useTransition();
|
|
|
|
const add = (c: PickedCollection) => {
|
|
if (picked.some(p => p.id === c.id)) return;
|
|
setPicked((cur) => [...cur, c]);
|
|
start(async () => { await addImageToCollection(c.id, imageId); });
|
|
};
|
|
|
|
const remove = (id: number) => {
|
|
setPicked((cur) => cur.filter(c => c.id !== id));
|
|
start(async () => { await removeImageFromCollection(id, imageId); });
|
|
};
|
|
|
|
const createNew = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const name = draft.trim();
|
|
if (!name) return;
|
|
setDraft("");
|
|
start(async () => {
|
|
const created = await createCollection(name);
|
|
if (created) {
|
|
setPool((cur) => [...cur, { id: created.id, name, slug: created.slug }]);
|
|
setPicked((cur) => [...cur, { id: created.id, name, slug: created.slug }]);
|
|
await addImageToCollection(created.id, imageId);
|
|
}
|
|
});
|
|
};
|
|
|
|
const remaining = pool.filter(p => !picked.some(pp => pp.id === p.id));
|
|
|
|
return (
|
|
<div>
|
|
<div className="text-[10px] uppercase tracking-wider font-mono text-[var(--color-fg-muted)] mb-2">
|
|
Collections
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{picked.map((c) => (
|
|
<span
|
|
key={c.id}
|
|
className="group flex items-center gap-1 px-2 py-1 rounded-full text-xs glass border-[var(--color-cyan)]/30 text-[var(--color-cyan)] bg-[color-mix(in_oklch,var(--color-cyan)_10%,transparent)]"
|
|
>
|
|
<FolderHeart className="w-3 h-3" />
|
|
{c.slug ? (
|
|
<Link
|
|
href={`/collection/${c.slug}`}
|
|
className="hover:underline"
|
|
>
|
|
{c.name}
|
|
</Link>
|
|
) : (
|
|
c.name
|
|
)}
|
|
<button onClick={() => remove(c.id)} className="opacity-50 hover:opacity-100">
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
<button
|
|
onClick={() => setOpen((o) => !o)}
|
|
className="flex items-center gap-1 px-2 py-1 rounded-full text-xs border border-dashed border-[var(--color-glass-border)] text-[var(--color-fg-dim)] hover:text-[var(--color-cyan)] hover:border-[var(--color-cyan)]"
|
|
>
|
|
<Plus className="w-3 h-3" />
|
|
Add to collection
|
|
</button>
|
|
</div>
|
|
|
|
{open && (
|
|
<div className="mt-2 glass rounded-xl p-3 space-y-2">
|
|
{remaining.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{remaining.map((c) => (
|
|
<button
|
|
key={c.id}
|
|
onClick={() => add(c)}
|
|
className="text-xs px-2 py-1 rounded-full glass-strong hover:text-[var(--color-cyan)]"
|
|
>
|
|
{c.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
<form onSubmit={createNew} className="flex gap-1.5">
|
|
<input
|
|
value={draft}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
placeholder="New collection…"
|
|
className="flex-1 bg-transparent text-xs px-2 py-1 rounded-md border border-[var(--color-glass-border)] outline-none focus:border-[var(--color-cyan)]"
|
|
/>
|
|
<button type="submit" className="text-xs px-2 py-1 rounded-md bg-[var(--color-cyan)] text-black font-medium">
|
|
Create
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|