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

@@ -1,19 +1,21 @@
import React from 'react';
import React, { Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronDown, ChevronLeft, Bell, QrCode } from 'lucide-react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@tamagui/core';
import { useEventContext } from '../../context/EventContext';
import { BottomNav, NavKey } from './BottomNav';
import { useMobileNav } from '../hooks/useMobileNav';
import { adminPath } from '../../constants';
import { MobileSheet } from './Sheet';
import { MobileCard, PillBadge } from './Primitives';
import { useAlertsBadge } from '../hooks/useAlertsBadge';
import { useNotificationsBadge } from '../hooks/useNotificationsBadge';
import { formatEventDate, resolveEventDisplayName } from '../../lib/events';
import { TenantEvent, getEvents } from '../../api';
const DevTenantSwitcher = React.lazy(() => import('../../components/DevTenantSwitcher'));
type MobileShellProps = {
title?: string;
@@ -29,11 +31,18 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const { go } = useMobileNav(activeEvent?.slug);
const navigate = useNavigate();
const { t, i18n } = useTranslation('mobile');
const { count: alertCount } = useAlertsBadge();
const { count: notificationCount } = useNotificationsBadge();
const theme = useTheme();
const backgroundColor = String(theme.background?.val ?? '#f7f8fb');
const surfaceColor = String(theme.surface?.val ?? '#ffffff');
const borderColor = String(theme.borderColor?.val ?? '#e5e7eb');
const textColor = String(theme.color?.val ?? '#111827');
const mutedText = String(theme.gray?.val ?? '#6b7280');
const [pickerOpen, setPickerOpen] = React.useState(false);
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
const [loadingEvents, setLoadingEvents] = React.useState(false);
const [attemptedFetch, setAttemptedFetch] = React.useState(false);
const showDevTenantSwitcher = import.meta.env.DEV && import.meta.env.VITE_ENABLE_TENANT_SWITCHER === 'true';
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
const effectiveEvents = events.length ? events : fallbackEvents;
@@ -81,11 +90,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const showQr = Boolean(effectiveActive?.slug);
return (
<YStack backgroundColor="#f7f8fb" minHeight="100vh">
<YStack backgroundColor={backgroundColor} minHeight="100vh">
<YStack
backgroundColor="white"
backgroundColor={surfaceColor}
borderBottomWidth={1}
borderColor="#e5e7eb"
borderColor={borderColor}
paddingHorizontal="$4"
paddingTop="$4"
paddingBottom="$3"
@@ -94,82 +103,88 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
shadowRadius={10}
shadowOffset={{ width: 0, height: 4 }}
>
<XStack alignItems="center" justifyContent="space-between" space="$3">
<XStack alignItems="center" space="$2">
{onBack ? (
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={18} color="#007AFF" />
</XStack>
</Pressable>
) : null}
<Pressable
disabled={!showEventSwitcher}
onPress={() => setPickerOpen(true)}
style={{ alignItems: 'flex-start' }}
>
<Text fontSize="$lg" fontWeight="800" color="#111827">
{eventTitle}
</Text>
{subtitleText ? (
<Text fontSize="$xs" color="#6b7280">
{subtitleText}
</Text>
) : null}
</Pressable>
{showEventSwitcher ? <ChevronDown size={14} color="#111827" /> : null}
</XStack>
<XStack alignItems="center" space="$2.5">
<Pressable onPress={() => navigate(adminPath('/mobile/alerts'))}>
<XStack
width={34}
height={34}
borderRadius={12}
backgroundColor="#f4f5f7"
alignItems="center"
justifyContent="center"
position="relative"
>
<Bell size={16} color="#111827" />
{alertCount > 0 ? (
<YStack
position="absolute"
top={-4}
right={-4}
minWidth={18}
height={18}
paddingHorizontal={6}
borderRadius={999}
backgroundColor="#ef4444"
alignItems="center"
justifyContent="center"
>
<Text fontSize={10} color="white" fontWeight="700">
{alertCount > 9 ? '9+' : alertCount}
</Text>
</YStack>
) : null}
<XStack alignItems="center" justifyContent="space-between" minHeight={48} space="$3">
{onBack ? (
<Pressable onPress={onBack}>
<XStack alignItems="center" space="$1.5">
<ChevronLeft size={18} color="#007AFF" />
</XStack>
</Pressable>
{showQr ? (
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}>
) : (
<XStack width={18} />
)}
<XStack alignItems="center" space="$2.5" flex={1} justifyContent="flex-end">
<XStack alignItems="center" space="$1" maxWidth="55%">
<Pressable
disabled={!showEventSwitcher}
onPress={() => setPickerOpen(true)}
style={{ alignItems: 'flex-end' }}
>
<Text fontSize="$lg" fontWeight="800" fontFamily="$display" color={textColor} textAlign="right" numberOfLines={1}>
{eventTitle}
</Text>
{subtitleText ? (
<Text fontSize="$xs" color={mutedText} textAlign="right" numberOfLines={1} fontFamily="$body">
{subtitleText}
</Text>
) : null}
</Pressable>
{showEventSwitcher ? <ChevronDown size={14} color={textColor} /> : null}
</XStack>
<XStack alignItems="center" space="$2">
<Pressable onPress={() => navigate(adminPath('/mobile/notifications'))}>
<XStack
width={34}
height={34}
paddingHorizontal="$3"
borderRadius={12}
backgroundColor="#0ea5e9"
backgroundColor={surfaceColor}
alignItems="center"
justifyContent="center"
space="$1.5"
position="relative"
>
<QrCode size={16} color="white" />
<Text fontSize="$xs" fontWeight="800" color="white">
{t('header.quickQr', 'Quick QR')}
</Text>
<Bell size={16} color={textColor} />
{notificationCount > 0 ? (
<YStack
position="absolute"
top={-4}
right={-4}
minWidth={18}
height={18}
paddingHorizontal={6}
borderRadius={999}
backgroundColor="#ef4444"
alignItems="center"
justifyContent="center"
>
<Text fontSize={10} color="white" fontWeight="700">
{notificationCount > 9 ? '9+' : notificationCount}
</Text>
</YStack>
) : null}
</XStack>
</Pressable>
) : null}
{headerActions ?? null}
{showQr ? (
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}>
<XStack
height={34}
paddingHorizontal="$3"
borderRadius={12}
backgroundColor="#0ea5e9"
alignItems="center"
justifyContent="center"
space="$1.5"
>
<QrCode size={16} color="white" />
<Text fontSize="$xs" fontWeight="800" color="white">
{t('header.quickQr', 'Quick QR')}
</Text>
</XStack>
</Pressable>
) : null}
{headerActions ?? null}
</XStack>
</XStack>
</XStack>
</YStack>
@@ -178,6 +193,12 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
{children}
</YStack>
{showDevTenantSwitcher ? (
<Suspense fallback={null}>
<DevTenantSwitcher bottomOffset={96} />
</Suspense>
) : null}
<BottomNav active={activeTab} onNavigate={go} />
<MobileSheet
@@ -190,10 +211,10 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
<YStack space="$2">
{effectiveEvents.length === 0 ? (
<MobileCard alignItems="flex-start" space="$2">
<Text fontSize="$sm" color="#111827" fontWeight="700">
<Text fontSize="$sm" color={textColor} fontWeight="700">
{t('header.noEventsTitle', 'Create your first event')}
</Text>
<Text fontSize="$xs" color="#4b5563">
<Text fontSize="$xs" color={mutedText}>
{t('header.noEventsBody', 'Start an event to access tasks, uploads, QR posters and more.')}
</Text>
<Pressable onPress={() => navigate(adminPath('/mobile/events/new'))}>
@@ -219,10 +240,10 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
>
<XStack alignItems="center" justifyContent="space-between" paddingVertical="$2">
<YStack space="$0.5">
<Text fontSize="$sm" fontWeight="700" color="#111827">
<Text fontSize="$sm" fontWeight="700" color={textColor}>
{resolveEventDisplayName(event)}
</Text>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={mutedText}>
{formatEventDate(event.event_date, locale) ?? t('header.noDate', 'Date tbd')}
</Text>
</YStack>