Files
kita-planer/src/app/dashboard/news-ticker.tsx
T

187 lines
6.3 KiB
TypeScript

"use client";
import Link from "next/link";
import { toast } from "sonner";
import { useState, useTransition } from "react";
import { Bell, FileText, Megaphone } from "lucide-react";
import { markAnnouncementRead } from "@/actions/announcements";
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,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { MarkdownContent } from "@/components/markdown-content";
export type NewsTickerItemDto = {
id: string;
title: string;
content: string;
excerpt: string;
createdAt: string;
authorName: string;
isUnread: boolean;
attachments: {
id: string;
fileName: string;
fileUrl: string;
fileType: string;
}[];
};
export function NewsTicker({
items,
archive = false,
}: {
items: NewsTickerItemDto[];
archive?: boolean;
}) {
const [selected, setSelected] = useState<NewsTickerItemDto | null>(null);
const [readIds, setReadIds] = useState<Set<string>>(new Set());
const [, startTransition] = useTransition();
function openAnnouncement(item: NewsTickerItemDto) {
setSelected(item);
if (!item.isUnread || readIds.has(item.id)) return;
startTransition(async () => {
try {
const result = await markAnnouncementRead(item.id);
if (result.error) {
toast.error(result.error);
return;
}
setReadIds((current) => new Set(current).add(item.id));
} catch {
toast.error("Lesestatus konnte nicht gespeichert werden.");
}
});
}
return (
<>
<Card className="mb-8 border-[var(--good)] bg-[var(--good-soft)]">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Megaphone className="h-5 w-5 text-[var(--good)] [stroke-width:1.7]" />
{archive ? "Mitteilungen" : "Neues aus der Kita"}
</CardTitle>
<CardDescription>
{archive
? "Veröffentlichte Ankündigungen deiner Kita."
: "Die neuesten offiziellen Ankündigungen deiner Kita."}
</CardDescription>
</div>
{archive ? (
<Badge variant="secondary">{items.length} Meldungen</Badge>
) : (
<Button asChild variant="outline" size="sm">
<Link href="/dashboard/mitteilungen">Alle Mitteilungen</Link>
</Button>
)}
</div>
</CardHeader>
<CardContent className="grid gap-3 lg:grid-cols-3">
{items.length === 0 ? (
<p className="col-span-full py-4 text-sm text-[var(--ink-soft)]">
Noch keine Mitteilungen für deine Empfängergruppe.
</p>
) : null}
{items.map((item) => {
const isUnread = item.isUnread && !readIds.has(item.id);
return (
<button
key={item.id}
type="button"
onClick={() => openAnnouncement(item)}
className="rounded-lg border border-[var(--hairline)] bg-[var(--surface)] p-4 text-left transition hover:border-[var(--accent-mid)] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)]"
>
<div className="mb-3 flex items-start justify-between gap-2">
<div className="flex items-center gap-2 text-xs text-[var(--ink-muted)]">
<Bell className="h-3.5 w-3.5 [stroke-width:1.7]" />
{item.createdAt}
</div>
{isUnread && <Badge variant="success">NEU</Badge>}
</div>
<h3 className="line-clamp-2 font-semibold leading-snug text-[var(--ink)]">
{item.title}
</h3>
<p className="mt-2 line-clamp-3 text-sm leading-6 text-[var(--ink-soft)]">
{item.excerpt}
</p>
{item.attachments.length > 0 && (
<p className="mt-3 flex items-center gap-1 text-xs font-medium text-[var(--accent-deep)]">
<FileText className="h-3.5 w-3.5 [stroke-width:1.7]" />
{item.attachments.length} Anhang
{item.attachments.length === 1 ? "" : "e"}
</p>
)}
</button>
);
})}
</CardContent>
</Card>
<Dialog
open={!!selected}
onOpenChange={(open) => !open && setSelected(null)}
>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
{selected && (
<>
<DialogHeader>
<DialogTitle className="text-2xl">{selected.title}</DialogTitle>
<DialogDescription>
{selected.createdAt} · {selected.authorName}
</DialogDescription>
</DialogHeader>
<div className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4 text-sm">
<MarkdownContent content={selected.content} />
</div>
{selected.attachments.length > 0 && (
<div>
<p className="mb-2 text-sm font-medium">Anhänge</p>
<div className="grid gap-2">
{selected.attachments.map((attachment) => (
<Button
key={attachment.id}
asChild
variant="outline"
className="justify-start"
>
<a
href={attachment.fileUrl}
target="_blank"
rel="noreferrer"
>
<FileText className="h-4 w-4 [stroke-width:1.7]" />
{attachment.fileName}
</a>
</Button>
))}
</div>
</div>
)}
</>
)}
</DialogContent>
</Dialog>
</>
);
}