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
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { subscribeQueueRemove } from "./watchQueueEvents";
const STORAGE_KEY = "pinkudex.watch-queue";
type Ctx = {
ids: number[];
has: (id: number) => boolean;
add: (id: number) => void;
addMany: (ids: number[]) => void;
remove: (id: number) => void;
removeMany: (ids: number[]) => void;
toggle: (id: number) => void;
clear: () => void;
};
const WatchQueueCtx = createContext<Ctx | null>(null);
function readStorage(): number[] {
if (typeof window === "undefined") return [];
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.filter((n): n is number => typeof n === "number");
} catch {
return [];
}
}
export function WatchQueueProvider({ children }: { children: React.ReactNode }) {
const [ids, setIds] = useState<number[]>([]);
const [hydrated, setHydrated] = useState(false);
// Hydrate from localStorage. SSR renders an empty queue; this fills it
// on mount so the server-rendered HTML always matches initial paint.
useEffect(() => {
setIds(readStorage());
setHydrated(true);
}, []);
// Persist on change (after hydration, so we don't blow away storage with
// the empty-array initial state).
useEffect(() => {
if (!hydrated) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
} catch {}
}, [ids, hydrated]);
// Cross-tab sync.
useEffect(() => {
function onStorage(e: StorageEvent) {
if (e.key !== STORAGE_KEY) return;
setIds(readStorage());
}
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, []);
// External signal — covers fire this after their watched flag flips true.
useEffect(() => subscribeQueueRemove((detail) => {
const drop = new Set(detail);
setIds((cur) => cur.filter((id) => !drop.has(id)));
}), []);
const add = useCallback((id: number) => {
setIds((cur) => (cur.includes(id) ? cur : [...cur, id]));
}, []);
const addMany = useCallback((newIds: number[]) => {
setIds((cur) => {
const have = new Set(cur);
const merged = [...cur];
for (const id of newIds) if (!have.has(id)) { merged.push(id); have.add(id); }
return merged;
});
}, []);
const remove = useCallback((id: number) => {
setIds((cur) => cur.filter((x) => x !== id));
}, []);
const removeMany = useCallback((dropIds: number[]) => {
const drop = new Set(dropIds);
setIds((cur) => cur.filter((id) => !drop.has(id)));
}, []);
const toggle = useCallback((id: number) => {
setIds((cur) => (cur.includes(id) ? cur.filter((x) => x !== id) : [...cur, id]));
}, []);
const clear = useCallback(() => setIds([]), []);
const value = useMemo<Ctx>(() => ({
ids,
has: (id) => ids.includes(id),
add,
addMany,
remove,
removeMany,
toggle,
clear,
}), [ids, add, addMany, remove, removeMany, toggle, clear]);
return <WatchQueueCtx.Provider value={value}>{children}</WatchQueueCtx.Provider>;
}
export function useWatchQueue(): Ctx {
const ctx = useContext(WatchQueueCtx);
if (!ctx) throw new Error("useWatchQueue must be used within WatchQueueProvider");
return ctx;
}