Add RSVP events and Unsplash image support
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { EventParticipationStatus } from "@prisma/client";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { AlertTriangle, Check, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { submitEventRsvp } from "@/actions/rsvp";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export type DashboardRsvpEventDto = {
|
||||
id: string;
|
||||
title: string;
|
||||
startDate: string;
|
||||
rsvpDeadline: string | null;
|
||||
children: {
|
||||
id: string;
|
||||
name: string;
|
||||
status: EventParticipationStatus | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function RsvpActionCard({ events }: { events: DashboardRsvpEventDto[] }) {
|
||||
if (events.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card className="mb-8 border-amber-200 bg-amber-50/70">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg text-amber-950">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-700" />
|
||||
Aktion erforderlich
|
||||
</CardTitle>
|
||||
<CardDescription className="text-amber-900/80">
|
||||
Für diese Termine fehlt noch mindestens eine Rückmeldung deiner
|
||||
Familie.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex flex-col gap-3 rounded-md border bg-background p-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{event.title}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Rückmeldung bis{" "}
|
||||
{format(
|
||||
new Date(event.rsvpDeadline ?? event.startDate),
|
||||
"dd.MM.yyyy HH:mm",
|
||||
{ locale: de },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<RsvpDialog event={event} />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RsvpDialog({ event }: { event: DashboardRsvpEventDto }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const initialSelection = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
event.children.map((child) => [child.id, child.status ?? ""]),
|
||||
) as Record<string, EventParticipationStatus | "">,
|
||||
[event.children],
|
||||
);
|
||||
const [selection, setSelection] = useState(initialSelection);
|
||||
|
||||
function submit() {
|
||||
const missingChild = event.children.find((child) => !selection[child.id]);
|
||||
if (missingChild) {
|
||||
toast.error(`Bitte Rückmeldung für ${missingChild.name} auswählen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
for (const child of event.children) {
|
||||
const status = selection[child.id];
|
||||
if (!status) continue;
|
||||
|
||||
const result = await submitEventRsvp({
|
||||
eventId: event.id,
|
||||
childId: child.id,
|
||||
status,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Rückmeldung gespeichert.");
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) setSelection(initialSelection);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">Jetzt abstimmen</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rückmeldung für {event.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Wähle pro Kind aus, ob es am Termin teilnimmt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3 py-2">
|
||||
{event.children.map((child) => (
|
||||
<div key={child.id} className="rounded-md border p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="font-medium">{child.name}</p>
|
||||
{child.status && (
|
||||
<Badge variant="outline">bereits gemeldet</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-md border p-3 text-sm hover:bg-muted/50">
|
||||
<input
|
||||
type="radio"
|
||||
name={`rsvp-${child.id}`}
|
||||
checked={
|
||||
selection[child.id] ===
|
||||
EventParticipationStatus.ATTENDING
|
||||
}
|
||||
onChange={() =>
|
||||
setSelection((current) => ({
|
||||
...current,
|
||||
[child.id]: EventParticipationStatus.ATTENDING,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Check className="h-4 w-4 text-emerald-600" />
|
||||
Nimmt teil
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded-md border p-3 text-sm hover:bg-muted/50">
|
||||
<input
|
||||
type="radio"
|
||||
name={`rsvp-${child.id}`}
|
||||
checked={
|
||||
selection[child.id] ===
|
||||
EventParticipationStatus.NOT_ATTENDING
|
||||
}
|
||||
onChange={() =>
|
||||
setSelection((current) => ({
|
||||
...current,
|
||||
[child.id]: EventParticipationStatus.NOT_ATTENDING,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<X className="h-4 w-4 text-rose-600" />
|
||||
Nimmt nicht teil
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="button" disabled={isPending} onClick={submit}>
|
||||
{isPending ? "Speichern..." : "Rückmeldung speichern"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user