Melden rechts in der Byline jeder Karte, auch anonym
- Melden-Knopf in der Byline aller Beiträge (außer eigenen), Grund-Formular
darunter; ersetzt den Knopf unter dem fokussierten Beitrag
- /api/entry/{pid}/report öffentlich, 10/min pro IP; ohne Session anonym
(uid 0), Doppelmeldungen nur für Angemeldete unterdrückt
- Klicks in Formularen öffnen die Karte nicht mehr
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
80c333cd58
commit
3d2447fe35
@@ -235,6 +235,9 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
auf 1rem liegt). Hier explizit angleichen -> ganze Meta-Zeile gleich groß. */
|
||||
.byline span { font-size: 1rem; }
|
||||
|
||||
/* Melden steht rechts in der Byline. */
|
||||
.byline .report { margin-left: auto; }
|
||||
|
||||
/* Profilkopf: großer Avatar links neben Name und "Mitglied seit". */
|
||||
.profile-head {
|
||||
display: flex;
|
||||
|
||||
@@ -4,13 +4,15 @@ import type { Entry } from "../types";
|
||||
import { avatarUrl, formatTime, linkify } from "../format";
|
||||
import { Votes } from "./Votes";
|
||||
import { useBump } from "./Bump";
|
||||
import { useReport } from "./Report";
|
||||
import { useMe } from "../auth";
|
||||
|
||||
// Klick auf die Karte öffnet den Beitrag -- außer der Klick galt einem Link
|
||||
// oder Knopf darin.
|
||||
// Klick auf die Karte öffnet den Beitrag -- außer der Klick galt einem Link,
|
||||
// Knopf oder Formular darin.
|
||||
function useOpenOnClick(pid: number) {
|
||||
const navigate = useNavigate();
|
||||
return (ev: MouseEvent) => {
|
||||
if (!(ev.target as Element).closest("a, button")) navigate(`/e/${pid}`);
|
||||
if (!(ev.target as Element).closest("a, button, form")) navigate(`/e/${pid}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +39,8 @@ function DeletedCard({ entry: e }: { entry: Entry }) {
|
||||
function LiveCard({ entry: e }: { entry: Entry }) {
|
||||
const open = useOpenOnClick(e.pid);
|
||||
const bump = useBump(e);
|
||||
const report = useReport(e.pid);
|
||||
const own = useMe().data?.uid === e.uid;
|
||||
const profile = `/u/${encodeURIComponent(e.username)}`;
|
||||
|
||||
return (
|
||||
@@ -49,10 +53,12 @@ function LiveCard({ entry: e }: { entry: Entry }) {
|
||||
<Link to={profile}>{e.username}</Link> · {formatTime(e.created_at)}
|
||||
<span className="cooldown">{bump.cooldown}</span>
|
||||
</span>
|
||||
{!own && report.button}
|
||||
</div>
|
||||
<div className="threadline-wrapper">
|
||||
<div className="threadline"></div>
|
||||
<div className="body">
|
||||
{report.form}
|
||||
<div className="content">{linkify(e.content)}</div>
|
||||
{e.filepath && <img src={`/${e.filepath}`} alt="" loading="lazy" />}
|
||||
<div className="interactions">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { api, errMsg } from "../api";
|
||||
|
||||
// useReport hält das Melden eines Beitrags: der Knopf steht rechts in der
|
||||
// Byline, das Formular für den optionalen Grund darunter im Beitrag --
|
||||
// deshalb ein Hook statt einer Komponente (wie useBump). Melden geht auch
|
||||
// ohne Anmeldung, der Melder ist dann anonym.
|
||||
export function useReport(pid: number) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const report = useMutation({
|
||||
mutationFn: (fd: FormData) => api.post(`/entry/${pid}/report`, fd),
|
||||
onSuccess: () => setOpen(false),
|
||||
});
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
report.mutate(new FormData(e.currentTarget));
|
||||
};
|
||||
const cancel = () => {
|
||||
report.reset();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
button: report.isSuccess
|
||||
? <span className="report">gemeldet</span>
|
||||
: <button className="report" type="button" disabled={open} onClick={() => setOpen(true)}>Melden</button>,
|
||||
form: open && (
|
||||
<form onSubmit={onSubmit}>
|
||||
<textarea name="reason" rows={2} maxLength={500} placeholder="Grund (optional)"></textarea>{" "}
|
||||
<button type="submit" disabled={report.isPending}>Melden</button>{" "}
|
||||
<button type="button" onClick={cancel}>Abbrechen</button>{" "}
|
||||
<p className="msg">{report.isError ? errMsg(report.error, "Melden fehlgeschlagen.") : ""}</p>
|
||||
</form>
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -38,8 +38,7 @@ function ThreadView({ pid }: { pid: string }) {
|
||||
);
|
||||
}
|
||||
const data = thread.data;
|
||||
const live = !!me && !!data && !data.entry.deleted;
|
||||
const isOwner = live && me.uid === data.entry.uid;
|
||||
const isOwner = !!me && !!data && me.uid === data.entry.uid && !data.entry.deleted;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -48,9 +47,7 @@ function ThreadView({ pid }: { pid: string }) {
|
||||
</section>
|
||||
<div id="focus">
|
||||
{data && <EntryCard entry={data.entry} />}
|
||||
{live && (isOwner
|
||||
? <OwnerActions entry={data.entry} onChange={refresh} />
|
||||
: <ReportAction pid={data.entry.pid} />)}
|
||||
{data && isOwner && <OwnerActions entry={data.entry} onChange={refresh} />}
|
||||
</div>
|
||||
|
||||
<section id="reply-box" hidden={!me}>
|
||||
@@ -119,35 +116,3 @@ function OwnerActions({ entry, onChange }: { entry: Entry; onChange: () => void
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ReportAction: fremden Beitrag melden, der Grund ist optional.
|
||||
function ReportAction({ pid }: { pid: number }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const report = useMutation({
|
||||
mutationFn: (fd: FormData) => api.post(`/entry/${pid}/report`, fd),
|
||||
});
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
report.mutate(new FormData(e.currentTarget));
|
||||
};
|
||||
const cancel = () => {
|
||||
report.reset();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (report.isSuccess) return <p className="muted">Danke, der Beitrag wurde gemeldet.</p>;
|
||||
return (
|
||||
<>
|
||||
<button disabled={open} onClick={() => setOpen(true)}>Melden</button>
|
||||
{open && (
|
||||
<form onSubmit={onSubmit}>
|
||||
<textarea name="reason" rows={2} maxLength={500} placeholder="Grund (optional)"></textarea>{" "}
|
||||
<button type="submit" disabled={report.isPending}>Melden</button>{" "}
|
||||
<button type="button" onClick={cancel}>Abbrechen</button>{" "}
|
||||
<p className="msg">{report.isError ? errMsg(report.error, "Melden fehlgeschlagen.") : ""}</p>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user