weiterer fortschritt mit tamagui und dem neuen mobile event admin
This commit is contained in:
@@ -43,7 +43,14 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
|
||||
const IconCmp = item.icon;
|
||||
return (
|
||||
<Pressable key={item.key} onPress={() => onNavigate(item.key)}>
|
||||
<YStack alignItems="center" space="$1" position="relative">
|
||||
<YStack
|
||||
alignItems="center"
|
||||
space="$1"
|
||||
position="relative"
|
||||
padding="$2"
|
||||
borderRadius={12}
|
||||
backgroundColor={activeState ? '#e8f1ff' : 'transparent'}
|
||||
>
|
||||
<IconCmp size={20} color={activeState ? String(theme.primary?.val ?? '#007AFF') : '#94a3b8'} />
|
||||
<Text fontSize="$xs" color={activeState ? '$primary' : '#6b7280'}>
|
||||
{item.label}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MobileSheet } from './Sheet';
|
||||
import { MobileCard, PillBadge } from './Primitives';
|
||||
import { useAlertsBadge } from '../hooks/useAlertsBadge';
|
||||
import { formatEventDate, resolveEventDisplayName } from '../../lib/events';
|
||||
import { TenantEvent } from '../../api';
|
||||
import { TenantEvent, getEvents } from '../../api';
|
||||
|
||||
type MobileShellProps = {
|
||||
title?: string;
|
||||
@@ -31,19 +31,54 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const { t, i18n } = useTranslation('mobile');
|
||||
const { count: alertCount } = useAlertsBadge();
|
||||
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 locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||
const eventTitle = title ?? (activeEvent ? resolveEventDisplayName(activeEvent) : t('header.appName', 'Event Admin'));
|
||||
const effectiveEvents = events.length ? events : fallbackEvents;
|
||||
const effectiveHasMultiple = hasMultipleEvents || effectiveEvents.length > 1;
|
||||
const effectiveHasEvents = hasEvents || effectiveEvents.length > 0;
|
||||
const effectiveActive = activeEvent ?? (effectiveEvents.length === 1 ? effectiveEvents[0] : null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (events.length || loadingEvents || attemptedFetch) {
|
||||
return;
|
||||
}
|
||||
setAttemptedFetch(true);
|
||||
setLoadingEvents(true);
|
||||
getEvents({ force: true })
|
||||
.then((list) => {
|
||||
setFallbackEvents(list ?? []);
|
||||
if (!activeEvent && list?.length === 1) {
|
||||
selectEvent(list[0]?.slug ?? null);
|
||||
}
|
||||
})
|
||||
.catch(() => setFallbackEvents([]))
|
||||
.finally(() => setLoadingEvents(false));
|
||||
}, [events.length, loadingEvents, attemptedFetch, activeEvent, selectEvent]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pickerOpen) return;
|
||||
if (effectiveEvents.length) return;
|
||||
setLoadingEvents(true);
|
||||
getEvents({ force: true })
|
||||
.then((list) => setFallbackEvents(list ?? []))
|
||||
.catch(() => setFallbackEvents([]))
|
||||
.finally(() => setLoadingEvents(false));
|
||||
}, [pickerOpen, effectiveEvents.length]);
|
||||
|
||||
const eventTitle = title ?? (effectiveActive ? resolveEventDisplayName(effectiveActive) : t('header.appName', 'Event Admin'));
|
||||
const subtitleText =
|
||||
subtitle ??
|
||||
(activeEvent?.event_date
|
||||
? formatEventDate(activeEvent.event_date, locale) ?? ''
|
||||
: hasEvents
|
||||
(effectiveActive?.event_date
|
||||
? formatEventDate(effectiveActive.event_date, locale) ?? ''
|
||||
: effectiveHasEvents
|
||||
? t('header.selectEvent', 'Select an event to continue')
|
||||
: t('header.empty', 'Create your first event to get started'));
|
||||
|
||||
const showEventSwitcher = hasMultipleEvents;
|
||||
const showQr = Boolean(activeEvent?.slug);
|
||||
const showEventSwitcher = effectiveHasMultiple;
|
||||
const showQr = Boolean(effectiveActive?.slug);
|
||||
|
||||
return (
|
||||
<YStack backgroundColor="#f7f8fb" minHeight="100vh">
|
||||
@@ -65,9 +100,6 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
<Pressable onPress={onBack}>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<ChevronLeft size={18} color="#007AFF" />
|
||||
<Text fontSize="$sm" color="#007AFF" fontWeight="600">
|
||||
{t('actions.back', 'Back')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
@@ -120,7 +152,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{showQr ? (
|
||||
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${activeEvent?.slug}/qr`))}>
|
||||
<Pressable onPress={() => navigate(adminPath(`/mobile/events/${effectiveActive?.slug}/qr`))}>
|
||||
<XStack
|
||||
height={34}
|
||||
paddingHorizontal="$3"
|
||||
@@ -156,7 +188,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
bottomOffsetPx={110}
|
||||
>
|
||||
<YStack space="$2">
|
||||
{events.length === 0 ? (
|
||||
{effectiveEvents.length === 0 ? (
|
||||
<MobileCard alignItems="flex-start" space="$2">
|
||||
<Text fontSize="$sm" color="#111827" fontWeight="700">
|
||||
{t('header.noEventsTitle', 'Create your first event')}
|
||||
@@ -173,12 +205,16 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
</Pressable>
|
||||
</MobileCard>
|
||||
) : (
|
||||
events.map((event) => (
|
||||
effectiveEvents.map((event) => (
|
||||
<Pressable
|
||||
key={event.slug}
|
||||
onPress={() => {
|
||||
selectEvent(event.slug ?? null);
|
||||
const targetSlug = event.slug ?? null;
|
||||
selectEvent(targetSlug);
|
||||
setPickerOpen(false);
|
||||
if (targetSlug) {
|
||||
navigate(adminPath(`/mobile/events/${targetSlug}`));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="space-between" paddingVertical="$2">
|
||||
|
||||
@@ -122,20 +122,22 @@ export function ActionTile({
|
||||
onPress: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Pressable onPress={onPress} style={{ width: '48%' }}>
|
||||
<Pressable onPress={onPress} style={{ width: '48%', marginBottom: 12 }}>
|
||||
<YStack
|
||||
borderRadius={16}
|
||||
padding="$3"
|
||||
space="$2"
|
||||
space="$2.5"
|
||||
backgroundColor={`${color}22`}
|
||||
borderWidth={1}
|
||||
borderColor={`${color}55`}
|
||||
minHeight={110}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<XStack width={38} height={38} borderRadius={12} backgroundColor={color} alignItems="center" justifyContent="center">
|
||||
<IconCmp size={18} color="white" />
|
||||
<XStack width={34} height={34} borderRadius={12} backgroundColor={color} alignItems="center" justifyContent="center">
|
||||
<IconCmp size={16} color="white" />
|
||||
</XStack>
|
||||
<Text fontSize="$sm" fontWeight="700" color="#111827">
|
||||
<Text fontSize="$sm" fontWeight="700" color="#111827" textAlign="center">
|
||||
{label}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
Reference in New Issue
Block a user