"use client"; import { useEffect, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Eye, EyeOff, Gem, Star, Package } from "lucide-react"; import { setWatched, setCoverVip, setCoverFavorite, setCoverOwned } from "@/app/actions/coverMeta"; import { cn } from "@/lib/utils"; type Kind = "watched" | "vip" | "favorite" | "owned"; const CONFIG: Record; OffIcon: React.ComponentType<{ className?: string }>; onClass: string; offClass: string; action: (id: number, on: boolean) => Promise; }> = { watched: { onLabel: "Watched", offLabel: "Not Watched", OnIcon: Eye, OffIcon: EyeOff, onClass: "bg-[var(--color-mint)]/10 border-[var(--color-mint)]/30 text-[var(--color-mint)] hover:bg-[var(--color-mint)]/20", offClass: "border-[var(--color-glass-border)] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:border-[var(--color-glass-border-strong)]", action: setWatched, }, vip: { onLabel: "VIP", offLabel: "VIP", OnIcon: Gem, OffIcon: Gem, onClass: "bg-cyan-400/15 border-cyan-400/40 text-cyan-200 hover:bg-cyan-400/25", offClass: "border-[var(--color-glass-border)] text-[var(--color-fg-muted)] hover:text-cyan-200 hover:border-cyan-400/40", action: setCoverVip, }, favorite: { onLabel: "Favorite", offLabel: "Favorite", OnIcon: Star, OffIcon: Star, onClass: "bg-amber-400/15 border-amber-400/40 text-amber-200 hover:bg-amber-400/25", offClass: "border-[var(--color-glass-border)] text-[var(--color-fg-muted)] hover:text-amber-200 hover:border-amber-400/40", action: setCoverFavorite, }, owned: { onLabel: "Owned", offLabel: "Owned", OnIcon: Package, OffIcon: Package, onClass: "bg-[var(--color-violet)]/15 border-[var(--color-violet)]/40 text-[var(--color-violet)] hover:bg-[var(--color-violet)]/25", offClass: "border-[var(--color-glass-border)] text-[var(--color-fg-muted)] hover:text-[var(--color-violet)] hover:border-[var(--color-violet)]/40", action: setCoverOwned, }, }; // Custom event that lets siblings predict the server-side mutual // exclusion between VIP and Favorite (one being set on clears the // other). Without this, the just-cleared pill would show as "on" // optimistically until router.refresh() round-trips the new initial // prop. The event lets the affected sibling clear its local state // immediately on the same render tick. const MUTEX_EVENT = "pinkudex:cover-flag-mutex"; interface MutexDetail { imageId: number; clearedKind: Kind } export function CoverFlagToggle({ kind, imageId, initial, }: { kind: Kind; imageId: number; initial: boolean; }) { const router = useRouter(); const cfg = CONFIG[kind]; const [on, setLocal] = useState(initial); const [, start] = useTransition(); // Sync to fresh server state — needed so VIP and Favorite stay mutually exclusive // when the other one is toggled and the page refreshes. useEffect(() => { setLocal(initial); }, [initial]); // Listen for sibling toggles that would mutex-clear our flag. useEffect(() => { if (kind !== "vip" && kind !== "favorite") return; const handler = (ev: Event) => { const d = (ev as CustomEvent).detail; if (d && d.imageId === imageId && d.clearedKind === kind) { setLocal(false); } }; window.addEventListener(MUTEX_EVENT, handler); return () => window.removeEventListener(MUTEX_EVENT, handler); }, [imageId, kind]); const Icon = on ? cfg.OnIcon : cfg.OffIcon; function toggle() { const next = !on; setLocal(next); // Server clears the opposite flag when VIP/Favorite is turned on. // Tell our sibling instance now so its UI doesn't lag the action. if (next && (kind === "vip" || kind === "favorite")) { const cleared: Kind = kind === "vip" ? "favorite" : "vip"; window.dispatchEvent(new CustomEvent(MUTEX_EVENT, { detail: { imageId, clearedKind: cleared }, })); } start(async () => { await cfg.action(imageId, next); router.refresh(); }); } return ( ); }