Initial commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"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<Kind, {
|
||||
onLabel: string;
|
||||
offLabel: string;
|
||||
OnIcon: React.ComponentType<{ className?: string }>;
|
||||
OffIcon: React.ComponentType<{ className?: string }>;
|
||||
onClass: string;
|
||||
offClass: string;
|
||||
action: (id: number, on: boolean) => Promise<void>;
|
||||
}> = {
|
||||
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<MutexDetail>).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<MutexDetail>(MUTEX_EVENT, {
|
||||
detail: { imageId, clearedKind: cleared },
|
||||
}));
|
||||
}
|
||||
start(async () => {
|
||||
await cfg.action(imageId, next);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
title={`Toggle ${cfg.onLabel}`}
|
||||
className={cn(
|
||||
"flex w-full min-w-0 items-center justify-center gap-1.5 px-2 py-1.5 rounded-full text-[10px] uppercase tracking-wider font-mono border transition-colors cursor-pointer",
|
||||
on ? cfg.onClass : cfg.offClass,
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("w-3 h-3", kind === "favorite" && on && "fill-amber-200")} />
|
||||
{on ? cfg.onLabel : cfg.offLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user