From df414a31cdca56a594fd25cb2594acd613016556 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sun, 23 Nov 2025 22:22:06 +0100 Subject: [PATCH] =?UTF-8?q?photobooth=20funktionen=20im=20event=20admin=20?= =?UTF-8?q?verlinkt,=20g=C3=A4ste=20pwa=20zeigt=20photobooth=20nur=20noch?= =?UTF-8?q?=20an,=20wenn=20diese=20aktiviert=20ist.=20kontaktformular=20op?= =?UTF-8?q?timiert.=20teilen-link=20mit=20iMessage=20und=20whatsapp=20erwe?= =?UTF-8?q?itert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/EventPublicController.php | 19 +- app/Http/Controllers/MarketingController.php | 1 + app/Providers/AppServiceProvider.php | 9 + app/Support/WatermarkConfigResolver.php | 12 +- resources/css/app.css | 16 +- resources/js/admin/components/EventNav.tsx | 2 + resources/js/admin/pages/EventDetailPage.tsx | 9 + resources/js/admin/pages/EventPhotosPage.tsx | 37 ++- resources/js/admin/pages/EventsPage.tsx | 2 + resources/js/components/ui/Steps.tsx | 14 +- resources/js/guest/components/FiltersBar.tsx | 20 +- .../js/guest/components/GalleryPreview.tsx | 15 +- .../js/guest/context/EventBrandingContext.tsx | 3 + resources/js/guest/i18n/messages.ts | 3 + resources/js/guest/pages/GalleryPage.tsx | 230 +++++++++++++++--- resources/js/guest/pages/PhotoLightbox.tsx | 165 +++++++++++-- resources/js/guest/pages/UploadPage.tsx | 12 +- .../js/guest/polling/usePollGalleryDelta.ts | 29 ++- resources/js/guest/router.tsx | 9 +- resources/js/guest/services/eventApi.ts | 1 + resources/js/layouts/app/Footer.tsx | 30 +-- resources/js/layouts/mainWebsite.tsx | 48 ++-- resources/js/pages/marketing/Blog.tsx | 2 +- resources/js/pages/marketing/BlogShow.tsx | 16 +- .../js/pages/marketing/CheckoutWizardPage.tsx | 2 +- resources/js/pages/marketing/Home.tsx | 127 +++++++--- resources/js/pages/marketing/Kontakt.tsx | 39 ++- resources/js/pages/marketing/Packages.tsx | 110 ++++----- .../marketing/checkout/steps/PackageStep.tsx | 34 +-- resources/views/legal/kontakt.blade.php | 53 ++-- resources/views/marketing.blade.php | 18 +- routes/web.php | 2 + 32 files changed, 809 insertions(+), 280 deletions(-) diff --git a/app/Http/Controllers/Api/EventPublicController.php b/app/Http/Controllers/Api/EventPublicController.php index f9c0e4a..7e7bd89 100644 --- a/app/Http/Controllers/Api/EventPublicController.php +++ b/app/Http/Controllers/Api/EventPublicController.php @@ -918,6 +918,21 @@ class EventPublicController extends BaseController ]; } + private function resolveFontFamily(Event $event): ?string + { + $fontFamily = Arr::get($event->settings, 'branding.font_family') + ?? Arr::get($event->tenant?->settings, 'branding.font_family'); + + if (! is_string($fontFamily)) { + return null; + } + + $normalized = strtolower(trim($fontFamily)); + $defaultInter = strtolower('Inter, sans-serif'); + + return $normalized === $defaultInter ? null : $fontFamily; + } + private function encodeGalleryCursor(Photo $photo): string { return base64_encode(json_encode([ @@ -1378,8 +1393,7 @@ class EventPublicController extends BaseController ]; $branding = $this->buildGalleryBranding($event); - $fontFamily = Arr::get($event->settings, 'branding.font_family') - ?? Arr::get($event->tenant?->settings, 'branding.font_family'); + $fontFamily = $this->resolveFontFamily($event); $brandingAllowed = $this->determineBrandingAllowed($event); $logoUrl = $brandingAllowed ? (Arr::get($event->settings, 'branding.logo_url') @@ -1399,6 +1413,7 @@ class EventPublicController extends BaseController 'updated_at' => $event->updated_at, 'type' => $eventTypeData, 'join_token' => $joinToken?->token, + 'photobooth_enabled' => (bool) $event->photobooth_enabled, 'branding' => [ 'primary_color' => $branding['primary_color'], 'secondary_color' => $branding['secondary_color'], diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index c6519b1..00eecfa 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -59,6 +59,7 @@ class MarketingController extends Controller 'name' => 'required|string|max:255', 'email' => 'required|email|max:255', 'message' => 'required|string|max:1000', + 'nickname' => 'present|size:0', ]); $locale = app()->getLocale(); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 15d16c1..fe61173 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -176,6 +176,15 @@ class AppServiceProvider extends ServiceProvider return Limit::perMinute(10)->by('coupon-preview:'.$identifier); }); + RateLimiter::for('contact-form', function (Request $request) { + $ip = $request->ip() ?? 'unknown'; + + return [ + Limit::perMinute(5)->by('contact:ip:'.$ip), + Limit::perHour(30)->by('contact:hour:'.$ip), + ]; + }); + Inertia::share('locale', fn () => app()->getLocale()); Inertia::share('analytics', static function () { $config = config('services.matomo'); diff --git a/app/Support/WatermarkConfigResolver.php b/app/Support/WatermarkConfigResolver.php index 0ed9490..68ef17d 100644 --- a/app/Support/WatermarkConfigResolver.php +++ b/app/Support/WatermarkConfigResolver.php @@ -4,6 +4,7 @@ namespace App\Support; use App\Models\Event; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Schema; class WatermarkConfigResolver { @@ -35,7 +36,15 @@ class WatermarkConfigResolver ]; } - $baseSetting = WatermarkSetting::query()->first(); + $baseSetting = null; + + if (class_exists(\App\Models\WatermarkSetting::class) && \Illuminate\Support\Facades\Schema::hasTable('watermark_settings')) { + try { + $baseSetting = \App\Models\WatermarkSetting::query()->first(); + } catch (\Throwable) { + $baseSetting = null; + } + } $base = [ 'asset' => $baseSetting?->asset ?? config('watermark.base.asset', 'branding/fotospiel-watermark.png'), 'position' => $baseSetting?->position ?? config('watermark.base.position', 'bottom-right'), @@ -87,4 +96,3 @@ class WatermarkConfigResolver ]; } } -use App\Models\WatermarkSetting; diff --git a/resources/css/app.css b/resources/css/app.css index d6c19db..1f10fc8 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -115,7 +115,9 @@ --guest-primary: #f43f5e; --guest-secondary: #fb7185; --guest-background: #ffffff; - --guest-font-family: inherit; + --guest-font-family: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + --guest-heading-font: 'Playfair Display', serif; + --guest-serif-font: 'Lora', serif; } @font-face { @@ -138,6 +140,18 @@ body { font-family: var(--guest-font-family), 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } +h1, +h2, +h3, +h4, +.font-display { + font-family: var(--guest-heading-font), var(--guest-font-family), 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +} + +.font-serif { + font-family: var(--guest-serif-font), 'Lora', 'Georgia', serif; +} + @keyframes aurora { 0%, 100% { diff --git a/resources/js/admin/components/EventNav.tsx b/resources/js/admin/components/EventNav.tsx index dab1658..0952ebf 100644 --- a/resources/js/admin/components/EventNav.tsx +++ b/resources/js/admin/components/EventNav.tsx @@ -23,6 +23,7 @@ import { ADMIN_EVENT_TASKS_PATH, ADMIN_EVENT_TOOLKIT_PATH, ADMIN_EVENT_VIEW_PATH, + ADMIN_EVENT_PHOTOBOOTH_PATH, } from '../constants'; import type { TenantEvent } from '../api'; import { cn } from '@/lib/utils'; @@ -62,6 +63,7 @@ function buildEventLinks(slug: string, t: ReturnType['t'] return [ { key: 'summary', label: t('eventMenu.summary', 'Übersicht'), href: ADMIN_EVENT_VIEW_PATH(slug) }, { key: 'photos', label: t('eventMenu.photos', 'Uploads'), href: ADMIN_EVENT_PHOTOS_PATH(slug) }, + { key: 'photobooth', label: t('eventMenu.photobooth', 'Photobooth'), href: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) }, { key: 'guests', label: t('eventMenu.guests', 'Team & Gäste'), href: ADMIN_EVENT_MEMBERS_PATH(slug) }, { key: 'tasks', label: t('eventMenu.tasks', 'Aufgaben'), href: ADMIN_EVENT_TASKS_PATH(slug) }, { key: 'invites', label: t('eventMenu.invites', 'Einladungen'), href: ADMIN_EVENT_INVITES_PATH(slug) }, diff --git a/resources/js/admin/pages/EventDetailPage.tsx b/resources/js/admin/pages/EventDetailPage.tsx index 56eb1f7..e02ca91 100644 --- a/resources/js/admin/pages/EventDetailPage.tsx +++ b/resources/js/admin/pages/EventDetailPage.tsx @@ -13,6 +13,7 @@ import { MessageSquare, Printer, QrCode, + PlugZap, RefreshCw, Smile, Sparkles, @@ -47,6 +48,7 @@ import { ADMIN_EVENT_EDIT_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_MEMBERS_PATH, + ADMIN_EVENT_PHOTOBOOTH_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_TASKS_PATH, } from '../constants'; @@ -590,6 +592,13 @@ function QuickActionsCard({ slug, busy, onToggle, navigate }: { slug: string; bu description: t('events.quickActions.rolesDesc', 'Verwalte Moderatoren und Co-Leads.'), onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)), }, + { + key: 'photobooth', + icon: , + label: t('events.quickActions.photobooth', 'Photobooth anbinden'), + description: t('events.quickActions.photoboothDesc', 'FTP-Link aktivieren und Zugangsdaten kopieren.'), + onClick: () => navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(slug)), + }, { key: 'print', icon: , diff --git a/resources/js/admin/pages/EventPhotosPage.tsx b/resources/js/admin/pages/EventPhotosPage.tsx index 9b7314f..d5426b1 100644 --- a/resources/js/admin/pages/EventPhotosPage.tsx +++ b/resources/js/admin/pages/EventPhotosPage.tsx @@ -4,6 +4,7 @@ import { Camera, Loader2, Sparkles, Trash2, AlertTriangle, ShoppingCart } from ' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import toast from 'react-hot-toast'; import { AddonsPicker } from '../components/Addons/AddonsPicker'; @@ -16,7 +17,7 @@ import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { buildLimitWarnings, type EventLimitSummary } from '../lib/limitWarnings'; import { useTranslation } from 'react-i18next'; -import { ADMIN_EVENTS_PATH, ADMIN_EVENT_VIEW_PATH } from '../constants'; +import { ADMIN_EVENTS_PATH, ADMIN_EVENT_VIEW_PATH, ADMIN_EVENT_PHOTOBOOTH_PATH } from '../constants'; export default function EventPhotosPage() { const params = useParams<{ slug?: string }>(); @@ -37,6 +38,10 @@ export default function EventPhotosPage() { const [limits, setLimits] = React.useState(null); const [addons, setAddons] = React.useState([]); const [eventAddons, setEventAddons] = React.useState([]); + const photoboothUploads = React.useMemo( + () => photos.filter((photo) => photo.ingest_source === 'photobooth').length, + [photos], + ); const load = React.useCallback(async () => { if (!slug) { @@ -165,13 +170,29 @@ export default function EventPhotosPage() { )} - - - {t('photos.gallery.title', 'Galerie')} - - - {t('photos.gallery.description', 'Klick auf ein Foto, um es hervorzuheben oder zu löschen.')} - + +
+ + {t('photos.gallery.title', 'Galerie')} + + + {t('photos.gallery.description', 'Klick auf ein Foto, um es hervorzuheben oder zu löschen.')} + +
+
+ + {t('photos.gallery.photoboothCount', '{{count}} Photobooth-Uploads', { count: photoboothUploads })} + + +
{loading ? ( diff --git a/resources/js/admin/pages/EventsPage.tsx b/resources/js/admin/pages/EventsPage.tsx index f04bc9e..9668ed1 100644 --- a/resources/js/admin/pages/EventsPage.tsx +++ b/resources/js/admin/pages/EventsPage.tsx @@ -32,6 +32,7 @@ import { ADMIN_EVENT_TASKS_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_TOOLKIT_PATH, + ADMIN_EVENT_PHOTOBOOTH_PATH, } from '../constants'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { useTranslation } from 'react-i18next'; @@ -368,6 +369,7 @@ function EventCard({ { key: 'members', label: translate('events.list.actions.members', 'Mitglieder'), to: ADMIN_EVENT_MEMBERS_PATH(slug) }, { key: 'tasks', label: translate('events.list.actions.tasks', 'Tasks'), to: ADMIN_EVENT_TASKS_PATH(slug) }, { key: 'invites', label: translate('events.list.actions.invites', 'QR-Einladungen'), to: ADMIN_EVENT_INVITES_PATH(slug) }, + { key: 'photobooth', label: translate('events.list.actions.photobooth', 'Photobooth'), to: ADMIN_EVENT_PHOTOBOOTH_PATH(slug) }, { key: 'toolkit', label: translate('events.list.actions.toolkit', 'Toolkit'), to: ADMIN_EVENT_TOOLKIT_PATH(slug) }, ]; diff --git a/resources/js/components/ui/Steps.tsx b/resources/js/components/ui/Steps.tsx index 30b8e3a..50833b4 100644 --- a/resources/js/components/ui/Steps.tsx +++ b/resources/js/components/ui/Steps.tsx @@ -25,8 +25,8 @@ const Steps = React.forwardRef( className={cn( "w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium border-2 transition-colors", index <= currentStep - ? "bg-blue-500 text-white border-blue-500" - : "bg-gray-200 text-gray-500 border-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:border-gray-700" + ? "bg-primary text-primary-foreground border-primary shadow-sm" + : "bg-muted text-muted-foreground border-muted" )} > {index + 1} @@ -34,23 +34,23 @@ const Steps = React.forwardRef(

{step.title}

-

{step.description}

+

{step.description}

{step.details && index === currentStep && ( -

+

{step.details}

)}
{index < steps.length - 1 && ( -
+
diff --git a/resources/js/guest/components/FiltersBar.tsx b/resources/js/guest/components/FiltersBar.tsx index 017d34a..7dbe471 100644 --- a/resources/js/guest/components/FiltersBar.tsx +++ b/resources/js/guest/components/FiltersBar.tsx @@ -5,15 +5,27 @@ import { useTranslation } from '../i18n/useTranslation'; export type GalleryFilter = 'latest' | 'popular' | 'mine' | 'photobooth'; -const filterConfig: Array<{ value: GalleryFilter; labelKey: string; icon: React.ReactNode }> = [ +type FilterConfig = Array<{ value: GalleryFilter; labelKey: string; icon: React.ReactNode }>; + +const baseFilters: FilterConfig = [ { value: 'latest', labelKey: 'galleryPage.filters.latest', icon: }, { value: 'popular', labelKey: 'galleryPage.filters.popular', icon: }, { value: 'mine', labelKey: 'galleryPage.filters.mine', icon: }, - { value: 'photobooth', labelKey: 'galleryPage.filters.photobooth', icon: }, ]; -export default function FiltersBar({ value, onChange, className }: { value: GalleryFilter; onChange: (v: GalleryFilter) => void; className?: string }) { +export default function FiltersBar({ + value, + onChange, + className, + showPhotobooth = true, +}: { value: GalleryFilter; onChange: (v: GalleryFilter) => void; className?: string; showPhotobooth?: boolean }) { const { t } = useTranslation(); + const filters: FilterConfig = React.useMemo( + () => (showPhotobooth + ? [...baseFilters, { value: 'photobooth', labelKey: 'galleryPage.filters.photobooth', icon: }] + : baseFilters), + [showPhotobooth], + ); return (
- {filterConfig.map((filter) => ( + {filters.map((filter) => (
- + {loading &&

{t('galleryPage.loading', 'Lade…')}

}
{list.map((p: GalleryPhoto) => { @@ -272,22 +354,32 @@ export default function GalleryPage() { }} loading="lazy" /> -
-
- {localizedTaskTitle &&

{localizedTaskTitle}

} -
- {createdLabel} - {p.uploader_name || t('galleryPage.photo.anonymous', 'Gast')} +
+
+ {localizedTaskTitle &&

{localizedTaskTitle}

} +
+ {createdLabel} + {p.uploader_name || t('galleryPage.photo.anonymous', 'Gast')}
-
+
+ {localizedTaskTitle && ( + + {localizedTaskTitle} + + )} +
+
+
+ +
+ + + + +
+
+
+ )} ); } diff --git a/resources/js/guest/pages/PhotoLightbox.tsx b/resources/js/guest/pages/PhotoLightbox.tsx index 85bea37..037ff24 100644 --- a/resources/js/guest/pages/PhotoLightbox.tsx +++ b/resources/js/guest/pages/PhotoLightbox.tsx @@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react'; import { useParams, useLocation, useNavigate } from 'react-router-dom'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Heart, ChevronLeft, ChevronRight, X, Share2 } from 'lucide-react'; -import { likePhoto } from '../services/photosApi'; +import { Heart, ChevronLeft, ChevronRight, X, Share2, MessageSquare, Copy } from 'lucide-react'; +import { likePhoto, createPhotoShareLink } from '../services/photosApi'; import { useTranslation } from '../i18n/useTranslation'; -import { sharePhotoLink } from '../lib/sharePhoto'; import { useToast } from '../components/ToastHost'; type Photo = { @@ -42,7 +41,10 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh const [taskLoading, setTaskLoading] = useState(false); const [likes, setLikes] = useState(0); const [liked, setLiked] = useState(false); - const [shareLoading, setShareLoading] = useState(false); + const [shareSheet, setShareSheet] = useState<{ url: string | null; loading: boolean }>({ + url: null, + loading: false, + }); // Determine mode and photo const isStandalone = !photos || photos.length === 0; @@ -209,30 +211,61 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh } } - async function onShare() { - if (!photo || !eventToken) return; - setShareLoading(true); - try { - const result = await sharePhotoLink({ - token: eventToken, - photoId: photo.id, - title: photo.task_title ?? task?.title ?? t('share.title', 'Geteiltes Foto'), - text: t('share.shareText', { event: '' }), - }); + const shareTitle = photo?.task_title ?? task?.title ?? t('share.title', 'Geteiltes Foto'); + const shareText = t('share.shareText', { event: shareTitle || 'Fotospiel' }); - if (result.method === 'clipboard') { - toast.push({ text: t('share.copySuccess', 'Link kopiert!') }); - } else if (result.method === 'manual') { - window.prompt(t('share.manualPrompt', 'Link kopieren'), result.url); - } + const WhatsAppIcon = (props: React.SVGProps) => ( + + + + ); + + async function openShareSheet() { + if (!photo || !eventToken) return; + setShareSheet({ url: null, loading: true }); + try { + const payload = await createPhotoShareLink(eventToken, photo.id); + setShareSheet({ url: payload.url, loading: false }); } catch (error) { console.error('share failed', error); toast.push({ text: t('share.error', 'Teilen fehlgeschlagen'), type: 'error' }); - } finally { - setShareLoading(false); + setShareSheet({ url: null, loading: false }); } } + function shareWhatsApp(url?: string | null) { + if (!url) return; + const waUrl = `https://wa.me/?text=${encodeURIComponent(`${shareText} ${url}`)}`; + window.open(waUrl, '_blank', 'noopener'); + setShareSheet({ url: null, loading: false }); + } + + function shareMessages(url?: string | null) { + if (!url) return; + const smsUrl = `sms:?&body=${encodeURIComponent(`${shareText} ${url}`)}`; + window.open(smsUrl, '_blank', 'noopener'); + setShareSheet({ url: null, loading: false }); + } + + async function copyLink(url?: string | null) { + if (!url) return; + try { + await navigator.clipboard?.writeText(url); + toast.push({ text: t('share.copySuccess', 'Link kopiert!') }); + } catch { + toast.push({ text: t('share.copyError', 'Link konnte nicht kopiert werden.'), type: 'error' }); + } finally { + setShareSheet({ url: null, loading: false }); + } + } + + function closeShareSheet() { + setShareSheet({ url: null, loading: false }); + } + function onOpenChange(open: boolean) { if (!open) handleClose(); } @@ -255,8 +288,8 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
)} + + {(shareSheet.url !== null || shareSheet.loading) && ( +
+
+
+
+

+ {t('share.title', 'Geteiltes Foto')} +

+

#{photo?.id}

+
+ +
+ +
+ + + + +
+
+
+ )} ); diff --git a/resources/js/guest/pages/UploadPage.tsx b/resources/js/guest/pages/UploadPage.tsx index 8d304f4..b2b230f 100644 --- a/resources/js/guest/pages/UploadPage.tsx +++ b/resources/js/guest/pages/UploadPage.tsx @@ -279,7 +279,7 @@ const [canUpload, setCanUpload] = useState(true); // Check upload limits useEffect(() => { - if (!eventKey || !task) return; + if (!eventKey) return; const checkLimits = async () => { try { @@ -326,7 +326,7 @@ const [canUpload, setCanUpload] = useState(true); }; checkLimits(); - }, [eventKey, task, t]); + }, [eventKey, t]); const stopStream = useCallback(() => { if (streamRef.current) { @@ -542,19 +542,19 @@ const [canUpload, setCanUpload] = useState(true); const navigateAfterUpload = useCallback( (photoId: number | undefined) => { - if (!eventKey || !task) return; + if (!eventKey) return; const params = new URLSearchParams(); params.set('uploaded', 'true'); - if (task.id) params.set('task', String(task.id)); + if (task?.id) params.set('task', String(task.id)); if (photoId) params.set('photo', String(photoId)); if (emotionSlug) params.set('emotion', emotionSlug); navigate(`/e/${encodeURIComponent(eventKey)}/gallery?${params.toString()}`); }, - [emotionSlug, navigate, eventKey, task] + [emotionSlug, navigate, eventKey, task?.id] ); const handleUsePhoto = useCallback(async () => { - if (!eventKey || !reviewPhoto || !task || !canUpload) return; + if (!eventKey || !reviewPhoto || !canUpload) return; setMode('uploading'); setUploadProgress(2); setUploadError(null); diff --git a/resources/js/guest/polling/usePollGalleryDelta.ts b/resources/js/guest/polling/usePollGalleryDelta.ts index bb10fb3..bba6562 100644 --- a/resources/js/guest/polling/usePollGalleryDelta.ts +++ b/resources/js/guest/polling/usePollGalleryDelta.ts @@ -64,17 +64,22 @@ export function usePollGalleryDelta(token: string, locale: LocaleCode) { if (newPhotos.length > 0) { const added = newPhotos.length; - - if (latestAt.current) { - // Delta mode: Add new photos to existing list - const merged = [...newPhotos, ...photos]; - const byId = new Map(); - merged.forEach((photo) => byId.set(photo.id, photo)); - setPhotos(Array.from(byId.values())); - if (added > 0) setNewCount((c) => c + added); - } else { - // Initial load: Set all photos - setPhotos(newPhotos); + const hasBaseline = latestAt.current !== null; + + setPhotos((prev) => { + if (hasBaseline) { + // Delta mode: merge new photos with existing list by id + const merged = [...newPhotos, ...prev]; + const byId = new Map(); + merged.forEach((photo) => byId.set(photo.id, photo)); + return Array.from(byId.values()); + } + + return newPhotos; + }); + + if (hasBaseline && added > 0) { + setNewCount((c) => c + added); } // Update latest timestamp @@ -103,7 +108,7 @@ export function usePollGalleryDelta(token: string, locale: LocaleCode) { setLoading(false); // Don't update state on error - keep previous photos } - }, [locale, photos, token]); + }, [locale, token]); useEffect(() => { const onVis = () => setVisible(document.visibilityState === 'visible'); diff --git a/resources/js/guest/router.tsx b/resources/js/guest/router.tsx index c155f58..1171ab5 100644 --- a/resources/js/guest/router.tsx +++ b/resources/js/guest/router.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Button } from '@/components/ui/button'; -import { createBrowserRouter, Outlet, useParams, Link } from 'react-router-dom'; +import { createBrowserRouter, Outlet, useParams, Link, Navigate } from 'react-router-dom'; import Header from './components/Header'; import BottomNav from './components/BottomNav'; import { useEventData } from './hooks/useEventData'; import { AlertTriangle, Loader2 } from 'lucide-react'; import { EventStatsProvider } from './context/EventStatsContext'; -import { GuestIdentityProvider } from './context/GuestIdentityContext'; +import { GuestIdentityProvider, useOptionalGuestIdentity } from './context/GuestIdentityContext'; import { EventBrandingProvider } from './context/EventBrandingContext'; import { LocaleProvider } from './i18n/LocaleContext'; import { DEFAULT_LOCALE, isLocaleCode } from './i18n/messages'; @@ -92,6 +92,7 @@ export const router = createBrowserRouter([ ]); function EventBoundary({ token }: { token: string }) { + const identity = useOptionalGuestIdentity(); const { event, status, error, errorCode } = useEventData(); if (status === 'loading') { @@ -102,6 +103,10 @@ function EventBoundary({ token }: { token: string }) { return ; } + if (identity?.hydrated && !identity.name) { + return ; + } + const eventLocale = isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE; const localeStorageKey = `guestLocale_event_${event.id ?? token}`; const branding = mapEventBranding(event.branding); diff --git a/resources/js/guest/services/eventApi.ts b/resources/js/guest/services/eventApi.ts index e6ff18e..95f44c6 100644 --- a/resources/js/guest/services/eventApi.ts +++ b/resources/js/guest/services/eventApi.ts @@ -16,6 +16,7 @@ export interface EventData { created_at: string; updated_at: string; join_token?: string | null; + photobooth_enabled?: boolean | null; type?: { slug: string; name: string; diff --git a/resources/js/layouts/app/Footer.tsx b/resources/js/layouts/app/Footer.tsx index d388944..7574488 100644 --- a/resources/js/layouts/app/Footer.tsx +++ b/resources/js/layouts/app/Footer.tsx @@ -20,7 +20,7 @@ const Footer: React.FC = () => { const currentYear = new Date().getFullYear(); return ( -