Add guest push notifications and queue alerts

This commit is contained in:
Codex Agent
2025-11-12 20:38:49 +01:00
parent 2c412e3764
commit 574aa47ce7
34 changed files with 1806 additions and 74 deletions

View File

@@ -26,6 +26,7 @@ import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext';
import { useOptionalNotificationCenter, type NotificationCenterValue } from '../context/NotificationCenterContext';
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
import { usePushSubscription } from '../hooks/usePushSubscription';
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
heart: Heart,
@@ -224,7 +225,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
</div>
</div>
<div className="flex items-center gap-2">
{notificationCenter && (
{notificationCenter && eventToken && (
<NotificationButton
eventToken={eventToken}
center={notificationCenter}
@@ -254,12 +255,15 @@ type NotificationButtonProps = {
t: TranslateFn;
};
type PushState = ReturnType<typeof usePushSubscription>;
function NotificationButton({ center, eventToken, open, onToggle, panelRef, checklistItems, taskProgress, t }: NotificationButtonProps) {
const badgeCount = center.totalCount;
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');
const pushState = usePushSubscription(eventToken);
React.useEffect(() => {
if (!open) {
@@ -338,6 +342,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, chec
<NotificationStatusBar
lastFetchedAt={center.lastFetchedAt}
isOffline={center.isOffline}
push={pushState}
t={t}
/>
<div className="mt-3 max-h-80 space-y-2 overflow-y-auto pr-1">
@@ -528,7 +533,6 @@ function NotificationCta({ cta, onFollow }: { cta: { label?: string; href?: stri
return (
<a
href={cta.href}
href={href}
target="_blank"
rel="noopener noreferrer"
@@ -623,19 +627,68 @@ function NotificationTabs({
);
}
function NotificationStatusBar({ lastFetchedAt, isOffline, t }: { lastFetchedAt: Date | null; isOffline: boolean; t: TranslateFn }) {
function NotificationStatusBar({
lastFetchedAt,
isOffline,
push,
t,
}: {
lastFetchedAt: Date | null;
isOffline: boolean;
push: PushState;
t: TranslateFn;
}) {
const label = lastFetchedAt ? formatRelativeTime(lastFetchedAt.toISOString()) : t('header.notifications.never', 'Noch keine Aktualisierung');
const pushDescription = React.useMemo(() => {
if (!push.supported) {
return t('header.notifications.pushUnsupported', 'Push wird nicht unterstützt');
}
if (push.permission === 'denied') {
return t('header.notifications.pushDenied', 'Browser blockiert Benachrichtigungen');
}
if (push.subscribed) {
return t('header.notifications.pushActive', 'Push aktiv');
}
return t('header.notifications.pushInactive', 'Push deaktiviert');
}, [push.permission, push.subscribed, push.supported, t]);
const buttonLabel = push.subscribed
? t('header.notifications.pushDisable', 'Deaktivieren')
: t('header.notifications.pushEnable', 'Aktivieren');
const pushButtonDisabled = push.loading || !push.supported || push.permission === 'denied';
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')}
<div className="mt-2 space-y-2 text-[11px] text-slate-500">
<div className="flex items-center justify-between">
<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>
<div className="flex items-center justify-between gap-2 rounded-full bg-slate-100/80 px-3 py-1 text-[11px] font-semibold text-slate-600">
<div className="flex items-center gap-1">
<Bell className="h-3.5 w-3.5" aria-hidden />
<span>{pushDescription}</span>
</div>
<button
type="button"
onClick={() => (push.subscribed ? push.disable() : push.enable())}
disabled={pushButtonDisabled}
className="rounded-full bg-white/80 px-3 py-0.5 text-[11px] font-semibold text-pink-600 shadow disabled:cursor-not-allowed disabled:opacity-60"
>
{push.loading ? t('header.notifications.pushLoading', '…') : buttonLabel}
</button>
</div>
{push.error && (
<p className="text-[11px] font-semibold text-rose-600">
{push.error}
</p>
)}
</div>
);