156 lines
5.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|