import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router"; import { API_URL } from "../config/api.ts"; import type { Dump } from "../model.ts"; import { deserializeDump } from "../model.ts"; import { useAuth } from "../hooks/useAuth.ts"; import { useWS } from "../hooks/useWS.ts"; import { useDumpListSync } from "../hooks/useDumpListSync.ts"; import { useFading } from "../hooks/useFading.ts"; import { useUserDumpFeed } from "../hooks/useUserDumpFeed.ts"; import { DumpCard } from "../components/DumpCard.tsx"; import { ProfileSubpageHeader } from "../components/ProfileSubpageHeader.tsx"; import { PageShell } from "../components/PageShell.tsx"; import { PageError } from "../components/PageError.tsx"; export function UserUpvoted() { const { username } = useParams(); const { user: me } = useAuth(); const { voteCounts, myVotes, lastVoteEvent, castVote, removeVote } = useWS(); const [votedIds, setVotedIds] = useState>(new Set()); const { fading, startFading, cancelFading, cancelAll } = useFading(); const prevMyVotesRef = useRef | null>(null); const onItemsAppended = useCallback((newItems: Dump[]) => { setVotedIds((prev) => new Set([...prev, ...newItems.map((d) => d.id)])); }, []); const { state, setState, setItems, sentinelRef } = useUserDumpFeed( username, "votes", `feed:user-upvoted-full:${username ?? ""}`, { onItemsAppended }, ); useDumpListSync(setItems); const profileUserId = state.status === "loaded" ? state.profileUser.id : null; // Reset vote tracking when username changes useEffect(() => { cancelAll(); setVotedIds(new Set()); prevMyVotesRef.current = null; }, [username]); // Seed votedIds once items are loaded useEffect(() => { if (state.status !== "loaded") return; setVotedIds(new Set(state.items.map((d) => d.id))); }, [state.status]); // Own profile: keep votedIds in sync with myVotes useEffect(() => { if (!profileUserId || me?.id !== profileUserId) return; if (prevMyVotesRef.current === null) { setVotedIds(new Set(myVotes)); prevMyVotesRef.current = new Set(myVotes); return; } const prev = prevMyVotesRef.current; setVotedIds(new Set(myVotes)); for (const id of prev) if (!myVotes.has(id)) startFading(id); for (const id of myVotes) if (!prev.has(id)) cancelFading(id); prevMyVotesRef.current = new Set(myVotes); }, [myVotes, me, profileUserId, startFading, cancelFading]); // WS vote events useEffect(() => { if (!lastVoteEvent || !profileUserId) return; const { dumpId, voterId, action } = lastVoteEvent; if (voterId !== profileUserId) return; if (action === "remove") { setVotedIds((prev) => { const n = new Set(prev); n.delete(dumpId); return n; }); startFading(dumpId); } else { setVotedIds((prev) => new Set([...prev, dumpId])); cancelFading(dumpId); fetch(`${API_URL}/api/dumps/${dumpId}`) .then((r) => r.json()) .then((body) => { if (!body.success) return; const dump = deserializeDump(body.data); setState((s) => { if (s.status !== "loaded" || s.items.some((d) => d.id === dumpId)) { return s; } return { ...s, items: [dump, ...s.items] }; }); }) .catch(() => {}); } }, [lastVoteEvent, profileUserId, startFading, cancelFading]); if (state.status === "loading") { return (

Loading…

); } if (state.status === "error") { return ( ← Back to profile } /> ); } const { profileUser, items: votes, hasMore, loadingMore } = state; const visibleDumps = votes.filter((d) => votedIds.has(d.id) || d.id in fading ); return ( {visibleDumps.length === 0 ?

Nothing here yet.

: (
    {visibleDumps.map((dump) => { const phase = fading[dump.id]; const extraCls = phase === "cooldown" ? "dump-card--fading" : phase === "dismissing" ? "dump-card--dismissing" : undefined; return ( ); })}
)}
{loadingMore &&

Loading more…

} {!hasMore && visibleDumps.length > 0 && (

All {votes.length} upvoted dumps loaded.

)} ); }