From 04c399aeb6439ab73e1d2dcf7cfe810858fc8799 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 5 Feb 2026 11:26:07 +0100 Subject: [PATCH] Fix demo task readiness and gate event creation --- resources/js/admin/mobile/DashboardPage.tsx | 85 +++++++++++++++++- resources/js/admin/mobile/EventTasksPage.tsx | 8 +- resources/js/admin/mobile/EventsPage.tsx | 28 +++--- .../mobile/__tests__/DashboardPage.test.tsx | 10 +++ .../mobile/__tests__/EventTasksPage.test.tsx | 28 +++++- .../mobile/__tests__/EventsPage.test.tsx | 35 ++++++++ .../mobile/components/EventSwitcherSheet.tsx | 18 ++-- .../admin/mobile/components/MobileShell.tsx | 89 +++++++++++++++++-- .../components/__tests__/MobileShell.test.tsx | 5 ++ resources/js/app.tsx | 5 +- resources/js/lib/__tests__/appTitle.test.ts | 28 ++++++ resources/js/lib/appTitle.ts | 8 ++ resources/js/ssr.tsx | 5 +- resources/views/app.blade.php | 2 +- 14 files changed, 318 insertions(+), 36 deletions(-) create mode 100644 resources/js/lib/__tests__/appTitle.test.ts create mode 100644 resources/js/lib/appTitle.ts diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index 6055b4a6..9cdebbe8 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; +import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, Plus, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; import { Button } from '@tamagui/button'; import { Card } from '@tamagui/card'; import { YGroup } from '@tamagui/group'; @@ -19,7 +19,7 @@ import toast from 'react-hot-toast'; import { MobileShell } from './components/MobileShell'; import { ADMIN_EVENTS_PATH, adminPath } from '../constants'; import { useEventContext } from '../context/EventContext'; -import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue } from '../api'; +import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue, getTenantPackagesOverview, type TenantPackageSummary } from '../api'; import { formatEventDate } from '../lib/events'; import { useAuth } from '../auth/context'; import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme'; @@ -37,6 +37,59 @@ function translateLimits(t: any) { return (key: string, options?: any) => t(`management:limits.${key}`, key, options); } +function collectActivePackages( + overview: { packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null } | null +): TenantPackageSummary[] { + if (!overview) { + return []; + } + + const packages = overview.packages ?? []; + const activePackage = overview.activePackage; + const activeList = packages.filter((pkg) => pkg.active); + + if (activePackage && !activeList.some((pkg) => pkg.id === activePackage.id) && activePackage.active) { + return [activePackage, ...activeList]; + } + + return activeList; +} + +function toNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + + if (typeof value === 'string' && value.trim() !== '') { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + + return null; +} + +function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean { + if (pkg.package_type !== 'reseller') { + return false; + } + + const remaining = toNumber(pkg.remaining_events); + if (remaining !== null) { + return remaining > 0; + } + + const limits = (pkg.package_limits ?? {}) as Record; + const limitMaxEvents = toNumber(limits.max_events_per_year); + if (limitMaxEvents === null) { + return false; + } + + const usedEvents = toNumber(pkg.used_events) ?? 0; + return limitMaxEvents > usedEvents; +} + // --- TAMAGUI-ALIGNED PRIMITIVES --- function DashboardCard({ @@ -129,6 +182,7 @@ export default function MobileDashboardPage() { const { user } = useAuth(); const theme = useAdminTheme(); const isMember = user?.role === 'member'; + const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin'; // --- LOGIC --- const memberPermissions = React.useMemo(() => { @@ -178,6 +232,29 @@ export default function MobileDashboardPage() { }, }); + const { data: packagesOverview, isLoading: packagesLoading } = useQuery({ + queryKey: ['mobile', 'dashboard', 'packages-overview'], + enabled: !isMember && !isSuperAdmin, + queryFn: () => getTenantPackagesOverview({ force: true }), + }); + + const canCreateEvent = React.useMemo(() => { + if (!canManageEvents) { + return false; + } + + if (isSuperAdmin) { + return true; + } + + if (isMember || packagesLoading) { + return false; + } + + const activePackages = collectActivePackages(packagesOverview ?? null); + return activePackages.some((pkg) => resellerHasRemainingEvents(pkg)); + }, [canManageEvents, isSuperAdmin, isMember, packagesLoading, packagesOverview]); + const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'; React.useEffect(() => { @@ -265,6 +342,7 @@ export default function MobileDashboardPage() { permissions={memberPermissions} isMember={isMember} isCompleted={isCompleted} + canCreateEvent={canCreateEvent} /> {/* 5. RECENT PHOTOS */} @@ -627,7 +705,7 @@ function PulseStrip({ event, stats, liveShowApprovedCount }: any) { return ; } -function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) { +function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted, canCreateEvent }: any) { const theme = useAdminTheme(); const { t } = useTranslation(['management', 'dashboard']); const slug = event?.slug; @@ -649,6 +727,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted } ].filter(Boolean) as ToolItem[]; const adminItems: ToolItem[] = [ + canCreateEvent ? { label: t('management:events.list.actions.create', 'Create Event'), icon: Plus, path: `/mobile/events/new`, color: theme.primary } : null, { label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics`, color: ADMIN_ACTION_COLORS.analytics }, !isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports`, color: ADMIN_ACTION_COLORS.recap } : null, { label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit`, color: ADMIN_ACTION_COLORS.settings }, diff --git a/resources/js/admin/mobile/EventTasksPage.tsx b/resources/js/admin/mobile/EventTasksPage.tsx index 5263b286..a6e93be8 100644 --- a/resources/js/admin/mobile/EventTasksPage.tsx +++ b/resources/js/admin/mobile/EventTasksPage.tsx @@ -66,7 +66,7 @@ function allowPermission(permissions: string[], permission: string): boolean { export default function MobileEventTasksPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); - const { activeEvent, selectEvent } = useEventContext(); + const { activeEvent, selectEvent, refetch } = useEventContext(); const slug = slugParam ?? activeEvent?.slug ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); @@ -258,6 +258,7 @@ export default function MobileEventTasksPage() { const result = await getEventTasks(eventId, 1); setAssignedTasks(result.data); setLibrary((prev) => prev.filter((t) => t.id !== taskId)); + refetch(); toast.success(t('events.tasks.assigned', 'Fotoaufgabe hinzugefügt')); } catch (err) { if (!isAuthError(err)) { @@ -281,6 +282,7 @@ export default function MobileEventTasksPage() { const assignedIds = new Set(result.data.map((t) => t.id)); setAssignedTasks(result.data); setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); + refetch(); toast.success(t('events.tasks.imported', 'Fotoaufgabenpaket importiert')); } catch (err) { if (!isAuthError(err)) { @@ -337,6 +339,7 @@ export default function MobileEventTasksPage() { setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id))); setShowTaskSheet(false); setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null }); + refetch(); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { @@ -361,6 +364,7 @@ export default function MobileEventTasksPage() { } return next; }); + refetch(); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt')); } catch (err) { if (!isAuthError(err)) { @@ -392,6 +396,7 @@ export default function MobileEventTasksPage() { setAssignedTasks((prev) => prev.filter((task) => !selectedTaskIds.has(task.id))); setSelectedTaskIds(new Set()); setSelectionMode(false); + refetch(); toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt')); } catch (err) { if (!isAuthError(err)) { @@ -519,6 +524,7 @@ export default function MobileEventTasksPage() { setAssignedTasks(result.data); setBulkLines(''); setShowBulkSheet(false); + refetch(); toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert')); } catch (err) { if (!isAuthError(err)) { diff --git a/resources/js/admin/mobile/EventsPage.tsx b/resources/js/admin/mobile/EventsPage.tsx index ea3a8932..b3a7ef08 100644 --- a/resources/js/admin/mobile/EventsPage.tsx +++ b/resources/js/admin/mobile/EventsPage.tsx @@ -28,6 +28,7 @@ export default function MobileEventsPage() { const navigate = useNavigate(); const { user } = useAuth(); const isMember = user?.role === 'member'; + const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin'; const [events, setEvents] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -85,13 +86,21 @@ export default function MobileEventsPage() { }, [isMember]); const canCreateEvent = React.useMemo(() => { - if (isMember || packagesLoading) { + if (isMember) { + return false; + } + + if (isSuperAdmin) { + return true; + } + + if (packagesLoading) { return false; } const activePackages = collectActivePackages(packagesOverview); - return activePackages.some((pkg) => packageHasRemainingEvents(pkg)); - }, [isMember, packagesLoading, packagesOverview]); + return activePackages.some((pkg) => resellerHasRemainingEvents(pkg)); + }, [isMember, isSuperAdmin, packagesLoading, packagesOverview]); return ( 0; - } - - const usedEvents = toNumber(pkg.used_events) ?? 0; - return usedEvents < 1; + return false; } + const remaining = toNumber(pkg.remaining_events); + if (remaining !== null) { return remaining > 0; } diff --git a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx index e80d1c06..18f40929 100644 --- a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx @@ -399,7 +399,17 @@ describe('MobileDashboardPage', () => { expect(screen.getByText('Experience')).toBeInTheDocument(); expect(screen.getByText('Settings')).toBeInTheDocument(); + expect(screen.queryByText('Create Event')).not.toBeInTheDocument(); authState.user = { role: 'tenant_admin' }; }); + + it('shows create event action for reseller packages', () => { + fixtures.activePackage.package_type = 'reseller'; + fixtures.activePackage.remaining_events = 2; + + render(); + + expect(screen.getByText('Create Event')).toBeInTheDocument(); + }); }); diff --git a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx index 22d3031b..fcb159e0 100644 --- a/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx @@ -63,11 +63,13 @@ vi.mock('../hooks/useBackNavigation', () => ({ })); const selectEventMock = vi.fn(); +const refetchMock = vi.fn(); vi.mock('../../context/EventContext', () => ({ useEventContext: () => ({ activeEvent: fixtures.event, selectEvent: selectEventMock, + refetch: refetchMock, }), })); @@ -245,7 +247,12 @@ vi.mock('../components/FormControls', () => ({ })); vi.mock('../components/Sheet', () => ({ - MobileSheet: ({ children }: { children: React.ReactNode }) =>
{children}
, + MobileSheet: ({ children, footer }: { children: React.ReactNode; footer?: React.ReactNode }) => ( +
+ {children} + {footer} +
+ ), })); vi.mock('../components/Tag', () => ({ @@ -336,4 +343,23 @@ describe('MobileEventTasksPage', () => { expect(screen.queryByLabelText('Add')).not.toBeInTheDocument(); }); }); + + it('refetches events after creating a task', async () => { + refetchMock.mockClear(); + (api.createTask as unknown as { mockResolvedValueOnce: (value: any) => void }).mockResolvedValueOnce({ id: 99 }); + + render(); + + expect(await screen.findByText('Photo tasks for guests')).toBeInTheDocument(); + + fireEvent.change(screen.getByPlaceholderText('z.B. Erstes Gruppenfoto'), { + target: { value: 'New Task' }, + }); + + fireEvent.click(screen.getAllByRole('button', { name: 'Fotoaufgabe speichern' })[0]); + + await waitFor(() => { + expect(refetchMock).toHaveBeenCalled(); + }); + }); }); diff --git a/resources/js/admin/mobile/__tests__/EventsPage.test.tsx b/resources/js/admin/mobile/__tests__/EventsPage.test.tsx index 1e75da23..cea30022 100644 --- a/resources/js/admin/mobile/__tests__/EventsPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/EventsPage.test.tsx @@ -255,6 +255,41 @@ describe('MobileEventsPage', () => { }); it('shows the create button when an active package has remaining events', async () => { + vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({ + packages: [ + { + id: 2, + package_id: 2, + package_name: 'Reseller', + package_type: 'reseller', + included_package_slug: 'standard', + active: true, + used_events: 1, + remaining_events: 2, + price: 240, + currency: 'EUR', + purchased_at: null, + expires_at: null, + package_limits: { max_events_per_year: 5 }, + }, + ], + activePackage: { + id: 2, + package_id: 2, + package_name: 'Reseller', + package_type: 'reseller', + included_package_slug: 'standard', + active: true, + used_events: 1, + remaining_events: 2, + price: 240, + currency: 'EUR', + purchased_at: null, + expires_at: null, + package_limits: { max_events_per_year: 5 }, + }, + }); + render(); expect(await screen.findByText('New event')).toBeInTheDocument(); diff --git a/resources/js/admin/mobile/components/EventSwitcherSheet.tsx b/resources/js/admin/mobile/components/EventSwitcherSheet.tsx index cb4b03b4..8cd2dccb 100644 --- a/resources/js/admin/mobile/components/EventSwitcherSheet.tsx +++ b/resources/js/admin/mobile/components/EventSwitcherSheet.tsx @@ -16,11 +16,13 @@ export function EventSwitcherSheet({ onClose, events, activeSlug, + canCreateEvent, }: { open: boolean; onClose: () => void; events: TenantEvent[]; activeSlug: string | null; + canCreateEvent: boolean; }) { const { t, i18n } = useTranslation(['management', 'mobile']); const navigate = useNavigate(); @@ -73,13 +75,15 @@ export function EventSwitcherSheet({ ); })} - { onClose(); navigate(adminPath('/mobile/events/new')); }}> - - - {t('mobile:header.createEvent', 'Create Event')} - - - + {canCreateEvent ? ( + { onClose(); navigate(adminPath('/mobile/events/new')); }}> + + + {t('mobile:header.createEvent', 'Create Event')} + + + + ) : null} ); diff --git a/resources/js/admin/mobile/components/MobileShell.tsx b/resources/js/admin/mobile/components/MobileShell.tsx index 90ff0a13..903e2ab5 100644 --- a/resources/js/admin/mobile/components/MobileShell.tsx +++ b/resources/js/admin/mobile/components/MobileShell.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import { ChevronLeft, Bell, QrCode, ChevronsUpDown, Search } from 'lucide-react'; import { YStack, XStack, SizableText as Text, Image } from 'tamagui'; import { Pressable } from '@tamagui/react-native-web-lite'; @@ -12,7 +13,7 @@ import { MobileCard, CTAButton } from './Primitives'; import { useNotificationsBadge } from '../hooks/useNotificationsBadge'; import { useOnlineStatus } from '../hooks/useOnlineStatus'; import { resolveEventDisplayName } from '../../lib/events'; -import { TenantEvent, getEvents } from '../../api'; +import { TenantEvent, getEvents, getTenantPackagesOverview, type TenantPackageSummary } from '../../api'; import { setTabHistory } from '../lib/tabHistory'; import { loadPhotoQueue } from '../lib/photoModerationQueue'; import { countQueuedPhotoActions } from '../lib/queueStatus'; @@ -31,6 +32,59 @@ type MobileShellProps = { headerActions?: React.ReactNode; }; +function collectActivePackages( + overview: { packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null } | null +): TenantPackageSummary[] { + if (!overview) { + return []; + } + + const packages = overview.packages ?? []; + const activePackage = overview.activePackage; + const activeList = packages.filter((pkg) => pkg.active); + + if (activePackage && !activeList.some((pkg) => pkg.id === activePackage.id) && activePackage.active) { + return [activePackage, ...activeList]; + } + + return activeList; +} + +function toNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + + if (typeof value === 'string' && value.trim() !== '') { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + + return null; +} + +function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean { + if (pkg.package_type !== 'reseller') { + return false; + } + + const remaining = toNumber(pkg.remaining_events); + if (remaining !== null) { + return remaining > 0; + } + + const limits = (pkg.package_limits ?? {}) as Record; + const limitMaxEvents = toNumber(limits.max_events_per_year); + if (limitMaxEvents === null) { + return false; + } + + const usedEvents = toNumber(pkg.used_events) ?? 0; + return limitMaxEvents > usedEvents; +} + export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) { const { events, activeEvent, selectEvent } = useEventContext(); const { user } = useAuth(); @@ -40,6 +94,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head const { t } = useTranslation('mobile'); const { count: notificationCount } = useNotificationsBadge(); const online = useOnlineStatus(); + const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin'; useDocumentTitle(title); @@ -144,6 +199,29 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head }; const showQr = Boolean(effectiveActive?.slug) && allowPermission('join-tokens:manage'); + const { data: packagesOverview, isLoading: packagesLoading } = useQuery({ + queryKey: ['mobile', 'header', 'packages-overview'], + enabled: !isMember && !isSuperAdmin, + queryFn: () => getTenantPackagesOverview({ force: true }), + }); + + const canCreateEvent = React.useMemo(() => { + if (isMember) { + return false; + } + + if (isSuperAdmin) { + return true; + } + + if (packagesLoading) { + return false; + } + + const activePackages = collectActivePackages(packagesOverview ?? null); + return activePackages.some((pkg) => resellerHasRemainingEvents(pkg)); + }, [isMember, isSuperAdmin, packagesLoading, packagesOverview]); + // --- CONTEXT PILL --- const EventContextPill = () => { if (!effectiveActive || isEventsIndex || isCompactHeader) { @@ -387,10 +465,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head setSwitcherOpen(false)} - events={effectiveEvents} - activeSlug={effectiveActive?.slug ?? null} + open={switcherOpen} + onClose={() => setSwitcherOpen(false)} + events={effectiveEvents} + activeSlug={effectiveActive?.slug ?? null} + canCreateEvent={canCreateEvent} /> ({ vi.mock('../../../api', () => ({ getEvents: vi.fn().mockResolvedValue([]), + getTenantPackagesOverview: vi.fn().mockResolvedValue({ packages: [], activePackage: null }), +})); + +vi.mock('@tanstack/react-query', () => ({ + useQuery: () => ({ data: { packages: [], activePackage: null }, isLoading: false }), })); vi.mock('../../lib/tabHistory', () => ({ diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 2700da62..54d7e244 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -14,8 +14,7 @@ import React from 'react'; import { usePage } from '@inertiajs/react'; import { useEffect } from 'react'; import { Sentry, initSentry } from './lib/sentry'; - -const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; +import { buildPageTitle } from './lib/appTitle'; const InertiaFallback: React.FC = () => (
Oberfläche konnte nicht geladen werden. @@ -48,7 +47,7 @@ const LocaleSync: React.FC<{ children: React.ReactNode }> = ({ children }) => { }; createInertiaApp({ - title: (title) => title ? `${title} - ${appName}` : appName, + title: (title) => buildPageTitle(title), resolve: (name) => resolvePageComponent( `./pages/${name}.tsx`, diff --git a/resources/js/lib/__tests__/appTitle.test.ts b/resources/js/lib/__tests__/appTitle.test.ts new file mode 100644 index 00000000..d59c3aed --- /dev/null +++ b/resources/js/lib/__tests__/appTitle.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { buildPageTitle, getAppName } from '../appTitle'; + +describe('app title helpers', () => { + it('falls back to Fotospiel when VITE_APP_NAME is missing', () => { + const original = import.meta.env.VITE_APP_NAME; + // @ts-expect-error - import.meta.env is mutable in vitest + import.meta.env.VITE_APP_NAME = ''; + + expect(getAppName()).toBe('Fotospiel'); + expect(buildPageTitle('Demo')).toBe('Demo - Fotospiel'); + + // @ts-expect-error - restore original value + import.meta.env.VITE_APP_NAME = original; + }); + + it('uses the configured VITE_APP_NAME when available', () => { + const original = import.meta.env.VITE_APP_NAME; + // @ts-expect-error - import.meta.env is mutable in vitest + import.meta.env.VITE_APP_NAME = 'Fotospiel App'; + + expect(getAppName()).toBe('Fotospiel App'); + expect(buildPageTitle('Demo')).toBe('Demo - Fotospiel App'); + + // @ts-expect-error - restore original value + import.meta.env.VITE_APP_NAME = original; + }); +}); diff --git a/resources/js/lib/appTitle.ts b/resources/js/lib/appTitle.ts new file mode 100644 index 00000000..5eb2f3a8 --- /dev/null +++ b/resources/js/lib/appTitle.ts @@ -0,0 +1,8 @@ +export function getAppName(): string { + return import.meta.env.VITE_APP_NAME || 'Fotospiel'; +} + +export function buildPageTitle(title?: string): string { + const appName = getAppName(); + return title ? `${title} - ${appName}` : appName; +} diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx index 759bd479..1527da70 100644 --- a/resources/js/ssr.tsx +++ b/resources/js/ssr.tsx @@ -4,14 +4,13 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import ReactDOMServer from 'react-dom/server'; import { I18nextProvider } from 'react-i18next'; import i18n from './i18n'; - -const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; +import { buildPageTitle } from './lib/appTitle'; createServer((page) => createInertiaApp({ page, render: ReactDOMServer.renderToString, - title: (title) => (title ? `${title} - ${appName}` : appName), + title: (title) => buildPageTitle(title), resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')), setup: ({ App, props }) => { const locale = (props.initialPage?.props as Record | undefined)?.locale as string | undefined; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 2ba94f2b..5497b424 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -28,7 +28,7 @@ })(); - {{ config('app.name', 'Laravel') }} + {{ config('app.name', 'Fotospiel') }}