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

+51 -2
View File
@@ -1,20 +1,39 @@
import type { Metadata } from "next";
import { formatDate } from "@/lib/format-date";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react";
import { postComment } from "@/actions/article-comments";
import { toggleReaction } from "@/actions/article-reactions";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { SanitizedHtml } from "@/components/shared/sanitized-html";
import { auth } from "@/lib/auth";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
import { cacheQuery } from "@/lib/prisma-cache";
import { sanitize } from "@/lib/sanitize";
import { SanitizedHtml } from "@/components/shared/sanitized-html";
export const revalidate = 60;
type SearchParams = Promise<{ comment?: 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}`,
};
}
export async function generateStaticParams() {
const slugs = await prisma.websiteArticles
.findMany({
@@ -62,10 +81,13 @@ const REACTIONS: { key: string; emoji: string }[] = [
export default async function ArticlePage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: SearchParams;
}) {
const { slug } = await params;
const { comment, error } = await searchParams;
const t = await getTranslations("pages.article");
const article = await cacheQuery(
@@ -145,8 +167,34 @@ export default async function ArticlePage({
}
}
const errorMessage =
error === "empty"
? t("errors.empty")
: error === "moderated"
? t("errors.moderated")
: error === "ratelimit"
? t("errors.ratelimit")
: error === "invalid"
? t("errors.invalid")
: error === "not_found"
? t("errors.notFound")
: error
? t("errors.error")
: null;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{comment === "posted" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.posted")}
</div>
) : null}
{errorMessage ? (
<div role="alert" style={feedbackStyle("error")}>
{errorMessage}
</div>
) : null}
<ContentCard
icon="📰"
title={article.title}
@@ -290,6 +338,7 @@ export default async function ArticlePage({
{loggedIn ? (
<form action={postComment} className="card">
<input type="hidden" name="articleId" value={String(articleId)} />
<input type="hidden" name="slug" value={slug} />
<label
htmlFor="comment"
style={{