Refine admin PWA layout and tamagui usage
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user