From 1719d96fed1f3adf3c7b65ae22c1bf0c0e7c53f9 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 12 Dec 2025 13:38:06 +0100 Subject: [PATCH] removed the old event admin components and pages --- .../{components => }/DevTenantSwitcher.tsx | 59 +- .../components/Addons/AddonSummaryList.tsx | 66 - .../admin/components/Addons/AddonsPicker.tsx | 64 - resources/js/admin/components/AdminLayout.tsx | 400 --- .../js/admin/components/CommandShelf.tsx | 415 ---- resources/js/admin/components/EventNav.tsx | 213 -- .../js/admin/components/FloatingActionBar.tsx | 70 - .../admin/components/GuestBroadcastCard.tsx | 256 -- .../js/admin/components/LanguageSwitcher.tsx | 77 - .../admin/components/NotificationCenter.tsx | 296 --- resources/js/admin/components/UserMenu.tsx | 167 -- .../dashboard/DashboardEventFocusCard.tsx | 205 -- .../admin/components/tenant/action-grid.tsx | 38 - .../admin/components/tenant/checklist-row.tsx | 64 - .../components/tenant/frosted-surface.tsx | 32 - .../js/admin/components/tenant/hero-card.tsx | 87 - resources/js/admin/components/tenant/index.ts | 8 - .../tenant/onboarding-checklist-card.tsx | 94 - .../admin/components/tenant/section-card.tsx | 41 - .../admin/components/tenant/stat-carousel.tsx | 34 - resources/js/admin/constants.ts | 51 +- resources/js/admin/dev-tools.ts | 2 +- resources/js/admin/main.tsx | 27 +- .../{pages => mobile}/AuthCallbackPage.tsx | 0 resources/js/admin/mobile/EventDetailPage.tsx | 9 + resources/js/admin/mobile/EventPhotosPage.tsx | 257 +- resources/js/admin/mobile/EventRecapPage.tsx | 442 ++++ .../{pages => mobile}/LoginStartPage.tsx | 0 .../js/admin/{pages => mobile}/LogoutPage.tsx | 0 .../js/admin/mobile/QrLayoutCustomizePage.tsx | 5 +- resources/js/admin/mobile/QrPrintPage.tsx | 33 +- .../js/admin/mobile/__tests__/addons.test.ts | 26 + .../mobile/__tests__/eventDetail.test.ts | 21 + .../mobile/__tests__/qrLayoutUtils.test.ts | 5 +- resources/js/admin/mobile/addons.ts | 16 + .../admin/mobile/components/MobileShell.tsx | 13 +- resources/js/admin/mobile/eventDate.ts | 7 + .../invite-layout/DesignerCanvas.tsx | 0 .../invite-layout/backgrounds.ts | 0 .../invite-layout/export-utils.ts | 0 .../invite-layout/fileNames.ts | 0 .../invite-layout/schema.ts | 0 resources/js/admin/mobile/qr/utils.ts | 73 + .../__tests__/WelcomeLandingPage.test.tsx | 79 - .../WelcomeOrderSummary.checkout.test.tsx | 73 - .../admin/onboarding/__tests__/store.test.tsx | 47 - .../components/OnboardingCTAList.tsx | 71 - .../components/OnboardingHighlightsGrid.tsx | 55 - .../components/TenantWelcomeLayout.tsx | 75 - .../onboarding/components/WelcomeHero.tsx | 98 - .../onboarding/components/WelcomeStepCard.tsx | 67 - .../onboarding/hooks/useTenantPackages.ts | 48 - resources/js/admin/onboarding/index.ts | 7 - .../onboarding/pages/WelcomeLandingPage.tsx | 55 - .../pages/WelcomeOrderSummaryPage.tsx | 60 - resources/js/admin/onboarding/store.tsx | 221 -- resources/js/admin/pages/BillingPage.tsx | 827 ------- resources/js/admin/pages/DashboardPage.tsx | 968 -------- resources/js/admin/pages/EmotionsPage.tsx | 429 ---- resources/js/admin/pages/EngagementPage.tsx | 200 -- .../js/admin/pages/EventBrandingPage.tsx | 325 --- resources/js/admin/pages/EventDetailPage.tsx | 426 ---- resources/js/admin/pages/EventFormPage.tsx | 779 ------ resources/js/admin/pages/EventInvitesPage.tsx | 1735 ------------- resources/js/admin/pages/EventMembersPage.tsx | 356 --- .../js/admin/pages/EventPhotoboothPage.tsx | 876 ------- resources/js/admin/pages/EventPhotosPage.tsx | 1034 -------- resources/js/admin/pages/EventRecapPage.tsx | 930 ------- resources/js/admin/pages/EventTasksPage.tsx | 1681 ------------- resources/js/admin/pages/EventToolkitPage.tsx | 7 - resources/js/admin/pages/EventsPage.tsx | 382 --- resources/js/admin/pages/FaqPage.tsx | 245 -- resources/js/admin/pages/LiveRedirectPage.tsx | 37 - resources/js/admin/pages/LoginPage.tsx | 212 -- resources/js/admin/pages/ProfilePage.tsx | 421 ---- resources/js/admin/pages/SettingsPage.tsx | 407 --- .../js/admin/pages/TaskCollectionsPage.tsx | 445 ---- resources/js/admin/pages/TasksPage.tsx | 552 ----- .../js/admin/pages/WelcomeTeaserPage.tsx | 493 ---- .../__tests__/DashboardPage.guard.test.tsx | 66 - .../__tests__/WelcomeTeaserPage.test.tsx | 79 - .../InviteLayoutCustomizerPanel.tsx | 2181 ----------------- resources/js/admin/router.tsx | 91 +- resources/js/admin/tamagui/primitives.tsx | 159 -- vitest.config.ts | 3 + 85 files changed, 994 insertions(+), 19981 deletions(-) rename resources/js/admin/{components => }/DevTenantSwitcher.tsx (64%) delete mode 100644 resources/js/admin/components/Addons/AddonSummaryList.tsx delete mode 100644 resources/js/admin/components/Addons/AddonsPicker.tsx delete mode 100644 resources/js/admin/components/AdminLayout.tsx delete mode 100644 resources/js/admin/components/CommandShelf.tsx delete mode 100644 resources/js/admin/components/EventNav.tsx delete mode 100644 resources/js/admin/components/FloatingActionBar.tsx delete mode 100644 resources/js/admin/components/GuestBroadcastCard.tsx delete mode 100644 resources/js/admin/components/LanguageSwitcher.tsx delete mode 100644 resources/js/admin/components/NotificationCenter.tsx delete mode 100644 resources/js/admin/components/UserMenu.tsx delete mode 100644 resources/js/admin/components/dashboard/DashboardEventFocusCard.tsx delete mode 100644 resources/js/admin/components/tenant/action-grid.tsx delete mode 100644 resources/js/admin/components/tenant/checklist-row.tsx delete mode 100644 resources/js/admin/components/tenant/frosted-surface.tsx delete mode 100644 resources/js/admin/components/tenant/hero-card.tsx delete mode 100644 resources/js/admin/components/tenant/index.ts delete mode 100644 resources/js/admin/components/tenant/onboarding-checklist-card.tsx delete mode 100644 resources/js/admin/components/tenant/section-card.tsx delete mode 100644 resources/js/admin/components/tenant/stat-carousel.tsx rename resources/js/admin/{pages => mobile}/AuthCallbackPage.tsx (100%) create mode 100644 resources/js/admin/mobile/EventRecapPage.tsx rename resources/js/admin/{pages => mobile}/LoginStartPage.tsx (100%) rename resources/js/admin/{pages => mobile}/LogoutPage.tsx (100%) create mode 100644 resources/js/admin/mobile/__tests__/addons.test.ts create mode 100644 resources/js/admin/mobile/__tests__/eventDetail.test.ts create mode 100644 resources/js/admin/mobile/addons.ts create mode 100644 resources/js/admin/mobile/eventDate.ts rename resources/js/admin/{pages/components => mobile}/invite-layout/DesignerCanvas.tsx (100%) rename resources/js/admin/{pages/components => mobile}/invite-layout/backgrounds.ts (100%) rename resources/js/admin/{pages/components => mobile}/invite-layout/export-utils.ts (100%) rename resources/js/admin/{pages/components => mobile}/invite-layout/fileNames.ts (100%) rename resources/js/admin/{pages/components => mobile}/invite-layout/schema.ts (100%) create mode 100644 resources/js/admin/mobile/qr/utils.ts delete mode 100644 resources/js/admin/onboarding/__tests__/WelcomeLandingPage.test.tsx delete mode 100644 resources/js/admin/onboarding/__tests__/WelcomeOrderSummary.checkout.test.tsx delete mode 100644 resources/js/admin/onboarding/__tests__/store.test.tsx delete mode 100644 resources/js/admin/onboarding/components/OnboardingCTAList.tsx delete mode 100644 resources/js/admin/onboarding/components/OnboardingHighlightsGrid.tsx delete mode 100644 resources/js/admin/onboarding/components/TenantWelcomeLayout.tsx delete mode 100644 resources/js/admin/onboarding/components/WelcomeHero.tsx delete mode 100644 resources/js/admin/onboarding/components/WelcomeStepCard.tsx delete mode 100644 resources/js/admin/onboarding/hooks/useTenantPackages.ts delete mode 100644 resources/js/admin/onboarding/index.ts delete mode 100644 resources/js/admin/onboarding/pages/WelcomeLandingPage.tsx delete mode 100644 resources/js/admin/onboarding/pages/WelcomeOrderSummaryPage.tsx delete mode 100644 resources/js/admin/onboarding/store.tsx delete mode 100644 resources/js/admin/pages/BillingPage.tsx delete mode 100644 resources/js/admin/pages/DashboardPage.tsx delete mode 100644 resources/js/admin/pages/EmotionsPage.tsx delete mode 100644 resources/js/admin/pages/EngagementPage.tsx delete mode 100644 resources/js/admin/pages/EventBrandingPage.tsx delete mode 100644 resources/js/admin/pages/EventDetailPage.tsx delete mode 100644 resources/js/admin/pages/EventFormPage.tsx delete mode 100644 resources/js/admin/pages/EventInvitesPage.tsx delete mode 100644 resources/js/admin/pages/EventMembersPage.tsx delete mode 100644 resources/js/admin/pages/EventPhotoboothPage.tsx delete mode 100644 resources/js/admin/pages/EventPhotosPage.tsx delete mode 100644 resources/js/admin/pages/EventRecapPage.tsx delete mode 100644 resources/js/admin/pages/EventTasksPage.tsx delete mode 100644 resources/js/admin/pages/EventToolkitPage.tsx delete mode 100644 resources/js/admin/pages/EventsPage.tsx delete mode 100644 resources/js/admin/pages/FaqPage.tsx delete mode 100644 resources/js/admin/pages/LiveRedirectPage.tsx delete mode 100644 resources/js/admin/pages/LoginPage.tsx delete mode 100644 resources/js/admin/pages/ProfilePage.tsx delete mode 100644 resources/js/admin/pages/SettingsPage.tsx delete mode 100644 resources/js/admin/pages/TaskCollectionsPage.tsx delete mode 100644 resources/js/admin/pages/TasksPage.tsx delete mode 100644 resources/js/admin/pages/WelcomeTeaserPage.tsx delete mode 100644 resources/js/admin/pages/__tests__/DashboardPage.guard.test.tsx delete mode 100644 resources/js/admin/pages/__tests__/WelcomeTeaserPage.test.tsx delete mode 100644 resources/js/admin/pages/components/InviteLayoutCustomizerPanel.tsx delete mode 100644 resources/js/admin/tamagui/primitives.tsx diff --git a/resources/js/admin/components/DevTenantSwitcher.tsx b/resources/js/admin/DevTenantSwitcher.tsx similarity index 64% rename from resources/js/admin/components/DevTenantSwitcher.tsx rename to resources/js/admin/DevTenantSwitcher.tsx index 27fc537..9786f76 100644 --- a/resources/js/admin/components/DevTenantSwitcher.tsx +++ b/resources/js/admin/DevTenantSwitcher.tsx @@ -21,9 +21,10 @@ declare global { type DevTenantSwitcherProps = { bottomOffset?: number; + variant?: 'floating' | 'inline'; }; -export function DevTenantSwitcher({ bottomOffset = 16 }: DevTenantSwitcherProps) { +export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: DevTenantSwitcherProps) { const helper = window.fotospielDemoAuth; const [loggingIn, setLoggingIn] = React.useState(null); const [collapsed, setCollapsed] = React.useState(() => { @@ -55,6 +56,62 @@ export function DevTenantSwitcher({ bottomOffset = 16 }: DevTenantSwitcherProps) return null; } + if (variant === 'inline') { + if (collapsed) { + return ( + + ); + } + + return ( +
+
+
+
+ Demo tenants + Dev mode +
+ +
+
+ {DEV_TENANT_KEYS.map(({ key, label }) => ( + + ))} +
+
+
+ ); + } + if (collapsed) { return ( - - ); -} diff --git a/resources/js/admin/components/AdminLayout.tsx b/resources/js/admin/components/AdminLayout.tsx deleted file mode 100644 index 798c4f0..0000000 --- a/resources/js/admin/components/AdminLayout.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import React from 'react'; -import { Link, NavLink, useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { LayoutDashboard, CalendarDays, Settings } from 'lucide-react'; -import toast from 'react-hot-toast'; -import { cn } from '@/lib/utils'; -import { Badge } from '@/components/ui/badge'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from '@/components/ui/sheet'; -import { - ADMIN_HOME_PATH, - ADMIN_EVENTS_PATH, - ADMIN_EVENT_VIEW_PATH, - ADMIN_EVENT_PHOTOS_PATH, - ADMIN_SETTINGS_PATH, - ADMIN_BILLING_PATH, -} from '../constants'; -import { registerApiErrorListener } from '../lib/apiError'; -import { getDashboardSummary, getEvents, getTenantPackagesOverview } from '../api'; -import { NotificationCenter } from './NotificationCenter'; -import { UserMenu } from './UserMenu'; -import { useEventContext } from '../context/EventContext'; -import { EventSwitcher, EventMenuBar } from './EventNav'; -import { useAuth } from '../auth/context'; -import { CommandShelf } from './CommandShelf'; - -type NavItem = { - key: string; - to: string; - label: string; - icon: React.ComponentType>; - end?: boolean; - highlight?: boolean; - prefetchKey?: string; -}; - -type PageTab = { - key: string; - label: string; - href: string; - badge?: React.ReactNode; -}; - -interface AdminLayoutProps { - title: string; - subtitle?: string; - actions?: React.ReactNode; - children: React.ReactNode; - disableCommandShelf?: boolean; - tabs?: PageTab[]; - currentTabKey?: string; -} - -export function AdminLayout({ title, subtitle, actions, children, disableCommandShelf, tabs, currentTabKey }: AdminLayoutProps) { - const { t } = useTranslation('common'); - const prefetchedPathsRef = React.useRef>(new Set()); - const { events } = useEventContext(); - const singleEvent = events.length === 1 ? events[0] : null; - const eventsPath = singleEvent?.slug ? ADMIN_EVENT_VIEW_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH; - const eventsLabel = events.length === 1 - ? t('navigation.event', { defaultValue: 'Event' }) - : t('navigation.events'); - - const photosPath = singleEvent?.slug ? ADMIN_EVENT_PHOTOS_PATH(singleEvent.slug) : ADMIN_EVENTS_PATH; - const billingLabel = t('navigation.billing', { defaultValue: 'Paket' }); - - const baseNavItems = React.useMemo(() => [ - { - key: 'dashboard', - to: ADMIN_HOME_PATH, - label: t('navigation.dashboard'), - icon: LayoutDashboard, - end: true, - prefetchKey: ADMIN_HOME_PATH, - }, - { - key: 'events', - to: eventsPath, - label: eventsLabel, - icon: CalendarDays, - end: Boolean(singleEvent?.slug), - highlight: events.length === 1, - prefetchKey: ADMIN_EVENTS_PATH, - }, - { - key: 'billing', - to: ADMIN_BILLING_PATH, - label: billingLabel, - icon: Settings, - prefetchKey: ADMIN_BILLING_PATH, - }, - ], [eventsLabel, eventsPath, billingLabel, singleEvent, events.length, t]); - - const { user } = useAuth(); - const isMember = user?.role === 'member'; - - const navItems = React.useMemo( - () => baseNavItems.filter((item) => { - if (!isMember) { - return true; - } - return !['dashboard', 'billing'].includes(item.key); - }), - [baseNavItems, isMember], - ); - - const prefetchers = React.useMemo(() => ({ - [ADMIN_HOME_PATH]: () => - Promise.all([ - getDashboardSummary(), - getEvents(), - getTenantPackagesOverview(), - ]).then(() => undefined), - [ADMIN_EVENTS_PATH]: () => getEvents().then(() => undefined), - [ADMIN_BILLING_PATH]: () => getTenantPackagesOverview().then(() => undefined), - [ADMIN_SETTINGS_PATH]: () => Promise.resolve(), - }), []); - - const triggerPrefetch = React.useCallback( - (path: string) => { - if (prefetchedPathsRef.current.has(path)) { - return; - } - - const runner = prefetchers[path as keyof typeof prefetchers]; - if (!runner) { - return; - } - - prefetchedPathsRef.current.add(path); - Promise.resolve(runner()).catch(() => { - prefetchedPathsRef.current.delete(path); - }); - }, - [prefetchers], - ); - - React.useEffect(() => { - document.body.classList.add('tenant-admin-theme'); - return () => { - document.body.classList.remove('tenant-admin-theme'); - }; - }, []); - - React.useEffect(() => { - const unsubscribe = registerApiErrorListener((detail) => { - const fallback = t('errors.generic'); - const message = detail?.message?.trim() ? detail.message : fallback; - toast.error(message, { - id: detail?.code ? `api-error-${detail.code}` : undefined, - }); - }); - - return unsubscribe; - }, [t]); - - return ( -
-
-
-
-
-
-
-

{t('app.brand')}

-
-

{title}

- {subtitle ?

{subtitle}

: null} -
-
-
- {disableCommandShelf ? : null} - {actions} - - -
-
- - {disableCommandShelf ? : } - {tabs && tabs.length ? : null} -
- -
-
{children}
-
- - -
-
- ); -} - -function PageTabsNav({ tabs, currentKey }: { tabs: PageTab[]; currentKey?: string }) { - const location = useLocation(); - const { t } = useTranslation('common'); - const [mobileOpen, setMobileOpen] = React.useState(false); - - const isActive = (tab: PageTab): boolean => { - if (currentKey) { - return tab.key === currentKey; - } - return location.pathname === tab.href || location.pathname.startsWith(tab.href); - }; - - const activeTab = React.useMemo(() => tabs.find((tab) => isActive(tab)), [tabs, location.pathname, currentKey]); - - const handleTabClick = React.useCallback( - (tab: PageTab) => { - setMobileOpen(false); - const [path, hash] = tab.href.split('#'); - if (location.pathname === path && hash) { - window.location.hash = `#${hash}`; - } - }, - [location.pathname], - ); - - return ( -
-
-
- {tabs.map((tab) => { - const active = isActive(tab); - return ( - handleTabClick(tab)} - className={cn( - 'flex items-center gap-2 rounded-2xl px-4 py-2 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60', - active - ? 'bg-rose-600 text-white shadow shadow-rose-300/40' - : 'bg-white text-slate-600 hover:text-slate-900 dark:bg-white/5 dark:text-slate-300 dark:hover:text-white' - )} - > - {tab.label} - {tab.badge !== undefined ? ( - - {tab.badge} - - ) : null} - - ); - })} -
-
- - - - - - - - {t('navigation.tabs.title')} - - - {t('navigation.tabs.subtitle')} - - -
- {tabs.map((tab) => { - const active = isActive(tab); - return ( - { - handleTabClick(tab); - setMobileOpen(false); - }} - className={cn( - 'flex items-center justify-between rounded-2xl border px-4 py-3 text-sm font-medium shadow-sm transition', - active - ? 'border-rose-200 bg-rose-50 text-rose-700' - : 'border-slate-200 bg-white text-slate-700 dark:border-white/10 dark:bg-white/5 dark:text-slate-200' - )} - > - {tab.label} - {tab.badge !== undefined ? ( - - {tab.badge} - - ) : null} - - ); - })} -
-
-
-
-
-
- ); -} - -function TenantMobileNav({ - items, - onPrefetch, -}: { - items: NavItem[]; - onPrefetch: (path: string) => void; -}) { - const { t } = useTranslation('common'); - - return ( - - ); -} - diff --git a/resources/js/admin/components/CommandShelf.tsx b/resources/js/admin/components/CommandShelf.tsx deleted file mode 100644 index 544f171..0000000 --- a/resources/js/admin/components/CommandShelf.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { - AlertTriangle, - Camera, - ClipboardList, - MessageSquare, - PlugZap, - PlusCircle, - QrCode, - Sparkles, -} from 'lucide-react'; - -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from '@/components/ui/sheet'; -import { useEventContext } from '../context/EventContext'; -import { EventSwitcher, EventMenuBar } from './EventNav'; -import { - ADMIN_EVENT_CREATE_PATH, - ADMIN_EVENT_MEMBERS_PATH, - ADMIN_EVENT_INVITES_PATH, - ADMIN_EVENT_PHOTOS_PATH, - ADMIN_EVENT_TASKS_PATH, - ADMIN_EVENT_PHOTOBOOTH_PATH, - ADMIN_EVENT_VIEW_PATH, -} from '../constants'; -import { formatEventDate, resolveEngagementMode, resolveEventDisplayName } from '../lib/events'; - -const MOBILE_SHELF_COACHMARK_KEY = 'tenant-admin:command-shelf-mobile-tip'; - -type CommandAction = { - key: string; - label: string; - description: string; - icon: React.ComponentType>; - href: string; -}; - -function formatNumber(value?: number | null): string { - if (typeof value !== 'number') { - return '–'; - } - if (value > 999) { - return `${(value / 1000).toFixed(1)}k`; - } - return String(value); -} - -export function CommandShelf() { - const { events, activeEvent, isLoading, isError, refetch } = useEventContext(); - const { t, i18n } = useTranslation('common'); - const navigate = useNavigate(); - const [mobileShelfOpen, setMobileShelfOpen] = React.useState(false); - const [coachmarkDismissed, setCoachmarkDismissed] = React.useState(() => { - if (typeof window === 'undefined') { - return false; - } - return window.localStorage.getItem(MOBILE_SHELF_COACHMARK_KEY) === '1'; - }); - - if (isLoading) { - return ( -
-
-
-
- {Array.from({ length: 4 }).map((_, index) => ( -
- ))} -
-
-
- ); - } - - if (isError) { - return ( -
-
-
- -
-

- {t('commandShelf.error.title', 'Events konnten nicht geladen werden')} -

-

- {t('commandShelf.error.hint', 'Bitte versuche es erneut oder lade die Seite neu.')} -

-
-
-
- - -
-
-
- ); - } - - if (!events.length) { - // Hide the empty hero entirely; dashboard content already handles the zero-events case. - return null; - } - - if (!activeEvent) { - return ( -
-
-
- -
-

- {t('commandShelf.selectEvent.title', 'Kein aktives Event ausgewählt')} -

-

- {t('commandShelf.selectEvent.hint', 'Wähle unten ein Event aus, um Status und Aktionen zu sehen.')} -

-
-
-
- -
-
-
- ); - } - - const slug = activeEvent.slug; - const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; - const formattedDate = formatEventDate(activeEvent.event_date, locale); - const engagementMode = resolveEngagementMode(activeEvent); - const handleActionClick = React.useCallback((href: string, closeSheet = false) => { - if (closeSheet) { - setMobileShelfOpen(false); - } - navigate(href); - }, [navigate]); - const handleDismissCoachmark = React.useCallback(() => { - setCoachmarkDismissed(true); - if (typeof window !== 'undefined') { - window.localStorage.setItem(MOBILE_SHELF_COACHMARK_KEY, '1'); - } - }, []); - const showCoachmark = !coachmarkDismissed && !mobileShelfOpen; - - const actionItems: CommandAction[] = [ - { - key: 'photos', - label: t('commandShelf.actions.photos.label', 'Fotos moderieren'), - description: t('commandShelf.actions.photos.desc', 'Prüfe neue Uploads, Highlights & Sperren.'), - icon: Camera, - href: ADMIN_EVENT_PHOTOS_PATH(slug), - }, - { - key: 'tasks', - label: t('commandShelf.actions.tasks.label', 'Aufgaben pflegen'), - description: t('commandShelf.actions.tasks.desc', 'Mission Cards & Moderation im Blick.'), - icon: ClipboardList, - href: ADMIN_EVENT_TASKS_PATH(slug), - }, - { - key: 'invites', - label: t('commandShelf.actions.invites.label', 'QR-Codes'), - description: t('commandShelf.actions.invites.desc', 'Layouts exportieren oder Links kopieren.'), - icon: QrCode, - href: ADMIN_EVENT_INVITES_PATH(slug), - }, - { - key: 'photobooth', - label: t('commandShelf.actions.photobooth.label', 'Photobooth anbinden'), - description: t('commandShelf.actions.photobooth.desc', 'FTP-Zugang und Rate-Limits steuern.'), - icon: PlugZap, - href: ADMIN_EVENT_PHOTOBOOTH_PATH(slug), - }, - { - key: 'members', - label: t('eventMenu.guests', 'Team & Gäste'), - description: t('commandShelf.actions.toolkit.desc', 'Broadcasts, Aufgaben & Quicklinks.'), - icon: MessageSquare, - href: ADMIN_EVENT_MEMBERS_PATH(slug), - }, - ]; - - const metrics = [ - { - key: 'photos', - label: t('commandShelf.metrics.photos', 'Uploads'), - value: activeEvent.photo_count, - hint: t('commandShelf.metrics.total', 'gesamt'), - }, - { - key: 'pending', - label: t('commandShelf.metrics.pending', 'Moderation'), - value: activeEvent.pending_photo_count, - hint: t('commandShelf.metrics.pendingHint', 'offen'), - }, - { - key: 'tasks', - label: t('commandShelf.metrics.tasks', 'Aufgaben'), - value: activeEvent.tasks_count, - hint: t('commandShelf.metrics.tasksHint', 'aktiv'), - }, - { - key: 'invites', - label: t('commandShelf.metrics.invites', 'QR-Codes'), - value: activeEvent.active_invites_count ?? activeEvent.total_invites_count, - hint: t('commandShelf.metrics.invitesHint', 'live'), - }, - ]; - - const statusLabel = activeEvent.status === 'published' - ? t('commandShelf.status.published', 'Veröffentlicht') - : t('commandShelf.status.draft', 'Entwurf'); - - const liveBadge = activeEvent.is_active - ? t('commandShelf.status.live', 'Live für Gäste') - : t('commandShelf.status.hidden', 'Versteckt'); - - const engagementLabel = engagementMode === 'photo_only' - ? t('commandShelf.status.photoOnly', 'Nur Foto-Modus') - : t('commandShelf.status.tasksMode', 'Mission Cards aktiv'); - - return ( - <> -
-
-
-
-

- {t('commandShelf.sectionTitle', 'Aktuelles Event')} -

-
-

- {resolveEventDisplayName(activeEvent)} -

- - {statusLabel} - - - {liveBadge} - - {engagementMode ? ( - - {engagementLabel} - - ) : null} -
-

- {formattedDate ? `${formattedDate} · ` : ''} - {activeEvent.package?.name ?? t('commandShelf.packageFallback', 'Standard-Paket')} -

-
-
- - -
-
- -
- {metrics.map((metric) => ( -
-

{formatNumber(metric.value)}

-

- {metric.label} -

-

{metric.hint}

-
- ))} -
- -
- {actionItems.map((action) => ( - - ))} -
- - -
-
- -
-
-
-
-

{resolveEventDisplayName(activeEvent)}

- - {statusLabel} - - - {liveBadge} - -
-

- {formattedDate ? `${formattedDate} · ` : ''} - {activeEvent.package?.name ?? t('commandShelf.packageFallback', 'Standard-Paket')} -

- {engagementMode ? ( -

{engagementLabel}

- ) : null} -
-
- {metrics.map((metric) => ( -
-

{formatNumber(metric.value)}

-

{metric.label}

-

{metric.hint}

-
- ))} -
- - {showCoachmark ? ( -
-

{t('commandShelf.mobile.tip', 'Tipp: Öffne hier deine wichtigsten Aktionen am Eventtag.')}

-
- -
-
- ) : null} - - - - - - -
-
- - - {t('commandShelf.mobile.sheetTitle', 'Schnellaktionen')} - - - {t('commandShelf.mobile.sheetDescription', 'Moderation, Aufgaben und QR-Codes an einem Ort.')} - - -
- {metrics.map((metric) => ( -
- {formatNumber(metric.value)} - {metric.label} -
- ))} -
-
- {actionItems.map((action) => ( - - ))} -
-
- -
-
- - -
-
- - ); -} diff --git a/resources/js/admin/components/EventNav.tsx b/resources/js/admin/components/EventNav.tsx deleted file mode 100644 index a797447..0000000 --- a/resources/js/admin/components/EventNav.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React from 'react'; -import { useNavigate, NavLink, useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { CalendarDays, ChevronDown, PlusCircle } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { type TenantEvent } from '../api'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from '@/components/ui/sheet'; - -import { useEventContext } from '../context/EventContext'; -import { - ADMIN_EVENT_CREATE_PATH, - ADMIN_EVENT_INVITES_PATH, - ADMIN_EVENT_MEMBERS_PATH, - ADMIN_EVENT_PHOTOS_PATH, - ADMIN_EVENT_TASKS_PATH, - ADMIN_EVENT_VIEW_PATH, - ADMIN_EVENT_PHOTOBOOTH_PATH, - ADMIN_EVENT_BRANDING_PATH, -} from '../constants'; -import { cn } from '@/lib/utils'; -import { resolveEventDisplayName, formatEventDate } from '../lib/events'; - -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', 'QR-Codes'), href: ADMIN_EVENT_INVITES_PATH(slug) }, - { key: 'branding', label: t('eventMenu.branding', 'Branding & Fonts'), href: ADMIN_EVENT_BRANDING_PATH(slug) }, - ]; -} - -type EventSwitcherProps = { - buttonClassName?: string; - compact?: boolean; -}; - -export function EventSwitcher({ buttonClassName, compact = false }: EventSwitcherProps = {}) { - const { events, activeEvent, selectEvent } = useEventContext(); - const { t, i18n } = useTranslation('common'); - const navigate = useNavigate(); - const [open, setOpen] = React.useState(false); - - const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; - const buttonLabel = activeEvent ? resolveEventDisplayName(activeEvent) : t('eventSwitcher.placeholder', 'Event auswählen'); - const buttonHint = activeEvent?.event_date - ? formatEventDate(activeEvent.event_date, locale) - : events.length > 1 - ? t('eventSwitcher.multiple', 'Mehrere Events') - : t('eventSwitcher.empty', 'Noch kein Event'); - - const handleSelect = (event: TenantEvent) => { - selectEvent(event.slug ?? null); - setOpen(false); - if (event.slug) { - navigate(ADMIN_EVENT_VIEW_PATH(event.slug)); - } - }; - - const buttonClasses = cn( - 'rounded-full border-rose-100 bg-white/80 px-4 text-sm font-semibold text-slate-700 shadow-sm hover:bg-rose-50 dark:border-white/20 dark:bg-white/10 dark:text-white', - compact && 'px-3 text-xs sm:text-sm', - buttonClassName, - ); - - const buttonLabelClasses = compact ? 'text-sm' : 'hidden sm:inline'; - const hintClasses = compact - ? 'text-xs text-slate-500 dark:text-slate-300' - : 'text-xs text-slate-500 dark:text-slate-300 sm:ml-2'; - - return ( - - - - - - - {t('eventSwitcher.title', 'Event auswählen')} - - {events.length === 0 - ? t('eventSwitcher.emptyDescription', 'Erstelle dein erstes Event, um loszulegen.') - : t('eventSwitcher.description', 'Wähle ein Event für die Bearbeitung oder lege ein neues an.')} - - -
- {events.length === 0 ? ( -
- {t('eventSwitcher.noEvents', 'Noch keine Events vorhanden.')} -
- ) : ( -
- {events.map((event) => { - const isActive = activeEvent?.id === event.id; - const date = formatEventDate(event.event_date, locale); - return ( - - ); - })} -
- )} - - {activeEvent?.slug ? ( -
-

- {t('eventSwitcher.actions', 'Event-Funktionen')} -

-
- {buildEventLinks(activeEvent.slug, t).map((action) => ( - - ))} -
-
- ) : null} -
-
-
- ); -} - -export function EventMenuBar() { - const { activeEvent } = useEventContext(); - const { t } = useTranslation('common'); - const location = useLocation(); - - if (!activeEvent?.slug) { - return null; - } - - const links = buildEventLinks(activeEvent.slug, t); - - return ( -
-
- {links.map((link) => ( - - cn( - 'whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold transition', - isActive || location.pathname.startsWith(link.href) - ? 'bg-rose-600 text-white shadow shadow-rose-400/40' - : 'bg-white text-slate-600 ring-1 ring-slate-200 hover:text-rose-600 dark:bg-white/10 dark:text-white dark:ring-white/10' - ) - } - > - {link.label} - - ))} -
-
- ); -} diff --git a/resources/js/admin/components/FloatingActionBar.tsx b/resources/js/admin/components/FloatingActionBar.tsx deleted file mode 100644 index 6ed5000..0000000 --- a/resources/js/admin/components/FloatingActionBar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Loader2 } from 'lucide-react'; -import type { LucideIcon } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; - -type ActionTone = 'primary' | 'secondary' | 'danger' | 'neutral'; - -export type FloatingAction = { - key: string; - label: string; - icon: LucideIcon; - onClick: () => void; - tone?: ActionTone; - disabled?: boolean; - loading?: boolean; - ariaLabel?: string; -}; - -export function FloatingActionBar({ actions, className }: { actions: FloatingAction[]; className?: string }): React.ReactElement | null { - if (!actions.length) { - return null; - } - - const toneClasses: Record = { - primary: 'bg-primary text-primary-foreground shadow-primary/25 hover:bg-primary/90 focus-visible:ring-primary/70 border border-primary/20', - secondary: 'bg-[var(--tenant-surface-strong)] text-[var(--tenant-foreground)] shadow-slate-300/60 hover:bg-[var(--tenant-surface)] focus-visible:ring-slate-200 border border-[var(--tenant-border-strong)]', - neutral: 'bg-white/90 text-slate-900 shadow-slate-200/80 hover:bg-white focus-visible:ring-slate-200 border border-slate-200 dark:bg-slate-800/80 dark:text-white dark:border-slate-700', - danger: 'bg-rose-500 text-white shadow-rose-300/50 hover:bg-rose-600 focus-visible:ring-rose-200 border border-rose-400/80', - }; - - return ( -
-
- {actions.map((action) => { - const Icon = action.icon; - const tone = action.tone ?? 'primary'; - - return ( - - ); - })} -
-
- ); -} diff --git a/resources/js/admin/components/GuestBroadcastCard.tsx b/resources/js/admin/components/GuestBroadcastCard.tsx deleted file mode 100644 index 0831a36..0000000 --- a/resources/js/admin/components/GuestBroadcastCard.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-hot-toast'; -import { AlertCircle, Send, RefreshCw } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Badge } from '@/components/ui/badge'; -import type { GuestNotificationSummary, SendGuestNotificationPayload } from '../api'; -import { listGuestNotifications, sendGuestNotification } from '../api'; - -const TYPE_OPTIONS = [ - { value: 'broadcast', label: 'Allgemein' }, - { value: 'support_tip', label: 'Support-Hinweis' }, - { value: 'upload_alert', label: 'Upload-Status' }, - { value: 'feedback_request', label: 'Feedback' }, -]; - -const AUDIENCE_OPTIONS = [ - { value: 'all', label: 'Alle Gäste' }, - { value: 'guest', label: 'Einzelne Geräte-ID' }, -]; - -type GuestBroadcastCardProps = { - eventSlug: string; - eventName?: string | null; -}; - -export function GuestBroadcastCard({ eventSlug, eventName }: GuestBroadcastCardProps) { - const { t } = useTranslation('management'); - const [form, setForm] = React.useState({ - title: '', - message: '', - type: 'broadcast', - audience: 'all', - guest_identifier: '', - cta_label: '', - cta_url: '', - expires_in_minutes: 120, - }); - const [history, setHistory] = React.useState([]); - const [loadingHistory, setLoadingHistory] = React.useState(true); - const [submitting, setSubmitting] = React.useState(false); - const [error, setError] = React.useState(null); - - const loadHistory = React.useCallback(async () => { - setLoadingHistory(true); - try { - const data = await listGuestNotifications(eventSlug); - setHistory(data.slice(0, 5)); - } catch (err) { - console.error(err); - } finally { - setLoadingHistory(false); - } - }, [eventSlug]); - - React.useEffect(() => { - void loadHistory(); - }, [loadHistory]); - - function updateField(field: string, value: string): void { - setForm((prev) => ({ ...prev, [field]: value })); - } - - async function handleSubmit(event: React.FormEvent): Promise { - event.preventDefault(); - setSubmitting(true); - setError(null); - - const payload: SendGuestNotificationPayload = { - title: form.title.trim(), - message: form.message.trim(), - type: form.type, - audience: form.audience as 'all' | 'guest', - guest_identifier: form.audience === 'guest' ? form.guest_identifier.trim() : undefined, - expires_in_minutes: Number(form.expires_in_minutes) || undefined, - cta: - form.cta_label.trim() && form.cta_url.trim() - ? { label: form.cta_label.trim(), url: form.cta_url.trim() } - : undefined, - }; - - try { - await sendGuestNotification(eventSlug, payload); - toast.success(t('events.notifications.toastSuccess', 'Nachricht gesendet.')); - setForm((prev) => ({ - ...prev, - title: '', - message: '', - guest_identifier: '', - cta_label: '', - cta_url: '', - })); - void loadHistory(); - } catch (err) { - console.error(err); - setError(t('events.notifications.toastError', 'Nachricht konnte nicht gesendet werden.')); - } finally { - setSubmitting(false); - } - } - - return ( -
-
-

- {t('events.notifications.description', 'Sende kurze Hinweise direkt an deine Gäste. Ideal für Programmpunkte, Upload-Hilfe oder Feedback-Aufrufe.')} {eventName && {eventName}} -

-
-
-
-
- - -
-
- - -
-
- {form.audience === 'guest' && ( -
- - updateField('guest_identifier', event.target.value)} - placeholder="z. B. device-123" - required - /> -
- )} -
- - updateField('title', event.target.value)} - placeholder={t('events.notifications.titlePlaceholder', 'Buffet schließt in 10 Minuten')} - required - /> -
-
- -