688 lines
25 KiB
TypeScript
688 lines
25 KiB
TypeScript
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, Download, Image as ImageIcon, Layout, ListTodo, Megaphone, 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';
|
|
import { ListItem } from '@tamagui/list-item';
|
|
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 { Separator } from 'tamagui';
|
|
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 } from '../lib/events';
|
|
import { useAuth } from '../auth/context';
|
|
import { useAdminTheme } from './theme';
|
|
import { buildLimitWarnings } from '../lib/limitWarnings';
|
|
import { withAlpha } from './components/colors';
|
|
import { useEventReadiness } from './hooks/useEventReadiness';
|
|
import { SetupChecklist } from './components/SetupChecklist';
|
|
import { KpiStrip, PillBadge } from './components/Primitives';
|
|
|
|
// --- HELPERS ---
|
|
|
|
function translateLimits(t: any) {
|
|
return (key: string, options?: any) => t(`management:limits.${key}`, key, options);
|
|
}
|
|
|
|
// --- TAMAGUI-ALIGNED PRIMITIVES ---
|
|
|
|
function DashboardCard({ children, style, ...rest }: React.ComponentProps<typeof Card>) {
|
|
const theme = useAdminTheme();
|
|
return (
|
|
<Card
|
|
backgroundColor={theme.surface}
|
|
borderRadius={20}
|
|
borderWidth={1}
|
|
borderColor={theme.border}
|
|
padding="$3.5"
|
|
shadowColor={theme.shadow}
|
|
shadowOpacity={0.16}
|
|
shadowRadius={16}
|
|
shadowOffset={{ width: 0, height: 10 }}
|
|
style={style}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function SectionHeader({
|
|
title,
|
|
subtitle,
|
|
action,
|
|
}: {
|
|
title: string;
|
|
subtitle?: string;
|
|
action?: React.ReactNode;
|
|
}) {
|
|
const theme = useAdminTheme();
|
|
return (
|
|
<YStack space="$1.5">
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong}>
|
|
{title}
|
|
</Text>
|
|
{action ?? null}
|
|
</XStack>
|
|
{subtitle ? (
|
|
<Text fontSize="$sm" color={theme.muted}>
|
|
{subtitle}
|
|
</Text>
|
|
) : null}
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
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 };
|
|
|
|
return <PillBadge tone={config.tone}>{config.label}</PillBadge>;
|
|
}
|
|
|
|
// --- MAIN PAGE COMPONENT ---
|
|
|
|
export default function MobileDashboardPage() {
|
|
const navigate = useNavigate();
|
|
const { slug: slugParam } = useParams<{ slug?: string }>();
|
|
const { t, i18n } = useTranslation(['management', 'dashboard', 'mobile']);
|
|
const { events, activeEvent, hasEvents, isLoading, selectEvent } = useEventContext();
|
|
const { user } = useAuth();
|
|
const isMember = user?.role === 'member';
|
|
|
|
// --- LOGIC ---
|
|
const memberPermissions = React.useMemo(() => {
|
|
if (!isMember) return ['*'];
|
|
return Array.isArray(activeEvent?.member_permissions) ? activeEvent?.member_permissions ?? [] : [];
|
|
}, [activeEvent?.member_permissions, isMember]);
|
|
|
|
function allowPermission(permissions: string[], permission: string): boolean {
|
|
if (permissions.includes('*') || permissions.includes(permission)) return true;
|
|
if (permission.includes(':')) {
|
|
const [prefix] = permission.split(':');
|
|
return permissions.includes(`${prefix}:*`);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const canManageEvents = React.useMemo(() => allowPermission(memberPermissions, 'events:manage'), [memberPermissions]);
|
|
|
|
const { data: stats } = useQuery<EventStats | null>({
|
|
queryKey: ['mobile', 'dashboard', 'stats', activeEvent?.slug],
|
|
enabled: Boolean(activeEvent?.slug),
|
|
queryFn: async () => {
|
|
if (!activeEvent?.slug) return null;
|
|
return await getEventStats(activeEvent.slug);
|
|
},
|
|
});
|
|
|
|
const { data: photoData } = useQuery({
|
|
queryKey: ['mobile', 'dashboard', 'recent-photos', activeEvent?.slug],
|
|
enabled: Boolean(activeEvent?.slug),
|
|
queryFn: async () => {
|
|
if (!activeEvent?.slug) return { photos: [] };
|
|
return await getEventPhotos(activeEvent.slug, { perPage: 6, sort: 'desc' });
|
|
},
|
|
});
|
|
|
|
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
|
|
|
React.useEffect(() => {
|
|
if (!slugParam || slugParam === activeEvent?.slug) return;
|
|
selectEvent(slugParam);
|
|
}, [activeEvent?.slug, selectEvent, slugParam]);
|
|
|
|
const shouldRedirectToSelector = !isLoading && !activeEvent && !slugParam;
|
|
|
|
React.useEffect(() => {
|
|
if (!shouldRedirectToSelector) return;
|
|
navigate(ADMIN_EVENTS_PATH, { replace: true });
|
|
}, [navigate, shouldRedirectToSelector]);
|
|
|
|
// --- RENDER ---
|
|
|
|
if (shouldRedirectToSelector) {
|
|
return null;
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
|
<DashboardCard height={120} />
|
|
</MobileShell>
|
|
);
|
|
}
|
|
|
|
if (!hasEvents && !events.length) {
|
|
return (
|
|
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
|
<EmptyState canManage={canManageEvents} onCreate={() => navigate(adminPath('/mobile/events/new'))} />
|
|
</MobileShell>
|
|
);
|
|
}
|
|
|
|
// Calculate Readiness
|
|
const readiness = useEventReadiness(activeEvent, t as any);
|
|
const phase = activeEvent ? getEventPhase(activeEvent) : 'setup';
|
|
const isCompleted = phase === 'post';
|
|
|
|
return (
|
|
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
|
<SectionHeader
|
|
title={t('dashboard:overview.title', 'At a glance')}
|
|
subtitle={t('dashboard:overview.description', 'Key customer metrics at a glance.')}
|
|
/>
|
|
|
|
{/* 1. LIFECYCLE HERO */}
|
|
<LifecycleHero
|
|
event={activeEvent}
|
|
stats={stats}
|
|
locale={locale}
|
|
navigate={navigate}
|
|
readiness={readiness}
|
|
/>
|
|
|
|
{/* 1b. SETUP CHECKLIST */}
|
|
{phase === 'setup' && (
|
|
<SetupChecklist
|
|
steps={readiness.steps}
|
|
title={t('management:photobooth.checklist.title', 'Checklist')}
|
|
/>
|
|
)}
|
|
|
|
{/* 2. PULSE STRIP */}
|
|
<PulseStrip event={activeEvent} stats={stats} />
|
|
|
|
{/* 3. ALERTS */}
|
|
<AlertsSection event={activeEvent} stats={stats} t={t} />
|
|
|
|
{/* 4. UNIFIED COMMAND GRID */}
|
|
<SectionHeader
|
|
title={t('dashboard:quickActions.title', 'Quick actions')}
|
|
subtitle={t('dashboard:quickActions.description', 'Jump straight to the most important actions.')}
|
|
/>
|
|
<UnifiedToolGrid
|
|
event={activeEvent}
|
|
navigate={navigate}
|
|
permissions={memberPermissions}
|
|
isMember={isMember}
|
|
isCompleted={isCompleted}
|
|
/>
|
|
|
|
{/* 5. RECENT PHOTOS */}
|
|
<RecentPhotosSection
|
|
photos={photoData?.photos ?? []}
|
|
navigate={navigate}
|
|
slug={activeEvent?.slug}
|
|
/>
|
|
|
|
</MobileShell>
|
|
);
|
|
}
|
|
|
|
// --- SUB COMPONENTS ---
|
|
|
|
type EventPhase = 'setup' | 'live' | 'post';
|
|
|
|
function getEventPhase(event: TenantEvent): EventPhase {
|
|
if (event.status === 'archived') return 'post';
|
|
if (!event.event_date) return 'setup';
|
|
const today = startOfDay(new Date());
|
|
const eventDate = parseISO(event.event_date);
|
|
if (isSameDay(today, eventDate)) return 'live';
|
|
if (isPast(eventDate)) return 'post';
|
|
return 'setup';
|
|
}
|
|
|
|
function LifecycleHero({ event, stats, locale, navigate, readiness }: any) {
|
|
const theme = useAdminTheme();
|
|
const { t } = useTranslation(['management', 'dashboard']);
|
|
|
|
if (!event) return null;
|
|
const phase = getEventPhase(event);
|
|
const pendingPhotos = stats?.pending_photos ?? event.pending_photo_count ?? 0;
|
|
|
|
// Header Row
|
|
const Header = () => (
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<YStack>
|
|
<Text fontSize="$xs" color={theme.muted} fontWeight="700" textTransform="uppercase" letterSpacing={1}>
|
|
{formatEventDate(event.event_date, locale)}
|
|
</Text>
|
|
</YStack>
|
|
<StatusBadge status={event.status} />
|
|
</XStack>
|
|
);
|
|
|
|
if (phase === 'live') {
|
|
return (
|
|
<YStack space="$2">
|
|
<Header />
|
|
<DashboardCard
|
|
backgroundColor={theme.primary}
|
|
borderColor="transparent"
|
|
style={{ backgroundImage: 'linear-gradient(135deg, #4F46E5 0%, #4338CA 100%)' }}
|
|
>
|
|
<YStack space="$3">
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<YStack space="$1">
|
|
<XStack alignItems="center" space="$2">
|
|
<YStack width={8} height={8} borderRadius={4} backgroundColor="#22C55E" />
|
|
<Text color="white" fontWeight="700" fontSize="$xs" textTransform="uppercase" letterSpacing={1}>
|
|
{t('dashboard:liveNow.status', 'Happening Now')}
|
|
</Text>
|
|
</XStack>
|
|
<Text color="white" fontSize="$lg" fontWeight="800">
|
|
{pendingPhotos > 0
|
|
? `${pendingPhotos} ${t('management:photos.filters.pending', 'Pending')}`
|
|
: t('dashboard:liveNow.description', 'Event is Running')}
|
|
</Text>
|
|
</YStack>
|
|
{pendingPhotos > 0 ? (
|
|
<Button
|
|
onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/control-room`))}
|
|
backgroundColor="white"
|
|
borderColor="transparent"
|
|
height={36}
|
|
borderRadius={999}
|
|
paddingHorizontal="$3"
|
|
>
|
|
<Text fontSize="$xs" fontWeight="800" color={theme.primary}>
|
|
{t('management:photos.openModeration', 'Review')}
|
|
</Text>
|
|
</Button>
|
|
) : null}
|
|
</XStack>
|
|
</YStack>
|
|
</DashboardCard>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
const daysToGo = event.event_date ? differenceInDays(parseISO(event.event_date), new Date()) : 0;
|
|
|
|
if (phase === 'post') {
|
|
return (
|
|
<YStack space="$2">
|
|
<Header />
|
|
<DashboardCard>
|
|
<YStack space="$3">
|
|
<XStack alignItems="center" space="$2.5">
|
|
<YStack width={40} height={40} borderRadius={20} backgroundColor={theme.success} alignItems="center" justifyContent="center">
|
|
<CheckCircle2 size={20} color="white" />
|
|
</YStack>
|
|
<YStack>
|
|
<Text fontSize="$md" fontWeight="800" color={theme.textStrong}>
|
|
{t('events.recap.completedTitle', 'Event completed')}
|
|
</Text>
|
|
<Text fontSize="$xs" color={theme.muted}>{t('events.recap.galleryOpen', 'Gallery online')}</Text>
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<Button
|
|
onPress={() => navigate(adminPath(`/mobile/exports`))}
|
|
backgroundColor={theme.primary}
|
|
borderColor="transparent"
|
|
height={48}
|
|
borderRadius={16}
|
|
>
|
|
<XStack alignItems="center" space="$2">
|
|
<Download size={16} color="white" />
|
|
<Text fontSize="$sm" fontWeight="800" color="white">
|
|
{t('events.recap.downloadAll', 'Download photos')}
|
|
</Text>
|
|
</XStack>
|
|
</Button>
|
|
|
|
<Button
|
|
onPress={() => navigate(adminPath(`/mobile/events/${event.slug}/recap`))}
|
|
backgroundColor="transparent"
|
|
borderColor={theme.border}
|
|
borderWidth={1}
|
|
height={48}
|
|
borderRadius={16}
|
|
>
|
|
<XStack alignItems="center" space="$2">
|
|
<Text fontSize="$sm" fontWeight="800" color={theme.textStrong}>
|
|
{t('events.recap.openRecap', 'Open recap')}
|
|
</Text>
|
|
<ChevronRight size={16} color={theme.muted} />
|
|
</XStack>
|
|
</Button>
|
|
</YStack>
|
|
</DashboardCard>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
// SETUP
|
|
const nextStep = readiness.nextStep;
|
|
const ctaLabel = nextStep ? nextStep.ctaLabel : t('dashboard:onboarding.hero.cta', 'Setup Complete');
|
|
const ctaAction = nextStep ? () => navigate(adminPath(nextStep.targetPath)) : undefined;
|
|
|
|
return (
|
|
<YStack space="$2">
|
|
<Header />
|
|
<DashboardCard>
|
|
<YStack space="$3">
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<YStack>
|
|
<Text fontSize="$xs" color={theme.muted} fontWeight="700" textTransform="uppercase">
|
|
{t('dashboard:upcoming.status.planning', 'Countdown')}
|
|
</Text>
|
|
<Text fontSize="$2xl" fontWeight="900" color={theme.primary}>
|
|
{daysToGo}{' '}
|
|
<Text fontSize="$sm" color={theme.muted} fontWeight="500">
|
|
{t('management:galleryStatus.daysLabel', 'days')}
|
|
</Text>
|
|
</Text>
|
|
</YStack>
|
|
<YStack width={48} height={48} borderRadius={24} backgroundColor={theme.surfaceMuted} alignItems="center" justifyContent="center">
|
|
<CalendarDays size={24} color={theme.primary} />
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
|
|
{!readiness.isReady ? (
|
|
<Button
|
|
onPress={ctaAction}
|
|
backgroundColor={theme.primary}
|
|
borderColor="transparent"
|
|
height={48}
|
|
borderRadius={16}
|
|
>
|
|
<XStack alignItems="center" space="$2">
|
|
<Text fontSize="$sm" fontWeight="800" color="white">
|
|
{ctaLabel}
|
|
</Text>
|
|
<ChevronRight size={16} color="white" />
|
|
</XStack>
|
|
</Button>
|
|
) : (
|
|
<XStack alignItems="center" space="$2">
|
|
<CheckCircle2 size={18} color={theme.successText} />
|
|
<Text fontSize="$sm" color={theme.successText} fontWeight="700">
|
|
{t('management:mobileDashboard.readyForLiftoff', 'Ready for Liftoff')}
|
|
</Text>
|
|
</XStack>
|
|
)}
|
|
</YStack>
|
|
</DashboardCard>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
function PulseStrip({ event, stats }: any) {
|
|
const theme = useAdminTheme();
|
|
const { t } = useTranslation('management');
|
|
const uploadCount = stats?.uploads_total ?? event?.photo_count ?? 0;
|
|
const guestCount = event?.active_invites_count ?? event?.total_invites_count ?? 0;
|
|
const pendingCount = stats?.pending_photos ?? event?.pending_photo_count ?? 0;
|
|
|
|
return (
|
|
<YStack space="$2">
|
|
<SectionHeader title={t('management:eventMenu.summary', 'Overview')} />
|
|
<KpiStrip items={[
|
|
{
|
|
icon: ImageIcon,
|
|
value: uploadCount,
|
|
label: t('management:events.list.stats.photos', 'Photos'),
|
|
color: theme.primary
|
|
},
|
|
{
|
|
icon: Users,
|
|
value: guestCount,
|
|
label: t('management:events.list.stats.guests', 'Guests'),
|
|
color: theme.textStrong
|
|
},
|
|
{
|
|
icon: ShieldCheck,
|
|
value: pendingCount,
|
|
label: t('management:photos.filters.pending', 'Pending'),
|
|
note: pendingCount > 0 ? t('management:common.actionNeeded', 'Review') : undefined,
|
|
color: pendingCount > 0 ? '#8B5CF6' : theme.textMuted
|
|
}
|
|
]} />
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) {
|
|
const theme = useAdminTheme();
|
|
const { t } = useTranslation(['management', 'dashboard']);
|
|
const slug = event?.slug;
|
|
if (!slug) return null;
|
|
|
|
const experienceItems = [
|
|
{ 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));
|
|
|
|
const operationsItems = [
|
|
!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: theme.text },
|
|
!isCompleted ? { label: t('management:events.quick.guestMessages', 'Messages'), icon: Megaphone, path: `/mobile/events/${slug}/guest-notifications`, color: theme.text } : null,
|
|
!isCompleted ? { label: t('events.branding.titleShort', 'Branding'), icon: Layout, path: `/mobile/events/${slug}/branding`, color: theme.text } : null,
|
|
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
|
|
|
const adminItems = [
|
|
{ label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics` },
|
|
!isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports` } : null,
|
|
{ label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit` },
|
|
].filter((item): item is { label: string; icon: any; path: string; color?: string } => Boolean(item));
|
|
|
|
const sections = [
|
|
{
|
|
title: t('management:mobileDashboard.quickActionsTitle', 'Experience'),
|
|
items: experienceItems,
|
|
},
|
|
{
|
|
title: t('management:events.quickActions.title', 'Operations'),
|
|
items: operationsItems,
|
|
},
|
|
{
|
|
title: t('management:settings.hero.badge', 'Admin'),
|
|
items: adminItems,
|
|
}
|
|
].filter((section) => section.items.length > 0);
|
|
|
|
return (
|
|
<YStack space="$3">
|
|
{sections.map((section) => (
|
|
<DashboardCard key={section.title} padding="$0">
|
|
<YStack padding="$3.5" paddingBottom="$2.5" space="$1">
|
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
|
{section.title}
|
|
</Text>
|
|
</YStack>
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
<YGroup {...({ borderRadius: "$4", borderWidth: 1, borderColor: theme.border, overflow: "hidden" } as any)}>
|
|
{section.items.map((item) => {
|
|
const iconColor = item.color || theme.textStrong;
|
|
return (
|
|
<YGroup.Item key={item.label}>
|
|
<ListItem
|
|
hoverTheme
|
|
pressTheme
|
|
paddingVertical="$2"
|
|
paddingHorizontal="$3"
|
|
onPress={() => navigate(adminPath(item.path))}
|
|
title={
|
|
<XStack alignItems="center" space="$2.5">
|
|
<XStack
|
|
width={32}
|
|
height={32}
|
|
borderRadius={10}
|
|
backgroundColor={withAlpha(iconColor, 0.12)}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
>
|
|
<item.icon size={18} color={iconColor} />
|
|
</XStack>
|
|
<Text fontSize="$sm" fontWeight="700" color={theme.textStrong}>
|
|
{item.label}
|
|
</Text>
|
|
</XStack>
|
|
}
|
|
iconAfter={<ChevronRight size={16} color={theme.muted} />}
|
|
/>
|
|
</YGroup.Item>
|
|
);
|
|
})}
|
|
</YGroup>
|
|
</DashboardCard>
|
|
))}
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[], navigate: any, slug: string }) {
|
|
const theme = useAdminTheme();
|
|
const { t } = useTranslation('management');
|
|
|
|
if (!photos.length) return null;
|
|
|
|
return (
|
|
<DashboardCard>
|
|
<YStack space="$2">
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
|
{t('photos.recentTitle', 'Latest Uploads')}
|
|
</Text>
|
|
<Button
|
|
onPress={() => navigate(adminPath(`/mobile/events/${slug}/control-room`))}
|
|
backgroundColor="transparent"
|
|
borderColor="transparent"
|
|
height={28}
|
|
paddingHorizontal={0}
|
|
>
|
|
<Text fontSize="$xs" color={theme.primary} fontWeight="700">
|
|
{t('common.all', 'See all')}
|
|
</Text>
|
|
</Button>
|
|
</XStack>
|
|
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
|
|
<XStack space="$2" overflow="scroll" paddingVertical="$1">
|
|
{photos.map((photo) => (
|
|
<Pressable key={photo.id} onPress={() => navigate(adminPath(`/mobile/events/${slug}/control-room`))}>
|
|
<YStack
|
|
width={80}
|
|
height={80}
|
|
borderRadius={12}
|
|
overflow="hidden"
|
|
backgroundColor={theme.surfaceMuted}
|
|
borderWidth={1}
|
|
borderColor={theme.border}
|
|
>
|
|
{photo.thumbnail_url ? (
|
|
<Image source={{ uri: photo.thumbnail_url }} width={80} height={80} resizeMode="cover" />
|
|
) : (
|
|
<YStack flex={1} alignItems="center" justifyContent="center">
|
|
<ImageIcon size={20} color={theme.muted} />
|
|
</YStack>
|
|
)}
|
|
</YStack>
|
|
</Pressable>
|
|
))}
|
|
</XStack>
|
|
</YStack>
|
|
</DashboardCard>
|
|
);
|
|
}
|
|
|
|
function AlertsSection({ event, stats, t }: any) {
|
|
const theme = useAdminTheme();
|
|
const limitWarnings = buildLimitWarnings(event?.limits ?? null, translateLimits(t));
|
|
|
|
if (!limitWarnings.length) return null;
|
|
|
|
return (
|
|
<DashboardCard>
|
|
<YStack space="$2">
|
|
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
|
|
{t('management:alertsTitle', 'Alerts')}
|
|
</Text>
|
|
<Separator backgroundColor={theme.border} opacity={0.6} />
|
|
<YStack space="$2">
|
|
{limitWarnings.map((w: any, idx: number) => {
|
|
const isDanger = w.tone === 'danger';
|
|
const bg = isDanger ? theme.dangerBg : theme.warningBg;
|
|
const border = isDanger ? theme.dangerText : theme.warningBorder;
|
|
const text = isDanger ? theme.dangerText : theme.warningText;
|
|
const Icon = isDanger ? AlertCircle : Bell;
|
|
|
|
return (
|
|
<XStack
|
|
key={idx}
|
|
backgroundColor={bg}
|
|
padding="$3"
|
|
borderRadius={12}
|
|
borderWidth={1}
|
|
borderColor={border}
|
|
alignItems="center"
|
|
space="$2"
|
|
>
|
|
<Icon size={16} color={text} />
|
|
<Text fontSize="$sm" color={text} fontWeight="600">
|
|
{w.message}
|
|
</Text>
|
|
</XStack>
|
|
);
|
|
})}
|
|
</YStack>
|
|
</YStack>
|
|
</DashboardCard>
|
|
);
|
|
}
|
|
|
|
function EmptyState({ canManage, onCreate }: any) {
|
|
const theme = useAdminTheme();
|
|
const { t } = useTranslation(['management', 'mobile']);
|
|
return (
|
|
<YStack flex={1} alignItems="center" justifyContent="center" space="$4">
|
|
<Sparkles size={48} color={theme.primary} />
|
|
<Text fontSize="$lg" fontWeight="800" color={theme.textStrong} textAlign="center">
|
|
{t('mobile:header.appName', 'Event Admin')}
|
|
</Text>
|
|
<Text fontSize="$sm" color={theme.muted} textAlign="center">
|
|
{t('mobile:header.noEventsBody', 'Create your first event.')}
|
|
</Text>
|
|
{canManage ? (
|
|
<Button
|
|
onPress={onCreate}
|
|
backgroundColor={theme.primary}
|
|
borderColor="transparent"
|
|
height={48}
|
|
borderRadius={16}
|
|
paddingHorizontal="$4"
|
|
>
|
|
<Text fontSize="$sm" fontWeight="800" color="white">
|
|
{t('management:events.list.actions.create', 'Create Event')}
|
|
</Text>
|
|
</Button>
|
|
) : null}
|
|
</YStack>
|
|
);
|
|
}
|