Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+129
View File
@@ -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>
);
}