diff --git a/public/manifest.json b/public/manifest.json index c7bd119..531806b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -18,11 +18,23 @@ "type": "image/svg+xml", "purpose": "any" }, + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "maskable" + }, { "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png", "purpose": "any" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "maskable" } ], "shortcuts": [ diff --git a/resources/css/app.css b/resources/css/app.css index 3b60cb5..926a1cb 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -511,6 +511,27 @@ h4, --sidebar-ring: oklch(0.439 0 0); } +@keyframes mobile-shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.mobile-skeleton { + border-color: transparent !important; + background: linear-gradient( + 90deg, + rgba(148, 163, 184, 0.12), + rgba(148, 163, 184, 0.28), + rgba(148, 163, 184, 0.12) + ); + background-size: 200% 100%; + animation: mobile-shimmer 1.4s ease-in-out infinite; +} + @layer base { * { @apply border-border; diff --git a/resources/js/admin/main.tsx b/resources/js/admin/main.tsx index 286aa0c..d2d5608 100644 --- a/resources/js/admin/main.tsx +++ b/resources/js/admin/main.tsx @@ -17,6 +17,7 @@ import MatomoTracker from '@/components/analytics/MatomoTracker'; import { ConsentProvider } from '@/contexts/consent'; import CookieBanner from '@/components/consent/CookieBanner'; import { Sentry, initSentry } from '@/lib/sentry'; +import { prefetchMobileRoutes } from './mobile/prefetch'; const DevTenantSwitcher = React.lazy(() => import('./DevTenantSwitcher')); @@ -65,6 +66,10 @@ function AdminApp() { const { resolved } = useAppearance(); const themeName = resolved ?? 'light'; + React.useEffect(() => { + prefetchMobileRoutes(); + }, []); + return ( diff --git a/resources/js/admin/mobile/AuthCallbackPage.tsx b/resources/js/admin/mobile/AuthCallbackPage.tsx index 22c14b9..72ceb33 100644 --- a/resources/js/admin/mobile/AuthCallbackPage.tsx +++ b/resources/js/admin/mobile/AuthCallbackPage.tsx @@ -10,6 +10,10 @@ export default function AuthCallbackPage(): React.ReactElement { const navigate = useNavigate(); const { t } = useTranslation('auth'); const [redirected, setRedirected] = React.useState(false); + const safeAreaStyle: React.CSSProperties = { + paddingTop: 'calc(env(safe-area-inset-top, 0px) + 16px)', + paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 16px)', + }; const searchParams = React.useMemo(() => new URLSearchParams(window.location.search), []); const rawReturnTo = searchParams.get('return_to'); @@ -36,7 +40,10 @@ export default function AuthCallbackPage(): React.ReactElement { }, [destination, navigate, redirected, status]); return ( -
+
{t('processing.title', 'Signing you in …')}

{t('processing.copy', 'One moment please while we prepare your dashboard.')}

diff --git a/resources/js/admin/mobile/BillingPage.tsx b/resources/js/admin/mobile/BillingPage.tsx index 779a30a..fed8554 100644 --- a/resources/js/admin/mobile/BillingPage.tsx +++ b/resources/js/admin/mobile/BillingPage.tsx @@ -6,7 +6,7 @@ import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import toast from 'react-hot-toast'; -import { MobileShell } from './components/MobileShell'; +import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge } from './components/Primitives'; import { createTenantBillingPortalSession, @@ -104,9 +104,9 @@ export default function MobileBillingPage() { title={t('billing.title', 'Billing & Packages')} onBack={() => navigate(-1)} headerActions={ - load()}> + load()} ariaLabel={t('common.refresh', 'Refresh')}> - + } > {error ? ( diff --git a/resources/js/admin/mobile/BrandingPage.tsx b/resources/js/admin/mobile/BrandingPage.tsx index 94433d7..c4a9b62 100644 --- a/resources/js/admin/mobile/BrandingPage.tsx +++ b/resources/js/admin/mobile/BrandingPage.tsx @@ -5,8 +5,8 @@ import { Image as ImageIcon, RefreshCcw, UploadCloud, Trash2, ChevronDown, Save, import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; -import { MobileShell } from './components/MobileShell'; -import { MobileCard, CTAButton } from './components/Primitives'; +import { MobileShell, HeaderActionButton } from './components/MobileShell'; +import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives'; import { TenantEvent, getEvent, updateEvent, getTenantFonts, TenantFont, WatermarkSettings } from '../api'; import { isAuthError } from '../auth/tokens'; import { ApiError, getApiErrorMessage } from '../lib/apiError'; @@ -403,9 +403,9 @@ export default function MobileBrandingPage() { title={t('events.branding.titleShort', 'Branding')} onBack={() => navigate(-1)} headerActions={ - handleSave()}> + handleSave()} ariaLabel={t('common.save', 'Save')}> - + } > {error ? ( @@ -618,7 +618,7 @@ export default function MobileBrandingPage() { > {fontsLoading ? ( - Array.from({ length: 4 }).map((_, idx) => ) + Array.from({ length: 4 }).map((_, idx) => ) ) : fonts.length === 0 ? ( {t('events.branding.noFonts', 'Keine Schriftarten gefunden.')} diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index 436a8b7..f4c5564 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -7,7 +7,7 @@ import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell, renderEventLocation } from './components/MobileShell'; -import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge } from './components/Primitives'; +import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } from './components/Primitives'; import { adminPath } from '../constants'; import { useEventContext } from '../context/EventContext'; import { getEventStats, EventStats, TenantEvent, getEvents } from '../api'; @@ -73,13 +73,13 @@ export default function MobileDashboardPage() { if (isLoading || fallbackLoading) { return ( - - {Array.from({ length: 3 }).map((_, idx) => ( - - ))} - - - ); + + {Array.from({ length: 3 }).map((_, idx) => ( + + ))} + + + ); } if (!effectiveHasEvents) { diff --git a/resources/js/admin/mobile/EventDetailPage.tsx b/resources/js/admin/mobile/EventDetailPage.tsx index 33c78bc..f2507d8 100644 --- a/resources/js/admin/mobile/EventDetailPage.tsx +++ b/resources/js/admin/mobile/EventDetailPage.tsx @@ -5,7 +5,7 @@ import { CalendarDays, MapPin, Settings, Users, Camera, Sparkles, QrCode, Image, import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; -import { MobileShell } from './components/MobileShell'; +import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileCard, PillBadge, KpiTile, ActionTile } from './components/Primitives'; import { TenantEvent, EventStats, EventToolkit, getEvent, getEventStats, getEventToolkit, getEvents } from '../api'; import { adminPath, ADMIN_EVENT_BRANDING_PATH, ADMIN_EVENT_GUEST_NOTIFICATIONS_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_MEMBERS_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_TASKS_PATH } from '../constants'; @@ -102,12 +102,12 @@ export default function MobileEventDetailPage() { onBack={() => navigate(-1)} headerActions={ - navigate(adminPath('/mobile/settings'))}> + navigate(adminPath('/mobile/settings'))} ariaLabel={t('mobileSettings.title', 'Settings')}> - - navigate(0)}> + + navigate(0)} ariaLabel={t('common.refresh', 'Refresh')}> - + } > diff --git a/resources/js/admin/mobile/EventFormPage.tsx b/resources/js/admin/mobile/EventFormPage.tsx index baea1a7..effa637 100644 --- a/resources/js/admin/mobile/EventFormPage.tsx +++ b/resources/js/admin/mobile/EventFormPage.tsx @@ -7,6 +7,7 @@ import { SizableText as Text } from '@tamagui/text'; import { Switch } from '@tamagui/switch'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton } from './components/Primitives'; +import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; import { LegalConsentSheet } from './components/LegalConsentSheet'; import { createEvent, getEvent, updateEvent, getEventTypes, TenantEvent, TenantEventType } from '../api'; import { resolveEventSlugAfterUpdate } from './eventFormNavigation'; @@ -206,38 +207,36 @@ export default function MobileEventFormPage() { ) : null} - - + setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder={t('eventForm.fields.name.placeholder', 'e.g. Summer Party 2025')} - style={inputStyle} /> - + - + - setForm((prev) => ({ ...prev, date: e.target.value }))} - style={{ ...inputStyle, flex: 1 }} + style={{ flex: 1 }} /> - + - + {typesLoading ? ( {t('eventForm.fields.type.loading', 'Loading event types…')} ) : eventTypes.length === 0 ? ( {t('eventForm.fields.type.empty', 'No event types available yet. Please add one in the admin area.')} ) : ( - + )} - + - -