Add announcement editing and reliable per-recipient mail delivery

This commit is contained in:
t.indorf
2026-10-13 08:53:22 +02:00
parent 661776e609
commit 1472779781
22 changed files with 1655 additions and 190 deletions
@@ -0,0 +1,174 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { Loader2, RotateCw, Save } from "lucide-react";
import {
updateAnnouncement,
retryAnnouncementEmails,
} from "@/actions/announcements";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { MarkdownContent } from "@/components/markdown-content";
export function EditNewsForm({
announcement,
}: {
announcement: {
id: string;
revision: number;
title: string;
content: string;
};
}) {
const router = useRouter();
const [pending, startTransition] = useTransition();
const [content, setContent] = useState(announcement.content);
const [sendEmail, setSendEmail] = useState(false);
function submit(formData: FormData) {
formData.set("id", announcement.id);
formData.set("revision", String(announcement.revision));
formData.set("sendEmail", sendEmail ? "on" : "off");
startTransition(async () => {
try {
const result = await updateAnnouncement(formData);
if (!("success" in result)) {
toast.error(result.error);
return;
}
toast.success(
result.queued
? `Mitteilung gespeichert. ${result.queued} E-Mails sind zum Versand vorgemerkt.`
: result.changed
? "Mitteilung aktualisiert."
: "Keine Änderungen vorhanden.",
);
setSendEmail(false);
router.refresh();
} catch {
toast.error(
"Mitteilung konnte nicht gespeichert werden. Bitte versuche es erneut.",
);
}
});
}
return (
<form action={submit} className="grid gap-5">
<div className="grid gap-2">
<Label htmlFor="edit-title">Titel</Label>
<Input
id="edit-title"
name="title"
defaultValue={announcement.title}
minLength={3}
maxLength={160}
required
disabled={pending}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-content">Text</Label>
<Textarea
id="edit-content"
name="content"
rows={10}
value={content}
onChange={(event) => setContent(event.target.value)}
minLength={3}
maxLength={20_000}
required
disabled={pending}
/>
<p className="text-xs text-[var(--ink-muted)]">
Markdown ist aktiv. HTML wird nicht interpretiert.
</p>
</div>
<details className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4">
<summary className="cursor-pointer text-sm font-medium">
Vorschau ansehen
</summary>
<div className="mt-4 text-sm">
<MarkdownContent content={content} />
</div>
</details>
<div className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4">
<div className="flex items-start gap-2">
<Checkbox
id="correction-email"
checked={sendEmail}
disabled={pending}
onCheckedChange={(checked) => setSendEmail(checked === true)}
/>
<Label
htmlFor="correction-email"
className="cursor-pointer leading-5"
>
Aktuelle Fassung auch per E-Mail senden
</Label>
</div>
<p className="mt-2 text-xs leading-5 text-[var(--ink-soft)]">
Bei einer Textänderung erscheint der Beitrag für alle wieder als
ungelesen. Jede Fassung wird je Person höchstens einmal zum Versand
vorgemerkt.
</p>
</div>
<Button type="submit" disabled={pending} className="justify-self-end">
{pending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
Änderungen speichern
</Button>
</form>
);
}
export function RetryNewsEmails({
id,
revision,
failed,
}: {
id: string;
revision: number;
failed: number;
}) {
const [pending, startTransition] = useTransition();
const router = useRouter();
return (
<Button
variant="outline"
disabled={pending || failed === 0}
onClick={() =>
startTransition(async () => {
try {
const result = await retryAnnouncementEmails(id, revision);
if (!("success" in result)) {
toast.error(result.error);
return;
}
toast.success(
result.queued
? `${result.queued} fehlgeschlagene E-Mails werden erneut versucht.`
: "Keine fehlgeschlagenen Sendungen vorhanden.",
);
router.refresh();
} catch {
toast.error("Versand konnte nicht gestartet werden.");
}
})
}
>
{pending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RotateCw className="h-4 w-4" />
)}
Fehlgeschlagene erneut versuchen
</Button>
);
}