feat: redesign CMS and unify German admin UI

This commit is contained in:
2026-09-17 16:51:24 +02:00
parent a2b3e99174
commit 7ec0c51ff5
129 changed files with 952 additions and 550 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
import { randomInt } from "node:crypto";
// Unambiguous alphabet (no 0/O/1/I/l) — easier to read aloud/type correctly.
// Unambiguous alphabet (no 0/O/1/I/l) - easier to read aloud/type correctly.
const ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
/**
* Cryptographically random meeting password, e.g. "K7mQ-42Px". Independent
* of the event's id/slug — never derive it from the meeting identifier.
* of the event's id/slug - never derive it from the meeting identifier.
*/
export function generateMeetingPassword(): string {
const chars = Array.from({ length: 8 }, () => ALPHABET[randomInt(ALPHABET.length)]);
+4 -4
View File
@@ -20,7 +20,7 @@ function formatTime(date: Date): string {
/**
* Checks all upcoming online events for due 60/30-minute reminders and sends
* them — to registered, non-cancelled participants and (optionally) to the
* them - to registered, non-cancelled participants and (optionally) to the
* admin host. Idempotent: every send is guarded by a "...Sent" flag so a
* cron job that runs every few minutes never double-sends.
*/
@@ -74,7 +74,7 @@ export async function runEventReminders() {
dateLabel,
timeLabel,
joinUrl,
meetingPassword: event.meetingPassword || "—",
meetingPassword: event.meetingPassword || "-",
minutesBefore: threshold.minutes,
});
results.emailsSent++;
@@ -82,7 +82,7 @@ export async function runEventReminders() {
await payload.update({ collection: "events", id: event.id, data: { [threshold.hostField]: true } });
}
// Participant reminders — only registered (not cancelled), not yet sent.
// Participant reminders - only registered (not cancelled), not yet sent.
const { docs: registrations } = await payload.find({
collection: "event-registrations",
where: {
@@ -102,7 +102,7 @@ export async function runEventReminders() {
dateLabel,
timeLabel,
joinUrl,
meetingPassword: event.meetingPassword || "—",
meetingPassword: event.meetingPassword || "-",
minutesBefore: threshold.minutes,
});
results.emailsSent++;
+1 -1
View File
@@ -4,7 +4,7 @@ export type MeetingStatus = "scheduled" | "open" | "live" | "ended";
/**
* Payload's "time only" date fields still store a full ISO timestamp, just
* with an arbitrary date portion — this combines the real event date with
* with an arbitrary date portion - this combines the real event date with
* the hour/minute from a time-only field into one correct Date.
*/
export function combineDateAndTime(dateISO: string, timeISO?: string | null): Date {
+2 -2
View File
@@ -6,13 +6,13 @@ export type MeetingTokenPayload = {
eventSlug: string;
participantId: string;
// Self-declared, persisted client-side (localStorage, not sessionStorage)
// identity for participants — participantId is fresh per /join call, so
// identity for participants - participantId is fresh per /join call, so
// without this a kick could always be undone by simply rejoining. Only
// used to make a kick stick across a rejoin (see rooms.ts kickedClientIds);
// never used to key live room membership, so a client choosing an
// arbitrary/colliding value can only affect their own future kick status,
// never another participant's connection. Still bypassable by clearing
// localStorage or joining from a different browser — there's no real
// localStorage or joining from a different browser - there's no real
// identity to anchor to for an unauthenticated participant.
clientId?: string;
name: string;