Files
fotospiel-app/resources/js/admin/mobile/components/UserMenuSheet.tsx
Codex Agent b1f9f7cee0
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Fix TypeScript typecheck errors
2026-01-30 15:56:06 +01:00

267 lines
8.9 KiB
TypeScript

import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronRight, CreditCard, FileText, HelpCircle, User, X } from 'lucide-react';
import { XStack, YStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { ListItem } from '@tamagui/list-item';
import { YGroup } from '@tamagui/group';
import { Switch } from '@tamagui/switch';
import { Separator } from 'tamagui';
import { useAppearance } from '@/hooks/use-appearance';
import { ADMIN_BILLING_PATH, ADMIN_DATA_EXPORTS_PATH, ADMIN_FAQ_PATH, ADMIN_PROFILE_ACCOUNT_PATH, adminPath } from '../../constants';
import { useAdminTheme } from '../theme';
import { MobileSelect } from './FormControls';
type UserMenuSheetProps = {
open: boolean;
onClose: () => void;
user?: { name?: string | null; email?: string | null; avatar_url?: string | null } | null;
isMember: boolean;
navigate: (path: string) => void;
};
const MENU_WIDTH = 320;
export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserMenuSheetProps) {
const { t, i18n } = useTranslation('management');
const theme = useAdminTheme();
const { appearance, resolved, updateAppearance } = useAppearance();
const [language, setLanguage] = React.useState<string>(() => (i18n.language?.startsWith('en') ? 'en' : 'de'));
React.useEffect(() => {
setLanguage(i18n.language?.startsWith('en') ? 'en' : 'de');
}, [i18n.language]);
const isDark = resolved === 'dark';
const handleNavigate = (path: string) => {
onClose();
navigate(path);
};
const menuItems = [
{
label: t('mobileProfile.account', 'Account bearbeiten'),
icon: User,
path: ADMIN_PROFILE_ACCOUNT_PATH,
show: true,
},
{
label: t('billing.sections.packages.title', 'Pakete'),
icon: CreditCard,
path: adminPath('/mobile/billing#packages'),
show: !isMember,
},
{
label: t('billing.sections.invoices.title', 'Rechnungen & Zahlungen'),
icon: CreditCard,
path: adminPath('/mobile/billing#invoices'),
show: !isMember,
},
{
label: t('dataExports.title', 'Datenexporte'),
icon: FileText,
path: ADMIN_DATA_EXPORTS_PATH,
show: !isMember,
},
{
label: t('common.help', 'Help'),
icon: HelpCircle,
path: ADMIN_FAQ_PATH,
show: true,
},
].filter((item) => item.show);
return (
<YStack
position="fixed"
top={0}
bottom={0}
left={0}
right={0}
zIndex={100000}
pointerEvents={open ? 'auto' : 'none'}
>
<Pressable
onPress={onClose}
style={{
position: 'absolute',
inset: 0,
backgroundColor: theme.overlay,
opacity: open ? 1 : 0,
transition: 'opacity 180ms ease',
}}
/>
<YStack
position="absolute"
top={0}
bottom={0}
right={0}
width="86vw"
maxWidth={MENU_WIDTH}
backgroundColor={theme.surface}
borderLeftWidth={1}
borderColor={theme.border}
padding="$4"
space="$3"
style={{
transform: open ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 220ms ease',
boxShadow: `-12px 0 24px ${theme.glassShadow ?? theme.shadow}`,
}}
>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color={theme.textStrong}>
{t('mobileProfile.title', 'Profil')}
</Text>
<Pressable onPress={onClose} aria-label={t('common.close', 'Close')}>
<XStack
width={32}
height={32}
borderRadius={10}
alignItems="center"
justifyContent="center"
backgroundColor={theme.surfaceMuted}
borderWidth={1}
borderColor={theme.border}
>
<X size={16} color={theme.muted} />
</XStack>
</Pressable>
</XStack>
<XStack alignItems="center" space="$3">
<XStack
width={48}
height={48}
borderRadius={16}
alignItems="center"
justifyContent="center"
backgroundColor={theme.accentSoft}
>
<Text fontSize="$lg" fontWeight="800" color={theme.primary}>
{user?.name?.charAt(0)?.toUpperCase() ?? 'U'}
</Text>
</XStack>
<YStack>
<Text fontSize="$sm" fontWeight="800" color={theme.textStrong}>
{user?.name ?? t('events.members.roles.guest', 'Guest')}
</Text>
<Text fontSize="$xs" color={theme.muted}>
{user?.email ?? ''}
</Text>
</YStack>
</XStack>
<Separator backgroundColor={theme.border} opacity={0.6} />
<YStack space="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('mobileProfile.settings', 'Einstellungen')}
</Text>
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
{menuItems.map((item) => (
<YGroup.Item key={item.label}>
<ListItem
hoverTheme
pressTheme
paddingVertical="$2"
paddingHorizontal="$3"
onPress={() => handleNavigate(item.path)}
title={
<XStack alignItems="center" space="$2">
<XStack
width={28}
height={28}
borderRadius={10}
alignItems="center"
justifyContent="center"
backgroundColor={theme.surfaceMuted}
borderWidth={1}
borderColor={theme.border}
>
<item.icon size={14} color={theme.textStrong} />
</XStack>
<Text fontSize="$sm" color={theme.textStrong}>
{item.label}
</Text>
</XStack>
}
iconAfter={<ChevronRight size={16} color={theme.muted} />}
/>
</YGroup.Item>
))}
</YGroup>
</YStack>
<YStack space="$2">
<Text fontSize="$xs" fontWeight="700" color={theme.muted} textTransform="uppercase" letterSpacing={1}>
{t('settings.appearance.title', 'Darstellung')}
</Text>
<YGroup {...({ borderRadius: '$4', borderWidth: 1, borderColor: theme.border, overflow: 'hidden' } as any)}>
<YGroup.Item>
<ListItem
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<Text fontSize="$sm" color={theme.textStrong}>
{t('mobileProfile.language', 'Sprache')}
</Text>
</XStack>
}
iconAfter={
<MobileSelect
value={language}
onChange={(event) => {
const lng = event.target.value;
setLanguage(lng);
void i18n.changeLanguage(lng);
}}
compact
style={{ minWidth: 120 }}
>
<option value="de">{t('mobileProfile.languageDe', 'Deutsch')}</option>
<option value="en">{t('mobileProfile.languageEn', 'English')}</option>
</MobileSelect>
}
/>
</YGroup.Item>
<YGroup.Item>
<ListItem
paddingVertical="$2"
paddingHorizontal="$3"
title={
<XStack space="$2" alignItems="center">
<Text fontSize="$sm" color={theme.textStrong}>
{t('mobileProfile.theme', 'Dark Mode')}
</Text>
</XStack>
}
iconAfter={
<Switch
size="$2"
checked={isDark}
onCheckedChange={(next: boolean) => updateAppearance(next ? 'dark' : 'light')}
aria-label={t('mobileProfile.theme', 'Dark Mode')}
>
<Switch.Thumb />
</Switch>
}
/>
</YGroup.Item>
</YGroup>
{appearance === 'system' ? (
<Text fontSize="$xs" color={theme.muted}>
{t('mobileProfile.themeSystem', 'System')}
</Text>
) : null}
</YStack>
</YStack>
</YStack>
);
}