weitere perfektionierung der neuen mobile app

This commit is contained in:
Codex Agent
2025-12-11 12:18:08 +01:00
parent 7b01a77083
commit b4417db5cd
38 changed files with 4265 additions and 3040 deletions

View File

@@ -12,6 +12,7 @@ import { adminPath } from '../constants';
import { useEventContext } from '../context/EventContext';
import { getEventStats, EventStats, TenantEvent, getEvents } from '../api';
import { formatEventDate, resolveEventDisplayName } from '../lib/events';
import { useTheme } from '@tamagui/core';
export default function MobileDashboardPage() {
const navigate = useNavigate();
@@ -20,6 +21,13 @@ export default function MobileDashboardPage() {
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
const [fallbackLoading, setFallbackLoading] = React.useState(false);
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
const border = String(theme.borderColor?.val ?? '#334155');
const surface = String(theme.surface?.val ?? '#ffffff');
const accentSoft = String(theme.blue3?.val ?? '#e0f2fe');
const accentText = String(theme.primary?.val ?? '#3b82f6');
const { data: stats, isLoading: statsLoading } = useQuery<EventStats | null>({
queryKey: ['mobile', 'dashboard', 'stats', activeEvent?.slug],
@@ -62,7 +70,7 @@ export default function MobileDashboardPage() {
if (isLoading || fallbackLoading) {
return (
<MobileShell activeTab="home" title={t('events.list.dashboardTitle', 'Dashboard')}>
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
<YStack space="$2">
{Array.from({ length: 3 }).map((_, idx) => (
<MobileCard key={`sk-${idx}`} height={110} opacity={0.6} />
@@ -74,7 +82,7 @@ export default function MobileDashboardPage() {
if (!effectiveHasEvents) {
return (
<MobileShell activeTab="home" title={t('events.list.dashboardTitle', 'Dashboard')}>
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
<OnboardingEmptyState />
</MobileShell>
);
@@ -84,10 +92,10 @@ export default function MobileDashboardPage() {
return (
<MobileShell
activeTab="home"
title={t('events.list.dashboardTitle', 'Dashboard')}
subtitle={t('header.selectEvent', 'Select an event to continue')}
title={t('mobileDashboard.title', 'Dashboard')}
subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')}
>
<EventPickerList events={effectiveEvents} locale={locale} />
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
</MobileShell>
);
}
@@ -122,28 +130,31 @@ export default function MobileDashboardPage() {
function OnboardingEmptyState() {
const { t } = useTranslation('management');
const navigate = useNavigate();
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
return (
<YStack space="$3">
<MobileCard alignItems="flex-start" space="$3">
<Text fontSize="$lg" fontWeight="800" color="#111827">
{t('events.list.empty.title', 'Create your first event')}
<Text fontSize="$lg" fontWeight="800" color={text}>
{t('mobileDashboard.emptyTitle', 'Create your first event')}
</Text>
<Text fontSize="$sm" color="#4b5563">
{t('events.list.empty.description', 'Start an event to manage tasks, QR posters and uploads.')}
<Text fontSize="$sm" color={muted}>
{t('mobileDashboard.emptyBody', 'Start an event to manage tasks, QR posters and uploads.')}
</Text>
<CTAButton label={t('events.actions.create', 'Create Event')} onPress={() => navigate(adminPath('/mobile/events/new'))} />
<CTAButton label={t('events.actions.preview', 'View Demo')} tone="ghost" onPress={() => navigate(adminPath('/mobile/events'))} />
<CTAButton label={t('mobileDashboard.ctaCreate', 'Create event')} onPress={() => navigate(adminPath('/mobile/events/new'))} />
<CTAButton label={t('mobileDashboard.ctaDemo', 'View demo')} tone="ghost" onPress={() => navigate(adminPath('/mobile/events'))} />
</MobileCard>
<MobileCard space="$2">
<Text fontSize="$sm" fontWeight="800" color="#111827">
{t('events.list.empty.highlights', 'What you can do')}
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.highlightsTitle', 'What you can do')}
</Text>
<YStack space="$1.5">
{[
t('events.quick.images', 'Review photos & uploads'),
t('events.quick.tasks', 'Assign tasks & challenges'),
t('events.quick.qr', 'Share QR posters'),
t('events.quick.guests', 'Invite helpers & guests'),
t('mobileDashboard.highlightImages', 'Review photos & uploads'),
t('mobileDashboard.highlightTasks', 'Assign tasks & challenges'),
t('mobileDashboard.highlightQr', 'Share QR posters'),
t('mobileDashboard.highlightGuests', 'Invite helpers & guests'),
].map((item) => (
<XStack key={item} alignItems="center" space="$2">
<PillBadge tone="muted">{item}</PillBadge>
@@ -155,7 +166,7 @@ function OnboardingEmptyState() {
);
}
function EventPickerList({ events, locale }: { events: TenantEvent[]; locale: string }) {
function EventPickerList({ events, locale, text, muted, border }: { events: TenantEvent[]; locale: string; text: string; muted: string; border: string }) {
const { t } = useTranslation('management');
const { selectEvent } = useEventContext();
const navigate = useNavigate();
@@ -179,8 +190,8 @@ function EventPickerList({ events, locale }: { events: TenantEvent[]; locale: st
return (
<YStack space="$2">
<Text fontSize="$sm" color="#111827" fontWeight="700">
{t('events.detail.pickEvent', 'Select an event')}
<Text fontSize="$sm" color={text} fontWeight="700">
{t('mobileDashboard.pickEvent', 'Select an event')}
</Text>
{localEvents.map((event) => (
<Pressable
@@ -192,18 +203,20 @@ function EventPickerList({ events, locale }: { events: TenantEvent[]; locale: st
}
}}
>
<MobileCard borderColor="#e5e7eb" space="$2">
<MobileCard borderColor={border} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack space="$1">
<Text fontSize="$md" fontWeight="800" color="#111827">
<Text fontSize="$md" fontWeight="800" color={text}>
{resolveEventDisplayName(event)}
</Text>
<Text fontSize="$xs" color="#6b7280">
{formatEventDate(event.event_date, locale) ?? t('events.status.draft', 'Draft')}
<Text fontSize="$xs" color={muted}>
{formatEventDate(event.event_date, locale) ?? t('mobileDashboard.status.draft', 'Draft')}
</Text>
</YStack>
<PillBadge tone={event.status === 'published' ? 'success' : 'warning'}>
{event.status === 'published' ? t('events.status.published', 'Live') : t('events.status.draft', 'Draft')}
{event.status === 'published'
? t('mobileDashboard.status.published', 'Live')
: t('mobileDashboard.status.draft', 'Draft')}
</PillBadge>
</XStack>
</MobileCard>
@@ -223,27 +236,30 @@ function FeaturedActions({
onShowQr: () => void;
}) {
const { t } = useTranslation('management');
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
const cards = [
{
key: 'photos',
label: t('events.quick.images', 'Review Photos'),
desc: t('events.quick.images.desc', 'Moderate uploads and highlights'),
label: t('mobileDashboard.photosLabel', 'Review photos'),
desc: t('mobileDashboard.photosDesc', 'Moderate uploads and highlights'),
icon: ImageIcon,
color: '#0ea5e9',
action: onReviewPhotos,
},
{
key: 'tasks',
label: t('events.quick.tasks', 'Manage Tasks & Challenges'),
desc: t('events.quick.tasks.desc', 'Assign and track progress'),
label: t('mobileDashboard.tasksLabel', 'Manage tasks & challenges'),
desc: t('mobileDashboard.tasksDesc', 'Assign and track progress'),
icon: ListTodo,
color: '#22c55e',
action: onManageTasks,
},
{
key: 'qr',
label: t('events.quick.qr', 'Show / Share QR Code'),
desc: t('events.quick.qr.desc', 'Posters, cards, and links'),
label: t('mobileDashboard.qrLabel', 'Show / share QR code'),
desc: t('mobileDashboard.qrDesc', 'Posters, cards, and links'),
icon: QrCode,
color: '#f59e0b',
action: onShowQr,
@@ -260,14 +276,14 @@ function FeaturedActions({
<card.icon size={20} color="white" />
</XStack>
<YStack space="$1" flex={1}>
<Text fontSize="$md" fontWeight="800" color="#111827">
<Text fontSize="$md" fontWeight="800" color={text}>
{card.label}
</Text>
<Text fontSize="$xs" color="#334155">
<Text fontSize="$xs" color={muted}>
{card.desc}
</Text>
</YStack>
<Text fontSize="$xl" color="#94a3b8">
<Text fontSize="$xl" color={String(theme.gray9?.val ?? '#94a3b8')}>
˃
</Text>
</XStack>
@@ -292,28 +308,33 @@ function SecondaryGrid({
onSettings: () => void;
}) {
const { t } = useTranslation('management');
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
const border = String(theme.borderColor?.val ?? '#334155');
const surface = String(theme.surface?.val ?? '#0b1220');
const tiles = [
{
icon: Users,
label: t('events.quick.guests', 'Guest management'),
label: t('mobileDashboard.shortcutGuests', 'Guest management'),
color: '#60a5fa',
action: onGuests,
},
{
icon: QrCode,
label: t('events.quick.prints', 'Print & poster downloads'),
label: t('mobileDashboard.shortcutPrints', 'Print & poster downloads'),
color: '#fbbf24',
action: onPrint,
},
{
icon: Sparkles,
label: t('events.quick.invites', 'Team / helper invites'),
label: t('mobileDashboard.shortcutInvites', 'Team / helper invites'),
color: '#a855f7',
action: onInvites,
},
{
icon: Settings,
label: t('events.quick.settings', 'Event settings'),
label: t('mobileDashboard.shortcutSettings', 'Event settings'),
color: '#10b981',
action: onSettings,
},
@@ -321,8 +342,8 @@ function SecondaryGrid({
return (
<YStack space="$2" marginTop="$2">
<Text fontSize="$sm" fontWeight="800" color="#111827">
{t('events.quick.more', 'Shortcuts')}
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.shortcutsTitle', 'Shortcuts')}
</Text>
<XStack flexWrap="wrap" space="$2">
{tiles.map((tile) => (
@@ -330,11 +351,11 @@ function SecondaryGrid({
))}
</XStack>
{event ? (
<MobileCard backgroundColor="#f8fafc" borderColor="#e2e8f0" space="$1.5">
<Text fontSize="$sm" fontWeight="700" color="#0f172a">
<MobileCard backgroundColor={surface} borderColor={border} space="$1.5">
<Text fontSize="$sm" fontWeight="700" color={text}>
{resolveEventDisplayName(event)}
</Text>
<Text fontSize="$xs" color="#475569">
<Text fontSize="$xs" color={muted}>
{renderEventLocation(event)}
</Text>
</MobileCard>
@@ -345,21 +366,24 @@ function SecondaryGrid({
function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null; stats: EventStats | null | undefined; loading: boolean; locale: string }) {
const { t } = useTranslation('management');
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const muted = String(theme.gray11?.val ?? theme.gray?.val ?? '#cbd5e1');
if (!event) return null;
const kpis = [
{
label: t('events.detail.kpi.tasks', 'Open tasks'),
label: t('mobileDashboard.kpiTasks', 'Open tasks'),
value: event.tasks_count ?? '—',
icon: ListTodo,
},
{
label: t('events.detail.kpi.photos', 'Photos'),
label: t('mobileDashboard.kpiPhotos', 'Photos'),
value: stats?.uploads_total ?? event.photo_count ?? '—',
icon: ImageIcon,
},
{
label: t('events.detail.kpi.guests', 'Guests'),
label: t('mobileDashboard.kpiGuests', 'Guests'),
value: event.active_invites_count ?? event.total_invites_count ?? '—',
icon: Users,
},
@@ -367,8 +391,8 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
return (
<YStack space="$2">
<Text fontSize="$sm" fontWeight="800" color="#111827">
{t('dashboard.kpis', 'Key Performance Indicators')}
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.kpiTitle', 'Key performance indicators')}
</Text>
{loading ? (
<XStack space="$2" flexWrap="wrap">
@@ -383,7 +407,7 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
))}
</XStack>
)}
<Text fontSize="$xs" color="#94a3b8">
<Text fontSize="$xs" color={muted}>
{formatEventDate(event.event_date, locale) ?? ''}
</Text>
</YStack>
@@ -392,14 +416,19 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
function AlertsAndHints({ event, stats }: { event: TenantEvent | null; stats: EventStats | null | undefined }) {
const { t } = useTranslation('management');
const theme = useTheme();
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
const warningBg = String(theme.yellow3?.val ?? '#fff7ed');
const warningBorder = String(theme.yellow6?.val ?? '#fed7aa');
const warningText = String(theme.yellow11?.val ?? '#9a3412');
if (!event) return null;
const alerts: string[] = [];
if (stats?.pending_photos) {
alerts.push(t('events.alerts.pendingPhotos', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos }));
alerts.push(t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos }));
}
if (event.tasks_count) {
alerts.push(t('events.alerts.tasksOpen', '{{count}} tasks due or open', { count: event.tasks_count }));
alerts.push(t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count }));
}
if (alerts.length === 0) {
@@ -408,12 +437,12 @@ function AlertsAndHints({ event, stats }: { event: TenantEvent | null; stats: Ev
return (
<YStack space="$1.5">
<Text fontSize="$sm" fontWeight="800" color="#111827">
{t('alerts.title', 'Alerts')}
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.alertsTitle', 'Alerts')}
</Text>
{alerts.map((alert) => (
<MobileCard key={alert} backgroundColor="#fff7ed" borderColor="#fed7aa" space="$2">
<Text fontSize="$sm" color="#9a3412">
<MobileCard key={alert} backgroundColor={warningBg} borderColor={warningBorder} space="$2">
<Text fontSize="$sm" color={warningText}>
{alert}
</Text>
</MobileCard>