Enhance analytics snapshot and empty states
This commit is contained in:
@@ -2877,16 +2877,24 @@
|
|||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"upgradeAction": "Upgrade auf Premium",
|
"upgradeAction": "Upgrade auf Premium",
|
||||||
|
"kpiTitle": "Event-Überblick",
|
||||||
|
"kpiUploads": "Uploads",
|
||||||
|
"kpiContributors": "Beitragende",
|
||||||
|
"kpiLikes": "Likes",
|
||||||
"activityTitle": "Aktivitäts-Zeitachse",
|
"activityTitle": "Aktivitäts-Zeitachse",
|
||||||
|
"timeframe": "Letzte {{hours}} Stunden",
|
||||||
"uploadsPerHour": "Uploads pro Stunde",
|
"uploadsPerHour": "Uploads pro Stunde",
|
||||||
"noActivity": "Noch keine Uploads",
|
"noActivity": "Noch keine Uploads",
|
||||||
|
"emptyActionShareQr": "QR-Code teilen",
|
||||||
"contributorsTitle": "Top-Beitragende",
|
"contributorsTitle": "Top-Beitragende",
|
||||||
"likesCount": "{{count}} Likes",
|
"likesCount": "{{count}} Likes",
|
||||||
"likesCount_one": "{{count}} Like",
|
"likesCount_one": "{{count}} Like",
|
||||||
"likesCount_other": "{{count}} Likes",
|
"likesCount_other": "{{count}} Likes",
|
||||||
"noContributors": "Noch keine Beitragenden",
|
"noContributors": "Noch keine Beitragenden",
|
||||||
|
"emptyActionInvite": "Gäste einladen",
|
||||||
"tasksTitle": "Beliebte Aufgaben",
|
"tasksTitle": "Beliebte Aufgaben",
|
||||||
"noTasks": "Noch keine Aufgabenaktivität",
|
"noTasks": "Noch keine Aufgabenaktivität",
|
||||||
|
"emptyActionOpenTasks": "Aufgaben öffnen",
|
||||||
"lockedTitle": "Analytics freischalten",
|
"lockedTitle": "Analytics freischalten",
|
||||||
"lockedBody": "Erhalte tiefe Einblicke in die Interaktionen deines Events mit dem Premium-Paket."
|
"lockedBody": "Erhalte tiefe Einblicke in die Interaktionen deines Events mit dem Premium-Paket."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2881,16 +2881,24 @@
|
|||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"upgradeAction": "Upgrade to Premium",
|
"upgradeAction": "Upgrade to Premium",
|
||||||
|
"kpiTitle": "Event snapshot",
|
||||||
|
"kpiUploads": "Uploads",
|
||||||
|
"kpiContributors": "Contributors",
|
||||||
|
"kpiLikes": "Likes",
|
||||||
"activityTitle": "Activity Timeline",
|
"activityTitle": "Activity Timeline",
|
||||||
|
"timeframe": "Last {{hours}} hours",
|
||||||
"uploadsPerHour": "Uploads per hour",
|
"uploadsPerHour": "Uploads per hour",
|
||||||
"noActivity": "No uploads yet",
|
"noActivity": "No uploads yet",
|
||||||
|
"emptyActionShareQr": "Share your QR code",
|
||||||
"contributorsTitle": "Top Contributors",
|
"contributorsTitle": "Top Contributors",
|
||||||
"likesCount": "{{count}} likes",
|
"likesCount": "{{count}} likes",
|
||||||
"likesCount_one": "{{count}} like",
|
"likesCount_one": "{{count}} like",
|
||||||
"likesCount_other": "{{count}} likes",
|
"likesCount_other": "{{count}} likes",
|
||||||
"noContributors": "No contributors yet",
|
"noContributors": "No contributors yet",
|
||||||
|
"emptyActionInvite": "Invite guests",
|
||||||
"tasksTitle": "Popular Tasks",
|
"tasksTitle": "Popular Tasks",
|
||||||
"noTasks": "No task activity yet",
|
"noTasks": "No task activity yet",
|
||||||
|
"emptyActionOpenTasks": "Open tasks",
|
||||||
"lockedTitle": "Unlock Analytics",
|
"lockedTitle": "Unlock Analytics",
|
||||||
"lockedBody": "Get deep insights into your event engagement with the Premium package."
|
"lockedBody": "Get deep insights into your event engagement with the Premium package."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React from 'react';
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { TrendingUp, ListTodo, Lock, Trophy } from 'lucide-react';
|
import { TrendingUp, Users, ListTodo, Lock, Trophy } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { de, enGB } from 'date-fns/locale';
|
import { de, enGB } from 'date-fns/locale';
|
||||||
|
|
||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
|
import { MobileCard, CTAButton, KpiTile, SkeletonCard } from './components/Primitives';
|
||||||
import { getEventAnalytics, EventAnalytics } from '../api';
|
import { getEventAnalytics, EventAnalytics } from '../api';
|
||||||
import { ApiError } from '../lib/apiError';
|
import { ApiError } from '../lib/apiError';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
@@ -98,10 +98,14 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
const hasTimeline = timeline.length > 0;
|
const hasTimeline = timeline.length > 0;
|
||||||
const hasContributors = contributors.length > 0;
|
const hasContributors = contributors.length > 0;
|
||||||
const hasTasks = tasks.length > 0;
|
const hasTasks = tasks.length > 0;
|
||||||
|
const timeframeHours = 12;
|
||||||
|
|
||||||
// Prepare chart data
|
// Prepare chart data
|
||||||
const maxTimelineCount = resolveMaxCount(timeline.map((point) => point.count));
|
const maxTimelineCount = resolveMaxCount(timeline.map((point) => point.count));
|
||||||
const maxTaskCount = resolveMaxCount(tasks.map((task) => task.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 (
|
return (
|
||||||
<MobileShell
|
<MobileShell
|
||||||
@@ -110,6 +114,28 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
onBack={() => navigate(-1)}
|
onBack={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<YStack space="$4">
|
<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 */}
|
{/* Activity Timeline */}
|
||||||
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
<MobileCard space="$3" borderColor={border} backgroundColor={surface}>
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
@@ -118,6 +144,9 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
{t('analytics.activityTitle', 'Activity Timeline')}
|
{t('analytics.activityTitle', 'Activity Timeline')}
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t('analytics.timeframe', 'Last {{hours}} hours', { hours: timeframeHours })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{hasTimeline ? (
|
{hasTimeline ? (
|
||||||
<YStack height={180} justifyContent="flex-end" space="$2">
|
<YStack height={180} justifyContent="flex-end" space="$2">
|
||||||
@@ -152,7 +181,11 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState message={t('analytics.noActivity', 'No uploads yet')} />
|
<EmptyState
|
||||||
|
message={t('analytics.noActivity', 'No uploads yet')}
|
||||||
|
actionLabel={t('analytics.emptyActionShareQr', 'Share your QR code')}
|
||||||
|
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/qr`))}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
@@ -198,7 +231,11 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
))}
|
))}
|
||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState message={t('analytics.noContributors', 'No contributors yet')} />
|
<EmptyState
|
||||||
|
message={t('analytics.noContributors', 'No contributors yet')}
|
||||||
|
actionLabel={t('analytics.emptyActionInvite', 'Invite guests')}
|
||||||
|
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/members`))}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
@@ -238,7 +275,11 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
})}
|
})}
|
||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState message={t('analytics.noTasks', 'No task activity yet')} />
|
<EmptyState
|
||||||
|
message={t('analytics.noTasks', 'No task activity yet')}
|
||||||
|
actionLabel={t('analytics.emptyActionOpenTasks', 'Open tasks')}
|
||||||
|
onAction={() => slug && navigate(adminPath(`/mobile/events/${slug}/tasks`))}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -246,13 +287,24 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyState({ message }: { message: string }) {
|
function EmptyState({
|
||||||
|
message,
|
||||||
|
actionLabel,
|
||||||
|
onAction,
|
||||||
|
}: {
|
||||||
|
message: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
onAction?: () => void;
|
||||||
|
}) {
|
||||||
const { muted } = useAdminTheme();
|
const { muted } = useAdminTheme();
|
||||||
return (
|
return (
|
||||||
<YStack padding="$4" alignItems="center" justifyContent="center">
|
<YStack padding="$4" alignItems="center" justifyContent="center" space="$2">
|
||||||
<Text fontSize="$sm" color={muted}>
|
<Text fontSize="$sm" color={muted}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
{actionLabel && onAction ? (
|
||||||
|
<CTAButton label={actionLabel} tone="ghost" fullWidth={false} onPress={onAction} />
|
||||||
|
) : null}
|
||||||
</YStack>
|
</YStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user