Adopt Tamagui defaults for tabs and filters
This commit is contained in:
@@ -6,7 +6,7 @@ import { Image as ImageIcon, RefreshCcw, UploadCloud, Trash2, ChevronDown, Save,
|
|||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { Slider } from 'tamagui';
|
import { Slider, Tabs } from 'tamagui';
|
||||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
|
import { MobileCard, CTAButton, SkeletonCard } from './components/Primitives';
|
||||||
import { MobileColorInput, MobileField, MobileFileInput, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
|
import { MobileColorInput, MobileField, MobileFileInput, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
|
||||||
@@ -637,15 +637,28 @@ export default function MobileBrandingPage() {
|
|||||||
<ContextHelpLink slug="event-branding-assets" />
|
<ContextHelpLink slug="event-branding-assets" />
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|
||||||
<MobileCard gap="$2">
|
<Tabs
|
||||||
<XStack gap="$2">
|
value={activeTab}
|
||||||
<TabButton label={t('events.branding.titleShort', 'Branding')} active={activeTab === 'branding'} onPress={() => setActiveTab('branding')} />
|
onValueChange={(value) => setActiveTab(value as TabKey)}
|
||||||
<TabButton label={t('events.watermark.tab', 'Wasserzeichen')} active={activeTab === 'watermark'} onPress={() => setActiveTab('watermark')} />
|
orientation="horizontal"
|
||||||
</XStack>
|
flexDirection="column"
|
||||||
</MobileCard>
|
>
|
||||||
|
<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">
|
<MobileCard gap="$3">
|
||||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||||
{t('events.branding.previewTitle', 'Guest App Preview')}
|
{t('events.branding.previewTitle', 'Guest App Preview')}
|
||||||
@@ -1216,12 +1229,14 @@ export default function MobileBrandingPage() {
|
|||||||
onChange={(value) => setForm((prev) => ({ ...prev, linkColor: value }))}
|
onChange={(value) => setForm((prev) => ({ ...prev, linkColor: value }))}
|
||||||
disabled={brandingDisabled}
|
disabled={brandingDisabled}
|
||||||
/>
|
/>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
</>
|
</>
|
||||||
</>
|
</Tabs.Content>
|
||||||
) : (
|
|
||||||
renderWatermarkTab()
|
<Tabs.Content value="watermark" paddingTop="$2">
|
||||||
)}
|
{renderWatermarkTab()}
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
<CTAButton label={saving ? t('events.branding.saving', 'Saving...') : t('events.branding.save', 'Save Branding')} onPress={() => handleSave()} />
|
<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({
|
function ModeButton({
|
||||||
label,
|
label,
|
||||||
active,
|
active,
|
||||||
|
|||||||
@@ -343,7 +343,6 @@ export default function MobileEventControlRoomPage() {
|
|||||||
const liveResetRef = React.useRef(false);
|
const liveResetRef = React.useRef(false);
|
||||||
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
const [fallbackAttempted, setFallbackAttempted] = React.useState(false);
|
||||||
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
||||||
const activeFilterBg = primary;
|
|
||||||
|
|
||||||
const saveControlRoomSettings = React.useCallback(
|
const saveControlRoomSettings = React.useCallback(
|
||||||
async (nextSettings: ControlRoomSettings) => {
|
async (nextSettings: ControlRoomSettings) => {
|
||||||
@@ -1376,60 +1375,52 @@ export default function MobileEventControlRoomPage() {
|
|||||||
{t('mobilePhotos.filtersTitle', 'Filter')}
|
{t('mobilePhotos.filtersTitle', 'Filter')}
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<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"
|
alignItems="center"
|
||||||
padding="$1"
|
padding="$1"
|
||||||
borderRadius={999}
|
gap="$1.5"
|
||||||
|
borderRadius="$pill"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surfaceMuted}
|
backgroundColor={surfaceMuted}
|
||||||
overflow="hidden"
|
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
{MODERATION_FILTERS.map((option) => {
|
||||||
type="single"
|
const count = moderationCounts[option.value] ?? 0;
|
||||||
value={moderationFilter}
|
return (
|
||||||
onValueChange={(value: string) => value && setModerationFilter(value as ModerationFilter)}
|
<ToggleGroup.Item
|
||||||
>
|
key={option.value}
|
||||||
<XStack gap="$1.5">
|
value={option.value}
|
||||||
{MODERATION_FILTERS.map((option) => {
|
borderRadius="$pill"
|
||||||
const active = option.value === moderationFilter;
|
paddingVertical="$1.5"
|
||||||
const count = moderationCounts[option.value] ?? 0;
|
paddingHorizontal="$3"
|
||||||
return (
|
>
|
||||||
<ToggleGroup.Item
|
<XStack alignItems="center" gap="$1.5">
|
||||||
key={option.value}
|
<Text fontSize="$xs" fontWeight="600">
|
||||||
value={option.value}
|
{t(option.labelKey, option.fallback)}
|
||||||
unstyled
|
</Text>
|
||||||
borderRadius={999}
|
<XStack
|
||||||
|
paddingHorizontal="$1.5"
|
||||||
|
paddingVertical="$0.5"
|
||||||
|
borderRadius="$pill"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor="transparent"
|
borderColor={border}
|
||||||
backgroundColor={active ? activeFilterBg : 'transparent'}
|
backgroundColor="$backgroundStrong"
|
||||||
paddingVertical="$1.5"
|
|
||||||
paddingHorizontal="$3"
|
|
||||||
pressStyle={{ opacity: 0.85 }}
|
|
||||||
>
|
>
|
||||||
<XStack alignItems="center" gap="$1.5">
|
<Text fontSize={10} fontWeight="700">
|
||||||
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
|
{count}
|
||||||
{t(option.labelKey, option.fallback)}
|
</Text>
|
||||||
</Text>
|
</XStack>
|
||||||
<XStack
|
</XStack>
|
||||||
paddingHorizontal="$1.5"
|
</ToggleGroup.Item>
|
||||||
paddingVertical="$0.5"
|
);
|
||||||
borderRadius={999}
|
})}
|
||||||
borderWidth={1}
|
</ToggleGroup>
|
||||||
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>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</YStack>
|
</YStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
@@ -1564,60 +1555,52 @@ export default function MobileEventControlRoomPage() {
|
|||||||
{t('liveShowQueue.filterLabel', 'Live status')}
|
{t('liveShowQueue.filterLabel', 'Live status')}
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<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"
|
alignItems="center"
|
||||||
padding="$1"
|
padding="$1"
|
||||||
borderRadius={999}
|
gap="$1.5"
|
||||||
|
borderRadius="$pill"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surfaceMuted}
|
backgroundColor={surfaceMuted}
|
||||||
overflow="hidden"
|
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
{LIVE_STATUS_OPTIONS.map((option) => {
|
||||||
type="single"
|
const count = liveCounts[option.value] ?? 0;
|
||||||
value={liveStatusFilter}
|
return (
|
||||||
onValueChange={(value: string) => value && setLiveStatusFilter(value as LiveShowQueueStatus)}
|
<ToggleGroup.Item
|
||||||
>
|
key={option.value}
|
||||||
<XStack gap="$1.5">
|
value={option.value}
|
||||||
{LIVE_STATUS_OPTIONS.map((option) => {
|
borderRadius="$pill"
|
||||||
const active = option.value === liveStatusFilter;
|
paddingVertical="$1.5"
|
||||||
const count = liveCounts[option.value] ?? 0;
|
paddingHorizontal="$3"
|
||||||
return (
|
>
|
||||||
<ToggleGroup.Item
|
<XStack alignItems="center" gap="$1.5">
|
||||||
key={option.value}
|
<Text fontSize="$xs" fontWeight="600">
|
||||||
value={option.value}
|
{t(option.labelKey, option.fallback)}
|
||||||
unstyled
|
</Text>
|
||||||
borderRadius={999}
|
<XStack
|
||||||
|
paddingHorizontal="$1.5"
|
||||||
|
paddingVertical="$0.5"
|
||||||
|
borderRadius="$pill"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor="transparent"
|
borderColor={border}
|
||||||
backgroundColor={active ? activeFilterBg : 'transparent'}
|
backgroundColor="$backgroundStrong"
|
||||||
paddingVertical="$1.5"
|
|
||||||
paddingHorizontal="$3"
|
|
||||||
pressStyle={{ opacity: 0.85 }}
|
|
||||||
>
|
>
|
||||||
<XStack alignItems="center" gap="$1.5">
|
<Text fontSize={10} fontWeight="700">
|
||||||
<Text fontSize="$xs" fontWeight="700" color={active ? '#fff' : muted}>
|
{count}
|
||||||
{t(option.labelKey, option.fallback)}
|
</Text>
|
||||||
</Text>
|
</XStack>
|
||||||
<XStack
|
</XStack>
|
||||||
paddingHorizontal="$1.5"
|
</ToggleGroup.Item>
|
||||||
paddingVertical="$0.5"
|
);
|
||||||
borderRadius={999}
|
})}
|
||||||
borderWidth={1}
|
</ToggleGroup>
|
||||||
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>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</YStack>
|
</YStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Trash2, Copy, RefreshCcw } from 'lucide-react';
|
|||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
|
import { ToggleGroup } from '@tamagui/toggle-group';
|
||||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
|
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
|
||||||
import { MobileField, MobileInput, MobileSelect } from './components/FormControls';
|
import { MobileField, MobileInput, MobileSelect } from './components/FormControls';
|
||||||
@@ -227,51 +228,67 @@ export default function MobileEventMembersPage() {
|
|||||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||||
{t('events.members.filters.statusLabel', 'Status')}
|
{t('events.members.filters.statusLabel', 'Status')}
|
||||||
</Text>
|
</Text>
|
||||||
<XStack gap="$2" flexWrap="wrap">
|
<ToggleGroup
|
||||||
{statusOptions.map((option) => {
|
type="single"
|
||||||
const isActive = statusFilter === option.key;
|
value={statusFilter}
|
||||||
return (
|
onValueChange={(value: string) => value && setStatusFilter(value as typeof statusFilter)}
|
||||||
<Pressable key={option.key} onPress={() => setStatusFilter(option.key)}>
|
disableDeactivation
|
||||||
<XStack
|
orientation="horizontal"
|
||||||
paddingHorizontal="$3"
|
flexDirection="row"
|
||||||
paddingVertical="$1.5"
|
flexWrap="wrap"
|
||||||
borderRadius={999}
|
gap="$2"
|
||||||
borderWidth={1}
|
padding="$1"
|
||||||
borderColor={isActive ? primary : border}
|
borderRadius="$pill"
|
||||||
backgroundColor={isActive ? primary : 'transparent'}
|
borderWidth={1}
|
||||||
>
|
borderColor={border}
|
||||||
<Text fontSize="$xs" fontWeight="700" color={isActive ? 'white' : text}>
|
backgroundColor="$background"
|
||||||
{option.label}
|
>
|
||||||
</Text>
|
{statusOptions.map((option) => (
|
||||||
</XStack>
|
<ToggleGroup.Item
|
||||||
</Pressable>
|
key={option.key}
|
||||||
);
|
value={option.key}
|
||||||
})}
|
borderRadius="$pill"
|
||||||
</XStack>
|
paddingHorizontal="$3"
|
||||||
|
paddingVertical="$1.5"
|
||||||
|
>
|
||||||
|
<Text fontSize="$xs" fontWeight="600">
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
))}
|
||||||
|
</ToggleGroup>
|
||||||
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
<Text fontSize="$xs" fontWeight="700" color={muted}>
|
||||||
{t('events.members.filters.roleLabel', 'Role')}
|
{t('events.members.filters.roleLabel', 'Role')}
|
||||||
</Text>
|
</Text>
|
||||||
<XStack gap="$2" flexWrap="wrap">
|
<ToggleGroup
|
||||||
{roleOptions.map((option) => {
|
type="single"
|
||||||
const isActive = roleFilter === option.key;
|
value={roleFilter}
|
||||||
return (
|
onValueChange={(value: string) => value && setRoleFilter(value as typeof roleFilter)}
|
||||||
<Pressable key={option.key} onPress={() => setRoleFilter(option.key)}>
|
disableDeactivation
|
||||||
<XStack
|
orientation="horizontal"
|
||||||
paddingHorizontal="$3"
|
flexDirection="row"
|
||||||
paddingVertical="$1.5"
|
flexWrap="wrap"
|
||||||
borderRadius={999}
|
gap="$2"
|
||||||
borderWidth={1}
|
padding="$1"
|
||||||
borderColor={isActive ? primary : border}
|
borderRadius="$pill"
|
||||||
backgroundColor={isActive ? primary : 'transparent'}
|
borderWidth={1}
|
||||||
>
|
borderColor={border}
|
||||||
<Text fontSize="$xs" fontWeight="700" color={isActive ? 'white' : text}>
|
backgroundColor="$background"
|
||||||
{option.label}
|
>
|
||||||
</Text>
|
{roleOptions.map((option) => (
|
||||||
</XStack>
|
<ToggleGroup.Item
|
||||||
</Pressable>
|
key={option.key}
|
||||||
);
|
value={option.key}
|
||||||
})}
|
borderRadius="$pill"
|
||||||
</XStack>
|
paddingHorizontal="$3"
|
||||||
|
paddingVertical="$1.5"
|
||||||
|
>
|
||||||
|
<Text fontSize="$xs" fontWeight="600">
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
))}
|
||||||
|
</ToggleGroup>
|
||||||
</YStack>
|
</YStack>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Share2, Sparkles, Trophy, Users, TrendingUp, Heart, Image as ImageIcon } from 'lucide-react';
|
import { Share2, Sparkles, Trophy, Users, TrendingUp, Heart, Image as ImageIcon } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
|
||||||
import { Switch } from '@tamagui/switch';
|
import { Switch } from '@tamagui/switch';
|
||||||
|
import { Tabs } from 'tamagui';
|
||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
|
import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives';
|
||||||
import { LegalConsentSheet } from './components/LegalConsentSheet';
|
import { LegalConsentSheet } from './components/LegalConsentSheet';
|
||||||
@@ -208,25 +208,31 @@ export default function MobileEventRecapPage() {
|
|||||||
onBack={back}
|
onBack={back}
|
||||||
>
|
>
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<XStack gap="$2">
|
<Tabs
|
||||||
<TabButton
|
value={activeTab}
|
||||||
label={t('events.recap.tabs.overview', 'Overview')}
|
onValueChange={(value) => setActiveTab(value as typeof activeTab)}
|
||||||
active={activeTab === 'overview'}
|
orientation="horizontal"
|
||||||
onPress={() => setActiveTab('overview')}
|
flexDirection="column"
|
||||||
/>
|
>
|
||||||
<TabButton
|
<Tabs.List gap="$2">
|
||||||
label={t('events.recap.tabs.engagement', 'Engagement')}
|
<Tabs.Tab value="overview" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
|
||||||
active={activeTab === 'engagement'}
|
<Text fontSize="$sm" fontWeight="600" textAlign="center">
|
||||||
onPress={() => setActiveTab('engagement')}
|
{t('events.recap.tabs.overview', 'Overview')}
|
||||||
/>
|
</Text>
|
||||||
<TabButton
|
</Tabs.Tab>
|
||||||
label={t('events.recap.tabs.compliance', 'Compliance')}
|
<Tabs.Tab value="engagement" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
|
||||||
active={activeTab === 'compliance'}
|
<Text fontSize="$sm" fontWeight="600" textAlign="center">
|
||||||
onPress={() => setActiveTab('compliance')}
|
{t('events.recap.tabs.engagement', 'Engagement')}
|
||||||
/>
|
</Text>
|
||||||
</XStack>
|
</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">
|
<YStack gap="$4">
|
||||||
<MobileCard gap="$3">
|
<MobileCard gap="$3">
|
||||||
<XStack alignItems="center" justifyContent="space-between">
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
@@ -362,9 +368,9 @@ export default function MobileEventRecapPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
</YStack>
|
</YStack>
|
||||||
) : null}
|
</Tabs.Content>
|
||||||
|
|
||||||
{activeTab === 'engagement' ? (
|
<Tabs.Content value="engagement" paddingTop="$2">
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
{engagementLoading ? (
|
{engagementLoading ? (
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
@@ -550,13 +556,14 @@ export default function MobileEventRecapPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
) : null}
|
</Tabs.Content>
|
||||||
|
|
||||||
{activeTab === 'compliance' ? (
|
<Tabs.Content value="compliance" paddingTop="$2">
|
||||||
<YStack gap="$4">
|
<YStack gap="$4">
|
||||||
<DataExportsPanel variant="recap" event={event} />
|
<DataExportsPanel variant="recap" event={event} />
|
||||||
</YStack>
|
</YStack>
|
||||||
) : null}
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
<LegalConsentSheet
|
<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 }) {
|
function LeaderboardRow({ rank, name, value }: { rank: number; name: string; value: string }) {
|
||||||
const { textStrong, muted, border, surfaceMuted } = useAdminTheme();
|
const { textStrong, muted, border, surfaceMuted } = useAdminTheme();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1170,7 +1170,6 @@ export default function MobileEventTasksPage() {
|
|||||||
borderRadius={16}
|
borderRadius={16}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surfaceMuted}
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
gap="$0"
|
gap="$0"
|
||||||
>
|
>
|
||||||
@@ -1186,16 +1185,13 @@ export default function MobileEventTasksPage() {
|
|||||||
key={tab.value}
|
key={tab.value}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
flex={1}
|
flex={1}
|
||||||
unstyled
|
|
||||||
paddingVertical="$2.5"
|
paddingVertical="$2.5"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor={isActive ? primary : 'transparent'}
|
|
||||||
borderRightWidth={index === arr.length - 1 ? 0 : 1}
|
borderRightWidth={index === arr.length - 1 ? 0 : 1}
|
||||||
borderRightColor={border}
|
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}
|
{tab.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|||||||
@@ -235,9 +235,7 @@ function EventsList({
|
|||||||
onEdit?: (slug: string) => void;
|
onEdit?: (slug: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const { text, muted, subtle, border, primary, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme();
|
const { text, muted, subtle, border, primary, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||||
const activeBg = accentSoft;
|
|
||||||
const activeBorder = accent;
|
|
||||||
|
|
||||||
const statusCounts = React.useMemo(() => buildEventStatusCounts(events), [events]);
|
const statusCounts = React.useMemo(() => buildEventStatusCounts(events), [events]);
|
||||||
const filteredByStatus = React.useMemo(
|
const filteredByStatus = React.useMemo(
|
||||||
@@ -313,56 +311,49 @@ function EventsList({
|
|||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<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"
|
alignItems="center"
|
||||||
padding="$1"
|
padding="$1"
|
||||||
borderRadius={999}
|
gap="$1.5"
|
||||||
|
borderRadius="$pill"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor={border}
|
||||||
backgroundColor={surfaceMuted}
|
backgroundColor={surfaceMuted}
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
{filters.map((filter) => (
|
||||||
type="single"
|
<ToggleGroup.Item
|
||||||
value={statusFilter}
|
key={filter.key}
|
||||||
onValueChange={(value: string) => value && onStatusChange(value as EventStatusKey)}
|
value={filter.key}
|
||||||
>
|
borderRadius="$pill"
|
||||||
<XStack gap="$1.5">
|
paddingVertical="$1.5"
|
||||||
{filters.map((filter) => {
|
paddingHorizontal="$3"
|
||||||
const active = filter.key === statusFilter;
|
>
|
||||||
return (
|
<XStack alignItems="center" gap="$1.5">
|
||||||
<ToggleGroup.Item
|
<Text fontSize="$xs" fontWeight="600">
|
||||||
key={filter.key}
|
{filter.label}
|
||||||
value={filter.key}
|
</Text>
|
||||||
borderRadius={999}
|
<XStack
|
||||||
borderWidth={1}
|
paddingHorizontal="$1.5"
|
||||||
borderColor={active ? activeBorder : 'transparent'}
|
paddingVertical="$0.5"
|
||||||
backgroundColor={active ? activeBg : 'transparent'}
|
borderRadius="$pill"
|
||||||
paddingVertical="$1.5"
|
borderWidth={1}
|
||||||
paddingHorizontal="$3"
|
borderColor={border}
|
||||||
>
|
backgroundColor="$backgroundStrong"
|
||||||
<XStack alignItems="center" gap="$1.5">
|
>
|
||||||
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
|
<Text fontSize={10} fontWeight="700">
|
||||||
{filter.label}
|
{filter.count}
|
||||||
</Text>
|
</Text>
|
||||||
<XStack
|
</XStack>
|
||||||
paddingHorizontal="$1.5"
|
</XStack>
|
||||||
paddingVertical="$0.5"
|
</ToggleGroup.Item>
|
||||||
borderRadius={999}
|
))}
|
||||||
borderWidth={1}
|
</ToggleGroup>
|
||||||
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>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</YStack>
|
</YStack>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Bell, Check, ChevronRight, RefreshCcw } from 'lucide-react';
|
|||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
|
import { ToggleGroup } from '@tamagui/toggle-group';
|
||||||
import { motion, useAnimationControls, type PanInfo } from 'framer-motion';
|
import { motion, useAnimationControls, type PanInfo } from 'framer-motion';
|
||||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||||
import { MobileCard, PillBadge, SkeletonCard, CTAButton } from './components/Primitives';
|
import { MobileCard, PillBadge, SkeletonCard, CTAButton } from './components/Primitives';
|
||||||
@@ -326,7 +327,7 @@ export default function MobileNotificationsPage() {
|
|||||||
const [events, setEvents] = React.useState<TenantEvent[]>([]);
|
const [events, setEvents] = React.useState<TenantEvent[]>([]);
|
||||||
const [showEventPicker, setShowEventPicker] = React.useState(false);
|
const [showEventPicker, setShowEventPicker] = React.useState(false);
|
||||||
const back = useBackNavigation(adminPath('/mobile/dashboard'));
|
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 warningIcon = warningText;
|
||||||
const infoIcon = primary;
|
const infoIcon = primary;
|
||||||
const errorText = danger;
|
const errorText = danger;
|
||||||
@@ -552,7 +553,22 @@ export default function MobileNotificationsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</XStack>
|
</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: 'all', label: t('notificationLogs.scope.all', 'All scopes') },
|
||||||
{ key: 'photos', label: t('notificationLogs.scope.photos', 'Photos') },
|
{ 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: 'events', label: t('notificationLogs.scope.events', 'Events') },
|
||||||
{ key: 'package', label: t('notificationLogs.scope.package', 'Package') },
|
{ key: 'package', label: t('notificationLogs.scope.package', 'Package') },
|
||||||
{ key: 'general', label: t('notificationLogs.scope.general', 'General') },
|
{ key: 'general', label: t('notificationLogs.scope.general', 'General') },
|
||||||
] as Array<{ key: NotificationScope | 'all'; label: string }>).map((filter) => {
|
] as Array<{ key: NotificationScope | 'all'; label: string }>).map((filter) => (
|
||||||
const active = scopeParam === filter.key;
|
<ToggleGroup.Item
|
||||||
return (
|
key={filter.key}
|
||||||
<Pressable key={filter.key} onPress={() => updateFilters({ scope: filter.key })} style={{ flexGrow: 1 }}>
|
value={filter.key}
|
||||||
<XStack
|
borderRadius="$pill"
|
||||||
alignItems="center"
|
paddingVertical="$2"
|
||||||
justifyContent="center"
|
paddingHorizontal="$3"
|
||||||
paddingVertical="$2"
|
flexGrow={1}
|
||||||
paddingHorizontal="$3"
|
justifyContent="center"
|
||||||
borderRadius={14}
|
>
|
||||||
borderWidth={1}
|
<Text fontSize="$xs" fontWeight="600" textAlign="center">
|
||||||
borderColor={active ? primary : border}
|
{filter.label}
|
||||||
backgroundColor={active ? accentSoft : 'transparent'}
|
</Text>
|
||||||
>
|
</ToggleGroup.Item>
|
||||||
<Text fontSize="$xs" fontWeight="700" color={active ? primary : muted}>
|
))}
|
||||||
{filter.label}
|
</ToggleGroup>
|
||||||
</Text>
|
|
||||||
</XStack>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</XStack>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<YStack gap="$2">
|
<YStack gap="$2">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { CheckCircle2, Lock, MailWarning, User } from 'lucide-react';
|
import { CheckCircle2, Lock, MailWarning, User } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
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 toast from 'react-hot-toast';
|
||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
||||||
@@ -300,6 +300,275 @@ export default function MobileProfileAccountPage() {
|
|||||||
form.password.trim().length > 0 &&
|
form.password.trim().length > 0 &&
|
||||||
form.passwordConfirmation.trim().length > 0;
|
form.passwordConfirmation.trim().length > 0;
|
||||||
const brandingDisabled = brandingLoading || brandingSaving;
|
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 (
|
return (
|
||||||
<MobileShell
|
<MobileShell
|
||||||
@@ -308,312 +577,38 @@ export default function MobileProfileAccountPage() {
|
|||||||
onBack={back}
|
onBack={back}
|
||||||
>
|
>
|
||||||
{brandingTabEnabled ? (
|
{brandingTabEnabled ? (
|
||||||
<XStack gap="$2">
|
<Tabs
|
||||||
<TabButton
|
value={activeTab}
|
||||||
label={t('profile.tabs.account', 'Account')}
|
onValueChange={(value) => setActiveTab(value as TabKey)}
|
||||||
active={activeTab === 'account'}
|
orientation="horizontal"
|
||||||
onPress={() => setActiveTab('account')}
|
flexDirection="column"
|
||||||
/>
|
>
|
||||||
<TabButton
|
<Tabs.List gap="$2">
|
||||||
label={t('profile.tabs.branding', 'Standard-Branding')}
|
<Tabs.Tab value="account" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
|
||||||
active={activeTab === 'branding'}
|
<Text fontSize="$sm" fontWeight="600" textAlign="center">
|
||||||
onPress={() => setActiveTab('branding')}
|
{t('profile.tabs.account', 'Account')}
|
||||||
/>
|
|
||||||
</XStack>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{activeTab === 'branding' && brandingTabEnabled ? (
|
|
||||||
<>
|
|
||||||
{brandingError ? (
|
|
||||||
<MobileCard>
|
|
||||||
<Text fontWeight="700" color={danger}>
|
|
||||||
{brandingError}
|
|
||||||
</Text>
|
</Text>
|
||||||
</MobileCard>
|
</Tabs.Tab>
|
||||||
) : null}
|
<Tabs.Tab value="branding" flex={1} paddingVertical="$2.5" paddingHorizontal="$3" borderRadius="$4">
|
||||||
|
<Text fontSize="$sm" fontWeight="600" textAlign="center">
|
||||||
<MobileCard gap="$3">
|
{t('profile.tabs.branding', 'Standard-Branding')}
|
||||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
</Text>
|
||||||
{t('profile.branding.title', 'Standard-Branding')}
|
</Tabs.Tab>
|
||||||
</Text>
|
</Tabs.List>
|
||||||
<Text fontSize="$sm" color={muted}>
|
<Tabs.Content value="account" paddingTop="$2">
|
||||||
{t('profile.branding.hint', 'Diese Werte dienen als Vorschlag für neue Events und beschleunigen die Einrichtung.')}
|
{accountContent}
|
||||||
</Text>
|
</Tabs.Content>
|
||||||
</MobileCard>
|
<Tabs.Content value="branding" paddingTop="$2">
|
||||||
|
{brandingContent}
|
||||||
<MobileCard gap="$3">
|
</Tabs.Content>
|
||||||
<Text fontSize="$md" fontWeight="800" color={text}>
|
</Tabs>
|
||||||
{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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</MobileShell>
|
</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({
|
function ColorField({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ vi.mock('../components/FormControls', () => ({
|
|||||||
MobileColorInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
MobileColorInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||||
MobileFileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
MobileFileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||||
MobileInput: (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>) => (
|
MobileSelect: ({ children, ...props }: React.SelectHTMLAttributes<HTMLSelectElement>) => (
|
||||||
<select {...props}>{children}</select>
|
<select {...props}>{children}</select>
|
||||||
),
|
),
|
||||||
@@ -111,16 +112,53 @@ vi.mock('@tamagui/react-native-web-lite', () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('tamagui', () => ({
|
vi.mock('tamagui', () => {
|
||||||
Slider: Object.assign(
|
const TabsValueContext = React.createContext<string | null>(null);
|
||||||
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
const TabsSetterContext = React.createContext<((value: string) => void) | null>(null);
|
||||||
{
|
const TabsRoot = ({
|
||||||
Track: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
children,
|
||||||
TrackActive: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
value,
|
||||||
Thumb: () => <div />,
|
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 getEventMock = vi.fn();
|
||||||
const updateEventMock = vi.fn();
|
const updateEventMock = vi.fn();
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ vi.mock('@tamagui/text', () => ({
|
|||||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
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', () => ({
|
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||||
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
|
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||||
<button type="button" onClick={onPress}>
|
<button type="button" onClick={onPress}>
|
||||||
|
|||||||
@@ -111,7 +111,11 @@ vi.mock('@tamagui/scroll-view', () => ({
|
|||||||
vi.mock('tamagui', () => ({
|
vi.mock('tamagui', () => ({
|
||||||
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
Tabs: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
|
||||||
List: ({ 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>,
|
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@@ -278,10 +282,10 @@ describe('MobileEventTasksPage', () => {
|
|||||||
render(<MobileEventTasksPage />);
|
render(<MobileEventTasksPage />);
|
||||||
|
|
||||||
expect(await screen.findByText('Photo tasks for guests')).toBeInTheDocument();
|
expect(await screen.findByText('Photo tasks for guests')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Tasks' })).toBeInTheDocument();
|
expect(screen.getByRole('tab', { name: 'Tasks' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Task Library' })).toBeInTheDocument();
|
expect(screen.getByRole('tab', { name: 'Task Library' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Emotions' })).toBeInTheDocument();
|
expect(screen.getByRole('tab', { name: 'Emotions' })).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Collections' })).toBeInTheDocument();
|
expect(screen.getByRole('tab', { name: 'Collections' })).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('Search photo tasks')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Search photo tasks')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Emotion')).toBeInTheDocument();
|
expect(screen.getByText('Emotion')).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ vi.mock('@tamagui/text', () => ({
|
|||||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
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', () => ({
|
vi.mock('../components/MobileShell', () => ({
|
||||||
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ vi.mock('@tamagui/text', () => ({
|
|||||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
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', () => ({
|
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||||
Pressable: ({ children, ...props }: { children: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
Pressable: ({ children, ...props }: { children: React.ReactNode } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||||
<button type="button" {...props}>
|
<button type="button" {...props}>
|
||||||
|
|||||||
@@ -434,8 +434,6 @@ export function ContentTabs({
|
|||||||
tabs: { value: string; label: string; content: React.ReactNode }[];
|
tabs: { value: string; label: string; content: React.ReactNode }[];
|
||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { border, muted, primary } = useAdminTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
@@ -445,27 +443,21 @@ export function ContentTabs({
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
borderRadius="$4"
|
borderRadius="$4"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={border}
|
borderColor="$borderColor"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom" backgroundColor="$surface">
|
<Tabs.List separator={<Separator vertical />} disablePassBorderRadius="bottom">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
flex={1}
|
flex={1}
|
||||||
unstyled
|
paddingVertical="$2.5"
|
||||||
paddingVertical="$3"
|
paddingHorizontal="$3"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor={value === tab.value ? primary : 'transparent'}
|
|
||||||
hoverStyle={{ backgroundColor: value === tab.value ? primary : '$backgroundHover' }}
|
|
||||||
>
|
>
|
||||||
<Text
|
<Text fontSize="$sm" fontWeight="600">
|
||||||
fontSize="$sm"
|
|
||||||
fontWeight={value === tab.value ? '700' : '500'}
|
|
||||||
color={value === tab.value ? '#fff' : muted}
|
|
||||||
>
|
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</Text>
|
</Text>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|||||||
Reference in New Issue
Block a user