Add announcement editing and reliable per-recipient mail delivery
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user