diff --git a/resources/js/admin/auth/context.tsx b/resources/js/admin/auth/context.tsx index ab5441f..aea37f6 100644 --- a/resources/js/admin/auth/context.tsx +++ b/resources/js/admin/auth/context.tsx @@ -16,9 +16,10 @@ export interface TenantProfile { id: number; tenant_id: number; role?: string | null; - name?: string; + name?: string | null; slug?: string; email?: string | null; + avatar_url?: string | null; features?: Record; [key: string]: unknown; } diff --git a/resources/js/admin/i18n/locales/de/auth.json b/resources/js/admin/i18n/locales/de/auth.json index 7eb4a8e..67d866b 100644 --- a/resources/js/admin/i18n/locales/de/auth.json +++ b/resources/js/admin/i18n/locales/de/auth.json @@ -1,6 +1,7 @@ { "login": { "title": "Event-Admin", + "pageTitle": "Login", "badge": "Fotospiel Event Admin", "hero_tagline": "Kontrolle behalten, entspannt bleiben", "hero_title": "Das Cockpit für dein Fotospiel Event", @@ -90,6 +91,9 @@ "appearance_label": "Darstellung" }, "redirecting": "Weiterleitung zum Login …", + "logout": { + "title": "Abmeldung wird vorbereitet …" + }, "processing": { "title": "Anmeldung wird verarbeitet …", "copy": "Einen Moment bitte, wir bereiten dein Dashboard vor." diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index ee1cd9c..d57853d 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -983,6 +983,7 @@ "layouts": "Druck-Layouts", "preview": "Anpassen & Exportieren", "createLink": "Neuen QR-Link erstellen", + "createLinkConfirm": "Neuen QR-Link erstellen? Dadurch werden alle bisherigen Ausdrucke ungültig und alle Personen mit dem alten Link verlieren den Zugang.", "mobileLinkLabel": "Mobiler Link", "created": "Neuer QR-Link erstellt", "createFailed": "Link konnte nicht erstellt werden.", diff --git a/resources/js/admin/i18n/locales/de/mobile.json b/resources/js/admin/i18n/locales/de/mobile.json index 419c986..20bc235 100644 --- a/resources/js/admin/i18n/locales/de/mobile.json +++ b/resources/js/admin/i18n/locales/de/mobile.json @@ -14,6 +14,7 @@ }, "header": { "appName": "Event Admin", + "documentTitle": "Fotospiel.App Event Admin", "selectEvent": "Wähle ein Event, um fortzufahren", "empty": "Lege dein erstes Event an, um zu starten", "eventSwitcher": "Event auswählen", diff --git a/resources/js/admin/i18n/locales/en/auth.json b/resources/js/admin/i18n/locales/en/auth.json index f9053ca..1f3b8b0 100644 --- a/resources/js/admin/i18n/locales/en/auth.json +++ b/resources/js/admin/i18n/locales/en/auth.json @@ -1,6 +1,7 @@ { "login": { "title": "Event Admin", + "pageTitle": "Login", "badge": "Fotospiel Event Admin", "hero_tagline": "Stay in control, stay relaxed", "hero_title": "Your cockpit for every Fotospiel event", @@ -90,6 +91,9 @@ "appearance_label": "Appearance" }, "redirecting": "Redirecting to login …", + "logout": { + "title": "Signing out …" + }, "processing": { "title": "Signing you in …", "copy": "One moment please while we prepare your dashboard." diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index b6eb7ef..5ba070c 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -979,6 +979,7 @@ "layouts": "Print Layouts", "preview": "Customize & Export", "createLink": "Create new QR link", + "createLinkConfirm": "Create a new QR link? This will invalidate all printed materials and everyone with the old link will lose access.", "mobileLinkLabel": "Mobile link", "created": "New QR link created", "createFailed": "Could not create link.", diff --git a/resources/js/admin/i18n/locales/en/mobile.json b/resources/js/admin/i18n/locales/en/mobile.json index 4f28b33..fc7c3e2 100644 --- a/resources/js/admin/i18n/locales/en/mobile.json +++ b/resources/js/admin/i18n/locales/en/mobile.json @@ -14,6 +14,7 @@ }, "header": { "appName": "Event Admin", + "documentTitle": "Fotospiel.App Event Admin", "selectEvent": "Select an event to continue", "empty": "Create your first event to get started", "eventSwitcher": "Choose an event", diff --git a/resources/js/admin/mobile/AuthCallbackPage.tsx b/resources/js/admin/mobile/AuthCallbackPage.tsx index 29de24e..cc234c1 100644 --- a/resources/js/admin/mobile/AuthCallbackPage.tsx +++ b/resources/js/admin/mobile/AuthCallbackPage.tsx @@ -9,6 +9,7 @@ import { useAuth } from '../auth/context'; import { ADMIN_DEFAULT_AFTER_LOGIN_PATH } from '../constants'; import { decodeReturnTo, resolveReturnTarget } from '../lib/returnTo'; import { useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; export default function AuthCallbackPage(): React.ReactElement { const { status } = useAuth(); @@ -21,6 +22,8 @@ export default function AuthCallbackPage(): React.ReactElement { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('processing.title', 'Signing you in …')); + const searchParams = React.useMemo(() => new URLSearchParams(window.location.search), []); const rawReturnTo = searchParams.get('return_to'); diff --git a/resources/js/admin/mobile/BrandingPage.tsx b/resources/js/admin/mobile/BrandingPage.tsx index a7791d3..0cb5a00 100644 --- a/resources/js/admin/mobile/BrandingPage.tsx +++ b/resources/js/admin/mobile/BrandingPage.tsx @@ -1324,7 +1324,7 @@ function LabeledSlider({ max={max} step={step} value={[value]} - onValueChange={(next) => onChange(next[0] ?? value)} + onValueChange={(next: number[]) => onChange(next[0] ?? value)} disabled={disabled} > diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index 00ac203..16b585e 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -108,12 +108,13 @@ function SectionHeader({ function StatusBadge({ status }: { status: string }) { const { t } = useTranslation('management'); - const config = - { - published: { tone: 'success', label: t('events.status.published', 'Live') }, - draft: { tone: 'warning', label: t('events.status.draft', 'Draft') }, - archived: { tone: 'muted', label: t('events.status.archived', 'Archived') }, - }[status] || { tone: 'muted', label: status }; + type StatusTone = 'success' | 'warning' | 'danger' | 'muted'; + const statuses: Record = { + published: { tone: 'success', label: t('events.status.published', 'Live') }, + draft: { tone: 'warning', label: t('events.status.draft', 'Draft') }, + archived: { tone: 'muted', label: t('events.status.archived', 'Archived') }, + }; + const config = statuses[status] ?? { tone: 'muted', label: status }; return {config.label}; } @@ -408,7 +409,7 @@ function LifecycleHero({ - + @@ -486,7 +487,7 @@ function LifecycleHero({ handlePublishChange(Boolean(checked))} + onCheckedChange={(checked: boolean) => handlePublishChange(Boolean(checked))} size="$2" disabled={isPublishing} aria-label={t('eventForm.fields.publish.label', 'Publish immediately')} @@ -625,26 +626,27 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted } const { t } = useTranslation(['management', 'dashboard']); const slug = event?.slug; if (!slug) return null; + type ToolItem = { label: string; icon: any; path: string; color?: string }; - const experienceItems = [ + const experienceItems: ToolItem[] = [ { label: t('management:photos.gallery.title', 'Photos'), icon: ImageIcon, path: `/mobile/events/${slug}/control-room`, color: theme.primary }, !isCompleted ? { label: t('management:events.quick.liveShowSettings', 'Slide Show'), icon: Tv, path: `/mobile/events/${slug}/live-show/settings`, color: '#F59E0B' } : null, !isCompleted ? { label: t('events.tasks.badge', 'Photo tasks'), icon: ListTodo, path: `/mobile/events/${slug}/tasks`, color: theme.accent } : null, !isCompleted ? { label: t('management:events.quick.photobooth', 'Photobooth'), icon: Camera, path: `/mobile/events/${slug}/photobooth`, color: '#8B5CF6' } : null, - ].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item)); + ].filter(Boolean) as ToolItem[]; - const operationsItems = [ + const operationsItems: ToolItem[] = [ !isCompleted ? { label: t('management:invites.badge', 'QR Codes'), icon: QrCode, path: `/mobile/events/${slug}/qr`, color: '#10B981' } : null, { label: t('management:events.quick.guests', 'Guests'), icon: Users, path: `/mobile/events/${slug}/members`, color: ADMIN_ACTION_COLORS.guests }, !isCompleted ? { label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: ADMIN_ACTION_COLORS.guestMessages } : null, !isCompleted ? { label: t('events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: ADMIN_ACTION_COLORS.branding } : null, - ].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item)); + ].filter(Boolean) as ToolItem[]; - const adminItems = [ + const adminItems: ToolItem[] = [ { 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 }, - ].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item)); + ].filter(Boolean) as ToolItem[]; const sections = [ { @@ -719,11 +721,11 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted } ); } -function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[], navigate: any, slug: string }) { +function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[]; navigate: any; slug?: string }) { const theme = useAdminTheme(); const { t } = useTranslation('management'); - if (!photos.length) return null; + if (!photos.length || !slug) return null; return ( diff --git a/resources/js/admin/mobile/EventControlRoomPage.tsx b/resources/js/admin/mobile/EventControlRoomPage.tsx index e37b593..69dea18 100644 --- a/resources/js/admin/mobile/EventControlRoomPage.tsx +++ b/resources/js/admin/mobile/EventControlRoomPage.tsx @@ -1110,7 +1110,7 @@ export default function MobileEventControlRoomPage() { size="$3" checked={autoApproveHighlights} disabled={controlRoomSaving} - onCheckedChange={(checked) => + onCheckedChange={(checked: boolean) => saveControlRoomSettings({ ...controlRoomSettings, auto_approve_highlights: Boolean(checked), @@ -1139,7 +1139,7 @@ export default function MobileEventControlRoomPage() { size="$3" checked={autoAddApprovedToLive} disabled={controlRoomSaving || isImmediateUploads} - onCheckedChange={(checked) => { + onCheckedChange={(checked: boolean) => { if (isImmediateUploads) { return; } @@ -1164,7 +1164,7 @@ export default function MobileEventControlRoomPage() { size="$3" checked={autoRemoveLiveOnHide} disabled={controlRoomSaving} - onCheckedChange={(checked) => + onCheckedChange={(checked: boolean) => saveControlRoomSettings({ ...controlRoomSettings, auto_remove_live_on_hide: Boolean(checked), @@ -1388,7 +1388,7 @@ export default function MobileEventControlRoomPage() { value && setModerationFilter(value as ModerationFilter)} + onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)} > {MODERATION_FILTERS.map((option) => { @@ -1576,7 +1576,7 @@ export default function MobileEventControlRoomPage() { value && setLiveStatusFilter(value as LiveShowQueueStatus)} + onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)} > {LIVE_STATUS_OPTIONS.map((option) => { diff --git a/resources/js/admin/mobile/EventFormPage.tsx b/resources/js/admin/mobile/EventFormPage.tsx index 0adc1d6..6cc830b 100644 --- a/resources/js/admin/mobile/EventFormPage.tsx +++ b/resources/js/admin/mobile/EventFormPage.tsx @@ -527,7 +527,7 @@ export default function MobileEventFormPage() { + onCheckedChange={(checked: boolean) => setForm((prev) => ({ ...prev, published: Boolean(checked) })) } size="$3" @@ -546,7 +546,7 @@ export default function MobileEventFormPage() { + onCheckedChange={(checked: boolean) => setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) })) } size="$3" @@ -577,7 +577,7 @@ export default function MobileEventFormPage() { + onCheckedChange={(checked: boolean) => setForm((prev) => ({ ...prev, autoApproveUploads: Boolean(checked) })) } size="$3" diff --git a/resources/js/admin/mobile/EventLiveShowSettingsPage.tsx b/resources/js/admin/mobile/EventLiveShowSettingsPage.tsx index 4ec8c86..4769066 100644 --- a/resources/js/admin/mobile/EventLiveShowSettingsPage.tsx +++ b/resources/js/admin/mobile/EventLiveShowSettingsPage.tsx @@ -677,7 +677,7 @@ function EffectSlider({ max={max} step={step} value={[value]} - onValueChange={(next) => onChange(next[0] ?? value)} + onValueChange={(next: number[]) => onChange(next[0] ?? value)} disabled={disabled} > diff --git a/resources/js/admin/mobile/EventTasksPage.tsx b/resources/js/admin/mobile/EventTasksPage.tsx index 5a179ff..509f9d4 100644 --- a/resources/js/admin/mobile/EventTasksPage.tsx +++ b/resources/js/admin/mobile/EventTasksPage.tsx @@ -1158,7 +1158,7 @@ export default function MobileEventTasksPage() { setActiveTab(value as TaskSectionKey)} + onValueChange={(value: string) => setActiveTab(value as TaskSectionKey)} flexDirection="column" alignItems="stretch" width="100%" @@ -1411,7 +1411,7 @@ export default function MobileEventTasksPage() { > { + onValueChange={(val: string) => { setEmotionFilter(val); setShowEmotionFilterSheet(false); }} @@ -1441,7 +1441,7 @@ export default function MobileEventTasksPage() { { + onOpenChange={(open: boolean) => { if (!open) { setDeleteCandidate(null); } @@ -1496,7 +1496,7 @@ export default function MobileEventTasksPage() { { + onOpenChange={(open: boolean) => { if (!open) { setBulkDeleteOpen(false); } diff --git a/resources/js/admin/mobile/EventsPage.tsx b/resources/js/admin/mobile/EventsPage.tsx index ee4bb8f..c1be3ee 100644 --- a/resources/js/admin/mobile/EventsPage.tsx +++ b/resources/js/admin/mobile/EventsPage.tsx @@ -324,7 +324,7 @@ function EventsList({ value && onStatusChange(value as EventStatusKey)} + onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)} > {filters.map((filter) => { diff --git a/resources/js/admin/mobile/ForgotPasswordPage.tsx b/resources/js/admin/mobile/ForgotPasswordPage.tsx index 26215b3..36e556b 100644 --- a/resources/js/admin/mobile/ForgotPasswordPage.tsx +++ b/resources/js/admin/mobile/ForgotPasswordPage.tsx @@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants'; import { MobileCard } from './components/Primitives'; import { MobileField, MobileInput } from './components/FormControls'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; type ResetResponse = { status: string; @@ -51,6 +52,8 @@ export default function ForgotPasswordPage() { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('login.forgot.title', 'Forgot your password?')); + const mutation = useMutation({ mutationKey: ['tenantAdminForgotPassword'], mutationFn: requestPasswordReset, diff --git a/resources/js/admin/mobile/LoginPage.tsx b/resources/js/admin/mobile/LoginPage.tsx index 7527253..263ebd2 100644 --- a/resources/js/admin/mobile/LoginPage.tsx +++ b/resources/js/admin/mobile/LoginPage.tsx @@ -20,6 +20,7 @@ import { Button } from '@tamagui/button'; import { MobileCard } from './components/Primitives'; import { MobileField, MobileInput } from './components/FormControls'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; type LoginResponse = { token: string; @@ -66,6 +67,8 @@ export default function MobileLoginPage() { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('login.pageTitle', 'Login')); + const searchParams = React.useMemo(() => new URLSearchParams(location.search), [location.search]); const rawReturnTo = searchParams.get('return_to'); const oauthError = searchParams.get('error'); diff --git a/resources/js/admin/mobile/LoginStartPage.tsx b/resources/js/admin/mobile/LoginStartPage.tsx index c47c570..a052a32 100644 --- a/resources/js/admin/mobile/LoginStartPage.tsx +++ b/resources/js/admin/mobile/LoginStartPage.tsx @@ -8,6 +8,7 @@ import { SizableText as Text } from '@tamagui/text'; import { Spinner } from 'tamagui'; import { ADMIN_LOGIN_PATH } from '../constants'; import { useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; export default function LoginStartPage(): React.ReactElement { const location = useLocation(); @@ -19,6 +20,8 @@ export default function LoginStartPage(): React.ReactElement { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('redirecting', 'Redirecting to login …')); + useEffect(() => { const params = new URLSearchParams(location.search); const returnTo = params.get('return_to'); diff --git a/resources/js/admin/mobile/LogoutPage.tsx b/resources/js/admin/mobile/LogoutPage.tsx index f988715..b59bff1 100644 --- a/resources/js/admin/mobile/LogoutPage.tsx +++ b/resources/js/admin/mobile/LogoutPage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; import { Card } from '@tamagui/card'; import { YStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; @@ -6,15 +7,19 @@ import { Spinner } from 'tamagui'; import { useAuth } from '../auth/context'; import { ADMIN_PUBLIC_LANDING_PATH } from '../constants'; import { useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; export default function LogoutPage() { const { logout } = useAuth(); + const { t } = useTranslation('auth'); const { textStrong, muted, border, surface, shadow, appBackground } = useAdminTheme(); const safeAreaStyle: React.CSSProperties = { paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)', paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('logout.title', 'Signing out …')); + React.useEffect(() => { logout({ redirect: ADMIN_PUBLIC_LANDING_PATH }); }, [logout]); diff --git a/resources/js/admin/mobile/PackageShopPage.tsx b/resources/js/admin/mobile/PackageShopPage.tsx index 2a222d1..697d326 100644 --- a/resources/js/admin/mobile/PackageShopPage.tsx +++ b/resources/js/admin/mobile/PackageShopPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import type { TFunction } from 'i18next'; import { Check, ChevronRight, ShieldCheck, Sparkles, X } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; @@ -395,7 +396,11 @@ function PackageShopCompareView({ } } - return t(`shop.features.${row.featureKey}`, row.featureKey); + if (row.type === 'feature') { + return t(`shop.features.${row.featureKey}`, row.featureKey); + } + + return row.id; }; const formatLimitValue = (value: number | null) => { @@ -562,7 +567,7 @@ function getPackageStatusLabel({ isActive, owned, }: { - t: (key: string, fallback?: string, options?: Record) => string; + t: TFunction; isActive?: boolean; owned?: TenantPackageSummary; }): string | null { @@ -622,7 +627,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () => id="agb" size="$4" checked={agbAccepted} - onCheckedChange={(checked) => setAgbAccepted(!!checked)} + onCheckedChange={(checked: boolean) => setAgbAccepted(!!checked)} > @@ -638,7 +643,7 @@ function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () => id="withdrawal" size="$4" checked={withdrawalAccepted} - onCheckedChange={(checked) => setWithdrawalAccepted(!!checked)} + onCheckedChange={(checked: boolean) => setWithdrawalAccepted(!!checked)} > @@ -687,7 +692,7 @@ function aggregateOwnedEntries(entries: TenantPackageSummary[]): TenantPackageSu } function resolveIncludedTierLabel( - t: (key: string, fallback?: string, options?: Record) => string, + t: TFunction, slug: string | null ): string | null { if (!slug) { diff --git a/resources/js/admin/mobile/PublicHelpPage.tsx b/resources/js/admin/mobile/PublicHelpPage.tsx index 6129380..d7484ae 100644 --- a/resources/js/admin/mobile/PublicHelpPage.tsx +++ b/resources/js/admin/mobile/PublicHelpPage.tsx @@ -8,6 +8,7 @@ import { Button } from '@tamagui/button'; import { ADMIN_LOGIN_PATH } from '../constants'; import { MobileCard, CTAButton } from './components/Primitives'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; type FaqItem = { question: string; @@ -44,6 +45,8 @@ export default function PublicHelpPage() { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('login.help_title', 'Help for event admins')); + return ( { if (!slug) return; + if ( + !window.confirm( + t( + 'events.qr.createLinkConfirm', + 'Neuen QR-Link erstellen? Dadurch werden alle bisherigen Ausdrucke ungültig und alle Personen mit dem alten Link verlieren den Zugang.' + ) + ) + ) { + return; + } try { const invite = await createQrInvite(slug, { label: t('events.qr.mobileLinkLabel', 'Mobile link') }); setQrUrl(invite.url); diff --git a/resources/js/admin/mobile/ResetPasswordPage.tsx b/resources/js/admin/mobile/ResetPasswordPage.tsx index bbf0861..f645658 100644 --- a/resources/js/admin/mobile/ResetPasswordPage.tsx +++ b/resources/js/admin/mobile/ResetPasswordPage.tsx @@ -10,6 +10,7 @@ import { ADMIN_LOGIN_PATH } from '../constants'; import { MobileCard } from './components/Primitives'; import { MobileField, MobileInput } from './components/FormControls'; import { ADMIN_GRADIENTS, useAdminTheme } from './theme'; +import { useDocumentTitle } from './hooks/useDocumentTitle'; type ResetResponse = { status: string; @@ -68,6 +69,8 @@ export default function ResetPasswordPage() { paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', }; + useDocumentTitle(t('login.reset.title', 'Reset password')); + const mutation = useMutation({ mutationKey: ['tenantAdminResetPassword'], mutationFn: resetPassword, diff --git a/resources/js/admin/mobile/SettingsPage.tsx b/resources/js/admin/mobile/SettingsPage.tsx index 030f592..e78f142 100644 --- a/resources/js/admin/mobile/SettingsPage.tsx +++ b/resources/js/admin/mobile/SettingsPage.tsx @@ -264,7 +264,7 @@ export default function MobileSettingsPage() { { + onCheckedChange={(value: boolean) => { if (value) { void pushState.enable(); } else { diff --git a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx index fd6f964..6cec7ee 100644 --- a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { render, screen, waitFor, within } from '@testing-library/react'; import { ADMIN_EVENTS_PATH } from '../../constants'; +import type { TenantEvent } from '../../api'; const fixtures = vi.hoisted(() => ({ event: { @@ -9,14 +10,17 @@ const fixtures = vi.hoisted(() => ({ name: 'Demo Wedding', slug: 'demo-event', event_date: '2026-02-19', - status: 'published' as const, - settings: { location: 'Berlin' }, + event_type_id: null, + event_type: null, + status: 'published', + engagement_mode: undefined, + settings: { location: 'Berlin', guest_upload_visibility: 'immediate' }, tasks_count: 4, photo_count: 12, active_invites_count: 3, total_invites_count: 5, member_permissions: ['photos:moderate', 'tasks:manage', 'join-tokens:manage'], - }, + } as TenantEvent, activePackage: { id: 1, package_id: 1, diff --git a/resources/js/admin/mobile/__tests__/EventsPage.test.tsx b/resources/js/admin/mobile/__tests__/EventsPage.test.tsx index 08697e1..6aa45e2 100644 --- a/resources/js/admin/mobile/__tests__/EventsPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/EventsPage.test.tsx @@ -138,7 +138,9 @@ vi.mock('../components/Primitives', () => ({ })); vi.mock('../components/FormControls', () => ({ - MobileInput: ({ compact: _compact, ...props }: React.InputHTMLAttributes) => , + MobileInput: ({ compact: _compact, ...props }: { compact?: boolean } & React.InputHTMLAttributes) => ( + + ), })); vi.mock('@tamagui/card', () => ({ diff --git a/resources/js/admin/mobile/__tests__/QrLayoutCustomizePage.test.tsx b/resources/js/admin/mobile/__tests__/QrLayoutCustomizePage.test.tsx index 928b471..c3cee0a 100644 --- a/resources/js/admin/mobile/__tests__/QrLayoutCustomizePage.test.tsx +++ b/resources/js/admin/mobile/__tests__/QrLayoutCustomizePage.test.tsx @@ -13,18 +13,36 @@ const fixtures = vi.hoisted(() => ({ invites: [ { id: 1, + token: 'invite-token', url: 'https://example.test/guest/demo-event', - qr_code_data_url: '', + label: null, + qr_code_data_url: null, + usage_limit: null, + usage_count: 0, + expires_at: null, + revoked_at: null, + is_active: true, + created_at: '2026-01-15T12:00:00Z', + metadata: {}, layouts: [ { id: 'layout-1', name: 'Poster Layout', description: 'Layout description', + subtitle: 'Layout subtitle', paper: 'a4', orientation: 'portrait', panel_mode: 'single', + preview: { + background: null, + background_gradient: null, + accent: null, + text: null, + }, + formats: [], }, ], + layouts_url: null, }, ], })); diff --git a/resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx b/resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx index b507180..f9cb5a4 100644 --- a/resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; const fixtures = vi.hoisted(() => ({ event: { @@ -74,7 +75,11 @@ vi.mock('../components/MobileShell', () => ({ vi.mock('../components/Primitives', () => ({ MobileCard: ({ children }: { children: React.ReactNode }) =>
{children}
, - CTAButton: ({ label }: { label: string }) => , + CTAButton: ({ label, onPress, disabled }: { label: string; onPress?: () => void; disabled?: boolean }) => ( + + ), PillBadge: ({ children }: { children: React.ReactNode }) =>
{children}
, })); @@ -116,6 +121,7 @@ vi.mock('../theme', () => ({ })); import MobileQrPrintPage from '../QrPrintPage'; +import { createQrInvite } from '../../api'; describe('MobileQrPrintPage', () => { it('renders QR overview content', async () => { @@ -125,4 +131,19 @@ describe('MobileQrPrintPage', () => { expect(screen.getByText('Schritt 1: Format wählen')).toBeInTheDocument(); expect(screen.getAllByText('Neuen QR-Link erstellen').length).toBeGreaterThan(0); }); + + it('requires confirmation before creating a new QR link', async () => { + const user = userEvent.setup(); + const confirmSpy = vi.fn().mockReturnValue(false); + window.confirm = confirmSpy; + + render(); + + const createButton = await screen.findByRole('button', { name: 'Neuen QR-Link erstellen' }); + await user.click(createButton); + + expect(confirmSpy).toHaveBeenCalled(); + expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('Ausdrucke')); + expect(createQrInvite).not.toHaveBeenCalled(); + }); }); diff --git a/resources/js/admin/mobile/__tests__/packageShop.test.ts b/resources/js/admin/mobile/__tests__/packageShop.test.ts index 6190607..7ae64ee 100644 --- a/resources/js/admin/mobile/__tests__/packageShop.test.ts +++ b/resources/js/admin/mobile/__tests__/packageShop.test.ts @@ -62,7 +62,7 @@ describe('selectRecommendedPackageId', () => { ] as any; it('returns null when no feature is requested', () => { - expect(selectRecommendedPackageId(packages, null, 100)).toBeNull(); + expect(selectRecommendedPackageId(packages, null, null)).toBeNull(); }); it('selects the cheapest upgrade with the feature', () => { diff --git a/resources/js/admin/mobile/components/FormControls.tsx b/resources/js/admin/mobile/components/FormControls.tsx index 3feffaf..7b7b583 100644 --- a/resources/js/admin/mobile/components/FormControls.tsx +++ b/resources/js/admin/mobile/components/FormControls.tsx @@ -184,7 +184,7 @@ export const MobileInput = React.forwardRef { + onChangeText={(value: string) => { onChange?.({ target: { value } } as React.ChangeEvent); }} size={compact ? '$3' : '$4'} @@ -222,7 +222,7 @@ export const MobileTextArea = React.forwardRef< ref={ref as React.Ref} {...props} {...({ minHeight: compact ? 72 : 96 } as any)} - onChangeText={(value) => { + onChangeText={(value: string) => { onChange?.({ target: { value } } as React.ChangeEvent); }} size={compact ? '$3' : '$4'} @@ -292,7 +292,7 @@ export function MobileSelect({