Adopt Tamagui defaults for tabs and filters
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-04 08:29:50 +01:00
parent 0535f63b40
commit eecb1a5b85
14 changed files with 650 additions and 622 deletions

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { CheckCircle2, Lock, MailWarning, User } 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 { Tabs } from 'tamagui';
import toast from 'react-hot-toast';
import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
@@ -300,6 +300,275 @@ export default function MobileProfileAccountPage() {
form.password.trim().length > 0 &&
form.passwordConfirmation.trim().length > 0;
const brandingDisabled = brandingLoading || brandingSaving;
const brandingContent = (
<>
{brandingError ? (
<MobileCard>
<Text fontWeight="700" color={danger}>
{brandingError}
</Text>
</MobileCard>
) : null}
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.title', 'Standard-Branding')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('profile.branding.hint', 'Diese Werte dienen als Vorschlag für neue Events und beschleunigen die Einrichtung.')}
</Text>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.theme', 'Theme')}
</Text>
<YStack gap="$3">
<MobileField label={t('events.branding.mode', 'Theme')}>
<MobileSelect
value={brandingForm.mode}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, mode: event.target.value as BrandingFormValues['mode'] }))}
disabled={brandingDisabled}
>
<option value="light">{t('events.branding.modeLight', 'Light')}</option>
<option value="auto">{t('events.branding.modeAuto', 'Auto')}</option>
<option value="dark">{t('events.branding.modeDark', 'Dark')}</option>
</MobileSelect>
</MobileField>
<MobileField label={t('events.branding.fontSize', 'Font Size')}>
<MobileSelect
value={brandingForm.fontSize}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, fontSize: event.target.value as BrandingFormValues['fontSize'] }))}
disabled={brandingDisabled}
>
<option value="s">{t('events.branding.fontSizeSmall', 'S')}</option>
<option value="m">{t('events.branding.fontSizeMedium', 'M')}</option>
<option value="l">{t('events.branding.fontSizeLarge', 'L')}</option>
</MobileSelect>
</MobileField>
</YStack>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.colors', 'Colors')}
</Text>
<YStack gap="$3">
<ColorField
label={t('events.branding.primary', 'Primary Color')}
value={brandingForm.primary}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, primary: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.accent', 'Accent Color')}
value={brandingForm.accent}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, accent: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.backgroundColor', 'Background Color')}
value={brandingForm.background}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, background: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.surfaceColor', 'Surface Color')}
value={brandingForm.surface}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, surface: value }))}
disabled={brandingDisabled}
/>
</YStack>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.fonts', 'Fonts')}
</Text>
<YStack gap="$3">
<MobileField label={t('events.branding.headingFont', 'Headline Font')}>
<MobileInput
value={brandingForm.headingFont}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, headingFont: event.target.value }))}
placeholder={t('events.branding.headingFontPlaceholder', 'Playfair Display')}
hasError={false}
disabled={brandingDisabled}
/>
</MobileField>
<MobileField label={t('events.branding.bodyFont', 'Body Font')}>
<MobileInput
value={brandingForm.bodyFont}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, bodyFont: event.target.value }))}
placeholder={t('events.branding.bodyFontPlaceholder', 'Montserrat')}
hasError={false}
disabled={brandingDisabled}
/>
</MobileField>
</YStack>
</MobileCard>
<CTAButton
label={brandingSaving ? t('profile.branding.saving', 'Saving...') : t('profile.branding.save', 'Save defaults')}
onPress={handleBrandingSave}
disabled={brandingDisabled}
loading={brandingSaving}
/>
</>
);
const accountContent = (
<>
{error ? (
<MobileCard>
<Text fontWeight="700" color={danger}>
{error}
</Text>
</MobileCard>
) : null}
<MobileCard gap="$3">
<XStack alignItems="center" gap="$3">
<XStack
width={48}
height={48}
borderRadius={16}
alignItems="center"
justifyContent="center"
backgroundColor={accentSoft}
>
<User size={20} color={primary} />
</XStack>
<YStack gap="$1">
<Text fontSize="$md" fontWeight="800" color={text}>
{form.name || profile?.email || t('profile.title', 'Profil')}
</Text>
<Text fontSize="$sm" color={muted}>
{form.email || profile?.email || '—'}
</Text>
</YStack>
</XStack>
<XStack alignItems="center" gap="$2" flexWrap="wrap">
{profile?.email_verified ? (
<CheckCircle2 size={14} color={subtle} />
) : (
<MailWarning size={14} color={subtle} />
)}
<PillBadge tone={profile?.email_verified ? 'success' : 'warning'}>
{emailStatusLabel}
</PillBadge>
<Text fontSize="$xs" color={muted}>
{emailHint}
</Text>
</XStack>
</MobileCard>
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<User size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.account.heading', 'Account-Informationen')}
</Text>
</XStack>
<Text fontSize="$sm" color={muted}>
{t('profile.sections.account.description', 'Passe Anzeigename, E-Mail-Adresse und Sprache der Admin-Oberfläche an.')}
</Text>
{loading ? (
<Text fontSize="$sm" color={muted}>
{t('profile.loading', 'Lädt ...')}
</Text>
) : (
<YStack gap="$3">
<MobileField label={t('profile.fields.name', 'Anzeigename')}>
<MobileInput
value={form.name}
onChange={(event) => setForm((prev) => ({ ...prev, name: event.target.value }))}
placeholder={t('profile.placeholders.name', 'z. B. Hochzeitsplanung Schmidt')}
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.email', 'E-Mail-Adresse')}>
<MobileInput
value={form.email}
onChange={(event) => setForm((prev) => ({ ...prev, email: event.target.value }))}
placeholder="mail@beispiel.de"
type="email"
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.locale', 'Bevorzugte Sprache')}>
<MobileSelect
value={form.preferredLocale}
onChange={(event) => setForm((prev) => ({ ...prev, preferredLocale: event.target.value }))}
>
{LOCALE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label ?? t(option.labelKey, option.fallback)}
</option>
))}
</MobileSelect>
</MobileField>
<CTAButton
label={t('profile.actions.save', 'Speichern')}
onPress={handleAccountSave}
disabled={savingAccount || loading}
loading={savingAccount}
/>
</YStack>
)}
</MobileCard>
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Lock size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.password.heading', 'Passwort ändern')}
</Text>
</XStack>
<Text fontSize="$sm" color={muted}>
{t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')}
</Text>
<YStack gap="$3">
<MobileField label={t('profile.fields.currentPassword', 'Aktuelles Passwort')}>
<MobileInput
value={form.currentPassword}
onChange={(event) => setForm((prev) => ({ ...prev, currentPassword: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<MobileField
label={t('profile.fields.newPassword', 'Neues Passwort')}
hint={t('profile.sections.password.hint', 'Nutze mindestens 8 Zeichen und kombiniere Buchstaben sowie Zahlen für mehr Sicherheit.')}
>
<MobileInput
value={form.password}
onChange={(event) => setForm((prev) => ({ ...prev, password: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.passwordConfirmation', 'Passwort bestätigen')}>
<MobileInput
value={form.passwordConfirmation}
onChange={(event) => setForm((prev) => ({ ...prev, passwordConfirmation: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<CTAButton
label={t('profile.actions.updatePassword', 'Passwort aktualisieren')}
onPress={handlePasswordSave}
disabled={!passwordReady || savingPassword || loading}
loading={savingPassword}
tone="ghost"
/>
</YStack>
</MobileCard>
</>
);
return (
<MobileShell
@@ -308,312 +577,38 @@ export default function MobileProfileAccountPage() {
onBack={back}
>
{brandingTabEnabled ? (
<XStack gap="$2">
<TabButton
label={t('profile.tabs.account', 'Account')}
active={activeTab === 'account'}
onPress={() => setActiveTab('account')}
/>
<TabButton
label={t('profile.tabs.branding', 'Standard-Branding')}
active={activeTab === 'branding'}
onPress={() => setActiveTab('branding')}
/>
</XStack>
) : null}
{activeTab === 'branding' && brandingTabEnabled ? (
<>
{brandingError ? (
<MobileCard>
<Text fontWeight="700" color={danger}>
{brandingError}
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as TabKey)}
orientation="horizontal"
flexDirection="column"
>
<Tabs.List gap="$2">
<Tabs.Tab value="account" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('profile.tabs.account', 'Account')}
</Text>
</MobileCard>
) : null}
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.title', 'Standard-Branding')}
</Text>
<Text fontSize="$sm" color={muted}>
{t('profile.branding.hint', 'Diese Werte dienen als Vorschlag für neue Events und beschleunigen die Einrichtung.')}
</Text>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.branding.theme', 'Theme')}
</Text>
<YStack gap="$3">
<MobileField label={t('events.branding.mode', 'Theme')}>
<MobileSelect
value={brandingForm.mode}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, mode: event.target.value as BrandingFormValues['mode'] }))}
disabled={brandingDisabled}
>
<option value="light">{t('events.branding.modeLight', 'Light')}</option>
<option value="auto">{t('events.branding.modeAuto', 'Auto')}</option>
<option value="dark">{t('events.branding.modeDark', 'Dark')}</option>
</MobileSelect>
</MobileField>
<MobileField label={t('events.branding.fontSize', 'Font Size')}>
<MobileSelect
value={brandingForm.fontSize}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, fontSize: event.target.value as BrandingFormValues['fontSize'] }))}
disabled={brandingDisabled}
>
<option value="s">{t('events.branding.fontSizeSmall', 'S')}</option>
<option value="m">{t('events.branding.fontSizeMedium', 'M')}</option>
<option value="l">{t('events.branding.fontSizeLarge', 'L')}</option>
</MobileSelect>
</MobileField>
</YStack>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.colors', 'Colors')}
</Text>
<YStack gap="$3">
<ColorField
label={t('events.branding.primary', 'Primary Color')}
value={brandingForm.primary}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, primary: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.accent', 'Accent Color')}
value={brandingForm.accent}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, accent: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.backgroundColor', 'Background Color')}
value={brandingForm.background}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, background: value }))}
disabled={brandingDisabled}
/>
<ColorField
label={t('events.branding.surfaceColor', 'Surface Color')}
value={brandingForm.surface}
onChange={(value) => setBrandingForm((prev) => ({ ...prev, surface: value }))}
disabled={brandingDisabled}
/>
</YStack>
</MobileCard>
<MobileCard gap="$3">
<Text fontSize="$md" fontWeight="800" color={text}>
{t('events.branding.fonts', 'Fonts')}
</Text>
<YStack gap="$3">
<MobileField label={t('events.branding.headingFont', 'Headline Font')}>
<MobileInput
value={brandingForm.headingFont}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, headingFont: event.target.value }))}
placeholder={t('events.branding.headingFontPlaceholder', 'Playfair Display')}
hasError={false}
disabled={brandingDisabled}
/>
</MobileField>
<MobileField label={t('events.branding.bodyFont', 'Body Font')}>
<MobileInput
value={brandingForm.bodyFont}
onChange={(event) => setBrandingForm((prev) => ({ ...prev, bodyFont: event.target.value }))}
placeholder={t('events.branding.bodyFontPlaceholder', 'Montserrat')}
hasError={false}
disabled={brandingDisabled}
/>
</MobileField>
</YStack>
</MobileCard>
<CTAButton
label={brandingSaving ? t('profile.branding.saving', 'Saving...') : t('profile.branding.save', 'Save defaults')}
onPress={handleBrandingSave}
disabled={brandingDisabled}
loading={brandingSaving}
/>
</>
</Tabs.Tab>
<Tabs.Tab value="branding" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('profile.tabs.branding', 'Standard-Branding')}
</Text>
</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="account" paddingTop="$2">
{accountContent}
</Tabs.Content>
<Tabs.Content value="branding" paddingTop="$2">
{brandingContent}
</Tabs.Content>
</Tabs>
) : (
<>
{error ? (
<MobileCard>
<Text fontWeight="700" color={danger}>
{error}
</Text>
</MobileCard>
) : null}
<MobileCard gap="$3">
<XStack alignItems="center" gap="$3">
<XStack
width={48}
height={48}
borderRadius={16}
alignItems="center"
justifyContent="center"
backgroundColor={accentSoft}
>
<User size={20} color={primary} />
</XStack>
<YStack gap="$1">
<Text fontSize="$md" fontWeight="800" color={text}>
{form.name || profile?.email || t('profile.title', 'Profil')}
</Text>
<Text fontSize="$sm" color={muted}>
{form.email || profile?.email || '—'}
</Text>
</YStack>
</XStack>
<XStack alignItems="center" gap="$2" flexWrap="wrap">
{profile?.email_verified ? (
<CheckCircle2 size={14} color={subtle} />
) : (
<MailWarning size={14} color={subtle} />
)}
<PillBadge tone={profile?.email_verified ? 'success' : 'warning'}>
{emailStatusLabel}
</PillBadge>
<Text fontSize="$xs" color={muted}>
{emailHint}
</Text>
</XStack>
</MobileCard>
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<User size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.account.heading', 'Account-Informationen')}
</Text>
</XStack>
<Text fontSize="$sm" color={muted}>
{t('profile.sections.account.description', 'Passe Anzeigename, E-Mail-Adresse und Sprache der Admin-Oberfläche an.')}
</Text>
{loading ? (
<Text fontSize="$sm" color={muted}>
{t('profile.loading', 'Lädt ...')}
</Text>
) : (
<YStack gap="$3">
<MobileField label={t('profile.fields.name', 'Anzeigename')}>
<MobileInput
value={form.name}
onChange={(event) => setForm((prev) => ({ ...prev, name: event.target.value }))}
placeholder={t('profile.placeholders.name', 'z. B. Hochzeitsplanung Schmidt')}
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.email', 'E-Mail-Adresse')}>
<MobileInput
value={form.email}
onChange={(event) => setForm((prev) => ({ ...prev, email: event.target.value }))}
placeholder="mail@beispiel.de"
type="email"
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.locale', 'Bevorzugte Sprache')}>
<MobileSelect
value={form.preferredLocale}
onChange={(event) => setForm((prev) => ({ ...prev, preferredLocale: event.target.value }))}
>
{LOCALE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label ?? t(option.labelKey, option.fallback)}
</option>
))}
</MobileSelect>
</MobileField>
<CTAButton
label={t('profile.actions.save', 'Speichern')}
onPress={handleAccountSave}
disabled={savingAccount || loading}
loading={savingAccount}
/>
</YStack>
)}
</MobileCard>
<MobileCard gap="$3">
<XStack alignItems="center" gap="$2">
<Lock size={16} color={text} />
<Text fontSize="$md" fontWeight="800" color={text}>
{t('profile.sections.password.heading', 'Passwort ändern')}
</Text>
</XStack>
<Text fontSize="$sm" color={muted}>
{t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')}
</Text>
<YStack gap="$3">
<MobileField label={t('profile.fields.currentPassword', 'Aktuelles Passwort')}>
<MobileInput
value={form.currentPassword}
onChange={(event) => setForm((prev) => ({ ...prev, currentPassword: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<MobileField
label={t('profile.fields.newPassword', 'Neues Passwort')}
hint={t('profile.sections.password.hint', 'Nutze mindestens 8 Zeichen und kombiniere Buchstaben sowie Zahlen für mehr Sicherheit.')}
>
<MobileInput
value={form.password}
onChange={(event) => setForm((prev) => ({ ...prev, password: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<MobileField label={t('profile.fields.passwordConfirmation', 'Passwort bestätigen')}>
<MobileInput
value={form.passwordConfirmation}
onChange={(event) => setForm((prev) => ({ ...prev, passwordConfirmation: event.target.value }))}
placeholder="••••••••"
type="password"
hasError={false}
/>
</MobileField>
<CTAButton
label={t('profile.actions.updatePassword', 'Passwort aktualisieren')}
onPress={handlePasswordSave}
disabled={!passwordReady || savingPassword || loading}
loading={savingPassword}
tone="ghost"
/>
</YStack>
</MobileCard>
</>
accountContent
)}
</MobileShell>
);
}
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
const { primary, surfaceMuted, border, surface, text } = useAdminTheme();
return (
<Pressable onPress={onPress} style={{ flex: 1 }}>
<XStack
alignItems="center"
justifyContent="center"
paddingVertical="$2.5"
borderRadius={12}
backgroundColor={active ? primary : surfaceMuted}
borderWidth={1}
borderColor={active ? primary : border}
>
<Text fontSize="$sm" color={active ? surface : text} fontWeight="700">
{label}
</Text>
</XStack>
</Pressable>
);
}
function ColorField({
label,
value,