more usage of tamagui primitives

This commit is contained in:
Codex Agent
2025-12-30 16:04:30 +01:00
parent efe2f25b3e
commit d7c2f85eeb
12 changed files with 744 additions and 315 deletions

View File

@@ -3,11 +3,14 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { YGroup } from '@tamagui/group';
import { SizableText as Text } from '@tamagui/text';
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 { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard, PillBadge } from './components/Primitives';
import { MobileCard, CTAButton, SkeletonCard, PillBadge, FloatingActionButton } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import {
getEvent,
@@ -42,32 +45,53 @@ import { buildTaskSummary } from './lib/taskSummary';
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
import { useAdminTheme } from './theme';
function InlineSeparator() {
const { border } = useAdminTheme();
return <XStack height={1} opacity={0.7} marginLeft="$3" backgroundColor={border} />;
}
function TaskSummaryCard({
summary,
text,
muted,
border,
surfaceMuted,
}: {
summary: ReturnType<typeof buildTaskSummary>;
text: string;
muted: string;
border: string;
surfaceMuted: string;
}) {
const { t } = useTranslation('management');
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} />
<SummaryItem label={t('events.tasks.summary.library', 'Library')} value={summary.library} text={text} muted={muted} />
<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} />
<SummaryItem label={t('events.tasks.summary.emotions', 'Emotions')} value={summary.emotions} text={text} muted={muted} />
<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>
);
@@ -78,14 +102,16 @@ function SummaryItem({
value,
text,
muted,
surfaceMuted,
}: {
label: string;
value: number;
text: string;
muted: string;
surfaceMuted: string;
}) {
return (
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor="rgba(15, 23, 42, 0.03)" space="$1">
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor={surfaceMuted} space="$1">
<Text fontSize={11} color={muted}>
{label}
</Text>
@@ -102,7 +128,7 @@ export default function MobileEventTasksPage() {
const slug = slugParam ?? activeEvent?.slug ?? null;
const navigate = useNavigate();
const { t } = useTranslation('management');
const { textStrong, muted, subtle, border, primary, danger, surface } = useAdminTheme();
const { textStrong, muted, subtle, border, primary, danger, surface, surfaceMuted, dangerBg, dangerText, overlay } = useAdminTheme();
const [assignedTasks, setAssignedTasks] = React.useState<TenantTask[]>([]);
const [library, setLibrary] = React.useState<TenantTask[]>([]);
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
@@ -120,6 +146,7 @@ export default function MobileEventTasksPage() {
const [error, setError] = React.useState<string | null>(null);
const [busyId, setBusyId] = React.useState<number | null>(null);
const [assigningId, setAssigningId] = React.useState<number | null>(null);
const [deleteCandidate, setDeleteCandidate] = React.useState<TenantTask | null>(null);
const [eventId, setEventId] = React.useState<number | null>(null);
const [searchTerm, setSearchTerm] = React.useState('');
const [emotionFilter, setEmotionFilter] = React.useState<string>('');
@@ -349,6 +376,13 @@ export default function MobileEventTasksPage() {
}
}
async function confirmDeleteTask() {
if (!deleteCandidate) return;
const taskId = deleteCandidate.id;
setDeleteCandidate(null);
await detachTask(taskId);
}
const startEdit = (task: TenantTask) => {
setNewTask({
id: task.id,
@@ -462,6 +496,7 @@ export default function MobileEventTasksPage() {
text={text}
muted={muted}
border={border}
surfaceMuted={surfaceMuted}
/>
) : null}
@@ -472,23 +507,26 @@ export default function MobileEventTasksPage() {
</Text>
<XStack space="$2" flexWrap="wrap">
{sectionCounts.map((section) => (
<Pressable key={section.key} onPress={() => handleQuickNav(section.key)} style={{ flexGrow: 1 }}>
<XStack
alignItems="center"
justifyContent="center"
space="$1.5"
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14}
borderWidth={1}
borderColor={border}
>
<Button
key={section.key}
unstyled
onPress={() => handleQuickNav(section.key)}
borderRadius={14}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
paddingVertical="$2"
paddingHorizontal="$3"
pressStyle={{ backgroundColor: surfaceMuted }}
style={{ flexGrow: 1 }}
>
<XStack alignItems="center" justifyContent="center" space="$1.5">
<Text fontSize="$xs" fontWeight="700" color={text}>
{t(`events.tasks.sections.${section.key}`, section.key)}
</Text>
<PillBadge tone="muted">{section.count}</PillBadge>
</XStack>
</Pressable>
</Button>
))}
</XStack>
</YStack>
@@ -523,11 +561,12 @@ export default function MobileEventTasksPage() {
/>
</XStack>
</MobileCard>
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<Pressable onPress={() => setShowTaskSheet(true)}>
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<YGroup.Item bordered>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowTaskSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
@@ -554,12 +593,12 @@ export default function MobileEventTasksPage() {
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</Pressable>
<InlineSeparator />
<Pressable onPress={() => setShowCollectionSheet(true)}>
</YGroup.Item>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
onPress={() => setShowCollectionSheet(true)}
title={
<XStack alignItems="center" space="$2">
<YStack
@@ -586,8 +625,8 @@ export default function MobileEventTasksPage() {
paddingHorizontal="$3"
iconAfter={<ChevronRight size={16} color={muted} />}
/>
</Pressable>
</YStack>
</YGroup.Item>
</YGroup>
</YStack>
) : (
<YStack space="$2">
@@ -622,44 +661,53 @@ export default function MobileEventTasksPage() {
<Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
</Text>
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
{filteredTasks.map((task, idx) => (
<React.Fragment key={task.id}>
<Pressable onPress={() => startEdit(task)}>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
<YGroup.Item key={task.id} bordered={idx < filteredTasks.length - 1}>
<ListItem
hoverTheme
pressTheme
onPress={() => startEdit(task)}
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{task.title}
</Text>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
}
subTitle={
task.description ? (
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{task.description}
</Text>
) : null
}
iconAfter={
<XStack space="$2" alignItems="flex-start">
{task.emotion ? (
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
) : null}
<Pressable disabled={busyId === task.id} onPress={() => detachTask(task.id)}>
<Trash2 size={14} color={danger} />
</Pressable>
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</Pressable>
{idx < assignedTasks.length - 1 ? <InlineSeparator /> : null}
</React.Fragment>
) : null
}
iconAfter={
<XStack space="$2" alignItems="center">
{task.emotion ? (
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
) : null}
<Button
size="$2"
circular
backgroundColor={dangerBg}
borderWidth={1}
borderColor={`${danger}33`}
icon={<Trash2 size={14} color={dangerText} />}
aria-label={t('events.tasks.remove', 'Remove task')}
disabled={busyId === task.id}
onPress={(event) => {
event?.stopPropagation?.();
setDeleteCandidate(task);
}}
/>
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YStack>
</YGroup>
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
<div ref={libraryRef} />
<Text fontSize={12.5} fontWeight="600" color={text}>
@@ -681,9 +729,9 @@ export default function MobileEventTasksPage() {
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')}
</Text>
) : (
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
<React.Fragment key={`lib-${task.id}`}>
<YGroup.Item key={`lib-${task.id}`} bordered={idx < arr.length - 1}>
<ListItem
hoverTheme
pressTheme
@@ -712,10 +760,9 @@ export default function MobileEventTasksPage() {
paddingVertical="$2"
paddingHorizontal="$3"
/>
{idx < arr.length - 1 ? <InlineSeparator /> : null}
</React.Fragment>
</YGroup.Item>
))}
</YStack>
</YGroup>
)}
</YStack>
)}
@@ -739,9 +786,9 @@ export default function MobileEventTasksPage() {
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
</Text>
) : (
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
<React.Fragment key={collection.id}>
<YGroup.Item key={collection.id} bordered={idx < arr.length - 1}>
<ListItem
hoverTheme
pressTheme
@@ -767,13 +814,12 @@ export default function MobileEventTasksPage() {
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
{idx < arr.length - 1 ? <InlineSeparator /> : null}
</React.Fragment>
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YStack>
</YGroup>
)}
</YStack>
</MobileSheet>
@@ -871,36 +917,39 @@ export default function MobileEventTasksPage() {
style={{ padding: 0 }}
/>
</MobileField>
<YStack space="$2">
{emotions.map((em) => (
<ListItem
key={`emo-${em.id}`}
hoverTheme
pressTheme
title={
<XStack alignItems="center" space="$2">
<Tag label={em.name ?? ''} color={em.color ?? border} />
</XStack>
}
iconAfter={
<XStack space="$2">
<Pressable
onPress={() => {
setEditingEmotion(em);
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
}}
>
<Pencil size={14} color={primary} />
</Pressable>
<Pressable onPress={() => removeEmotion(em.id)}>
<Trash2 size={14} color={danger} />
</Pressable>
<ChevronRight size={14} color={subtle} />
</XStack>
}
/>
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
{emotions.map((em, idx) => (
<YGroup.Item key={`emo-${em.id}`} bordered={idx < emotions.length - 1}>
<ListItem
hoverTheme
pressTheme
title={
<XStack alignItems="center" space="$2">
<Tag label={em.name ?? ''} color={em.color ?? border} />
</XStack>
}
iconAfter={
<XStack space="$2">
<Pressable
onPress={() => {
setEditingEmotion(em);
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
}}
>
<Pencil size={14} color={primary} />
</Pressable>
<Pressable onPress={() => removeEmotion(em.id)}>
<Trash2 size={14} color={danger} />
</Pressable>
<ChevronRight size={14} color={subtle} />
</XStack>
}
paddingVertical="$2"
paddingHorizontal="$3"
/>
</YGroup.Item>
))}
</YStack>
</YGroup>
</YStack>
</MobileSheet>
@@ -942,24 +991,66 @@ export default function MobileEventTasksPage() {
</RadioGroup>
</MobileSheet>
<Pressable
onPress={() => setShowFabMenu(true)}
style={{
position: 'fixed',
right: 20,
bottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)',
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: primary,
justifyContent: 'center',
alignItems: 'center',
boxShadow: '0 10px 25px rgba(0,122,255,0.35)',
zIndex: 60,
}}
>
<Plus size={20} color={surface} />
</Pressable>
<AlertDialog
open={Boolean(deleteCandidate)}
onOpenChange={(open) => {
if (!open) {
setDeleteCandidate(null);
}
}}
>
<AlertDialog.Portal>
<AlertDialog.Overlay backgroundColor={`${overlay}66`} />
<AlertDialog.Content
borderRadius={20}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
padding="$4"
maxWidth={420}
width="90%"
>
<YStack space="$3">
<AlertDialog.Title>
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.tasks.removeTitle', 'Remove task?')}
</Text>
</AlertDialog.Title>
<AlertDialog.Description>
<Text fontSize="$sm" color={muted}>
{deleteCandidate
? t('events.tasks.removeBody', 'This will remove "{{title}}" from the event.', { title: deleteCandidate.title })
: t('events.tasks.removeBodyFallback', 'This will remove the task from the event.')}
</Text>
</AlertDialog.Description>
<XStack space="$2" justifyContent="flex-end">
<AlertDialog.Cancel asChild>
<CTAButton
label={t('common.cancel', 'Cancel')}
tone="ghost"
fullWidth={false}
onPress={() => setDeleteCandidate(null)}
/>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<CTAButton
label={t('events.tasks.remove', 'Remove')}
tone="danger"
fullWidth={false}
onPress={() => confirmDeleteTask()}
/>
</AlertDialog.Action>
</XStack>
</YStack>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog>
<FloatingActionButton
onPress={() => setShowFabMenu(true)}
label={t('events.tasks.add', 'Add')}
icon={Plus}
/>
<MobileSheet
open={showFabMenu}
@@ -967,61 +1058,67 @@ export default function MobileEventTasksPage() {
title={t('events.tasks.actions', 'Aktionen')}
footer={null}
>
<YStack space="$1">
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowTaskSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.bulkAdd', 'Bulk add')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowBulkSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.manageEmotions', 'Manage emotions')}
</Text>
}
subTitle={
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowEmotionSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
</YStack>
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
<YGroup.Item bordered>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowTaskSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
</YGroup.Item>
<YGroup.Item bordered>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.bulkAdd', 'Bulk add')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowBulkSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
</YGroup.Item>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
title={
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.manageEmotions', 'Manage emotions')}
</Text>
}
subTitle={
<Text fontSize={11.5} fontWeight="400" color={subtle}>
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
</Text>
}
onPress={() => {
setShowFabMenu(false);
setShowEmotionSheet(true);
}}
paddingVertical="$2"
paddingHorizontal="$3"
iconAfter={<ChevronRight size={14} color={subtle} />}
/>
</YGroup.Item>
</YGroup>
</MobileSheet>
</MobileShell>
);