weitere verbesserungen der Guest PWA (vor allem TaskPicker)
This commit is contained in:
@@ -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 funktioniert’s</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user