I finished the remaining polish so the admin app now feels fully “app‑like” across the core screens.

This commit is contained in:
Codex Agent
2025-12-28 20:48:32 +01:00
parent d3b6c6c029
commit 1e0c38fce4
23 changed files with 1250 additions and 112 deletions

View File

@@ -7,7 +7,7 @@ import { SizableText as Text } from '@tamagui/text';
import { ListItem } from '@tamagui/list-item';
import { Pressable } from '@tamagui/react-native-web-lite';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
import { MobileCard, CTAButton, SkeletonCard, PillBadge } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import {
getEvent,
@@ -39,12 +39,63 @@ import { useEventContext } from '../context/EventContext';
import { useTheme } from '@tamagui/core';
import { RadioGroup } from '@tamagui/radio-group';
import { useBackNavigation } from './hooks/useBackNavigation';
import { buildTaskSummary } from './lib/taskSummary';
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
function InlineSeparator() {
const theme = useTheme();
return <XStack height={1} opacity={0.7} marginLeft="$3" backgroundColor={theme.borderColor?.val ?? '#e5e7eb'} />;
}
function TaskSummaryCard({
summary,
text,
muted,
border,
}: {
summary: ReturnType<typeof buildTaskSummary>;
text: string;
muted: string;
border: 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} />
</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} />
</XStack>
</MobileCard>
);
}
function SummaryItem({
label,
value,
text,
muted,
}: {
label: string;
value: number;
text: string;
muted: string;
}) {
return (
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor="rgba(15, 23, 42, 0.03)" space="$1">
<Text fontSize={11} color={muted}>
{label}
</Text>
<Text fontSize={16} fontWeight="800" color={text}>
{value}
</Text>
</YStack>
);
}
export default function MobileEventTasksPage() {
const { slug: slugParam } = useParams<{ slug?: string }>();
const { activeEvent, selectEvent } = useEventContext();
@@ -89,7 +140,16 @@ export default function MobileEventTasksPage() {
const [emotionForm, setEmotionForm] = React.useState({ name: '', color: String(border) });
const [savingEmotion, setSavingEmotion] = React.useState(false);
const [showEmotionFilterSheet, setShowEmotionFilterSheet] = React.useState(false);
const assignedRef = React.useRef<HTMLDivElement>(null);
const libraryRef = React.useRef<HTMLDivElement>(null);
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
const summary = buildTaskSummary({
assigned: assignedTasks.length,
library: library.length,
collections: collections.length,
emotions: emotions.length,
});
const sectionCounts = React.useMemo(() => buildTaskSectionCounts(summary), [summary]);
React.useEffect(() => {
if (slugParam && activeEvent?.slug !== slugParam) {
selectEvent(slugParam);
@@ -102,6 +162,28 @@ export default function MobileEventTasksPage() {
setSearchTerm('');
}, [slug]);
const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => {
if (ref.current) {
ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
const handleQuickNav = (key: TaskSectionKey) => {
if (key === 'assigned') {
scrollToSection(assignedRef);
return;
}
if (key === 'library') {
scrollToSection(libraryRef);
return;
}
if (key === 'collections') {
setShowCollectionSheet(true);
return;
}
setShowEmotionSheet(true);
};
const load = React.useCallback(async () => {
if (!slug) {
try {
@@ -374,6 +456,44 @@ export default function MobileEventTasksPage() {
</MobileCard>
) : null}
{!loading ? (
<TaskSummaryCard
summary={summary}
text={text}
muted={muted}
border={border}
/>
) : 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) => (
<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}
>
<Text fontSize="$xs" fontWeight="700" color={text}>
{t(`events.tasks.sections.${section.key}`, section.key)}
</Text>
<PillBadge tone="muted">{section.count}</PillBadge>
</XStack>
</Pressable>
))}
</XStack>
</YStack>
) : null}
{loading ? (
<YStack space="$2">
{Array.from({ length: 4 }).map((_, idx) => (
@@ -471,6 +591,7 @@ export default function MobileEventTasksPage() {
</YStack>
) : (
<YStack space="$2">
<div ref={assignedRef} />
<YStack space="$2">
<MobileInput
type="search"
@@ -540,6 +661,7 @@ export default function MobileEventTasksPage() {
))}
</YStack>
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
<div ref={libraryRef} />
<Text fontSize={12.5} fontWeight="600" color={text}>
{t('events.tasks.library', 'Weitere Aufgaben')}
</Text>