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

@@ -6,7 +6,7 @@ import { Image as ImageIcon, RefreshCcw, UploadCloud, Trash2, ChevronDown, Save,
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite';
import { Slider } from 'tamagui';
import { Slider, Tabs } from 'tamagui';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
import { MobileColorInput, MobileField, MobileFileInput, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
@@ -637,15 +637,28 @@ export default function MobileBrandingPage() {
<ContextHelpLink slug="event-branding-assets" />
</XStack>
<MobileCard gap="$2">
<XStack gap="$2">
<TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} />
<TabButton label={t('events.watermark.tab', 'Wasserzeichen')} active={activeTab === 'watermark'} onPress={() => setActiveTab('watermark')} />
</XStack>
</MobileCard>
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as TabKey)}
orientation="horizontal"
flexDirection="column"
>
<MobileCard gap="$2">
<Tabs.List gap="$2">
<Tabs.Tab value="branding" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('events.branding.titleShort', 'Branding')}
</Text>
</Tabs.Tab>
<Tabs.Tab value="watermark" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('events.watermark.tab', 'Wasserzeichen')}
</Text>
</Tabs.Tab>
</Tabs.List>
</MobileCard>
{activeTab === 'branding' ? (
<>
<Tabs.Content value="branding" paddingTop="$2">
<MobileCard gap="$3">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.branding.previewTitle', 'Guest App Preview')}
@@ -1216,12 +1229,14 @@ export default function MobileBrandingPage() {
onChange={(value) => setForm((prev) => ({ ...prev, linkColor: value }))}
disabled={brandingDisabled}
/>
</MobileCard>
</MobileCard>
</>
</>
) : (
renderWatermarkTab()
)}
</Tabs.Content>
<Tabs.Content value="watermark" paddingTop="$2">
{renderWatermarkTab()}
</Tabs.Content>
</Tabs>
<YStack gap="$2">
<CTAButton label={saving ? t('events.branding.saving', 'Saving...') : t('events.branding.save', 'Save Branding')} onPress={() => handleSave()} />
@@ -1744,27 +1759,6 @@ function UpgradeCard({
);
}
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
const { primary, surfaceMuted, border, surface, textStrong } = 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 : textStrong} fontWeight="700">
{label}
</Text>
</XStack>
</Pressable>
);
}
function ModeButton({
label,
active,

View File

@@ -343,7 +343,6 @@ export default function MobileEventControlRoomPage() {
const liveResetRef = React.useRef(false);
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
const activeFilterBg = primary;
const saveControlRoomSettings = React.useCallback(
async (nextSettings: ControlRoomSettings) => {
@@ -1376,60 +1375,52 @@ export default function MobileEventControlRoomPage() {
{t('mobilePhotos.filtersTitle', 'Filter')}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<XStack
<ToggleGroup
type="single"
value={moderationFilter}
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
disableDeactivation
orientation="horizontal"
flexDirection="row"
alignItems="center"
padding="$1"
borderRadius={999}
gap="$1.5"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
overflow="hidden"
>
<ToggleGroup
type="single"
value={moderationFilter}
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
>
<XStack gap="$1.5">
{MODERATION_FILTERS.map((option) => {
const active = option.value === moderationFilter;
const count = moderationCounts[option.value] ?? 0;
return (
<ToggleGroup.Item
key={option.value}
value={option.value}
unstyled
borderRadius={999}
{MODERATION_FILTERS.map((option) => {
const count = moderationCounts[option.value] ?? 0;
return (
<ToggleGroup.Item
key={option.value}
value={option.value}
borderRadius="$pill"
paddingVertical="$1.5"
paddingHorizontal="$3"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="600">
{t(option.labelKey, option.fallback)}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius="$pill"
borderWidth={1}
borderColor="transparent"
backgroundColor={active ? activeFilterBg : 'transparent'}
paddingVertical="$1.5"
paddingHorizontal="$3"
pressStyle={{ opacity: 0.85 }}
borderColor={border}
backgroundColor="$backgroundStrong"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
{t(option.labelKey, option.fallback)}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius={999}
borderWidth={1}
borderColor={active ? 'transparent' : border}
backgroundColor={active ? withAlpha('#ffffff', 0.2) : surface}
>
<Text fontSize={10} fontWeight="800" color={active ? '#fff' : muted}>
{count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
);
})}
</XStack>
</ToggleGroup>
</XStack>
<Text fontSize={10} fontWeight="700">
{count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
);
})}
</ToggleGroup>
</ScrollView>
</YStack>
</MobileCard>
@@ -1564,60 +1555,52 @@ export default function MobileEventControlRoomPage() {
{t('liveShowQueue.filterLabel', 'Live status')}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<XStack
<ToggleGroup
type="single"
value={liveStatusFilter}
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
disableDeactivation
orientation="horizontal"
flexDirection="row"
alignItems="center"
padding="$1"
borderRadius={999}
gap="$1.5"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
overflow="hidden"
>
<ToggleGroup
type="single"
value={liveStatusFilter}
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
>
<XStack gap="$1.5">
{LIVE_STATUS_OPTIONS.map((option) => {
const active = option.value === liveStatusFilter;
const count = liveCounts[option.value] ?? 0;
return (
<ToggleGroup.Item
key={option.value}
value={option.value}
unstyled
borderRadius={999}
{LIVE_STATUS_OPTIONS.map((option) => {
const count = liveCounts[option.value] ?? 0;
return (
<ToggleGroup.Item
key={option.value}
value={option.value}
borderRadius="$pill"
paddingVertical="$1.5"
paddingHorizontal="$3"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="600">
{t(option.labelKey, option.fallback)}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius="$pill"
borderWidth={1}
borderColor="transparent"
backgroundColor={active ? activeFilterBg : 'transparent'}
paddingVertical="$1.5"
paddingHorizontal="$3"
pressStyle={{ opacity: 0.85 }}
borderColor={border}
backgroundColor="$backgroundStrong"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
{t(option.labelKey, option.fallback)}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius={999}
borderWidth={1}
borderColor={active ? 'transparent' : border}
backgroundColor={active ? withAlpha('#ffffff', 0.2) : surface}
>
<Text fontSize={10} fontWeight="800" color={active ? '#fff' : muted}>
{count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
);
})}
</XStack>
</ToggleGroup>
</XStack>
<Text fontSize={10} fontWeight="700">
{count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
);
})}
</ToggleGroup>
</ScrollView>
</YStack>
</MobileCard>

View File

@@ -5,6 +5,7 @@ import { Trash2, Copy, RefreshCcw } 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 { ToggleGroup } from '@tamagui/toggle-group';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect } from './components/FormControls';
@@ -227,51 +228,67 @@ export default function MobileEventMembersPage() {
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t('events.members.filters.statusLabel', 'Status')}
</Text>
<XStack gap="$2" flexWrap="wrap">
{statusOptions.map((option) => {
const isActive = statusFilter === option.key;
return (
<Pressable key={option.key} onPress={() => setStatusFilter(option.key)}>
<XStack
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={isActive ? primary : border}
backgroundColor={isActive ? primary : 'transparent'}
>
<Text fontSize="$xs" fontWeight="700" color={isActive ? 'white' : text}>
{option.label}
</Text>
</XStack>
</Pressable>
);
})}
</XStack>
<ToggleGroup
type="single"
value={statusFilter}
onValueChange={(value: string) => value && setStatusFilter(value as typeof statusFilter)}
disableDeactivation
orientation="horizontal"
flexDirection="row"
flexWrap="wrap"
gap="$2"
padding="$1"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor="$background"
>
{statusOptions.map((option) => (
<ToggleGroup.Item
key={option.key}
value={option.key}
borderRadius="$pill"
paddingHorizontal="$3"
paddingVertical="$1.5"
>
<Text fontSize="$xs" fontWeight="600">
{option.label}
</Text>
</ToggleGroup.Item>
))}
</ToggleGroup>
<Text fontSize="$xs" fontWeight="700" color={muted}>
{t('events.members.filters.roleLabel', 'Role')}
</Text>
<XStack gap="$2" flexWrap="wrap">
{roleOptions.map((option) => {
const isActive = roleFilter === option.key;
return (
<Pressable key={option.key} onPress={() => setRoleFilter(option.key)}>
<XStack
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={isActive ? primary : border}
backgroundColor={isActive ? primary : 'transparent'}
>
<Text fontSize="$xs" fontWeight="700" color={isActive ? 'white' : text}>
{option.label}
</Text>
</XStack>
</Pressable>
);
})}
</XStack>
<ToggleGroup
type="single"
value={roleFilter}
onValueChange={(value: string) => value && setRoleFilter(value as typeof roleFilter)}
disableDeactivation
orientation="horizontal"
flexDirection="row"
flexWrap="wrap"
gap="$2"
padding="$1"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor="$background"
>
{roleOptions.map((option) => (
<ToggleGroup.Item
key={option.key}
value={option.key}
borderRadius="$pill"
paddingHorizontal="$3"
paddingVertical="$1.5"
>
<Text fontSize="$xs" fontWeight="600">
{option.label}
</Text>
</ToggleGroup.Item>
))}
</ToggleGroup>
</YStack>
) : null}

View File

@@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
import { Share2, Sparkles, Trophy, Users, TrendingUp, Heart, Image as ImageIcon } 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 { Switch } from '@tamagui/switch';
import { Tabs } from 'tamagui';
import { MobileShell } from './components/MobileShell';
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
import { LegalConsentSheet } from './components/LegalConsentSheet';
@@ -208,25 +208,31 @@ export default function MobileEventRecapPage() {
onBack={back}
>
<YStack gap="$4">
<XStack gap="$2">
<TabButton
label={t('events.recap.tabs.overview', 'Overview')}
active={activeTab === 'overview'}
onPress={() => setActiveTab('overview')}
/>
<TabButton
label={t('events.recap.tabs.engagement', 'Engagement')}
active={activeTab === 'engagement'}
onPress={() => setActiveTab('engagement')}
/>
<TabButton
label={t('events.recap.tabs.compliance', 'Compliance')}
active={activeTab === 'compliance'}
onPress={() => setActiveTab('compliance')}
/>
</XStack>
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as typeof activeTab)}
orientation="horizontal"
flexDirection="column"
>
<Tabs.List gap="$2">
<Tabs.Tab value="overview" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('events.recap.tabs.overview', 'Overview')}
</Text>
</Tabs.Tab>
<Tabs.Tab value="engagement" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('events.recap.tabs.engagement', 'Engagement')}
</Text>
</Tabs.Tab>
<Tabs.Tab value="compliance" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
<Text fontSize="$sm" fontWeight="600" textAlign="center">
{t('events.recap.tabs.compliance', 'Compliance')}
</Text>
</Tabs.Tab>
</Tabs.List>
{activeTab === 'overview' ? (
<Tabs.Content value="overview" paddingTop="$2">
<YStack gap="$4">
<MobileCard gap="$3">
<XStack alignItems="center" justifyContent="space-between">
@@ -362,9 +368,9 @@ export default function MobileEventRecapPage() {
</YStack>
</MobileCard>
</YStack>
) : null}
</Tabs.Content>
{activeTab === 'engagement' ? (
<Tabs.Content value="engagement" paddingTop="$2">
<YStack gap="$4">
{engagementLoading ? (
<YStack gap="$2">
@@ -550,13 +556,14 @@ export default function MobileEventRecapPage() {
</YStack>
)}
</YStack>
) : null}
</Tabs.Content>
{activeTab === 'compliance' ? (
<YStack gap="$4">
<DataExportsPanel variant="recap" event={event} />
</YStack>
) : null}
<Tabs.Content value="compliance" paddingTop="$2">
<YStack gap="$4">
<DataExportsPanel variant="recap" event={event} />
</YStack>
</Tabs.Content>
</Tabs>
</YStack>
<LegalConsentSheet
@@ -609,27 +616,6 @@ function ToggleOption({ label, value, onToggle }: { label: string; value: boolea
);
}
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
const { primary, surfaceMuted, border, surface, textStrong } = 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 : textStrong} fontWeight="700">
{label}
</Text>
</XStack>
</Pressable>
);
}
function LeaderboardRow({ rank, name, value }: { rank: number; name: string; value: string }) {
const { textStrong, muted, border, surfaceMuted } = useAdminTheme();
return (

View File

@@ -1170,7 +1170,6 @@ export default function MobileEventTasksPage() {
borderRadius={16}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
overflow="hidden"
gap="$0"
>
@@ -1186,16 +1185,13 @@ export default function MobileEventTasksPage() {
key={tab.value}
value={tab.value}
flex={1}
unstyled
paddingVertical="$2.5"
alignItems="center"
justifyContent="center"
backgroundColor={isActive ? primary : 'transparent'}
borderRightWidth={index === arr.length - 1 ? 0 : 1}
borderRightColor={border}
pressStyle={{ backgroundColor: isActive ? primary : surface }}
>
<Text fontSize="$sm" fontWeight={isActive ? '700' : '500'} color={isActive ? 'white' : text}>
<Text fontSize="$sm" fontWeight={isActive ? '700' : '600'}>
{tab.label}
</Text>
</Tabs.Tab>

View File

@@ -235,9 +235,7 @@ function EventsList({
onEdit?: (slug: string) => void;
}) {
const { t } = useTranslation('management');
const { text, muted, subtle, border, primary, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme();
const activeBg = accentSoft;
const activeBorder = accent;
const { text, muted, subtle, border, primary, surface, surfaceMuted, shadow } = useAdminTheme();
const statusCounts = React.useMemo(() => buildEventStatusCounts(events), [events]);
const filteredByStatus = React.useMemo(
@@ -313,56 +311,49 @@ function EventsList({
</Text>
</XStack>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<XStack
<ToggleGroup
type="single"
value={statusFilter}
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
disableDeactivation
orientation="horizontal"
flexDirection="row"
alignItems="center"
padding="$1"
borderRadius={999}
gap="$1.5"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<ToggleGroup
type="single"
value={statusFilter}
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
>
<XStack gap="$1.5">
{filters.map((filter) => {
const active = filter.key === statusFilter;
return (
<ToggleGroup.Item
key={filter.key}
value={filter.key}
borderRadius={999}
borderWidth={1}
borderColor={active ? activeBorder : 'transparent'}
backgroundColor={active ? activeBg : 'transparent'}
paddingVertical="$1.5"
paddingHorizontal="$3"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
{filter.label}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius={999}
borderWidth={1}
borderColor={active ? activeBorder : border}
backgroundColor={surface}
>
<Text fontSize={10} fontWeight="800" color={active ? primary : muted}>
{filter.count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
);
})}
</XStack>
</ToggleGroup>
</XStack>
{filters.map((filter) => (
<ToggleGroup.Item
key={filter.key}
value={filter.key}
borderRadius="$pill"
paddingVertical="$1.5"
paddingHorizontal="$3"
>
<XStack alignItems="center" gap="$1.5">
<Text fontSize="$xs" fontWeight="600">
{filter.label}
</Text>
<XStack
paddingHorizontal="$1.5"
paddingVertical="$0.5"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor="$backgroundStrong"
>
<Text fontSize={10} fontWeight="700">
{filter.count}
</Text>
</XStack>
</XStack>
</ToggleGroup.Item>
))}
</ToggleGroup>
</ScrollView>
</YStack>

View File

@@ -5,6 +5,7 @@ import { Bell, Check, ChevronRight, RefreshCcw } 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 { ToggleGroup } from '@tamagui/toggle-group';
import { motion, useAnimationControls, type PanInfo } from 'framer-motion';
import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, PillBadge, SkeletonCard, CTAButton } from './components/Primitives';
@@ -326,7 +327,7 @@ export default function MobileNotificationsPage() {
const [events, setEvents] = React.useState<TenantEvent[]>([]);
const [showEventPicker, setShowEventPicker] = React.useState(false);
const back = useBackNavigation(adminPath('/mobile/dashboard'));
const { text, muted, border, warningBg, warningText, infoBg, primary, danger, accentSoft, subtle } = useAdminTheme();
const { text, muted, border, warningBg, warningText, infoBg, primary, danger, subtle } = useAdminTheme();
const warningIcon = warningText;
const infoIcon = primary;
const errorText = danger;
@@ -552,7 +553,22 @@ export default function MobileNotificationsPage() {
) : null}
</XStack>
<XStack gap="$2" flexWrap="wrap" marginBottom="$2">
<ToggleGroup
type="single"
value={scopeParam ?? 'all'}
onValueChange={(value: string) => value && updateFilters({ scope: value as NotificationScope | 'all' })}
disableDeactivation
orientation="horizontal"
flexDirection="row"
flexWrap="wrap"
gap="$2"
padding="$1"
borderRadius="$pill"
borderWidth={1}
borderColor={border}
backgroundColor="$background"
marginBottom="$2"
>
{([
{ key: 'all', label: t('notificationLogs.scope.all', 'All scopes') },
{ key: 'photos', label: t('notificationLogs.scope.photos', 'Photos') },
@@ -561,28 +577,22 @@ export default function MobileNotificationsPage() {
{ key: 'events', label: t('notificationLogs.scope.events', 'Events') },
{ key: 'package', label: t('notificationLogs.scope.package', 'Package') },
{ key: 'general', label: t('notificationLogs.scope.general', 'General') },
] as Array<{ key: NotificationScope | 'all'; label: string }>).map((filter) => {
const active = scopeParam === filter.key;
return (
<Pressable key={filter.key} onPress={() => updateFilters({ scope: filter.key })} style={{ flexGrow: 1 }}>
<XStack
alignItems="center"
justifyContent="center"
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14}
borderWidth={1}
borderColor={active ? primary : border}
backgroundColor={active ? accentSoft : 'transparent'}
>
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
{filter.label}
</Text>
</XStack>
</Pressable>
);
})}
</XStack>
] as Array<{ key: NotificationScope | 'all'; label: string }>).map((filter) => (
<ToggleGroup.Item
key={filter.key}
value={filter.key}
borderRadius="$pill"
paddingVertical="$2"
paddingHorizontal="$3"
flexGrow={1}
justifyContent="center"
>
<Text fontSize="$xs" fontWeight="600" textAlign="center">
{filter.label}
</Text>
</ToggleGroup.Item>
))}
</ToggleGroup>
{loading ? (
<YStack gap="$2">

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,

View File

@@ -50,6 +50,7 @@ vi.mock('../components/FormControls', () => ({
MobileColorInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
MobileFileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
MobileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
MobileTextArea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
MobileSelect: ({ children, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) => (
<select {...props}>{children}</select>
),
@@ -111,16 +112,53 @@ vi.mock('@tamagui/react-native-web-lite', () => ({
),
}));
vi.mock('tamagui', () => ({
Slider: Object.assign(
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
{
Track: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
TrackActive: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
Thumb: () => <div />,
}
),
}));
vi.mock('tamagui', () => {
const TabsValueContext = React.createContext<string | null>(null);
const TabsSetterContext = React.createContext<((value: string) => void) | null>(null);
const TabsRoot = ({
children,
value,
onValueChange,
}: {
children: React.ReactNode;
value?: string;
onValueChange?: (value: string) => void;
}) => (
<TabsValueContext.Provider value={value ?? null}>
<TabsSetterContext.Provider value={onValueChange ?? null}>{children}</TabsSetterContext.Provider>
</TabsValueContext.Provider>
);
const Tabs = Object.assign(TabsRoot, {
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Tab: ({ children, value }: { children: React.ReactNode; value: string }) => {
const setValue = React.useContext(TabsSetterContext);
return (
<button type="button" onClick={() => setValue?.(value)}>
{children}
</button>
);
},
Content: ({ children, value }: { children: React.ReactNode; value: string }) => {
const active = React.useContext(TabsValueContext);
if (active !== value) {
return null;
}
return <div>{children}</div>;
},
});
return {
Slider: Object.assign(
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
{
Track: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
TrackActive: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
Thumb: () => <div />,
}
),
Tabs,
};
});
const getEventMock = vi.fn();
const updateEventMock = vi.fn();

View File

@@ -113,6 +113,14 @@ vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('tamagui', () => ({
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Tab: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>

View File

@@ -111,7 +111,11 @@ vi.mock('@tamagui/scroll-view', () => ({
vi.mock('tamagui', () => ({
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Tab: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
Tab: ({ children }: { children: React.ReactNode }) => (
<button type="button" role="tab">
{children}
</button>
),
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
@@ -278,10 +282,10 @@ describe('MobileEventTasksPage', () => {
render(<MobileEventTasksPage />);
expect(await screen.findByText('Photo tasks for guests')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Tasks' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Task Library' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Emotions' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Collections' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Tasks' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Task Library' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Emotions' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Collections' })).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search photo tasks')).toBeInTheDocument();
expect(screen.getByText('Emotion')).toBeInTheDocument();

View File

@@ -82,6 +82,12 @@ vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/toggle-group', () => ({
ToggleGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
}),
}));
vi.mock('../components/MobileShell', () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,

View File

@@ -63,6 +63,14 @@ vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('tamagui', () => ({
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
List: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Tab: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, ...props }: { children: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button type="button" {...props}>

View File

@@ -434,8 +434,6 @@ export function ContentTabs({
tabs: { value: string; label: string; content: React.ReactNode }[];
header?: React.ReactNode;
}) {
const { border, muted, primary } = useAdminTheme();
return (
<Tabs
defaultValue={value}
@@ -445,27 +443,21 @@ export function ContentTabs({
flexDirection="column"
borderRadius="$4"
borderWidth={1}
borderColor={border}
borderColor="$borderColor"
overflow="hidden"
>
<Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom" backgroundColor="$surface">
<Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom">
{tabs.map((tab) => (
<Tabs.Tab
key={tab.value}
value={tab.value}
flex={1}
unstyled
paddingVertical="$3"
paddingVertical="$2.5"
paddingHorizontal="$3"
alignItems="center"
justifyContent="center"
backgroundColor={value === tab.value ? primary : 'transparent'}
hoverStyle={{ backgroundColor: value === tab.value ? primary : '$backgroundHover' }}
>
<Text
fontSize="$sm"
fontWeight={value === tab.value ? '700' : '500'}
color={value === tab.value ? '#fff' : muted}
>
<Text fontSize="$sm" fontWeight="600">
{tab.label}
</Text>
</Tabs.Tab>