Refactor: Update Tenant PWA headers and tabs to use Playfair Display and Tamagui components
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user