Refactor: Update Tenant PWA headers and tabs to use Playfair Display and Tamagui components
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-22 13:29:56 +01:00
parent b9d91c8f40
commit 911880f1a0
14 changed files with 425778 additions and 426862 deletions

View File

@@ -1,4 +1,14 @@
{
"mobileProfile": {
"settings": "Einstellungen"
},
"readiness": {
"steps": {
"basicsDescription": "Grundlage für die Gäste-Info.",
"accessDescription": "Der Schlüssel für deine Gäste.",
"tasksDescription": "Sorgt für 3x mehr Interaktion."
}
},
"billing": {
"title": "Pakete & Abrechnung",
"subtitle": "Verwalte deine gebuchten Pakete und behalte Laufzeiten im Blick.",
@@ -2244,6 +2254,9 @@
},
"mobileDashboard": {
"title": "Dashboard",
"shortcutAnalytics": "Analytics",
"quickActionsTitle": "Experience",
"readyForLiftoff": "Alles erledigt.",
"selectEvent": "Wähle ein Event, um fortzufahren",
"emptyBadge": "Willkommen!",
"emptyTitle": "Willkommen! Lass uns dein erstes Event starten",
@@ -2629,7 +2642,9 @@
"themeLight": "Hell",
"themeDark": "Dunkel",
"themeSystem": "System",
"logout": "Abmelden"
"logout": "Abmelden",
"logoutTitle": "Ausloggen",
"logoutHint": "Aus der App ausloggen"
},
"mobileSettings": {
"title": "Einstellungen",

View File

@@ -1,4 +1,14 @@
{
"mobileProfile": {
"settings": "Settings"
},
"readiness": {
"steps": {
"basicsDescription": "Foundation for guest info.",
"accessDescription": "Key for your guests.",
"tasksDescription": "Ensures 3x more engagement."
}
},
"billing": {
"title": "Packages & billing",
"subtitle": "Manage your purchased packages and track their durations.",
@@ -2246,6 +2256,9 @@
},
"mobileDashboard": {
"title": "Dashboard",
"shortcutAnalytics": "Analytics",
"quickActionsTitle": "Experience",
"readyForLiftoff": "Ready for Liftoff",
"selectEvent": "Select an event to continue",
"emptyBadge": "Welcome aboard",
"emptyTitle": "Welcome! Let's launch your first event",
@@ -2631,7 +2644,9 @@
"themeLight": "Light",
"themeDark": "Dark",
"themeSystem": "System",
"logout": "Log out"
"logout": "Log out",
"logoutTitle": "Sign out",
"logoutHint": "Sign out from this app."
},
"mobileSettings": {
"title": "Settings",

View File

@@ -1,19 +1,19 @@
import React from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Activity, Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users, ArrowRight, Play, Clock, AlertCircle } from 'lucide-react';
import { Bell, CalendarDays, Camera, CheckCircle2, Download, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users, ArrowRight, AlertCircle } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Image } from '@tamagui/image';
import { isSameDay, isPast, isFuture, parseISO, differenceInDays, startOfDay } from 'date-fns';
import { isSameDay, isPast, parseISO, differenceInDays, startOfDay } from 'date-fns';
import { MobileShell } from './components/MobileShell';
import { ADMIN_EVENTS_PATH, adminPath } from '../constants';
import { useEventContext } from '../context/EventContext';
import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto } from '../api';
import { formatEventDate, resolveEventDisplayName } from '../lib/events';
import { formatEventDate } from '../lib/events';
import { useAuth } from '../auth/context';
import { useAdminTheme } from './theme';
import { buildLimitWarnings } from '../lib/limitWarnings';
@@ -172,8 +172,6 @@ export default function MobileDashboardPage() {
navigate(ADMIN_EVENTS_PATH, { replace: true });
}, [navigate, shouldRedirectToSelector]);
const [eventSwitcherOpen, setEventSwitcherOpen] = React.useState(false);
// --- RENDER ---
if (shouldRedirectToSelector) {
@@ -391,7 +389,9 @@ function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch, re
{readiness.isReady && (
<XStack alignItems="center" space="$2">
<CheckCircle2 size={18} color={theme.successText} />
<Text fontSize="$sm" color={theme.successText} fontWeight="700">Ready for Liftoff</Text>
<Text fontSize="$sm" color={theme.successText} fontWeight="700">
{t('management:mobileDashboard.readyForLiftoff', 'Ready for Liftoff')}
</Text>
</XStack>
)}
</ModernCard>
@@ -461,11 +461,11 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
const sections = [
{
title: t('management:branding.badge', 'Experience'),
title: t('management:mobileDashboard.quickActionsTitle', 'Experience'),
items: experienceItems,
},
{
title: t('management:workspace.hero.badge', 'Operations'),
title: t('management:events.quickActions.title', 'Operations'),
items: operationsItems,
},
{

View File

@@ -10,7 +10,7 @@ import { Accordion } from '@tamagui/accordion';
import { ScrollView } from '@tamagui/scroll-view';
import { ToggleGroup } from '@tamagui/toggle-group';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
import { MobileCard, CTAButton, SkeletonCard, ContentTabs } from './components/Primitives';
import { MobileField, MobileSelect } from './components/FormControls';
import { useEventContext } from '../context/EventContext';
import {
@@ -279,7 +279,7 @@ export default function MobileEventControlRoomPage() {
const isMember = user?.role === 'member';
const slug = slugParam ?? activeEvent?.slug ?? null;
const online = useOnlineStatus();
const { textStrong, text, muted, border, accentSoft, accent, danger, primary, surfaceMuted, surface } = useAdminTheme();
const { textStrong, muted, border, primary, surfaceMuted, surface } = useAdminTheme();
const [activeTab, setActiveTab] = React.useState<'moderation' | 'live'>('moderation');
const [moderationPhotos, setModerationPhotos] = React.useState<TenantPhoto[]>([]);
@@ -342,8 +342,6 @@ export default function MobileEventControlRoomPage() {
const liveResetRef = React.useRef(false);
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
const infoBg = accentSoft;
const infoBorder = accent;
const activeFilterBg = primary;
const saveControlRoomSettings = React.useCallback(
@@ -1022,17 +1020,6 @@ export default function MobileEventControlRoomPage() {
}
}
function resolveGalleryLabel(status?: string | null): string {
const key = status ?? 'pending';
const fallbackMap: Record<string, string> = {
approved: 'Gallery approved',
pending: 'Gallery pending',
rejected: 'Gallery rejected',
hidden: 'Hidden',
};
return t(`liveShowQueue.galleryStatus.${key}`, fallbackMap[key] ?? key);
}
function resolveLiveLabel(status?: string | null): string {
const key = normalizeLiveStatus(status);
return t(`liveShowQueue.status.${key}`, key);
@@ -1078,24 +1065,10 @@ export default function MobileEventControlRoomPage() {
onBack={back}
headerActions={headerActions}
>
<XStack space="$2">
{([
{ key: 'moderation', label: t('controlRoom.tabs.moderation', 'Moderation') },
{ key: 'live', label: t('controlRoom.tabs.live', 'Live Show') },
] as const).map((tab) => (
<Pressable key={tab.key} onPress={() => setActiveTab(tab.key)} style={{ flex: 1 }}>
<MobileCard
backgroundColor={activeTab === tab.key ? infoBg : 'transparent'}
borderColor={activeTab === tab.key ? infoBorder : border}
padding="$2.5"
>
<Text fontSize="$sm" fontWeight="700" textAlign="center" color={textStrong}>
{tab.label}
</Text>
</MobileCard>
</Pressable>
))}
</XStack>
<ContentTabs
value={activeTab}
onValueChange={(val) => setActiveTab(val as 'moderation' | 'live')}
header={(
<MobileCard>
<Accordion type="single" collapsible>
@@ -1360,8 +1333,12 @@ export default function MobileEventControlRoomPage() {
</Accordion.Item>
</Accordion>
</MobileCard>
{activeTab === 'moderation' ? (
)}
tabs={[
{
value: 'moderation',
label: t('controlRoom.tabs.moderation', 'Moderation'),
content: (
<YStack space="$2">
{queuedEventCount > 0 ? (
<MobileCard>
@@ -1555,7 +1532,12 @@ export default function MobileEventControlRoomPage() {
</MobileCard>
) : null}
</YStack>
) : (
),
},
{
value: 'live',
label: t('controlRoom.tabs.live', 'Live Show'),
content: (
<YStack space="$2">
<MobileCard borderColor={border} backgroundColor="transparent">
<Text fontSize="$sm" color={muted}>
@@ -1744,7 +1726,10 @@ export default function MobileEventControlRoomPage() {
</MobileCard>
) : null}
</YStack>
)}
),
},
]}
/>
<LegalConsentSheet
open={consentOpen}

View File

@@ -206,7 +206,7 @@ export default function MobileProfilePage() {
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={textColor}>
{t('mobileProfile.preferences', 'Preferences')}
{t('mobileProfile.settings', 'Preferences')}
</Text>
</XStack>
<YGroup {...({ borderRadius: "$4", borderWidth: 1, borderColor: borderColor, overflow: "hidden" } as any)}>

View File

@@ -143,7 +143,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const EventContextPill = () => {
if (!effectiveActive || isEventsIndex || isCompactHeader) {
return (
<Text fontSize="$md" fontWeight="800" fontFamily="$display" color="white">
<Text fontSize="$md" fontWeight="700" fontFamily="$display" color="white">
{pageTitle}
</Text>
);
@@ -153,7 +153,14 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
if (!canSwitchEvents) {
return (
<Text fontSize="$sm" fontWeight="800" color="white" numberOfLines={1} ellipsizeMode="tail">
<Text
fontSize="$lg"
fontWeight="700"
fontFamily="$display"
color="white"
numberOfLines={1}
ellipsizeMode="tail"
>
{displayName}
</Text>
);

View File

@@ -2,7 +2,8 @@ import React from 'react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { ADMIN_GRADIENTS, useAdminTheme } from '../theme';
import { Tabs, Separator } from 'tamagui';
import { useAdminTheme } from '../theme';
import { withAlpha } from './colors';
export function MobileCard({
@@ -161,7 +162,7 @@ export function KpiTile({
note?: string;
color?: string;
}) {
const { surfaceMuted, textStrong, textMuted, primary, accentSoft } = useAdminTheme();
const { textStrong, textMuted, primary, accentSoft } = useAdminTheme();
const iconBg = color ? withAlpha(color, 0.12) : accentSoft;
const iconColor = color || primary;
@@ -207,7 +208,7 @@ export function KpiStrip({
note?: string;
}>
}) {
const { glassSurface, border, textStrong, textMuted, primary, accentSoft } = useAdminTheme();
const { glassSurface, border, textStrong, textMuted, primary } = useAdminTheme();
return (
<YStack
@@ -402,3 +403,63 @@ export function FloatingActionButton({
</Pressable>
);
}
export function ContentTabs({
value,
onValueChange,
tabs,
header,
}: {
value: string;
onValueChange: (val: string) => void;
tabs: { value: string; label: string; content: React.ReactNode }[];
header?: React.ReactNode;
}) {
const { border, muted, primary } = useAdminTheme();
return (
<Tabs
defaultValue={value}
value={value}
onValueChange={onValueChange}
orientation="horizontal"
flexDirection="column"
borderRadius="$4"
borderWidth={1}
borderColor={border}
overflow="hidden"
>
<Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom" backgroundColor="$surface">
{tabs.map((tab) => (
<Tabs.Tab
key={tab.value}
value={tab.value}
flex={1}
unstyled
paddingVertical="$3"
alignItems="center"
justifyContent="center"
backgroundColor={value === tab.value ? primary : 'transparent'}
hoverStyle={{ backgroundColor: value === tab.value ? primary : '$backgroundHover' }}
>
<Text
fontSize="$sm"
fontWeight={value === tab.value ? '700' : '500'}
color={value === tab.value ? '#fff' : muted}
>
{tab.label}
</Text>
</Tabs.Tab>
))}
</Tabs.List>
{header}
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value}>
{tab.content}
</Tabs.Content>
))}
</Tabs>
);
}

View File

@@ -55,7 +55,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
<Text />
)}
</XStack>
<Text fontSize="$lg" fontWeight="800" color={text}>
<Text fontSize="$lg" fontWeight="700" fontFamily="$display" color={text}>
{title}
</Text>
<XStack minWidth={40} justifyContent="flex-end">

View File

@@ -1,5 +1,4 @@
import { TenantEvent } from '../../api';
import { adminPath } from '../../constants';
export type ReadinessStep = {
id: string;
@@ -43,7 +42,7 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
{
id: 'basics',
label: t('management:events.form.date', 'Datum & Ort'),
description: 'Grundlage für die Gäste-Info.',
description: t('management:readiness.steps.basicsDescription', 'Grundlage für die Gäste-Info.'),
isComplete: hasDate && hasLocation,
ctaLabel: t('management:events.actions.edit', 'Bearbeiten'),
targetPath: `/mobile/events/${event.slug}/edit`,
@@ -52,7 +51,7 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
{
id: 'access',
label: t('management:invites.badge', 'QR-Codes'),
description: 'Der Schlüssel für deine Gäste.',
description: t('management:readiness.steps.accessDescription', 'Der Schlüssel für deine Gäste.'),
ctaLabel: t('management:invites.actions.create', 'QR-Code erstellen'),
isComplete: hasInvite,
targetPath: `/mobile/events/${event.slug}/qr`,
@@ -64,7 +63,7 @@ export function useEventReadiness(event: TenantEvent | null, t: (key: string, fa
steps.push({
id: 'tasks',
label: t('management:tasks.badge', 'Fotoaufgaben'),
description: 'Sorgt für 3x mehr Interaktion.',
description: t('management:readiness.steps.tasksDescription', 'Sorgt für 3x mehr Interaktion.'),
isComplete: hasTasks,
ctaLabel: t('management:tasks.actions.assign', 'Fotoaufgaben hinzufügen'),
targetPath: `/mobile/events/${event.slug}/tasks`,