- Feed per useInfiniteQuery mit Sentinel-Nachladen, Duplikate gefiltert, Stopp an der Backend-Seitengrenze - EntryCard mit unverändertem Markup (inkl. [deleted]-Platzhalter) - Votes mit optimistischem Umschalten, Rollback bei Fehler - Repost mit Cooldown-Anzeige und 429-Auswertung - linkify ohne innerHTML Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { api, errMsg } from "../api";
|
||
import type { VoteMode, VoteTally } from "../types";
|
||
|
||
// toggled rechnet vor, was der Server aus einer Stimme macht (siehe
|
||
// handleVote): gleiche Stimme zieht sie zurück, andere wechselt. Damit
|
||
// schaltet der Knopf sofort um; die Serverantwort überschreibt danach.
|
||
function toggled(t: VoteTally, mode: VoteMode): VoteTally {
|
||
const next = { ...t };
|
||
if (t.selected !== "none") next[t.selected]--;
|
||
if (t.selected === mode) {
|
||
next.selected = "none";
|
||
} else {
|
||
next[mode]++;
|
||
next.selected = mode;
|
||
}
|
||
return next;
|
||
}
|
||
|
||
export function Votes({ pid }: { pid: number }) {
|
||
const qc = useQueryClient();
|
||
const key = ["votes", pid];
|
||
const tally = useQuery({
|
||
queryKey: key,
|
||
queryFn: () => api.get<VoteTally>(`/entry/${pid}/votes`),
|
||
});
|
||
|
||
const vote = useMutation({
|
||
mutationFn: (mode: VoteMode) => {
|
||
const fd = new FormData();
|
||
fd.append("mode", mode);
|
||
return api.post<VoteTally>(`/entry/${pid}/vote`, fd);
|
||
},
|
||
onMutate: async (mode) => {
|
||
await qc.cancelQueries({ queryKey: key });
|
||
const prev = qc.getQueryData<VoteTally>(key);
|
||
if (prev) qc.setQueryData(key, toggled(prev, mode));
|
||
return { prev };
|
||
},
|
||
onError: (_err, _mode, ctx) => {
|
||
if (ctx?.prev) qc.setQueryData(key, ctx.prev);
|
||
},
|
||
onSuccess: (data) => qc.setQueryData(key, data),
|
||
});
|
||
|
||
const t = tally.data;
|
||
let counts = "– / –";
|
||
if (vote.isError) counts = errMsg(vote.error, "Fehler");
|
||
else if (tally.isError) counts = errMsg(tally.error, "Fehler");
|
||
else if (t) counts = `${t.left} / ${t.right}`;
|
||
|
||
const button = (mode: VoteMode, label: string) => (
|
||
<button
|
||
className={t?.selected === mode ? "ghost selected" : "ghost"}
|
||
data-mode={mode}
|
||
onClick={() => vote.mutate(mode)}
|
||
>
|
||
{label}
|
||
</button>
|
||
);
|
||
|
||
return (
|
||
<div className="row">
|
||
{button("left", "Links")}
|
||
<span className="muted">{counts}</span>
|
||
{button("right", "Rechts")}
|
||
</div>
|
||
);
|
||
}
|