weitere verbesserungen der Guest PWA (vor allem TaskPicker)

This commit is contained in:
Codex Agent
2025-11-12 13:19:28 +01:00
parent 1cec116933
commit d91108c883
20 changed files with 2306 additions and 653 deletions

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { Link } from 'react-router-dom';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import { User, Heart, Users, PartyPopper, Camera } from 'lucide-react';
import { User, Heart, Users, PartyPopper, Camera, Bell, ArrowUpRight } from 'lucide-react';
import { useEventData } from '../hooks/useEventData';
import { useOptionalEventStats } from '../context/EventStatsContext';
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
import { SettingsSheet } from './settings-sheet';
import { useTranslation } from '../i18n/useTranslation';
import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext';
import { useOptionalNotificationCenter } from '../context/NotificationCenterContext';
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
heart: Heart,
@@ -86,6 +89,31 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING;
const primaryForeground = '#ffffff';
const { event, status } = useEventData();
const notificationCenter = useOptionalNotificationCenter();
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
const taskProgress = useGuestTaskProgress(eventToken);
const panelRef = React.useRef<HTMLDivElement | null>(null);
const checklistItems = React.useMemo(
() => [
t('home.checklist.steps.first'),
t('home.checklist.steps.second'),
t('home.checklist.steps.third'),
],
[t],
);
React.useEffect(() => {
if (!notificationsOpen) {
return;
}
const handler = (event: MouseEvent) => {
if (!panelRef.current) return;
if (panelRef.current.contains(event.target as Node)) return;
setNotificationsOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [notificationsOpen]);
if (!eventToken) {
const guestName = identity?.name && identity?.hydrated ? identity.name : null;
@@ -139,7 +167,6 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
const stats =
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
return (
<div
className="sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
@@ -172,6 +199,17 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
</div>
</div>
<div className="flex items-center gap-2">
{notificationCenter && (
<NotificationButton
eventToken={eventToken}
center={notificationCenter}
open={notificationsOpen}
onToggle={() => setNotificationsOpen((prev) => !prev)}
panelRef={panelRef}
checklistItems={checklistItems}
taskProgress={taskProgress?.hydrated ? taskProgress : undefined}
/>
)}
<AppearanceToggleDropdown />
<SettingsSheet />
</div>
@@ -179,4 +217,101 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
);
}
export {}
function NotificationButton({
center,
eventToken,
open,
onToggle,
panelRef,
checklistItems,
taskProgress,
}: {
center: {
queueCount: number;
inviteCount: number;
totalCount: number;
};
eventToken: string;
open: boolean;
onToggle: () => void;
panelRef: React.RefObject<HTMLDivElement>;
checklistItems: string[];
taskProgress?: ReturnType<typeof useGuestTaskProgress>;
}) {
if (!center) {
return null;
}
const totalCount = center.totalCount;
const progressRatio = taskProgress
? Math.min(1, taskProgress.completedCount / TASK_BADGE_TARGET)
: 0;
return (
<div className="relative">
<button
type="button"
onClick={onToggle}
className="relative rounded-full bg-white/15 p-2 text-white transition hover:bg-white/30"
aria-label="Benachrichtigungen anzeigen"
>
<Bell className="h-5 w-5" aria-hidden />
{totalCount > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-white px-1.5 text-[10px] font-semibold text-pink-600">
{totalCount}
</span>
)}
</button>
{open && (
<div
ref={panelRef}
className="absolute right-0 mt-2 w-72 rounded-2xl border border-white/30 bg-white/95 p-4 text-slate-900 shadow-2xl"
>
<p className="text-sm font-semibold text-slate-900">Benachrichtigungen</p>
<p className="text-xs text-slate-500">Uploads in Warteschlange: {center.queueCount}</p>
<Link
to={`/e/${encodeURIComponent(eventToken)}/queue`}
className="mt-2 flex items-center justify-between rounded-xl border border-slate-200 px-3 py-2 text-sm font-semibold text-pink-600 transition hover:border-pink-300"
>
Zur Warteschlange
<ArrowUpRight className="h-4 w-4" aria-hidden />
</Link>
{taskProgress && (
<div className="mt-3 rounded-2xl border border-slate-200 bg-slate-50/90 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs uppercase tracking-[0.3em] text-slate-400">Badge-Fortschritt</p>
<p className="text-lg font-semibold text-slate-900">
{taskProgress.completedCount}/{TASK_BADGE_TARGET}
</p>
</div>
<Link
to={`/e/${encodeURIComponent(eventToken)}/tasks`}
className="rounded-full border border-slate-200 px-3 py-1 text-xs font-semibold text-pink-600 transition hover:border-pink-300"
>
Weiter
</Link>
</div>
<div className="mt-3 h-1.5 w-full rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-pink-500"
style={{ width: `${progressRatio * 100}%` }}
/>
</div>
</div>
)}
<div className="my-3 h-px w-full bg-slate-100" />
<p className="text-[11px] uppercase tracking-[0.3em] text-slate-400">So funktionierts</p>
<ul className="mt-2 space-y-2 text-sm text-slate-600">
{checklistItems.map((item) => (
<li key={item} className="flex gap-2">
<span className="mt-0.5 h-1.5 w-1.5 rounded-full bg-pink-500" />
<span>{item}</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}