diff --git a/resources/css/app.css b/resources/css/app.css index e97d485..9d5b04f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -660,6 +660,31 @@ html.guest-theme.dark { 50% { background-position: 100% 50%; } } +.guest-aurora { + background-size: 300% 300%; + animation: aurora 18s ease infinite; +} + +.guest-aurora-soft { + background-size: 260% 260%; + animation: aurora 26s ease-in-out infinite; +} + +@keyframes guest-cta-pulse { + 0%, 100% { + box-shadow: 0 12px 28px var(--cta-glow, rgba(0, 0, 0, 0.25)), 0 0 0 0 var(--cta-ring, rgba(0, 0, 0, 0)); + transform: translateY(0); + } + 50% { + box-shadow: 0 18px 45px var(--cta-glow, rgba(0, 0, 0, 0.35)), 0 0 0 12px var(--cta-ring, rgba(0, 0, 0, 0.12)); + transform: translateY(-1px); + } +} + +.guest-cta-pulse { + animation: guest-cta-pulse 3.2s ease-in-out infinite; +} + .bg-aurora-enhanced { background: radial-gradient(circle at 20% 80%, #a8edea 0%, #fed6e3 50%, #d299c2 100%), linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-size: 400% 400%, 400% 400%; @@ -672,6 +697,14 @@ html.guest-theme.dark { animation: aurora 20s ease infinite; } +@media (prefers-reduced-motion: reduce) { + .guest-aurora, + .guest-aurora-soft, + .guest-cta-pulse { + animation: none; + } +} + .guest-immersive .guest-header { display: none !important; } diff --git a/resources/js/guest/components/BottomNav.tsx b/resources/js/guest/components/BottomNav.tsx index 63769a5..ca81168 100644 --- a/resources/js/guest/components/BottomNav.tsx +++ b/resources/js/guest/components/BottomNav.tsx @@ -86,11 +86,12 @@ export default function BottomNav() { return (
+
handleEmotionSelect(emotion)} - className="group flex flex-col gap-2 rounded-2xl border border-muted/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70" + className="group relative flex flex-col gap-2 rounded-2xl p-[1px] text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-lg active:scale-[0.98]" + style={{ + backgroundImage: 'linear-gradient(135deg, color-mix(in oklch, var(--guest-primary) 45%, white), color-mix(in oklch, var(--guest-secondary) 40%, white))', + }} > -
- - {emotion.emoji} - -
-
{localizedName}
- {localizedDescription && ( -
{localizedDescription}
- )} +
+
+ + {emotion.emoji} + +
+
{localizedName}
+ {localizedDescription && ( +
{localizedDescription}
+ )} +
+
-
); diff --git a/resources/js/guest/components/GalleryPreview.tsx b/resources/js/guest/components/GalleryPreview.tsx index 4e6ed74..76404d1 100644 --- a/resources/js/guest/components/GalleryPreview.tsx +++ b/resources/js/guest/components/GalleryPreview.tsx @@ -8,6 +8,7 @@ import { Heart } from 'lucide-react'; import { useTranslation } from '../i18n/useTranslation'; import { useEventBranding } from '../context/EventBrandingContext'; import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; type Props = { token: string }; @@ -97,14 +98,16 @@ export default function GalleryPreview({ token }: Props) { style={{ borderRadius: radius, fontFamily: bodyFont }} > -
+
-

Live-Galerie

+
+ Live-Galerie +

Alle Uploads auf einen Blick

Alle ansehen → @@ -112,28 +115,31 @@ export default function GalleryPreview({ token }: Props) {
-
- {filters.map((filter, index) => { +
+ {filters.map((filter) => { const isActive = mode === filter.value; return ( -
- - {index < filters.length - 1 && ( - +
+ > + {isActive && ( + + )} + {filter.label} + ); })}
diff --git a/resources/js/guest/components/Header.tsx b/resources/js/guest/components/Header.tsx index c780d6d..a279fcd 100644 --- a/resources/js/guest/components/Header.tsx +++ b/resources/js/guest/components/Header.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { createPortal } from 'react-dom'; -import { Link, useLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import AppearanceToggleDropdown from '@/components/appearance-dropdown'; import { User, @@ -22,13 +22,12 @@ import { } 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, type TranslateFn } from '../i18n/useTranslation'; import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext'; import { useOptionalNotificationCenter, type NotificationCenterValue } from '../context/NotificationCenterContext'; import { usePushSubscription } from '../hooks/usePushSubscription'; -import { getContrastingTextColor, relativeLuminance } from '../lib/color'; +import { getContrastingTextColor, relativeLuminance, hexToRgb } from '../lib/color'; import { isTaskModeEnabled } from '../lib/engagement'; const EVENT_ICON_COMPONENTS: Record> = { @@ -81,6 +80,14 @@ function getInitials(name: string): string { return name.substring(0, 2).toUpperCase(); } +function toRgba(value: string, alpha: number): string { + const rgb = hexToRgb(value); + if (!rgb) { + return `rgba(255, 255, 255, ${alpha})`; + } + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`; +} + function EventAvatar({ name, icon, @@ -167,9 +174,7 @@ function EventAvatar({ } export default function Header({ eventToken, title = '' }: { eventToken?: string; title?: string }) { - const location = useLocation(); const statsContext = useOptionalEventStats(); - const identity = useOptionalGuestIdentity(); const { t } = useTranslation(); const brandingContext = useOptionalEventBranding(); const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING; @@ -206,23 +211,6 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string return () => document.removeEventListener('mousedown', handler); }, [notificationsOpen]); - if (!eventToken) { - return ( -
-
-
{title}
-
-
- - -
-
- ); - } - const headerFont = branding.typography?.heading ?? branding.fontFamily ?? undefined; const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined; const logoPosition = branding.logo?.position ?? 'left'; @@ -231,16 +219,48 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string color: headerTextColor, fontFamily: headerFont, }; + const headerGlowPrimary = toRgba(branding.primaryColor, 0.35); + const headerGlowSecondary = toRgba(branding.secondaryColor, 0.35); + const headerShimmer = `linear-gradient(120deg, ${toRgba(branding.primaryColor, 0.28)}, transparent 45%, ${toRgba(branding.secondaryColor, 0.32)})`; + const headerHairline = `linear-gradient(90deg, transparent, ${toRgba(headerTextColor, 0.4)}, transparent)`; + + if (!eventToken) { + return ( +
+
+
+
+
+
+
{title}
+
+
+ + +
+
+
+ ); + } const accentColor = branding.secondaryColor; if (status === 'loading') { return ( -
-
{t('header.loading')}
-
- - +
+
+
+
+
+
+
{t('header.loading')}
+
+ + +
); @@ -254,16 +274,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string statsContext && statsContext.eventKey === eventToken ? statsContext : undefined; return (
+
+
+
+
-
{event.name}
+
{event.name}
{stats && tasksEnabled && ( <> @@ -295,7 +319,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
-
+
{notificationCenter && eventToken && ( (); const { name, hydrated } = useGuestIdentity(); @@ -56,6 +65,14 @@ export default function HomePage() { } }, [heroStorageKey]); + React.useEffect(() => { + if (typeof document === 'undefined') { + return; + } + document.body.classList.remove('guest-immersive'); + document.body.classList.remove('guest-nav-visible'); + }, []); + const dismissHero = React.useCallback(() => { setHeroVisible(false); if (typeof window === 'undefined') { @@ -73,6 +90,13 @@ export default function HomePage() { const eventNameDisplay = event?.name ?? t('home.hero.defaultEventName'); const accentColor = branding.primaryColor; const secondaryAccent = branding.secondaryColor; + const glowPrimary = toRgba(accentColor, 0.35); + const glowSecondary = toRgba(secondaryAccent, 0.32); + const shimmerGradient = `linear-gradient(120deg, ${toRgba(accentColor, 0.22)}, transparent 45%, ${toRgba(secondaryAccent, 0.32)})`; + const welcomePanelStyle = React.useMemo(() => ({ + background: `linear-gradient(135deg, color-mix(in oklch, ${accentColor} 18%, var(--background)), color-mix(in oklch, ${secondaryAccent} 16%, var(--background)))`, + borderColor: toRgba(accentColor, 0.25), + }), [accentColor, secondaryAccent]); const uploadsRequireApproval = (event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate'; const tasksEnabled = isTaskModeEnabled(event); @@ -80,6 +104,31 @@ export default function HomePage() { const containerMotion = getMotionContainerProps(motionEnabled, STAGGER_FAST); const fadeUpMotion = getMotionItemProps(motionEnabled, FADE_UP); const fadeScaleMotion = getMotionItemProps(motionEnabled, FADE_SCALE); + const backdropStyle = React.useMemo(() => ({ + '--guest-glow-primary': glowPrimary, + '--guest-glow-secondary': glowSecondary, + }) as React.CSSProperties & Record, [glowPrimary, glowSecondary]); + + const renderWithBackdrop = (content: React.ReactNode) => ( +
+
+
+
+
+
+
+
+
+ {content} +
+
+ ); const [missionDeck, setMissionDeck] = React.useState([]); const [missionPool, setMissionPool] = React.useState([]); @@ -293,17 +342,24 @@ export default function HomePage() { const introMessage = introMessageRef.current; if (!tasksEnabled) { - return ( - + return renderWithBackdrop( + -

- {t('home.welcomeLine').replace('{name}', displayName)} -

- {introMessage &&

{introMessage}

} +
+
+

+ {t('home.welcomeLine').replace('{name}', displayName)} +

+ {introMessage &&

{introMessage}

} +
+
@@ -328,21 +384,28 @@ export default function HomePage() { ); } - return ( - + return renderWithBackdrop( + -

- {t('home.welcomeLine').replace('{name}', displayName)} -

- {introMessage && ( -

- {introMessage} -

- )} +
+
+

+ {t('home.welcomeLine').replace('{name}', displayName)} +

+ {introMessage && ( +

+ {introMessage} +

+ )} +
+
{heroVisible && ( @@ -507,6 +570,7 @@ export function MissionActionCard({ const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined; const cards = mission ? [mission, ...stack] : stack; const shellRadius = `${radius + 10}px`; + const motionEnabled = !prefersReducedMotion(); const normalizeText = (value: string | undefined | null) => (value ?? '').trim().toLowerCase().replace(/\s+/g, ' '); const [expandedTaskId, setExpandedTaskId] = React.useState(null); @@ -516,8 +580,31 @@ export function MissionActionCard({ const [showSwipeHint, setShowSwipeHint] = React.useState(false); const hintTimeoutRef = React.useRef(null); const hintStorageKey = token ? `guestMissionSwipeHintSeen_${token}` : 'guestMissionSwipeHintSeen'; - const motionEnabled = !prefersReducedMotion(); const shouldShowHint = motionEnabled && cards.length > 1 && Boolean(swipeHintLabel); + const ctaPulseKey = token ? `guestMissionCtaPulse_${token}` : 'guestMissionCtaPulse'; + const [ctaPulse, setCtaPulse] = React.useState(false); + + React.useEffect(() => { + if (!motionEnabled || typeof window === 'undefined') { + return; + } + try { + if (window.sessionStorage.getItem(ctaPulseKey)) { + return; + } + } catch { + // ignore storage exceptions + } + + setCtaPulse(true); + const timeout = window.setTimeout(() => setCtaPulse(false), 4800); + try { + window.sessionStorage.setItem(ctaPulseKey, '1'); + } catch { + // ignore storage exceptions + } + return () => window.clearTimeout(timeout); + }, [ctaPulseKey, motionEnabled]); const dismissSwipeHint = React.useCallback((persist = true) => { if (!showSwipeHint) { @@ -622,6 +709,13 @@ export function MissionActionCard({ const titleClamp = isExpanded ? '' : 'line-clamp-2 sm:line-clamp-3'; const titleClasses = `text-xl font-semibold leading-snug text-slate-900 dark:text-white sm:text-2xl break-words py-1 min-h-[3.75rem] sm:min-h-[4.5rem] ${titleClamp}`; const titleId = card ? `task-title-${card.id}` : undefined; + const ctaStyles = { + borderRadius: `${radius}px`, + background: `linear-gradient(120deg, ${primary}, ${secondary})`, + boxShadow: `0 12px 28px ${primary}25`, + '--cta-glow': toRgba(primary, 0.35), + '--cta-ring': toRgba(primary, 0.16), + } as React.CSSProperties & Record; const toggleExpanded = () => { if (!card) return; setExpandedTaskId((prev) => (prev === card.id ? null : card.id)); @@ -748,12 +842,8 @@ export function MissionActionCard({