Refine admin PWA layout and tamagui usage

This commit is contained in:
Codex Agent
2026-01-15 22:24:10 +01:00
parent 8941860140
commit c533d43c0f
37 changed files with 51503 additions and 21989 deletions

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
import { Card } from '@tamagui/card';
import { YStack, XStack } from '@tamagui/stacks';
import { YGroup } from '@tamagui/group';
import { SizableText as Text } from '@tamagui/text';
@@ -9,8 +10,9 @@ import { ListItem } from '@tamagui/list-item';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Button } from '@tamagui/button';
import { AlertDialog } from '@tamagui/alert-dialog';
import { ScrollView } from '@tamagui/scroll-view';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard, PillBadge, FloatingActionButton } from './components/Primitives';
import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import {
getEvent,
@@ -43,82 +45,183 @@ import { RadioGroup } from '@tamagui/radio-group';
import { useBackNavigation } from './hooks/useBackNavigation';
import { buildTaskSummary } from './lib/taskSummary';
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
import { useAdminTheme } from './theme';
import { withAlpha } from './components/colors';
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme';
function TaskSummaryCard({
summary,
text,
muted,
border,
surfaceMuted,
}: {
summary: ReturnType<typeof buildTaskSummary>;
text: string;
muted: string;
border: string;
surfaceMuted: string;
}) {
function TaskSummaryCard({ summary }: { summary: ReturnType<typeof buildTaskSummary> }) {
const { t } = useTranslation('management');
const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
const total = summary.assigned + summary.library + summary.collections + summary.emotions;
const segments = [
{
key: 'assigned',
label: t('events.tasks.summary.assigned', 'Assigned'),
value: summary.assigned,
color: ADMIN_ACTION_COLORS.tasks,
},
{
key: 'library',
label: t('events.tasks.summary.library', 'Library'),
value: summary.library,
color: ADMIN_ACTION_COLORS.qr,
},
{
key: 'collections',
label: t('events.tasks.summary.collections', 'Collections'),
value: summary.collections,
color: ADMIN_ACTION_COLORS.settings,
},
{
key: 'emotions',
label: t('events.tasks.summary.emotions', 'Emotions'),
value: summary.emotions,
color: ADMIN_ACTION_COLORS.branding,
},
];
return (
<MobileCard space="$2" borderColor={border}>
<XStack alignItems="center" justifyContent="space-between" space="$2">
<SummaryItem
label={t('events.tasks.summary.assigned', 'Assigned')}
value={summary.assigned}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
<SummaryItem
label={t('events.tasks.summary.library', 'Library')}
value={summary.library}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
</XStack>
<XStack alignItems="center" justifyContent="space-between" space="$2">
<SummaryItem
label={t('events.tasks.summary.collections', 'Collections')}
value={summary.collections}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
<SummaryItem
label={t('events.tasks.summary.emotions', 'Emotions')}
value={summary.emotions}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
</XStack>
</MobileCard>
<Card
borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.14}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
{t('events.tasks.summary.title', 'Task overview')}
</Text>
</XStack>
</XStack>
<XStack alignItems="baseline" space="$2">
<Text fontSize="$xl" fontWeight="900" color={textStrong}>
{total}
</Text>
<Text fontSize="$xs" color={muted}>
{t('events.tasks.summary.total', 'Tasks total')}
</Text>
</XStack>
<XStack
height={10}
borderRadius={999}
overflow="hidden"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
{total > 0 ? (
segments.map((segment) =>
segment.value > 0 ? (
<XStack
key={segment.key}
flex={segment.value}
backgroundColor={withAlpha(segment.color, 0.55)}
/>
) : null,
)
) : (
<XStack flex={1} backgroundColor={withAlpha(border, 0.4)} />
)}
</XStack>
<XStack flexWrap="wrap" space="$2">
{segments.map((segment) => (
<SummaryLegendItem key={segment.key} label={segment.label} value={segment.value} color={segment.color} />
))}
</XStack>
</YStack>
</Card>
);
}
function SummaryItem({
function SummaryLegendItem({
label,
value,
text,
muted,
surfaceMuted,
color,
}: {
label: string;
value: number;
text: string;
muted: string;
surfaceMuted: string;
color: string;
}) {
const { textStrong } = useAdminTheme();
return (
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor={surfaceMuted} space="$1">
<Text fontSize={11} color={muted}>
<XStack
alignItems="center"
space="$1.5"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={withAlpha(color, 0.35)}
backgroundColor={withAlpha(color, 0.12)}
>
<XStack width={8} height={8} borderRadius={999} backgroundColor={color} />
<Text fontSize={11} fontWeight="700" color={textStrong}>
{label}
</Text>
<Text fontSize={16} fontWeight="800" color={text}>
<Text fontSize={11} fontWeight="700" color={textStrong}>
{value}
</Text>
</YStack>
</XStack>
);
}
function QuickNavChip({
label,
count,
onPress,
}: {
label: string;
count: number;
onPress: () => void;
}) {
const { textStrong, border, surface, surfaceMuted } = useAdminTheme();
return (
<Pressable onPress={onPress}>
<XStack
alignItems="center"
space="$1.5"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
style={{ minHeight: 36 }}
>
<Text fontSize="$xs" fontWeight="700" color={textStrong}>
{label}
</Text>
<XStack
paddingHorizontal="$2"
paddingVertical="$0.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize={10} fontWeight="800" color={textStrong}>
{count}
</Text>
</XStack>
</XStack>
</Pressable>
);
}
@@ -491,45 +594,82 @@ export default function MobileEventTasksPage() {
) : null}
{!loading ? (
<TaskSummaryCard
summary={summary}
text={text}
muted={muted}
border={border}
surfaceMuted={surfaceMuted}
/>
<TaskSummaryCard summary={summary} />
) : null}
{!loading ? (
<YStack space="$2">
<Text fontSize={12} fontWeight="700" color={muted}>
{t('events.tasks.quickNav', 'Quick jump')}
</Text>
<XStack space="$2" flexWrap="wrap">
{sectionCounts.map((section) => (
<Button
key={section.key}
unstyled
onPress={() => handleQuickNav(section.key)}
borderRadius={14}
<Card
borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
paddingVertical="$2"
paddingHorizontal="$3"
pressStyle={{ backgroundColor: surfaceMuted }}
style={{ flexGrow: 1 }}
backgroundColor={surfaceMuted}
>
<XStack alignItems="center" justifyContent="center" space="$1.5">
<Text fontSize="$xs" fontWeight="700" color={text}>
{t(`events.tasks.sections.${section.key}`, section.key)}
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('events.tasks.quickNav', 'Quick jump')}
</Text>
</XStack>
</XStack>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<XStack space="$2" paddingVertical="$1">
{sectionCounts.map((section) => (
<QuickNavChip
key={section.key}
label={t(`events.tasks.sections.${section.key}`, section.key)}
count={section.count}
onPress={() => handleQuickNav(section.key)}
/>
))}
</XStack>
</ScrollView>
<XStack alignItems="center" space="$2">
<XStack flex={1}>
<MobileInput
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('events.tasks.search', 'Search tasks')}
compact
/>
</XStack>
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
<XStack
alignItems="center"
space="$1.5"
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
>
<Text fontSize={11} fontWeight="700" color={text}>
{t('events.tasks.emotionFilterShort', 'Emotion')}
</Text>
<PillBadge tone="muted">{section.count}</PillBadge>
<Text fontSize={11} color={muted}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
<ChevronDown size={14} color={muted} />
</XStack>
</Button>
))}
</XStack>
</YStack>
</Pressable>
</XStack>
</YStack>
</Card>
) : null}
{loading ? (
@@ -631,33 +771,6 @@ export default function MobileEventTasksPage() {
) : (
<YStack space="$2">
<div ref={assignedRef} />
<YStack space="$2">
<MobileInput
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('events.tasks.search', 'Search tasks')}
compact
/>
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
<MobileCard borderColor={border} backgroundColor={surface} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack>
<Text fontSize={12} fontWeight="700" color={text}>
{t('events.tasks.emotionFilter', 'Emotion filter')}
</Text>
<Text fontSize={11} color={muted}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
</YStack>
<ChevronDown size={16} color={muted} />
</XStack>
</MobileCard>
</Pressable>
</YStack>
<Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
</Text>