weitere perfektionierung der neuen mobile app
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user