bd sync: 2026-01-12 17:24:05
This commit is contained in:
@@ -2,18 +2,17 @@ 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 { BarChart2, TrendingUp, Users, ListTodo, Lock, Trophy, Calendar } 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 { MobileCard, CTAButton, 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() {
|
||||
@@ -98,17 +97,9 @@ export default function MobileEventAnalyticsPage() {
|
||||
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;
|
||||
const maxCount = Math.max(...timeline.map((p) => p.count), 1);
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
@@ -117,28 +108,6 @@ export default function MobileEventAnalyticsPage() {
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<YStack space="$4">
|
||||
<YStack space="$2">
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{t('analytics.kpiTitle', 'Event snapshot')}
|
||||
</Text>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
<KpiTile
|
||||
icon={TrendingUp}
|
||||
label={t('analytics.kpiUploads', 'Uploads')}
|
||||
value={totalUploads}
|
||||
/>
|
||||
<KpiTile
|
||||
icon={Users}
|
||||
label={t('analytics.kpiContributors', 'Contributors')}
|
||||
value={totalContributors}
|
||||
/>
|
||||
<KpiTile
|
||||
icon={Trophy}
|
||||
label={t('analytics.kpiLikes', 'Likes')}
|
||||
value={totalLikes}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
{/* Activity Timeline */}
|
||||
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
@@ -147,22 +116,12 @@ export default function MobileEventAnalyticsPage() {
|
||||
{t('analytics.activityTitle', 'Activity Timeline')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack space="$0.5">
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('analytics.timeframe', 'Last {{hours}} hours', { hours: timeframeHours })}
|
||||
</Text>
|
||||
{isTimeframeCapped ? (
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('analytics.timeframeHint', 'Older activity hidden')}
|
||||
</Text>
|
||||
) : null}
|
||||
</YStack>
|
||||
|
||||
{hasTimeline ? (
|
||||
<YStack height={180} justifyContent="flex-end" space="$2">
|
||||
<XStack alignItems="flex-end" justifyContent="space-between" height={150} gap="$1">
|
||||
{timeline.map((point, index) => {
|
||||
const heightPercent = (point.count / maxTimelineCount) * 100;
|
||||
const heightPercent = (point.count / maxCount) * 100;
|
||||
const date = parseISO(point.timestamp);
|
||||
// Show label every 3rd point or if few points
|
||||
const showLabel = timeline.length < 8 || index % 3 === 0;
|
||||
@@ -179,7 +138,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
/>
|
||||
{showLabel && (
|
||||
<Text fontSize={10} color={muted} numberOfLines={1}>
|
||||
{format(date, 'HH:mm', { locale: dateLocale })}
|
||||
{format(date, 'HH:mm')}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
@@ -191,11 +150,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
</Text>
|
||||
</YStack>
|
||||
) : (
|
||||
<EmptyState
|
||||
message={t('analytics.noActivity', 'No uploads yet')}
|
||||
actionLabel={t('analytics.emptyActionShareQr', 'Share your QR code')}
|
||||
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/qr`))}
|
||||
/>
|
||||
<EmptyState message={t('analytics.noActivity', 'No uploads yet')} />
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
@@ -241,11 +196,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
))}
|
||||
</YStack>
|
||||
) : (
|
||||
<EmptyState
|
||||
message={t('analytics.noContributors', 'No contributors yet')}
|
||||
actionLabel={t('analytics.emptyActionInvite', 'Invite guests')}
|
||||
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/members`))}
|
||||
/>
|
||||
<EmptyState message={t('analytics.noContributors', 'No contributors yet')} />
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
@@ -261,6 +212,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
{hasTasks ? (
|
||||
<YStack space="$3">
|
||||
{tasks.map((task) => {
|
||||
const maxTaskCount = Math.max(...tasks.map(t => t.count), 1);
|
||||
const percent = (task.count / maxTaskCount) * 100;
|
||||
return (
|
||||
<YStack key={task.task_id} space="$1">
|
||||
@@ -285,11 +237,7 @@ export default function MobileEventAnalyticsPage() {
|
||||
})}
|
||||
</YStack>
|
||||
) : (
|
||||
<EmptyState
|
||||
message={t('analytics.noTasks', 'No task activity yet')}
|
||||
actionLabel={t('analytics.emptyActionOpenTasks', 'Open tasks')}
|
||||
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/tasks`))}
|
||||
/>
|
||||
<EmptyState message={t('analytics.noTasks', 'No task activity yet')} />
|
||||
)}
|
||||
</MobileCard>
|
||||
</YStack>
|
||||
@@ -297,24 +245,13 @@ export default function MobileEventAnalyticsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState({
|
||||
message,
|
||||
actionLabel,
|
||||
onAction,
|
||||
}: {
|
||||
message: string;
|
||||
actionLabel?: string;
|
||||
onAction?: () => void;
|
||||
}) {
|
||||
function EmptyState({ message }: { message: string }) {
|
||||
const { muted } = useAdminTheme();
|
||||
return (
|
||||
<YStack padding="$4" alignItems="center" justifyContent="center" space="$2">
|
||||
<YStack padding="$4" alignItems="center" justifyContent="center">
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{message}
|
||||
</Text>
|
||||
{actionLabel && onAction ? (
|
||||
<CTAButton label={actionLabel} tone="ghost" fullWidth={false} onPress={onAction} />
|
||||
) : null}
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user