updated table structure for photobooth/sparkbooth settings. now there's a separate table for it. update all references and tests. also fixed the notification panel and the lightbox in the guest app.
This commit is contained in:
@@ -56,7 +56,7 @@ export default function GalleryPreview({ token }: Props) {
|
||||
arr.sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
|
||||
}
|
||||
|
||||
return arr.slice(0, 4); // 2x2 = 4 items
|
||||
return arr.slice(0, 9); // up to 3x3 preview
|
||||
}, [typedPhotos, mode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -134,7 +134,7 @@ export default function GalleryPreview({ token }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="grid gap-2 grid-cols-2 md:grid-cols-3">
|
||||
{items.map((p: PreviewPhoto) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
|
||||
@@ -149,7 +149,6 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
const { event, status } = useEventData();
|
||||
const notificationCenter = useOptionalNotificationCenter();
|
||||
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
|
||||
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
|
||||
const taskProgress = useGuestTaskProgress(eventToken);
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
@@ -275,8 +274,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
? Math.min(1, taskProgress.completedCount / TASK_BADGE_TARGET)
|
||||
: 0;
|
||||
const [activeTab, setActiveTab] = React.useState<'unread' | 'all' | 'status'>(center.unreadCount > 0 ? 'unread' : 'all');
|
||||
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
|
||||
const [scopeFilter, setScopeFilter] = React.useState<'all' | 'uploads' | 'tips' | 'general'>('all');
|
||||
const [scopeFilter, setScopeFilter] = React.useState<'all' | 'tips' | 'general'>('all');
|
||||
const pushState = usePushSubscription(eventToken);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -306,19 +304,14 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
default:
|
||||
base = center.notifications;
|
||||
}
|
||||
|
||||
if (statusFilter === 'all') return base;
|
||||
return base.filter((item) => item.status === (statusFilter === 'new' ? 'new' : statusFilter));
|
||||
}, [activeTab, center.notifications, unreadNotifications, uploadNotifications, statusFilter]);
|
||||
return base;
|
||||
}, [activeTab, center.notifications, unreadNotifications, uploadNotifications]);
|
||||
|
||||
const scopedNotifications = React.useMemo(() => {
|
||||
if (scopeFilter === 'all') {
|
||||
return filteredNotifications;
|
||||
}
|
||||
return filteredNotifications.filter((item) => {
|
||||
if (scopeFilter === 'uploads') {
|
||||
return item.type === 'upload_alert' || item.type === 'photo_activity';
|
||||
}
|
||||
if (scopeFilter === 'tips') {
|
||||
return item.type === 'support_tip' || item.type === 'achievement_major';
|
||||
}
|
||||
@@ -332,7 +325,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="relative rounded-full bg-white/15 p-2 text-white transition hover:bg-white/30"
|
||||
aria-label={t('header.notifications.open', 'Benachrichtigungen anzeigen')}
|
||||
aria-label={open ? t('header.notifications.close', 'Benachrichtigungen schließen') : t('header.notifications.open', 'Benachrichtigungen anzeigen')}
|
||||
>
|
||||
<Bell className="h-5 w-5" aria-hidden />
|
||||
{badgeCount > 0 && (
|
||||
@@ -374,46 +367,33 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
activeTab={activeTab}
|
||||
onTabChange={(next) => setActiveTab(next as typeof activeTab)}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<label className="sr-only" htmlFor="notification-scope">
|
||||
{t('header.notifications.scopeLabel', 'Bereich filtern')}
|
||||
</label>
|
||||
<select
|
||||
id="notification-scope"
|
||||
value={scopeFilter}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as typeof scopeFilter;
|
||||
setScopeFilter(next);
|
||||
notificationCenter?.setFilters({ scope: next, status: statusFilter });
|
||||
}}
|
||||
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
|
||||
>
|
||||
<option value="all">{t('header.notifications.scope.all', 'Alle')}</option>
|
||||
<option value="uploads">{t('header.notifications.scope.uploads', 'Uploads/Status')}</option>
|
||||
<option value="tips">{t('header.notifications.scope.tips', 'Tipps & Achievements')}</option>
|
||||
<option value="general">{t('header.notifications.scope.general', 'Allgemein')}</option>
|
||||
</select>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value as typeof statusFilter;
|
||||
setStatusFilter(value);
|
||||
notificationCenter?.setFilters({ status: value, scope: scopeFilter });
|
||||
}}
|
||||
className="mt-2 w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
|
||||
>
|
||||
<option value="new">{t('header.notifications.filter.unread', 'Neu')}</option>
|
||||
<option value="read">{t('header.notifications.filter.read', 'Gelesen')}</option>
|
||||
<option value="dismissed">{t('header.notifications.filter.dismissed', 'Ausgeblendet')}</option>
|
||||
<option value="all">{t('header.notifications.filter.all', 'Alle')}</option>
|
||||
</select>
|
||||
<div className="mt-3">
|
||||
<div className="flex gap-2 overflow-x-auto text-xs whitespace-nowrap pb-1">
|
||||
{(
|
||||
[
|
||||
{ key: 'all', label: t('header.notifications.scope.all', 'Alle') },
|
||||
{ key: 'tips', label: t('header.notifications.scope.tips', 'Tipps & Achievements') },
|
||||
{ key: 'general', label: t('header.notifications.scope.general', 'Allgemein') },
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setScopeFilter(option.key);
|
||||
center.setFilters({ scope: option.key });
|
||||
}}
|
||||
className={`rounded-full border px-3 py-1 font-semibold transition ${
|
||||
scopeFilter === option.key
|
||||
? 'border-pink-200 bg-pink-50 text-pink-700'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-pink-200 hover:text-pink-700'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
{center.loading ? (
|
||||
<NotificationSkeleton />
|
||||
@@ -440,26 +420,27 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 rounded-2xl border border-slate-200 bg-slate-50/80 p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-600">
|
||||
{t('header.notifications.queueLabel', 'Uploads in Warteschlange')}
|
||||
</span>
|
||||
<span className="font-semibold text-slate-900">{center.queueCount}</span>
|
||||
{activeTab === 'status' && (
|
||||
<div className="mt-3 flex items-center justify-between rounded-xl bg-slate-50/90 px-3 py-2 text-xs text-slate-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<UploadCloud className="h-4 w-4 text-slate-400" aria-hidden />
|
||||
<span>{t('header.notifications.queueLabel', 'Uploads in Warteschlange')}</span>
|
||||
<span className="font-semibold text-slate-900">{center.queueCount}</span>
|
||||
</div>
|
||||
<Link
|
||||
to={`/e/${encodeURIComponent(eventToken)}/queue`}
|
||||
className="inline-flex items-center gap-1 font-semibold text-pink-600"
|
||||
onClick={() => {
|
||||
if (center.unreadCount > 0) {
|
||||
void center.refresh();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('header.notifications.queueCta', 'Verlauf')}
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
to={`/e/${encodeURIComponent(eventToken)}/queue`}
|
||||
className="mt-2 inline-flex items-center gap-1 text-sm font-semibold text-pink-600"
|
||||
onClick={() => {
|
||||
if (center.unreadCount > 0) {
|
||||
void center.refresh();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('header.notifications.queueCta', 'Upload-Verlauf öffnen')}
|
||||
<ArrowUpRight className="h-4 w-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{taskProgress && (
|
||||
<div className="mt-3 rounded-2xl border border-slate-200 bg-slate-50/90 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -484,6 +465,12 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, task
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<NotificationStatusBar
|
||||
lastFetchedAt={center.lastFetchedAt}
|
||||
isOffline={center.isOffline}
|
||||
push={pushState}
|
||||
t={t}
|
||||
/>
|
||||
</div>,
|
||||
typeof document !== 'undefined' ? document.body : undefined
|
||||
)}
|
||||
@@ -719,7 +706,7 @@ function NotificationStatusBar({
|
||||
const pushButtonDisabled = push.loading || !push.supported || push.permission === 'denied';
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-2 text-[11px] text-slate-500">
|
||||
<div className="mt-4 space-y-2 border-t border-slate-200 pt-3 text-[11px] text-slate-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{t('header.notifications.lastSync', 'Zuletzt aktualisiert')}: {label}
|
||||
|
||||
Reference in New Issue
Block a user