Fix auth translations and admin PWA UI
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CalendarDays, MapPin, Plus, Search, Camera, Users, Sparkles } from 'lucide-react';
|
||||
import { Card } from '@tamagui/card';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
import { ScrollView } from '@tamagui/scroll-view';
|
||||
import { ToggleGroup } from '@tamagui/toggle-group';
|
||||
import { Separator } from '@tamagui/separator';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||
import { MobileCard, PillBadge, CTAButton, FloatingActionButton, SkeletonCard } from './components/Primitives';
|
||||
import { PillBadge, CTAButton, FloatingActionButton, SkeletonCard } from './components/Primitives';
|
||||
import { MobileInput } from './components/FormControls';
|
||||
import { getEvents, TenantEvent } from '../api';
|
||||
import { adminPath } from '../constants';
|
||||
@@ -27,7 +31,7 @@ export default function MobileEventsPage() {
|
||||
const [statusFilter, setStatusFilter] = React.useState<EventStatusKey>('all');
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const back = useBackNavigation();
|
||||
const { text, muted, subtle, border, primary, danger, surface, accentSoft, accent } = useAdminTheme();
|
||||
const { text, muted, subtle, border, primary, danger, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme();
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -54,22 +58,61 @@ export default function MobileEventsPage() {
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<MobileCard>
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.16}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<Text fontWeight="700" color={danger}>
|
||||
{error}
|
||||
</Text>
|
||||
</MobileCard>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<MobileInput
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('events.list.search', 'Search events')}
|
||||
compact
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.16}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
||||
{t('events.list.filters.title', 'Filters & Search')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<MobileInput
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('events.list.search', 'Search events')}
|
||||
compact
|
||||
/>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('events.list.filters.hint', 'Filter your events by status or search by name.')}
|
||||
</Text>
|
||||
</YStack>
|
||||
</Card>
|
||||
|
||||
{loading ? (
|
||||
<YStack space="$2">
|
||||
@@ -78,15 +121,27 @@ export default function MobileEventsPage() {
|
||||
))}
|
||||
</YStack>
|
||||
) : events.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$3">
|
||||
<Text fontSize="$md" fontWeight="700">
|
||||
{t('events.list.empty.title', 'Noch kein Event angelegt')}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted} textAlign="center">
|
||||
{t('events.list.empty.description', 'Starte jetzt mit deinem ersten Event.')}
|
||||
</Text>
|
||||
<CTAButton label={t('events.actions.create', 'Create New Event')} onPress={() => navigate(adminPath('/events/new'))} />
|
||||
</MobileCard>
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.16}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<Text fontSize="$md" fontWeight="700">
|
||||
{t('events.list.empty.title', 'Noch kein Event angelegt')}
|
||||
</Text>
|
||||
<Text fontSize="$sm" color={muted} textAlign="center">
|
||||
{t('events.list.empty.description', 'Starte jetzt mit deinem ersten Event.')}
|
||||
</Text>
|
||||
<CTAButton label={t('events.actions.create', 'Create New Event')} onPress={() => navigate(adminPath('/events/new'))} />
|
||||
</YStack>
|
||||
</Card>
|
||||
) : (
|
||||
<EventsList
|
||||
events={events}
|
||||
@@ -123,7 +178,7 @@ function EventsList({
|
||||
onEdit: (slug: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { text, muted, subtle, border, primary, surface, accentSoft, accent } = useAdminTheme();
|
||||
const { text, muted, subtle, border, primary, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme();
|
||||
const activeBg = accentSoft;
|
||||
const activeBorder = accent;
|
||||
|
||||
@@ -150,47 +205,93 @@ function EventsList({
|
||||
|
||||
return (
|
||||
<YStack space="$3">
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
{filters.map((filter) => {
|
||||
const active = filter.key === statusFilter;
|
||||
return (
|
||||
<Pressable key={filter.key} onPress={() => onStatusChange(filter.key)} style={{ flexGrow: 1 }}>
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
space="$1.5"
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
borderRadius={14}
|
||||
backgroundColor={active ? activeBg : surface}
|
||||
borderWidth={1}
|
||||
borderColor={active ? activeBorder : border}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
|
||||
{filter.label}
|
||||
</Text>
|
||||
<PillBadge tone={active ? 'success' : 'muted'}>{filter.count}</PillBadge>
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.14}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="800" color={text}>
|
||||
{t('events.list.filters.status', 'Status')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={statusFilter}
|
||||
onValueChange={(value) => value && onStatusChange(value as EventStatusKey)}
|
||||
>
|
||||
<XStack space="$2" paddingVertical="$1">
|
||||
{filters.map((filter) => {
|
||||
const active = filter.key === statusFilter;
|
||||
return (
|
||||
<ToggleGroup.Item
|
||||
key={filter.key}
|
||||
value={filter.key}
|
||||
borderRadius={999}
|
||||
borderWidth={1}
|
||||
borderColor={active ? activeBorder : border}
|
||||
backgroundColor={active ? activeBg : surface}
|
||||
paddingVertical="$2"
|
||||
paddingHorizontal="$3"
|
||||
>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
|
||||
{filter.label}
|
||||
</Text>
|
||||
<PillBadge tone={active ? 'success' : 'muted'}>{filter.count}</PillBadge>
|
||||
</XStack>
|
||||
</ToggleGroup.Item>
|
||||
);
|
||||
})}
|
||||
</XStack>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</XStack>
|
||||
</ToggleGroup>
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Card>
|
||||
|
||||
{filteredEvents.length === 0 ? (
|
||||
<MobileCard alignItems="center" justifyContent="center" space="$2">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('events.list.empty.filtered', 'No events match this filter.')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted} textAlign="center">
|
||||
{t('events.list.empty.filteredHint', 'Try a different status or clear your search.')}
|
||||
</Text>
|
||||
<CTAButton
|
||||
label={t('events.list.filters.all', 'All')}
|
||||
tone="ghost"
|
||||
fullWidth={false}
|
||||
onPress={() => onStatusChange('all')}
|
||||
/>
|
||||
</MobileCard>
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.14}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2" alignItems="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={text}>
|
||||
{t('events.list.empty.filtered', 'No events match this filter.')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted} textAlign="center">
|
||||
{t('events.list.empty.filteredHint', 'Try a different status or clear your search.')}
|
||||
</Text>
|
||||
<CTAButton
|
||||
label={t('events.list.filters.all', 'All')}
|
||||
tone="ghost"
|
||||
fullWidth={false}
|
||||
onPress={() => onStatusChange('all')}
|
||||
/>
|
||||
</YStack>
|
||||
</Card>
|
||||
) : (
|
||||
filteredEvents.map((event) => {
|
||||
const statusKey = resolveEventStatusKey(event);
|
||||
@@ -210,6 +311,8 @@ function EventsList({
|
||||
subtle={subtle}
|
||||
border={border}
|
||||
primary={primary}
|
||||
surface={surface}
|
||||
shadow={shadow}
|
||||
statusLabel={statusLabel}
|
||||
statusTone={statusTone}
|
||||
onOpen={onOpen}
|
||||
@@ -229,6 +332,8 @@ function EventRow({
|
||||
subtle,
|
||||
border,
|
||||
primary,
|
||||
surface,
|
||||
shadow,
|
||||
statusLabel,
|
||||
statusTone,
|
||||
onOpen,
|
||||
@@ -240,6 +345,8 @@ function EventRow({
|
||||
subtle: string;
|
||||
border: string;
|
||||
primary: string;
|
||||
surface: string;
|
||||
shadow: string;
|
||||
statusLabel: string;
|
||||
statusTone: 'success' | 'warning' | 'muted';
|
||||
onOpen: (slug: string) => void;
|
||||
@@ -248,62 +355,77 @@ function EventRow({
|
||||
const { t } = useTranslation('management');
|
||||
const stats = buildEventListStats(event);
|
||||
return (
|
||||
<MobileCard borderColor={border}>
|
||||
<XStack justifyContent="space-between" alignItems="flex-start" space="$2">
|
||||
<YStack space="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{renderName(event.name)}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<CalendarDays size={14} color={subtle} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{formatDate(event.event_date)}
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.14}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<XStack justifyContent="space-between" alignItems="flex-start" space="$2">
|
||||
<YStack space="$1">
|
||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||
{renderName(event.name)}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MapPin size={14} color={subtle} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{resolveLocation(event)}
|
||||
<XStack alignItems="center" space="$2">
|
||||
<CalendarDays size={14} color={subtle} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{formatDate(event.event_date)}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<MapPin size={14} color={subtle} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{resolveLocation(event)}
|
||||
</Text>
|
||||
</XStack>
|
||||
<PillBadge tone={statusTone}>{statusLabel}</PillBadge>
|
||||
</YStack>
|
||||
<Pressable onPress={() => onEdit(event.slug)}>
|
||||
<Text fontSize="$xl" color={muted}>
|
||||
˅
|
||||
</Text>
|
||||
</XStack>
|
||||
<PillBadge tone={statusTone}>{statusLabel}</PillBadge>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<EventStatChip
|
||||
icon={Camera}
|
||||
label={t('events.list.stats.photos', 'Photos')}
|
||||
value={stats.photos}
|
||||
muted={subtle}
|
||||
/>
|
||||
<EventStatChip
|
||||
icon={Users}
|
||||
label={t('events.list.stats.guests', 'Guests')}
|
||||
value={stats.guests}
|
||||
muted={subtle}
|
||||
/>
|
||||
<EventStatChip
|
||||
icon={Sparkles}
|
||||
label={t('events.list.stats.tasks', 'Tasks')}
|
||||
value={stats.tasks}
|
||||
muted={subtle}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
<Pressable onPress={() => onEdit(event.slug)}>
|
||||
<Text fontSize="$xl" color={muted}>
|
||||
˅
|
||||
</Text>
|
||||
</Pressable>
|
||||
</XStack>
|
||||
|
||||
<Pressable onPress={() => onOpen(event.slug)} style={{ marginTop: 8 }}>
|
||||
<XStack alignItems="center" justifyContent="flex-start" space="$2">
|
||||
<Plus size={16} color={primary} />
|
||||
<Text fontSize="$sm" color={primary} fontWeight="700">
|
||||
{t('events.list.actions.open', 'Open event')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
</MobileCard>
|
||||
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<EventStatChip
|
||||
icon={Camera}
|
||||
label={t('events.list.stats.photos', 'Photos')}
|
||||
value={stats.photos}
|
||||
muted={subtle}
|
||||
/>
|
||||
<EventStatChip
|
||||
icon={Users}
|
||||
label={t('events.list.stats.guests', 'Guests')}
|
||||
value={stats.guests}
|
||||
muted={subtle}
|
||||
/>
|
||||
<EventStatChip
|
||||
icon={Sparkles}
|
||||
label={t('events.list.stats.tasks', 'Tasks')}
|
||||
value={stats.tasks}
|
||||
muted={subtle}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
<Separator borderColor={border} />
|
||||
|
||||
<Pressable onPress={() => onOpen(event.slug)}>
|
||||
<XStack alignItems="center" justifyContent="flex-start" space="$2">
|
||||
<Plus size={16} color={primary} />
|
||||
<Text fontSize="$sm" color={primary} fontWeight="700">
|
||||
{t('events.list.actions.open', 'Open event')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
</YStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user