feat: add guest notification insights

This commit is contained in:
Codex Agent
2025-11-12 19:31:13 +01:00
parent 642541c8fb
commit 2c412e3764
7 changed files with 440 additions and 7 deletions

View File

@@ -259,6 +259,33 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
const progressRatio = taskProgress
? Math.min(1, taskProgress.completedCount / TASK_BADGE_TARGET)
: 0;
const [activeTab, setActiveTab] = React.useState<'unread' | 'all' | 'status'>(center.unreadCount > 0 ? 'unread' : 'all');
React.useEffect(() => {
if (!open) {
setActiveTab(center.unreadCount > 0 ? 'unread' : 'all');
}
}, [open, center.unreadCount]);
const uploadNotifications = React.useMemo(
() => center.notifications.filter((item) => item.type === 'upload_alert'),
[center.notifications]
);
const unreadNotifications = React.useMemo(
() => center.notifications.filter((item) => item.status === 'new'),
[center.notifications]
);
const filteredNotifications = React.useMemo(() => {
switch (activeTab) {
case 'unread':
return unreadNotifications;
case 'status':
return uploadNotifications;
default:
return center.notifications;
}
}, [activeTab, center.notifications, unreadNotifications, uploadNotifications]);
return (
<div className="relative">
@@ -299,13 +326,36 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
{t('header.notifications.refresh', 'Aktualisieren')}
</button>
</div>
<NotificationTabs
tabs={[
{ key: 'unread', label: t('header.notifications.tabUnread', 'Neu'), badge: unreadNotifications.length },
{ key: 'status', label: t('header.notifications.tabStatus', 'Uploads/Status'), badge: uploadNotifications.length },
{ key: 'all', label: t('header.notifications.tabAll', 'Alle'), badge: center.notifications.length },
]}
activeTab={activeTab}
onTabChange={(next) => setActiveTab(next as typeof activeTab)}
/>
<NotificationStatusBar
lastFetchedAt={center.lastFetchedAt}
isOffline={center.isOffline}
t={t}
/>
<div className="mt-3 max-h-80 space-y-2 overflow-y-auto pr-1">
{center.loading ? (
<NotificationSkeleton />
) : center.notifications.length === 0 ? (
<NotificationEmptyState t={t} />
) : filteredNotifications.length === 0 ? (
<NotificationEmptyState
t={t}
message={
activeTab === 'unread'
? t('header.notifications.emptyUnread', 'Du bist auf dem neuesten Stand!')
: activeTab === 'status'
? t('header.notifications.emptyStatus', 'Keine Upload-Hinweise oder Wartungen aktiv.')
: undefined
}
/>
) : (
center.notifications.map((item) => (
filteredNotifications.map((item) => (
<NotificationListItem
key={item.id}
item={item}
@@ -490,11 +540,11 @@ function NotificationCta({ cta, onFollow }: { cta: { label?: string; href?: stri
);
}
function NotificationEmptyState({ t }: { t: TranslateFn }) {
function NotificationEmptyState({ t, message }: { t: TranslateFn; message?: string }) {
return (
<div className="rounded-2xl border border-dashed border-slate-200 bg-white/70 p-4 text-center text-sm text-slate-500">
<AlertCircle className="mx-auto mb-2 h-5 w-5 text-slate-400" aria-hidden />
<p>{t('header.notifications.empty', 'Gerade gibt es keine neuen Hinweise.')}</p>
<p>{message ?? t('header.notifications.empty', 'Gerade gibt es keine neuen Hinweise.')}</p>
</div>
);
}
@@ -542,3 +592,51 @@ function formatRelativeTime(value: string): string {
const diffDays = Math.round(diffHours / 24);
return `${diffDays} d`;
}
function NotificationTabs({
tabs,
activeTab,
onTabChange,
}: {
tabs: Array<{ key: string; label: string; badge?: number }>;
activeTab: string;
onTabChange: (key: string) => void;
}) {
return (
<div className="mt-3 flex gap-2 rounded-full bg-slate-100/80 p-1 text-xs font-semibold text-slate-600">
{tabs.map((tab) => (
<button
key={tab.key}
type="button"
className={`flex flex-1 items-center justify-center gap-1 rounded-full px-3 py-1 transition ${
activeTab === tab.key ? 'bg-white text-pink-600 shadow' : 'text-slate-500'
}`}
onClick={() => onTabChange(tab.key)}
>
{tab.label}
{typeof tab.badge === 'number' && tab.badge > 0 && (
<span className="rounded-full bg-pink-100 px-2 text-[11px] text-pink-600">{tab.badge}</span>
)}
</button>
))}
</div>
);
}
function NotificationStatusBar({ lastFetchedAt, isOffline, t }: { lastFetchedAt: Date | null; isOffline: boolean; t: TranslateFn }) {
const label = lastFetchedAt ? formatRelativeTime(lastFetchedAt.toISOString()) : t('header.notifications.never', 'Noch keine Aktualisierung');
return (
<div className="mt-2 flex items-center justify-between text-[11px] text-slate-500">
<span>
{t('header.notifications.lastSync', 'Zuletzt aktualisiert')}: {label}
</span>
{isOffline && (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 font-semibold text-amber-700">
<AlertCircle className="h-3 w-3" aria-hidden />
{t('header.notifications.offline', 'Offline')}
</span>
)}
</div>
);
}