Refine analytics page and i18n
This commit is contained in:
@@ -176,6 +176,8 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
|
"anonymous": "Anonym",
|
||||||
|
"error": "Etwas ist schiefgelaufen",
|
||||||
"loadMore": "Mehr laden",
|
"loadMore": "Mehr laden",
|
||||||
"processing": "Verarbeite …",
|
"processing": "Verarbeite …",
|
||||||
"select": "Auswählen",
|
"select": "Auswählen",
|
||||||
|
|||||||
@@ -172,6 +172,8 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
"anonymous": "Anonymous",
|
||||||
|
"error": "Something went wrong",
|
||||||
"loadMore": "Load more",
|
"loadMore": "Load more",
|
||||||
"processing": "Processing…",
|
"processing": "Processing…",
|
||||||
"select": "Select",
|
"select": "Select",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 { BarChart2, TrendingUp, Users, ListTodo, Lock, Trophy, Calendar } from 'lucide-react';
|
import { TrendingUp, 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';
|
||||||
@@ -13,6 +13,7 @@ import { MobileCard, CTAButton, 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';
|
||||||
|
import { resolveMaxCount } from './lib/analytics';
|
||||||
import { adminPath } from '../constants';
|
import { adminPath } from '../constants';
|
||||||
|
|
||||||
export default function MobileEventAnalyticsPage() {
|
export default function MobileEventAnalyticsPage() {
|
||||||
@@ -99,7 +100,8 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
const hasTasks = tasks.length > 0;
|
const hasTasks = tasks.length > 0;
|
||||||
|
|
||||||
// Prepare chart data
|
// Prepare chart data
|
||||||
const maxCount = Math.max(...timeline.map((p) => p.count), 1);
|
const maxTimelineCount = resolveMaxCount(timeline.map((point) => point.count));
|
||||||
|
const maxTaskCount = resolveMaxCount(tasks.map((task) => task.count));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell
|
<MobileShell
|
||||||
@@ -121,7 +123,7 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
<YStack height={180} justifyContent="flex-end" space="$2">
|
<YStack height={180} justifyContent="flex-end" space="$2">
|
||||||
<XStack alignItems="flex-end" justifyContent="space-between" height={150} gap="$1">
|
<XStack alignItems="flex-end" justifyContent="space-between" height={150} gap="$1">
|
||||||
{timeline.map((point, index) => {
|
{timeline.map((point, index) => {
|
||||||
const heightPercent = (point.count / maxCount) * 100;
|
const heightPercent = (point.count / maxTimelineCount) * 100;
|
||||||
const date = parseISO(point.timestamp);
|
const date = parseISO(point.timestamp);
|
||||||
// Show label every 3rd point or if few points
|
// Show label every 3rd point or if few points
|
||||||
const showLabel = timeline.length < 8 || index % 3 === 0;
|
const showLabel = timeline.length < 8 || index % 3 === 0;
|
||||||
@@ -138,7 +140,7 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
/>
|
/>
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<Text fontSize={10} color={muted} numberOfLines={1}>
|
<Text fontSize={10} color={muted} numberOfLines={1}>
|
||||||
{format(date, 'HH:mm')}
|
{format(date, 'HH:mm', { locale: dateLocale })}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -212,7 +214,6 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
{hasTasks ? (
|
{hasTasks ? (
|
||||||
<YStack space="$3">
|
<YStack space="$3">
|
||||||
{tasks.map((task) => {
|
{tasks.map((task) => {
|
||||||
const maxTaskCount = Math.max(...tasks.map(t => t.count), 1);
|
|
||||||
const percent = (task.count / maxTaskCount) * 100;
|
const percent = (task.count / maxTaskCount) * 100;
|
||||||
return (
|
return (
|
||||||
<YStack key={task.task_id} space="$1">
|
<YStack key={task.task_id} space="$1">
|
||||||
|
|||||||
16
resources/js/admin/mobile/__tests__/analytics.test.ts
Normal file
16
resources/js/admin/mobile/__tests__/analytics.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { resolveMaxCount } from '../lib/analytics';
|
||||||
|
|
||||||
|
describe('resolveMaxCount', () => {
|
||||||
|
it('defaults to 1 for empty input', () => {
|
||||||
|
expect(resolveMaxCount([])).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the highest count', () => {
|
||||||
|
expect(resolveMaxCount([2, 5, 3])).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never returns less than 1', () => {
|
||||||
|
expect(resolveMaxCount([0])).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
7
resources/js/admin/mobile/lib/analytics.ts
Normal file
7
resources/js/admin/mobile/lib/analytics.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export function resolveMaxCount(values: number[]): number {
|
||||||
|
if (!Array.isArray(values) || values.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(...values, 1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user