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

@@ -1,6 +1,7 @@
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react";
import { postThread } from "@/actions/social";
import { ContentCard } from "@/components/public/ui";
import { auth } from "@/lib/auth";
@@ -8,13 +9,34 @@ import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
type SearchParams = Promise<{ 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}`,
};
}
export default async function NewThreadPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: SearchParams;
}) {
const t = await getTranslations("pages.guildForumNew");
const { id } = await params;
const { error } = await searchParams;
const guildId = Number(id);
if (!Number.isInteger(guildId) || guildId <= 0) notFound();
@@ -35,8 +57,25 @@ export default async function NewThreadPage({
if (!guild) notFound();
const errorMessage =
error === "invalid"
? t("errors.invalid")
: error === "not_found"
? t("errors.notFound")
: error === "ratelimit"
? t("errors.ratelimit")
: error
? t("errors.error")
: null;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{errorMessage ? (
<div role="alert" style={feedbackStyle("error")}>
{errorMessage}
</div>
) : null}
<p style={{ margin: 0 }}>
<Link href={`/guilds/${guild.id}/forum`}>{t("backToForum")}</Link>
</p>
+28
View File
@@ -1,11 +1,30 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const revalidate = 300;
type SearchParams = Promise<{ posted?: 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}`,
};
}
type ThreadRow = {
id: number;
openerId: number | null;
@@ -24,11 +43,14 @@ function formatTimestamp(ts: number | null | undefined): string {
export default async function GuildForumPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: SearchParams;
}) {
const t = await getTranslations("pages.guildForum");
const { id } = await params;
const { posted } = await searchParams;
const guildId = Number(id);
if (!Number.isInteger(guildId) || guildId <= 0) notFound();
@@ -89,6 +111,12 @@ export default async function GuildForumPage({
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{posted === "1" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.posted")}
</div>
) : null}
<p style={{ margin: 0 }}>
<Link href={`/guilds/${guild.id}`}>{t("backToGuild")}</Link>
</p>