From 2e6eb07301e05e2120e1821a484b6944be49d11f Mon Sep 17 00:00:00 2001 From: Anais Saraiva Date: Mon, 21 Sep 2026 17:06:20 +0200 Subject: [PATCH] . --- frontend/src/components/AccessRequests.tsx | 28 +++---- frontend/src/components/ResetPage.tsx | 30 ++++---- frontend/src/components/ReviewPage.tsx | 72 +++++++++--------- frontend/src/components/SharePage.tsx | 18 +++-- frontend/src/components/dialogs.tsx | 85 +++++++++++++++++----- src/db.ts | 23 +++++- src/index.ts | 18 ++++- 7 files changed, 182 insertions(+), 92 deletions(-) diff --git a/frontend/src/components/AccessRequests.tsx b/frontend/src/components/AccessRequests.tsx index 1bf884d..cde44fd 100644 --- a/frontend/src/components/AccessRequests.tsx +++ b/frontend/src/components/AccessRequests.tsx @@ -2,13 +2,14 @@ import { useEffect, useState } from "react"; import { CheckCircle2, Clock, Inbox, Search, SendHorizontal, Undo2, XCircle } from "lucide-react"; import { api, type AccessMessage, type AccessRequest, type AccessStatus } from "../lib/api.ts"; import { Button, CopyButton, Field, Input, Spinner } from "./ui.tsx"; +import { useT, type StringKey } from "../lib/i18n.ts"; import { cn } from "../lib/cn.ts"; -const STATUS_STYLE: Record = { - pending: { label: "Pending", cls: "bg-amber-500/10 text-amber-700 dark:text-amber-400" }, - reviewing: { label: "Reviewing", cls: "bg-sky-500/10 text-sky-700 dark:text-sky-400" }, - accepted: { label: "Accepted", cls: "bg-accent-500/10 text-accent-700 dark:text-accent-400" }, - refused: { label: "Refused", cls: "bg-red-500/10 text-red-600 dark:text-red-400" }, +const STATUS_STYLE: Record = { + pending: { cls: "bg-amber-500/10 text-amber-700 dark:text-amber-400" }, + reviewing: { cls: "bg-sky-500/10 text-sky-700 dark:text-sky-400" }, + accepted: { cls: "bg-accent-500/10 text-accent-700 dark:text-accent-400" }, + refused: { cls: "bg-red-500/10 text-red-600 dark:text-red-400" }, }; type Filter = "all" | AccessStatus; @@ -21,6 +22,7 @@ export function AccessRequestsPanel() { const [selectedId, setSelectedId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + const { t } = useT(); async function refresh(select?: string) { try { @@ -28,7 +30,7 @@ export function AccessRequestsPanel() { setRequests(res.requests); if (select) setSelectedId(select); } catch (err) { - setError(err instanceof Error ? err.message : "Load failed"); + setError(err instanceof Error ? err.message : t("admin.loadFailed")); } finally { setLoading(false); } @@ -57,27 +59,27 @@ export function AccessRequestsPanel() { : "bg-stone-200/70 hover:bg-stone-300/60 dark:bg-zinc-800 dark:hover:bg-zinc-700", )} > - {f} · {n} + {t(`access.filter.${f}` as StringKey)} · {n} ); })} {error &&

{error}

} - {loading &&

Loading…

} + {loading &&

{t("common.loading")}

} {!loading && visible.length === 0 && (

- {pendingCount > 0 ? "Nothing with this status." : "No access requests. The wishlist is empty."} + {pendingCount > 0 ? t("access.emptyFiltered") : t("access.empty")}

)} @@ -98,10 +100,10 @@ export function AccessRequestsPanel() { @{r.username} {r.email} - {r.message} · {r.message_count} message{r.message_count === 1 ? "" : "s"} + {r.message} · {r.message_count === 1 ? t("access.msgOne") : t("access.msgMany", { n: r.message_count })} - {STATUS_STYLE[r.status].label} + {t(`access.status.${r.status}` as StringKey)} {selectedId === r.id && refresh(r.id)} />} diff --git a/frontend/src/components/ResetPage.tsx b/frontend/src/components/ResetPage.tsx index 72c28fc..a341a7c 100644 --- a/frontend/src/components/ResetPage.tsx +++ b/frontend/src/components/ResetPage.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { KeyRound, MailWarning, Sparkles } from "lucide-react"; import { api } from "../lib/api.ts"; import { Button, CopyButton, FlowerMark, Spinner } from "./ui.tsx"; +import { useT } from "../lib/i18n.ts"; type State = | { kind: "loading" } @@ -13,6 +14,7 @@ type State = /** Public one-time key reset page: /reset/:token */ export function ResetPage({ token }: { token: string }) { + const { t } = useT(); const [state, setState] = useState({ kind: "loading" }); useEffect(() => { @@ -27,7 +29,7 @@ export function ResetPage({ token }: { token: string }) { const r = await api.resetConsume(token); setState({ kind: "done", key: r.key }); } catch (err) { - setState({ kind: "error", message: err instanceof Error ? err.message : "Reset failed" }); + setState({ kind: "error", message: err instanceof Error ? err.message : t("reset.failed") }); } } @@ -35,40 +37,40 @@ export function ResetPage({ token }: { token: string }) {
- {state.kind === "loading" &&

Checking link…

} + {state.kind === "loading" &&

{t("reset.checking")}

} {state.kind === "invalid" && (
-

Link invalid

-

This reset link is unknown, already used, or expired. Request a new one from the login page.

- +

{t("reset.invalid")}

+

{t("reset.invalidSub")}

+
)} {state.kind === "ready" && (
-

Reset key for @{state.username}?

-

This issues a fresh access key immediately. Your old key stops working.

+

{t("reset.readyTitle", { user: state.username })}

+

{t("reset.readySub")}

)} - {state.kind === "working" &&

Issuing…

} + {state.kind === "working" &&

{t("reset.issuing")}

} {state.kind === "done" && (
-

New key ready

-

Copy it now — it won't be shown again. Then log in with it.

+

{t("reset.doneTitle")}

+

{t("reset.doneSub")}

{state.key}
- +
)} {state.kind === "error" && (
-

Reset failed

+

{t("reset.failed")}

{state.message}

- +
)}
diff --git a/frontend/src/components/ReviewPage.tsx b/frontend/src/components/ReviewPage.tsx index 518bbf3..c7baa81 100644 --- a/frontend/src/components/ReviewPage.tsx +++ b/frontend/src/components/ReviewPage.tsx @@ -2,21 +2,23 @@ import { useEffect, useState } from "react"; import { CheckCircle2, Clock, MailWarning, MessageSquareText, Search, SendHorizontal, XCircle } from "lucide-react"; import { api, type AccessMessage, type AccessRequest } from "../lib/api.ts"; import { Button, CopyButton, Field, FlowerMark, Input, Modal, Spinner } from "./ui.tsx"; +import { useT, type StringKey } from "../lib/i18n.ts"; import { cn } from "../lib/cn.ts"; -const STATUS_STYLE: Record = { - pending: { label: "Pending review", cls: "bg-amber-500/10 text-amber-700 dark:text-amber-400", icon: Clock }, - reviewing: { label: "Under review", cls: "bg-sky-500/10 text-sky-700 dark:text-sky-400", icon: Search }, - accepted: { label: "Accepted", cls: "bg-accent-500/10 text-accent-700 dark:text-accent-400", icon: CheckCircle2 }, - refused: { label: "Refused", cls: "bg-red-500/10 text-red-600 dark:text-red-400", icon: XCircle }, +const STATUS_STYLE: Record = { + pending: { cls: "bg-amber-500/10 text-amber-700 dark:text-amber-400", icon: Clock }, + reviewing: { cls: "bg-sky-500/10 text-sky-700 dark:text-sky-400", icon: Search }, + accepted: { cls: "bg-accent-500/10 text-accent-700 dark:text-accent-400", icon: CheckCircle2 }, + refused: { cls: "bg-red-500/10 text-red-600 dark:text-red-400", icon: XCircle }, }; function StatusBadge({ status }: { status: AccessRequest["status"] }) { + const { t } = useT(); const s = STATUS_STYLE[status]; return ( - {s.label} + {t(`review.status.${status}` as StringKey)} ); } @@ -28,6 +30,7 @@ export function ReviewPage({ ticketId }: { ticketId: string }) { const [draft, setDraft] = useState(""); const [loading, setLoading] = useState(true); const [invalid, setInvalid] = useState(false); + const { t } = useT(); const [sending, setSending] = useState(false); const [error, setError] = useState(""); @@ -59,7 +62,7 @@ export function ReviewPage({ ticketId }: { ticketId: string }) { setMessages((prev) => [...prev, res.message]); setDraft(""); } catch (err) { - setError(err instanceof Error ? err.message : "Send failed"); + setError(err instanceof Error ? err.message : t("access.sendFailed"); } finally { setSending(false); } @@ -71,15 +74,15 @@ export function ReviewPage({ ticketId }: { ticketId: string }) {
-

Access request

+

{t("review.title")}

- {loading &&

Loading ticket…

} + {loading &&

{t("review.loadingTicket")}

} {invalid && (
-

Ticket not found

-

This link is unknown. Check the URL from your email, or file a new request from the login page.

- +

{t("review.notFound")}

+

{t("review.notFoundSub")}

+
)} @@ -91,12 +94,12 @@ export function ReviewPage({ ticketId }: { ticketId: string }) {
{request.reason && (

- Admin note: {request.reason} + {t("review.adminNote")} {request.reason}

)} {request.status === "accepted" && (

- Your account was created — check your email for the access key, then log in. + {t("review.acceptedHint")}

)} @@ -109,7 +112,7 @@ export function ReviewPage({ ticketId }: { ticketId: string }) { ? "rounded-tl-lg bg-stone-200/70 dark:bg-zinc-800" : "rounded-tr-lg bg-accent-600 text-white dark:bg-accent-500 dark:text-zinc-950", )}> -

{m.author === "admin" ? "Admin" : "You"}

+

{m.author === "admin" ? t("review.admin") : t("review.you")}

{m.body}

@@ -118,7 +121,7 @@ export function ReviewPage({ ticketId }: { ticketId: string }) { {closed ? (

- This ticket is closed. Replies by email are not read — if needed, file a new request. + {t("review.closedHint")}

) : (
@@ -127,14 +130,14 @@ export function ReviewPage({ ticketId }: { ticketId: string }) { rows={2} value={draft} onChange={(e) => setDraft(e.target.value)} - placeholder="Argue your case…" - aria-label="Reply to your ticket" + placeholder={t("review.replyPh")} + aria-label={t("review.replyAria")} className="max-h-40 flex-1 resize-none rounded-2xl border border-stone-200 bg-white px-3.5 py-2.5 text-sm outline-none transition placeholder:text-stone-400 focus:border-accent-500/60 dark:border-zinc-700 dark:bg-zinc-900 dark:placeholder:text-zinc-500" />
) : (

- Reserve a username and tell us why you want in. An admin reviews every request — - you follow the discussion on your personal ticket page and by email. + {t("review.requestIntro")}

- + setUsername(e.target.value)} placeholder="alice" required minLength={2} maxLength={32} /> - + setEmail(e.target.value)} placeholder="alice@example.com" inputMode="email" required /> - +