feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 2m55s

Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-21 21:08:33 +02:00
1 parent 2ff08e5127
commit ed7db6e048
76 files changed
+4834 -1376

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
export default function MessagesLoading() {
return (
<div
style={{ display: "flex", justifyContent: "center", padding: "4rem 0" }}
>
<div
className="animate-spin w-8 h-8 border-4 rounded-full"
style={{
borderColor: "var(--color-primary)",
borderTopColor: "transparent",
}}
/>
</div>
);
}
+85 -12
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { acceptFriend } from "@/actions/messenger";
import type { CSSProperties } from "react";
import { acceptFriend, declineFriendRequest } from "@/actions/messenger";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { avatarImageUrl } from "@/lib/format";
@@ -10,18 +11,45 @@ import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
type SearchParams = Promise<{
accepted?: string;
declined?: string;
error?: string;
}>;
function feedbackStyle(tone: "success" | "error"): CSSProperties {
const accent =
tone === "error" ? "var(--color-danger)" : "var(--color-primary)";
return {
margin: 0,
padding: "0.85rem 1rem",
borderRadius: "var(--radius-md)",
border: `1px solid ${accent}`,
color: "var(--color-text-readable, var(--color-text))",
fontSize: "0.9rem",
fontWeight: 600,
background: "var(--color-surface)",
borderLeft: `4px solid ${accent}`,
};
}
function formatTimestamp(seconds: number): string {
if (!seconds) return "";
// sended_on is a unix timestamp in seconds (emulator convention).
return new Date(seconds * 1000).toISOString().slice(0, 16).replace("T", " ");
}
export default async function MessagesPage() {
export default async function MessagesPage({
searchParams,
}: {
searchParams: SearchParams;
}) {
const t = await getTranslations("pages.messages");
const session = await auth();
if (!session?.user?.id) redirect("/login");
const userId = Number(session.user.id);
const { accepted, declined, error } = await searchParams;
// Pending friend requests addressed to the session user, the most recent
// offline messages they've received, and the avatar-imager base URL.
@@ -85,8 +113,35 @@ export default async function MessagesPage() {
}
}
const errorMessage =
error === "not_found"
? t("errors.notFound")
: error === "unauthorized"
? t("errors.unauthorized")
: error === "invalid"
? t("errors.invalid")
: error
? t("errors.error")
: null;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{accepted === "1" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.accepted")}
</div>
) : null}
{declined === "1" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.declined")}
</div>
) : null}
{errorMessage ? (
<div role="alert" style={feedbackStyle("error")}>
{errorMessage}
</div>
) : null}
<ContentCard icon="✉️" title={t("title")} subtitle={t("subtitle")} />
{/* ── Pending friend requests ───────────────────────────── */}
@@ -144,16 +199,34 @@ export default async function MessagesPage() {
{t("wantsToBeFriend")}
</p>
</div>
<form action={acceptFriend}>
<input
type="hidden"
name="requestId"
value={String(req.id)}
/>
<button type="submit" className="btn btn-secondary">
{t("accept")}
</button>
</form>
<div
style={{
display: "flex",
gap: "0.5rem",
flexWrap: "wrap",
}}
>
<form action={acceptFriend}>
<input
type="hidden"
name="requestId"
value={String(req.id)}
/>
<button type="submit" className="btn btn-secondary">
{t("accept")}
</button>
</form>
<form action={declineFriendRequest}>
<input
type="hidden"
name="requestId"
value={String(req.id)}
/>
<button type="submit" className="btn">
{t("decline")}
</button>
</form>
</div>
</div>
);
})}