Code-Review: tote Stellen entfernt, Umwege vereinfacht
- SQLite-Import, migrate.sh, notes/migrations.md und altes jQuery-Skript entfernt - Backend: scanEntry/queryEntries statt doppelter Scan-Liste, Feed-Queries direkt statt feedWhere, Thread ohne Map-Umweg, gemeinsame Helfer für Cookie-Löschen und Mediennamen, usernameTaken - Frontend: PostForm für Beitrag und Antwort, EntryCard in Deleted/LiveCard aufgeteilt, tote CSS-Regeln und Typen entfernt, Strich-Regeln zusammengelegt - Rechtstexte verlinken /stats; Kommentare ohne Vorgeschichte 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
d1bd0f5f1d
commit
df0cfec132
@@ -10,9 +10,8 @@ function plural(n: number, one: string, many: string) {
|
||||
return `${n} ${n === 1 ? one : many}`;
|
||||
}
|
||||
|
||||
// Layout ist der gemeinsame Seitenrahmen. Markup und IDs entsprechen dem alten
|
||||
// Frontend (web/*.html), damit app.css unverändert greift: die Startseite hat
|
||||
// Tagline und Headerbar, alle anderen Seiten den "zurück zum Feed"-Knopf.
|
||||
// Layout ist der gemeinsame Seitenrahmen: die Startseite hat Tagline und
|
||||
// Headerbar, alle anderen Seiten den "zurück zum Feed"-Knopf.
|
||||
export function Layout() {
|
||||
const isHome = useLocation().pathname === "/";
|
||||
const days = Math.floor((Date.now() - dataLossStart) / 86400000);
|
||||
|
||||
+14
-45
@@ -87,23 +87,6 @@ h2 {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Dezente Reihe anarchistischer Sigillen unter der Tagline. Einheitliche Höhe,
|
||||
gedämpft per opacity (die SVGs sind teils Strich-, teils Flächenzeichnungen,
|
||||
eine gemeinsame Farbe gäbe es nicht -> Transparenz vereinheitlicht den Look).
|
||||
aria-hidden im Markup: reine Dekoration, kein Vorlesen. */
|
||||
#sigils {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
|
||||
#sigils img {
|
||||
height: 50px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#headerbar {
|
||||
background-color: white;
|
||||
text-align: center;
|
||||
@@ -258,9 +241,13 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
Die Strich-Spalte ist halb so breit wie der Avatar, ihr rechter Rand (= der
|
||||
Strich) liegt damit unter der Mitte des Byline-Avatars. Der Abstand zum
|
||||
Inhalt ist ebenfalls die halbe Avatarbreite -> Inhalt beginnt unter dem
|
||||
rechten Avatarrand. Der .body füllt per flex:1 den Rest. */
|
||||
rechten Avatarrand. Der .body füllt per flex:1 den Rest.
|
||||
Dasselbe gilt für die Diskussionskette in Feed und Antworten (Discussion:
|
||||
Beitrag, ggf. "k weitere Beiträge", jüngste Antwort) an allen Gliedern
|
||||
außer dem letzten. */
|
||||
#ancestors .threadline,
|
||||
#replies .threadline {
|
||||
#replies .threadline,
|
||||
.chain > :not(:last-child) .threadline {
|
||||
display: block;
|
||||
/* Box um die Linienbreite schmaler -> die rechts gezeichnete border-right
|
||||
landet genau auf der Avatarmitte statt rechts daneben. */
|
||||
@@ -268,16 +255,10 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
margin-right: calc(var(--avatar-sm) / 2);
|
||||
}
|
||||
|
||||
/* Nur die Vorfahren bilden einen Thread -> Linie. Antworten nur eingerückt. */
|
||||
#ancestors .threadline { border-right: var(--line-w) solid gray; }
|
||||
|
||||
/* Diskussionskette in Feed und Antworten (Discussion): Beitrag, ggf.
|
||||
"k weitere Beiträge", jüngste Antwort. Gleiches Bild wie die Ahnenkette --
|
||||
Strich an allen Gliedern außer dem letzten. */
|
||||
/* Strich nur, wo eine Kette ist: Vorfahren und Diskussionsketten. Direkte
|
||||
Antworten ohne Kette sind nur eingerückt. */
|
||||
#ancestors .threadline,
|
||||
.chain > :not(:last-child) .threadline {
|
||||
display: block;
|
||||
flex: 0 0 calc(var(--avatar-sm) / 2 - var(--line-w));
|
||||
margin-right: calc(var(--avatar-sm) / 2);
|
||||
border-right: var(--line-w) solid gray;
|
||||
}
|
||||
|
||||
@@ -299,7 +280,7 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
|
||||
/* Interaktionsleiste: Antworten-Button links, Vote-Buttons mittig, Repost
|
||||
rechts. Antwort- und Repost-Zahl stehen in den Buttons; der Repost-Cooldown
|
||||
wandert in die Byline (siehe setupBump). */
|
||||
wandert in die Byline (siehe useBump). */
|
||||
.interactions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -308,8 +289,7 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Bump-Button: tritt an die Stelle der früheren Bump-Kommentare. Im Cooldown
|
||||
gesperrt (gilt pro Beitrag für alle), der Button zeigt dann die Restzeit. */
|
||||
/* Repost-Button: im Cooldown gesperrt (gilt pro Beitrag für alle). */
|
||||
.bump:disabled {
|
||||
color: gray;
|
||||
border-color: #ccc;
|
||||
@@ -328,12 +308,6 @@ img.avatar-lg { width: 72px; height: 72px; }
|
||||
.ghost.selected { font-weight: bold; }
|
||||
.msg { color: red; }
|
||||
|
||||
p.warning {
|
||||
background-color: #faa;
|
||||
padding: 10px 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* --- Formulare --- */
|
||||
form {
|
||||
background-color: white;
|
||||
@@ -360,11 +334,6 @@ form input[type="file"] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* --- Statistik-Seite --- */
|
||||
.statgrid {
|
||||
display: flex;
|
||||
@@ -381,8 +350,8 @@ hr {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Horizontale Balken (Aufrufe/Tag, beliebteste Seiten). Breite wird per JS
|
||||
gesetzt (CSSOM, kein Inline-style -> CSP-konform). */
|
||||
/* Horizontale Balken (Aufrufe/Tag, Betreiber). Breite setzt React per CSSOM
|
||||
(kein Inline-style im HTML -> CSP-konform). */
|
||||
.bars {
|
||||
padding: 0.3rem 0.5rem 0.6rem;
|
||||
}
|
||||
@@ -414,7 +383,7 @@ hr {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ASN-Namen sind länger als Datum/Pfad -> mehr Platz, Umbruch statt Ellipse. */
|
||||
/* ASN-Namen sind länger als ein Datum -> mehr Platz, Umbruch statt Ellipse. */
|
||||
#stat-asns .bar-label {
|
||||
flex-basis: 13rem;
|
||||
white-space: normal;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiError, api } from "./api";
|
||||
import type { Me } from "./types";
|
||||
|
||||
export const meKey = ["me"];
|
||||
const meKey = ["me"];
|
||||
|
||||
// useMe liefert das eigene Konto oder null, wenn niemand angemeldet ist. 401
|
||||
// ist hier kein Fehler, sondern die Antwort "nicht angemeldet".
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useState, type FormEvent, type MouseEvent } from "react";
|
||||
import { api, errMsg } from "../api";
|
||||
import { useAuthChanged } from "../auth";
|
||||
|
||||
// AuthBox: Login und Registrierung im Wechsel. Die {" "} bilden die
|
||||
// Leerzeichen des alten Markups nach -- Überschrift, Inputs und Knöpfe sind
|
||||
// inline, der Abstand zwischen ihnen ist also sichtbar.
|
||||
// AuthBox: Login und Registrierung im Wechsel. Die {" "} sind Absicht:
|
||||
// Überschrift, Inputs und Knöpfe sind inline, der Abstand ist sichtbar.
|
||||
export function AuthBox() {
|
||||
const [mode, setMode] = useState<"login" | "register">("login");
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
@@ -8,7 +8,7 @@ const nowSec = () => Math.floor(Date.now() / 1000);
|
||||
|
||||
// cooldownText ist der Hinweis in der Byline: Restzeit auf Stunden
|
||||
// aufgerundet (mindestens 1 -> nie "0h", solange Restzeit übrig ist).
|
||||
export function cooldownText(retryAfter: number): string {
|
||||
function cooldownText(retryAfter: number): string {
|
||||
return ` · Repost in ${Math.max(1, Math.ceil(retryAfter / 3600))}h`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, errMsg } from "../api";
|
||||
import { pasteImage } from "../paste";
|
||||
import type { CreatedEntry } from "../types";
|
||||
|
||||
// Compose: neuer Beitrag. Nach Erfolg springt der Feed zurück auf Seite 0,
|
||||
// damit der eigene Beitrag oben steht.
|
||||
export function Compose() {
|
||||
const qc = useQueryClient();
|
||||
const [pasteMsg, setPasteMsg] = useState("");
|
||||
const create = useMutation({
|
||||
mutationFn: (fd: FormData) => api.post<CreatedEntry>("/entry/create", fd),
|
||||
});
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
create.mutate(new FormData(form), {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setPasteMsg("");
|
||||
qc.resetQueries({ queryKey: ["feed"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="compose">
|
||||
<form
|
||||
id="entry-form"
|
||||
onSubmit={onSubmit}
|
||||
onPaste={(e) => {
|
||||
if (pasteImage(e)) setPasteMsg("Bild aus Zwischenablage übernommen.");
|
||||
}}
|
||||
>
|
||||
<textarea name="content" rows={4} maxLength={1000} placeholder="Was gibt's Neues?"></textarea>{" "}
|
||||
<p className="muted" id="paste-msg">{pasteMsg}</p>{" "}
|
||||
<input name="file" type="file" accept="image/*" />{" "}
|
||||
<button type="submit" disabled={create.isPending}>Eintrag erstellen</button>
|
||||
</form>
|
||||
<p className="msg" id="entry-msg">{create.isError ? errMsg(create.error, "Beitrag fehlgeschlagen.") : ""}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -14,28 +14,28 @@ function useOpenOnClick(pid: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export function EntryCard({ entry: e }: { entry: Entry }) {
|
||||
const open = useOpenOnClick(e.pid);
|
||||
const when = formatTime(e.created_at);
|
||||
|
||||
// Soft-gelöschter Beitrag: nur Platzhalter, damit der Thread navigierbar bleibt.
|
||||
if (e.deleted) {
|
||||
return (
|
||||
<article className="card" onClick={open}>
|
||||
<div className="muted byline"><span>[deleted] · {when}</span></div>
|
||||
<div className="threadline-wrapper">
|
||||
<div className="threadline"></div>
|
||||
<div className="body">
|
||||
<div className="content">[deleted]</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
return <LiveCard entry={e} when={when} open={open} />;
|
||||
export function EntryCard({ entry }: { entry: Entry }) {
|
||||
return entry.deleted ? <DeletedCard entry={entry} /> : <LiveCard entry={entry} />;
|
||||
}
|
||||
|
||||
function LiveCard({ entry: e, when, open }: { entry: Entry; when: string; open: (ev: MouseEvent) => void }) {
|
||||
// Soft-gelöschter Beitrag: nur Platzhalter, damit der Thread navigierbar bleibt.
|
||||
function DeletedCard({ entry: e }: { entry: Entry }) {
|
||||
const open = useOpenOnClick(e.pid);
|
||||
return (
|
||||
<article className="card" onClick={open}>
|
||||
<div className="muted byline"><span>[deleted] · {formatTime(e.created_at)}</span></div>
|
||||
<div className="threadline-wrapper">
|
||||
<div className="threadline"></div>
|
||||
<div className="body">
|
||||
<div className="content">[deleted]</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function LiveCard({ entry: e }: { entry: Entry }) {
|
||||
const open = useOpenOnClick(e.pid);
|
||||
const bump = useBump(e);
|
||||
const profile = `/u/${encodeURIComponent(e.username)}`;
|
||||
|
||||
@@ -46,7 +46,7 @@ function LiveCard({ entry: e, when, open }: { entry: Entry; when: string; open:
|
||||
<img className="avatar avatar-sm" src={avatarUrl(e.avatar)} alt="" loading="lazy" />
|
||||
</Link>
|
||||
<span>
|
||||
<Link to={profile}>{e.username}</Link> · {when}
|
||||
<Link to={profile}>{e.username}</Link> · {formatTime(e.created_at)}
|
||||
<span className="cooldown">{bump.cooldown}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ const maxPage = 100;
|
||||
|
||||
// Feed lädt Seiten per Infinite-Scroll: queryKey identifiziert den Feed im
|
||||
// Cache (zum Invalidieren nach eigenen Beiträgen), urlFor(page) baut den
|
||||
// API-Pfad der Seite. as ist das Container-Element wie im alten Markup:
|
||||
// <section> auf der Startseite, <div> innerhalb des Profils.
|
||||
// API-Pfad der Seite. as ist das Container-Element: <section> auf der
|
||||
// Startseite, <div> innerhalb des Profils.
|
||||
export function Feed({ queryKey, urlFor, as: Container = "section" }: {
|
||||
queryKey: unknown[];
|
||||
urlFor: (page: number) => string;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { api, errMsg } from "../api";
|
||||
import { pasteImage } from "../paste";
|
||||
|
||||
// PostForm verfasst einen Beitrag, mit replyTo eine Antwort darauf.
|
||||
export function PostForm({ replyTo, rows, placeholder, submitLabel, fallback, onDone }: {
|
||||
replyTo?: string;
|
||||
rows: number;
|
||||
placeholder: string;
|
||||
submitLabel: string;
|
||||
fallback: string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const [pasteMsg, setPasteMsg] = useState("");
|
||||
const create = useMutation({
|
||||
mutationFn: (fd: FormData) => api.post("/entry/create", fd),
|
||||
});
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const fd = new FormData(form);
|
||||
if (replyTo) fd.append("reply_to", replyTo);
|
||||
create.mutate(fd, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setPasteMsg("");
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
onPaste={(e) => {
|
||||
if (pasteImage(e)) setPasteMsg("Bild aus Zwischenablage übernommen.");
|
||||
}}
|
||||
>
|
||||
<textarea name="content" rows={rows} maxLength={1000} placeholder={placeholder}></textarea>{" "}
|
||||
<p className="muted">{pasteMsg}</p>{" "}
|
||||
<input name="file" type="file" accept="image/*" />{" "}
|
||||
<button type="submit" disabled={create.isPending}>{submitLabel}</button>
|
||||
</form>
|
||||
<p className="msg">{create.isError ? errMsg(create.error, fallback) : ""}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,9 +11,8 @@ export function formatTime(unix: number): string {
|
||||
|
||||
const urlPattern = /https?:\/\/[^\s<]+/g;
|
||||
|
||||
// linkify macht aus URLs im Text Links und aus Zeilenumbrüchen <br>. React
|
||||
// escaped den Text selbst -- anders als im alten Frontend gibt es hier kein
|
||||
// innerHTML und damit keinen Escaping-Schritt, der vergessen werden könnte.
|
||||
// linkify macht aus URLs im Text Links und aus Zeilenumbrüchen <br>. Den Text
|
||||
// escaped React selbst.
|
||||
export function linkify(text: string): ReactNode[] {
|
||||
const out: ReactNode[] = [];
|
||||
text.split("\n").forEach((line, i) => {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useParams } from "react-router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, errMsg } from "../api";
|
||||
import { useMe } from "../auth";
|
||||
import { pasteImage } from "../paste";
|
||||
import { Discussion, EntryCard } from "../components/EntryCard";
|
||||
import type { CreatedEntry, Entry, Thread } from "../types";
|
||||
import { PostForm } from "../components/PostForm";
|
||||
import type { Entry, Thread } from "../types";
|
||||
|
||||
// Beim Wechsel zu einem anderen Beitrag frisch anfangen (offene Bearbeitung,
|
||||
// Meldungen) -- deshalb hängt die Seite per key an der pid.
|
||||
@@ -51,7 +51,14 @@ function ThreadView({ pid }: { pid: string }) {
|
||||
</div>
|
||||
|
||||
<section id="reply-box" hidden={!me}>
|
||||
<ReplyForm pid={pid} onDone={refresh} />
|
||||
<PostForm
|
||||
replyTo={pid}
|
||||
rows={3}
|
||||
placeholder="Antworten…"
|
||||
submitLabel="Antworten"
|
||||
fallback="Antwort fehlgeschlagen."
|
||||
onDone={refresh}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section id="replies">
|
||||
@@ -63,8 +70,6 @@ function ThreadView({ pid }: { pid: string }) {
|
||||
}
|
||||
|
||||
// OwnerActions: Bearbeiten und Löschen für den eigenen, nicht gelöschten Beitrag.
|
||||
// Die {" "} bilden die Leerzeichen des alten Markups nach -- Inputs und Knöpfe
|
||||
// sind inline, der Abstand zwischen ihnen ist also sichtbar.
|
||||
function OwnerActions({ entry, onChange }: { entry: Entry; onChange: () => void }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
@@ -111,43 +116,3 @@ function OwnerActions({ entry, onChange }: { entry: Entry; onChange: () => void
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReplyForm({ pid, onDone }: { pid: string; onDone: () => void }) {
|
||||
const [pasteMsg, setPasteMsg] = useState("");
|
||||
|
||||
const reply = useMutation({
|
||||
mutationFn: (fd: FormData) => api.post<CreatedEntry>("/entry/create", fd),
|
||||
});
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const fd = new FormData(form);
|
||||
fd.append("reply_to", pid);
|
||||
reply.mutate(fd, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setPasteMsg("");
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
id="reply-form"
|
||||
onSubmit={onSubmit}
|
||||
onPaste={(e) => {
|
||||
if (pasteImage(e)) setPasteMsg("Bild aus Zwischenablage übernommen.");
|
||||
}}
|
||||
>
|
||||
<textarea name="content" rows={3} maxLength={1000} placeholder="Antworten…"></textarea>{" "}
|
||||
<p className="muted" id="reply-paste-msg">{pasteMsg}</p>{" "}
|
||||
<input name="file" type="file" accept="image/*" />{" "}
|
||||
<button type="submit" disabled={reply.isPending}>Antworten</button>
|
||||
</form>
|
||||
<p className="msg" id="reply-msg">{reply.isError ? errMsg(reply.error, "Antwort fehlgeschlagen.") : ""}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMe } from "../auth";
|
||||
import { AuthBox } from "../components/AuthBox";
|
||||
import { Compose } from "../components/Compose";
|
||||
import { Feed } from "../components/Feed";
|
||||
import { PostForm } from "../components/PostForm";
|
||||
|
||||
// Solange der Anmeldestatus noch lädt, weder Login noch Formular zeigen --
|
||||
// sonst blitzt bei Angemeldeten kurz die Login-Box auf.
|
||||
@@ -14,3 +15,20 @@ export function FeedPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Nach einem neuen Beitrag springt der Feed zurück auf Seite 0, damit er oben
|
||||
// steht.
|
||||
function Compose() {
|
||||
const qc = useQueryClient();
|
||||
return (
|
||||
<section id="compose">
|
||||
<PostForm
|
||||
rows={4}
|
||||
placeholder="Was gibt's Neues?"
|
||||
submitLabel="Eintrag erstellen"
|
||||
fallback="Beitrag fehlgeschlagen."
|
||||
onDone={() => qc.resetQueries({ queryKey: ["feed"] })}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ function ProfileView({ username }: { username: string }) {
|
||||
<img
|
||||
id="profile-avatar"
|
||||
className="avatar avatar-lg"
|
||||
src={info ? avatarUrl(info.avatar) + avatarBust : "/static/assets/no_profile_pic.jpg"}
|
||||
src={avatarUrl(info?.avatar ?? "") + avatarBust}
|
||||
alt=""
|
||||
/>
|
||||
<div>
|
||||
@@ -74,8 +74,7 @@ function useFormAction<T>(path: string, fallback: string, onSuccess: (form: HTML
|
||||
return { onSubmit, busy: m.isPending, msg: m.isError ? errMsg(m.error, fallback) : "" };
|
||||
}
|
||||
|
||||
// Settings: nur für den Eigentümer. Die {" "} bilden die Leerzeichen des alten
|
||||
// Markups nach -- Inputs und Knöpfe sind inline.
|
||||
// Settings: nur für den Eigentümer.
|
||||
function Settings({ onAvatar }: { onAvatar: () => void }) {
|
||||
const qc = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -43,13 +43,6 @@ export interface BumpResult {
|
||||
retry_after: number;
|
||||
}
|
||||
|
||||
export interface CreatedEntry {
|
||||
pid: number;
|
||||
content: string;
|
||||
filepath: string;
|
||||
reply_to: number;
|
||||
}
|
||||
|
||||
// GET /api/user/info -- das eigene Konto.
|
||||
export interface Me {
|
||||
uid: number;
|
||||
|
||||
Reference in New Issue
Block a user