React-Frontend: Feed mit Beitragskarte, Votes und Repost
- 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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
37279ac90d
commit
bd5c59bcad
@@ -0,0 +1,69 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user