feat: add guest notification insights
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user