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

+60
View File
@@ -0,0 +1,60 @@
"use client";
import Script from "next/script";
export type CaptchaPublicConfig = {
provider: string;
siteKey?: string;
field?: string;
};
/** Renders Turnstile / reCAPTCHA widget + script when a provider is configured. */
export function CaptchaWidget({
captcha,
nonce,
className,
}: {
captcha: CaptchaPublicConfig;
nonce?: string;
className?: string;
}) {
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
if (!showCaptcha) return null;
return (
<div className={className}>
{captcha.provider === "turnstile" ? (
<>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async
defer
nonce={nonce}
/>
<div className="cf-turnstile" data-sitekey={captcha.siteKey} />
</>
) : null}
{captcha.provider === "recaptcha" ? (
<>
<Script
src="https://www.google.com/recaptcha/api.js"
async
defer
nonce={nonce}
/>
<div className="g-recaptcha" data-sitekey={captcha.siteKey} />
</>
) : null}
</div>
);
}
/** Read the provider token from a form (widget injects a hidden input). */
export function readCaptchaToken(
form: HTMLFormElement,
captcha: CaptchaPublicConfig,
): string {
if (captcha.provider === "none" || !captcha.field) return "";
const fd = new FormData(form);
return String(fd.get(captcha.field) ?? "").normalize("NFC");
}