import React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { TrendingUp, Users, ListTodo, Lock, Trophy } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { format, parseISO } from 'date-fns'; import { de, enGB } from 'date-fns/locale'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, KpiTile, SkeletonCard } from './components/Primitives'; import { getEventAnalytics, EventAnalytics } from '../api'; import { ApiError } from '../lib/apiError'; import { useAdminTheme } from './theme'; import { resolveMaxCount, resolveTimelineHours } from './lib/analytics'; import { adminPath } from '../constants'; export default function MobileEventAnalyticsPage() { const { slug } = useParams<{ slug: string }>(); const { t, i18n } = useTranslation('management'); const navigate = useNavigate(); const { textStrong, muted, border, surface, primary, accentSoft } = useAdminTheme(); const dateLocale = i18n.language.startsWith('de') ? de : enGB; const { data, isLoading, error } = useQuery({ queryKey: ['event-analytics', slug], queryFn: () => getEventAnalytics(slug!), enabled: Boolean(slug), retry: false, // Don't retry if 403 }); const isFeatureLocked = error?.status === 403 || error?.code === 'feature_locked'; if (isFeatureLocked) { return ( {t('analytics.lockedTitle', 'Unlock Analytics')} {t('analytics.lockedBody', 'Get deep insights into your event engagement with the Premium package.')} navigate(adminPath('/mobile/billing/shop?feature=advanced_analytics'))} /> ); } if (isLoading) { return ( ); } if (error || !data) { return ( {t('common.error', 'Something went wrong')} ); } const { timeline, contributors, tasks } = data; const hasTimeline = timeline.length > 0; const hasContributors = contributors.length > 0; const hasTasks = tasks.length > 0; const fallbackHours = 12; const rawTimelineHours = resolveTimelineHours(timeline.map((point) => point.timestamp), fallbackHours); const timeframeHours = Math.min(rawTimelineHours, fallbackHours); const isTimeframeCapped = rawTimelineHours > fallbackHours; // Prepare chart data const maxTimelineCount = resolveMaxCount(timeline.map((point) => point.count)); const maxTaskCount = resolveMaxCount(tasks.map((task) => task.count)); const totalUploads = timeline.reduce((total, point) => total + point.count, 0); const totalLikes = contributors.reduce((total, contributor) => total + contributor.likes, 0); const totalContributors = contributors.length; return ( navigate(-1)} > {t('analytics.kpiTitle', 'Event snapshot')} {/* Activity Timeline */} {t('analytics.activityTitle', 'Activity Timeline')} {t('analytics.timeframe', 'Last {{hours}} hours', { hours: timeframeHours })} {isTimeframeCapped ? ( {t('analytics.timeframeHint', 'Older activity hidden')} ) : null} {hasTimeline ? ( {timeline.map((point, index) => { const heightPercent = (point.count / maxTimelineCount) * 100; const date = parseISO(point.timestamp); // Show label every 3rd point or if few points const showLabel = timeline.length < 8 || index % 3 === 0; return ( {showLabel && ( {format(date, 'HH:mm', { locale: dateLocale })} )} ); })} {t('analytics.uploadsPerHour', 'Uploads per hour')} ) : ( slug && navigate(adminPath(`/mobile/events/${slug}/qr`))} /> )} {/* Top Contributors */} {t('analytics.contributorsTitle', 'Top Contributors')} {hasContributors ? ( {contributors.map((contributor, idx) => ( {idx + 1} {contributor.name || t('common.anonymous', 'Anonymous')} {t('analytics.likesCount', { count: contributor.likes, defaultValue: '{{count}} likes' })} {contributor.count} ))} ) : ( slug && navigate(adminPath(`/mobile/events/${slug}/members`))} /> )} {/* Task Stats */} {t('analytics.tasksTitle', 'Popular Tasks')} {hasTasks ? ( {tasks.map((task) => { const percent = (task.count / maxTaskCount) * 100; return ( {task.task_name} {task.count} ); })} ) : ( slug && navigate(adminPath(`/mobile/events/${slug}/tasks`))} /> )} ); } function EmptyState({ message, actionLabel, onAction, }: { message: string; actionLabel?: string; onAction?: () => void; }) { const { muted } = useAdminTheme(); return ( {message} {actionLabel && onAction ? ( ) : null} ); }