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

156 lines
5.4 KiB
TypeScript

"use client";
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;
createdAt: string;
authorName: string;
isUnread: boolean;
attachments: {
id: string;
fileName: string;
fileUrl: string;
fileType: string;
}[];
};
export function NewsTicker({ items }: { items: NewsTickerItemDto[] }) {
const [selected, setSelected] = useState<NewsTickerItemDto | null>(null);
const [readIds, setReadIds] = useState<Set<string>>(new Set());
const [, startTransition] = useTransition();
if (items.length === 0) {
return null;
}
function openAnnouncement(item: NewsTickerItemDto) {
setSelected(item);
if (!item.isUnread || readIds.has(item.id)) return;
setReadIds((current) => new Set(current).add(item.id));
startTransition(async () => {
await markAnnouncementRead(item.id);
});
}
return (
<>
<Card className="mb-8 border-[var(--good)] bg-[var(--good-soft)]">
<CardHeader className="pb-3">
<div className="flex 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]" />
Schwarzes Brett
</CardTitle>
<CardDescription>
Die neuesten offiziellen Ankündigungen deiner Kita.
</CardDescription>
</div>
<Badge variant="secondary">{items.length}</Badge>
</div>
</CardHeader>
<CardContent className="grid gap-3 lg:grid-cols-3">
{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.content}
</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>
</>
);
}