267 lines
8.9 KiB
TypeScript
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 };
|
|
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) => 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>
|
|
);
|
|
}
|