more usage of tamagui primitives
This commit is contained in:
@@ -35,7 +35,7 @@ export default function MobileEventFormPage() {
|
|||||||
const isEdit = Boolean(slug);
|
const isEdit = Boolean(slug);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation(['management', 'common']);
|
const { t } = useTranslation(['management', 'common']);
|
||||||
const { text, muted, subtle, danger, border, surfaceMuted } = useAdminTheme();
|
const { text, muted, subtle, danger } = useAdminTheme();
|
||||||
|
|
||||||
const [form, setForm] = React.useState<FormState>({
|
const [form, setForm] = React.useState<FormState>({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -359,20 +359,11 @@ export default function MobileEventFormPage() {
|
|||||||
|
|
||||||
<YStack space="$2">
|
<YStack space="$2">
|
||||||
{!isEdit ? (
|
{!isEdit ? (
|
||||||
<button
|
<CTAButton
|
||||||
type="button"
|
label={t('eventForm.actions.saveDraft', 'Save as draft')}
|
||||||
onClick={back}
|
tone="ghost"
|
||||||
style={{
|
onPress={back}
|
||||||
...inputStyle,
|
/>
|
||||||
height: 48,
|
|
||||||
borderRadius: 12,
|
|
||||||
border: `1px solid ${border}`,
|
|
||||||
background: surfaceMuted,
|
|
||||||
fontWeight: 700,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('eventForm.actions.saveDraft', 'Save as draft')}
|
|
||||||
</button>
|
|
||||||
) : null}
|
) : null}
|
||||||
<CTAButton
|
<CTAButton
|
||||||
label={
|
label={
|
||||||
|
|||||||
@@ -1479,7 +1479,12 @@ function MobileAddonsPicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<XStack space="$2" alignItems="center">
|
<XStack space="$2" alignItems="center">
|
||||||
<MobileSelect value={selected} onChange={(event) => setSelected(event.target.value)} style={{ flex: 1 }} compact>
|
<MobileSelect
|
||||||
|
value={selected}
|
||||||
|
onChange={(event) => setSelected(event.target.value)}
|
||||||
|
containerStyle={{ flex: 1, minWidth: 0 }}
|
||||||
|
compact
|
||||||
|
>
|
||||||
{options.map((addon) => (
|
{options.map((addon) => (
|
||||||
<option key={addon.key} value={addon.key}>
|
<option key={addon.key} value={addon.key}>
|
||||||
{addon.label ?? addon.key}
|
{addon.label ?? addon.key}
|
||||||
@@ -1497,6 +1502,7 @@ function MobileAddonsPicker({
|
|||||||
disabled={!selected || busy}
|
disabled={!selected || busy}
|
||||||
onPress={() => selected && onCheckout(selected)}
|
onPress={() => selected && onCheckout(selected)}
|
||||||
loading={busy}
|
loading={busy}
|
||||||
|
fullWidth={false}
|
||||||
/>
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
|
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
import { YStack, XStack } from '@tamagui/stacks';
|
import { YStack, XStack } from '@tamagui/stacks';
|
||||||
|
import { YGroup } from '@tamagui/group';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
import { SizableText as Text } from '@tamagui/text';
|
||||||
import { ListItem } from '@tamagui/list-item';
|
import { ListItem } from '@tamagui/list-item';
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
|
import { Button } from '@tamagui/button';
|
||||||
|
import { AlertDialog } from '@tamagui/alert-dialog';
|
||||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton, SkeletonCard, PillBadge } from './components/Primitives';
|
import { MobileCard, CTAButton, SkeletonCard, PillBadge, FloatingActionButton } from './components/Primitives';
|
||||||
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
|
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
|
||||||
import {
|
import {
|
||||||
getEvent,
|
getEvent,
|
||||||
@@ -42,32 +45,53 @@ import { buildTaskSummary } from './lib/taskSummary';
|
|||||||
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
|
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
|
|
||||||
function InlineSeparator() {
|
|
||||||
const { border } = useAdminTheme();
|
|
||||||
return <XStack height={1} opacity={0.7} marginLeft="$3" backgroundColor={border} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TaskSummaryCard({
|
function TaskSummaryCard({
|
||||||
summary,
|
summary,
|
||||||
text,
|
text,
|
||||||
muted,
|
muted,
|
||||||
border,
|
border,
|
||||||
|
surfaceMuted,
|
||||||
}: {
|
}: {
|
||||||
summary: ReturnType<typeof buildTaskSummary>;
|
summary: ReturnType<typeof buildTaskSummary>;
|
||||||
text: string;
|
text: string;
|
||||||
muted: string;
|
muted: string;
|
||||||
border: string;
|
border: string;
|
||||||
|
surfaceMuted: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
return (
|
return (
|
||||||
<MobileCard space="$2" borderColor={border}>
|
<MobileCard space="$2" borderColor={border}>
|
||||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||||
<SummaryItem label={t('events.tasks.summary.assigned', 'Assigned')} value={summary.assigned} text={text} muted={muted} />
|
<SummaryItem
|
||||||
<SummaryItem label={t('events.tasks.summary.library', 'Library')} value={summary.library} text={text} muted={muted} />
|
label={t('events.tasks.summary.assigned', 'Assigned')}
|
||||||
|
value={summary.assigned}
|
||||||
|
text={text}
|
||||||
|
muted={muted}
|
||||||
|
surfaceMuted={surfaceMuted}
|
||||||
|
/>
|
||||||
|
<SummaryItem
|
||||||
|
label={t('events.tasks.summary.library', 'Library')}
|
||||||
|
value={summary.library}
|
||||||
|
text={text}
|
||||||
|
muted={muted}
|
||||||
|
surfaceMuted={surfaceMuted}
|
||||||
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||||
<SummaryItem label={t('events.tasks.summary.collections', 'Collections')} value={summary.collections} text={text} muted={muted} />
|
<SummaryItem
|
||||||
<SummaryItem label={t('events.tasks.summary.emotions', 'Emotions')} value={summary.emotions} text={text} muted={muted} />
|
label={t('events.tasks.summary.collections', 'Collections')}
|
||||||
|
value={summary.collections}
|
||||||
|
text={text}
|
||||||
|
muted={muted}
|
||||||
|
surfaceMuted={surfaceMuted}
|
||||||
|
/>
|
||||||
|
<SummaryItem
|
||||||
|
label={t('events.tasks.summary.emotions', 'Emotions')}
|
||||||
|
value={summary.emotions}
|
||||||
|
text={text}
|
||||||
|
muted={muted}
|
||||||
|
surfaceMuted={surfaceMuted}
|
||||||
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
);
|
);
|
||||||
@@ -78,14 +102,16 @@ function SummaryItem({
|
|||||||
value,
|
value,
|
||||||
text,
|
text,
|
||||||
muted,
|
muted,
|
||||||
|
surfaceMuted,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
text: string;
|
text: string;
|
||||||
muted: string;
|
muted: string;
|
||||||
|
surfaceMuted: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor="rgba(15, 23, 42, 0.03)" space="$1">
|
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor={surfaceMuted} space="$1">
|
||||||
<Text fontSize={11} color={muted}>
|
<Text fontSize={11} color={muted}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -102,7 +128,7 @@ export default function MobileEventTasksPage() {
|
|||||||
const slug = slugParam ?? activeEvent?.slug ?? null;
|
const slug = slugParam ?? activeEvent?.slug ?? null;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const { textStrong, muted, subtle, border, primary, danger, surface } = useAdminTheme();
|
const { textStrong, muted, subtle, border, primary, danger, surface, surfaceMuted, dangerBg, dangerText, overlay } = useAdminTheme();
|
||||||
const [assignedTasks, setAssignedTasks] = React.useState<TenantTask[]>([]);
|
const [assignedTasks, setAssignedTasks] = React.useState<TenantTask[]>([]);
|
||||||
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
const [library, setLibrary] = React.useState<TenantTask[]>([]);
|
||||||
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
const [collections, setCollections] = React.useState<TenantTaskCollection[]>([]);
|
||||||
@@ -120,6 +146,7 @@ export default function MobileEventTasksPage() {
|
|||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [busyId, setBusyId] = React.useState<number | null>(null);
|
const [busyId, setBusyId] = React.useState<number | null>(null);
|
||||||
const [assigningId, setAssigningId] = React.useState<number | null>(null);
|
const [assigningId, setAssigningId] = React.useState<number | null>(null);
|
||||||
|
const [deleteCandidate, setDeleteCandidate] = React.useState<TenantTask | null>(null);
|
||||||
const [eventId, setEventId] = React.useState<number | null>(null);
|
const [eventId, setEventId] = React.useState<number | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
const [emotionFilter, setEmotionFilter] = React.useState<string>('');
|
const [emotionFilter, setEmotionFilter] = React.useState<string>('');
|
||||||
@@ -349,6 +376,13 @@ export default function MobileEventTasksPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmDeleteTask() {
|
||||||
|
if (!deleteCandidate) return;
|
||||||
|
const taskId = deleteCandidate.id;
|
||||||
|
setDeleteCandidate(null);
|
||||||
|
await detachTask(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
const startEdit = (task: TenantTask) => {
|
const startEdit = (task: TenantTask) => {
|
||||||
setNewTask({
|
setNewTask({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
@@ -462,6 +496,7 @@ export default function MobileEventTasksPage() {
|
|||||||
text={text}
|
text={text}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
border={border}
|
border={border}
|
||||||
|
surfaceMuted={surfaceMuted}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -472,23 +507,26 @@ export default function MobileEventTasksPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
<XStack space="$2" flexWrap="wrap">
|
<XStack space="$2" flexWrap="wrap">
|
||||||
{sectionCounts.map((section) => (
|
{sectionCounts.map((section) => (
|
||||||
<Pressable key={section.key} onPress={() => handleQuickNav(section.key)} style={{ flexGrow: 1 }}>
|
<Button
|
||||||
<XStack
|
key={section.key}
|
||||||
alignItems="center"
|
unstyled
|
||||||
justifyContent="center"
|
onPress={() => handleQuickNav(section.key)}
|
||||||
space="$1.5"
|
borderRadius={14}
|
||||||
paddingVertical="$2"
|
borderWidth={1}
|
||||||
paddingHorizontal="$3"
|
borderColor={border}
|
||||||
borderRadius={14}
|
backgroundColor={surface}
|
||||||
borderWidth={1}
|
paddingVertical="$2"
|
||||||
borderColor={border}
|
paddingHorizontal="$3"
|
||||||
>
|
pressStyle={{ backgroundColor: surfaceMuted }}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
<XStack alignItems="center" justifyContent="center" space="$1.5">
|
||||||
<Text fontSize="$xs" fontWeight="700" color={text}>
|
<Text fontSize="$xs" fontWeight="700" color={text}>
|
||||||
{t(`events.tasks.sections.${section.key}`, section.key)}
|
{t(`events.tasks.sections.${section.key}`, section.key)}
|
||||||
</Text>
|
</Text>
|
||||||
<PillBadge tone="muted">{section.count}</PillBadge>
|
<PillBadge tone="muted">{section.count}</PillBadge>
|
||||||
</XStack>
|
</XStack>
|
||||||
</Pressable>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</XStack>
|
</XStack>
|
||||||
</YStack>
|
</YStack>
|
||||||
@@ -523,11 +561,12 @@ export default function MobileEventTasksPage() {
|
|||||||
/>
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
<Pressable onPress={() => setShowTaskSheet(true)}>
|
<YGroup.Item bordered>
|
||||||
<ListItem
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
pressTheme
|
pressTheme
|
||||||
|
onPress={() => setShowTaskSheet(true)}
|
||||||
title={
|
title={
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<YStack
|
<YStack
|
||||||
@@ -554,12 +593,12 @@ export default function MobileEventTasksPage() {
|
|||||||
paddingHorizontal="$3"
|
paddingHorizontal="$3"
|
||||||
iconAfter={<ChevronRight size={16} color={muted} />}
|
iconAfter={<ChevronRight size={16} color={muted} />}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</YGroup.Item>
|
||||||
<InlineSeparator />
|
<YGroup.Item>
|
||||||
<Pressable onPress={() => setShowCollectionSheet(true)}>
|
|
||||||
<ListItem
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
pressTheme
|
pressTheme
|
||||||
|
onPress={() => setShowCollectionSheet(true)}
|
||||||
title={
|
title={
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<YStack
|
<YStack
|
||||||
@@ -586,8 +625,8 @@ export default function MobileEventTasksPage() {
|
|||||||
paddingHorizontal="$3"
|
paddingHorizontal="$3"
|
||||||
iconAfter={<ChevronRight size={16} color={muted} />}
|
iconAfter={<ChevronRight size={16} color={muted} />}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</YGroup.Item>
|
||||||
</YStack>
|
</YGroup>
|
||||||
</YStack>
|
</YStack>
|
||||||
) : (
|
) : (
|
||||||
<YStack space="$2">
|
<YStack space="$2">
|
||||||
@@ -622,44 +661,53 @@ export default function MobileEventTasksPage() {
|
|||||||
<Text fontSize="$sm" color={muted}>
|
<Text fontSize="$sm" color={muted}>
|
||||||
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
|
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
|
||||||
</Text>
|
</Text>
|
||||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
{filteredTasks.map((task, idx) => (
|
{filteredTasks.map((task, idx) => (
|
||||||
<React.Fragment key={task.id}>
|
<YGroup.Item key={task.id} bordered={idx < filteredTasks.length - 1}>
|
||||||
<Pressable onPress={() => startEdit(task)}>
|
<ListItem
|
||||||
<ListItem
|
hoverTheme
|
||||||
hoverTheme
|
pressTheme
|
||||||
pressTheme
|
onPress={() => startEdit(task)}
|
||||||
title={
|
title={
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
{task.title}
|
{task.title}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
subTitle={
|
||||||
|
task.description ? (
|
||||||
|
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||||
|
{task.description}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
) : null
|
||||||
subTitle={
|
}
|
||||||
task.description ? (
|
iconAfter={
|
||||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
<XStack space="$2" alignItems="center">
|
||||||
{task.description}
|
{task.emotion ? (
|
||||||
</Text>
|
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
|
||||||
) : null
|
) : null}
|
||||||
}
|
<Button
|
||||||
iconAfter={
|
size="$2"
|
||||||
<XStack space="$2" alignItems="flex-start">
|
circular
|
||||||
{task.emotion ? (
|
backgroundColor={dangerBg}
|
||||||
<Tag label={task.emotion.name ?? ''} color={task.emotion.color ?? text} />
|
borderWidth={1}
|
||||||
) : null}
|
borderColor={`${danger}33`}
|
||||||
<Pressable disabled={busyId === task.id} onPress={() => detachTask(task.id)}>
|
icon={<Trash2 size={14} color={dangerText} />}
|
||||||
<Trash2 size={14} color={danger} />
|
aria-label={t('events.tasks.remove', 'Remove task')}
|
||||||
</Pressable>
|
disabled={busyId === task.id}
|
||||||
<ChevronRight size={14} color={subtle} />
|
onPress={(event) => {
|
||||||
</XStack>
|
event?.stopPropagation?.();
|
||||||
}
|
setDeleteCandidate(task);
|
||||||
paddingVertical="$2"
|
}}
|
||||||
paddingHorizontal="$3"
|
/>
|
||||||
/>
|
<ChevronRight size={14} color={subtle} />
|
||||||
</Pressable>
|
</XStack>
|
||||||
{idx < assignedTasks.length - 1 ? <InlineSeparator /> : null}
|
}
|
||||||
</React.Fragment>
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YGroup>
|
||||||
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
|
<XStack justifyContent="space-between" alignItems="center" marginTop="$2">
|
||||||
<div ref={libraryRef} />
|
<div ref={libraryRef} />
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
@@ -681,9 +729,9 @@ export default function MobileEventTasksPage() {
|
|||||||
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')}
|
{t('events.tasks.libraryEmpty', 'Keine weiteren Aufgaben verfügbar.')}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
|
{(expandedLibrary ? library : library.slice(0, 6)).map((task, idx, arr) => (
|
||||||
<React.Fragment key={`lib-${task.id}`}>
|
<YGroup.Item key={`lib-${task.id}`} bordered={idx < arr.length - 1}>
|
||||||
<ListItem
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
pressTheme
|
pressTheme
|
||||||
@@ -712,10 +760,9 @@ export default function MobileEventTasksPage() {
|
|||||||
paddingVertical="$2"
|
paddingVertical="$2"
|
||||||
paddingHorizontal="$3"
|
paddingHorizontal="$3"
|
||||||
/>
|
/>
|
||||||
{idx < arr.length - 1 ? <InlineSeparator /> : null}
|
</YGroup.Item>
|
||||||
</React.Fragment>
|
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YGroup>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
)}
|
)}
|
||||||
@@ -739,9 +786,9 @@ export default function MobileEventTasksPage() {
|
|||||||
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
|
{t('events.tasks.collectionsEmpty', 'Keine Pakete vorhanden.')}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<YStack borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
|
{(expandedCollections ? collections : collections.slice(0, 6)).map((collection, idx, arr) => (
|
||||||
<React.Fragment key={collection.id}>
|
<YGroup.Item key={collection.id} bordered={idx < arr.length - 1}>
|
||||||
<ListItem
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
pressTheme
|
pressTheme
|
||||||
@@ -767,13 +814,12 @@ export default function MobileEventTasksPage() {
|
|||||||
<ChevronRight size={14} color={subtle} />
|
<ChevronRight size={14} color={subtle} />
|
||||||
</XStack>
|
</XStack>
|
||||||
}
|
}
|
||||||
paddingVertical="$2"
|
paddingVertical="$2"
|
||||||
paddingHorizontal="$3"
|
paddingHorizontal="$3"
|
||||||
/>
|
/>
|
||||||
{idx < arr.length - 1 ? <InlineSeparator /> : null}
|
</YGroup.Item>
|
||||||
</React.Fragment>
|
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YGroup>
|
||||||
)}
|
)}
|
||||||
</YStack>
|
</YStack>
|
||||||
</MobileSheet>
|
</MobileSheet>
|
||||||
@@ -871,36 +917,39 @@ export default function MobileEventTasksPage() {
|
|||||||
style={{ padding: 0 }}
|
style={{ padding: 0 }}
|
||||||
/>
|
/>
|
||||||
</MobileField>
|
</MobileField>
|
||||||
<YStack space="$2">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
{emotions.map((em) => (
|
{emotions.map((em, idx) => (
|
||||||
<ListItem
|
<YGroup.Item key={`emo-${em.id}`} bordered={idx < emotions.length - 1}>
|
||||||
key={`emo-${em.id}`}
|
<ListItem
|
||||||
hoverTheme
|
hoverTheme
|
||||||
pressTheme
|
pressTheme
|
||||||
title={
|
title={
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<Tag label={em.name ?? ''} color={em.color ?? border} />
|
<Tag label={em.name ?? ''} color={em.color ?? border} />
|
||||||
</XStack>
|
</XStack>
|
||||||
}
|
}
|
||||||
iconAfter={
|
iconAfter={
|
||||||
<XStack space="$2">
|
<XStack space="$2">
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setEditingEmotion(em);
|
setEditingEmotion(em);
|
||||||
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
|
setEmotionForm({ name: em.name ?? '', color: em.color ?? border });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil size={14} color={primary} />
|
<Pencil size={14} color={primary} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={() => removeEmotion(em.id)}>
|
<Pressable onPress={() => removeEmotion(em.id)}>
|
||||||
<Trash2 size={14} color={danger} />
|
<Trash2 size={14} color={danger} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<ChevronRight size={14} color={subtle} />
|
<ChevronRight size={14} color={subtle} />
|
||||||
</XStack>
|
</XStack>
|
||||||
}
|
}
|
||||||
/>
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YGroup>
|
||||||
</YStack>
|
</YStack>
|
||||||
</MobileSheet>
|
</MobileSheet>
|
||||||
|
|
||||||
@@ -942,24 +991,66 @@ export default function MobileEventTasksPage() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</MobileSheet>
|
</MobileSheet>
|
||||||
|
|
||||||
<Pressable
|
<AlertDialog
|
||||||
onPress={() => setShowFabMenu(true)}
|
open={Boolean(deleteCandidate)}
|
||||||
style={{
|
onOpenChange={(open) => {
|
||||||
position: 'fixed',
|
if (!open) {
|
||||||
right: 20,
|
setDeleteCandidate(null);
|
||||||
bottom: 'calc(env(safe-area-inset-bottom, 0px) + 96px)',
|
}
|
||||||
width: 56,
|
}}
|
||||||
height: 56,
|
>
|
||||||
borderRadius: 28,
|
<AlertDialog.Portal>
|
||||||
backgroundColor: primary,
|
<AlertDialog.Overlay backgroundColor={`${overlay}66`} />
|
||||||
justifyContent: 'center',
|
<AlertDialog.Content
|
||||||
alignItems: 'center',
|
borderRadius={20}
|
||||||
boxShadow: '0 10px 25px rgba(0,122,255,0.35)',
|
borderWidth={1}
|
||||||
zIndex: 60,
|
borderColor={border}
|
||||||
}}
|
backgroundColor={surface}
|
||||||
>
|
padding="$4"
|
||||||
<Plus size={20} color={surface} />
|
maxWidth={420}
|
||||||
</Pressable>
|
width="90%"
|
||||||
|
>
|
||||||
|
<YStack space="$3">
|
||||||
|
<AlertDialog.Title>
|
||||||
|
<Text fontSize="$md" fontWeight="800" color={text}>
|
||||||
|
{t('events.tasks.removeTitle', 'Remove task?')}
|
||||||
|
</Text>
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
<Text fontSize="$sm" color={muted}>
|
||||||
|
{deleteCandidate
|
||||||
|
? t('events.tasks.removeBody', 'This will remove "{{title}}" from the event.', { title: deleteCandidate.title })
|
||||||
|
: t('events.tasks.removeBodyFallback', 'This will remove the task from the event.')}
|
||||||
|
</Text>
|
||||||
|
</AlertDialog.Description>
|
||||||
|
<XStack space="$2" justifyContent="flex-end">
|
||||||
|
<AlertDialog.Cancel asChild>
|
||||||
|
<CTAButton
|
||||||
|
label={t('common.cancel', 'Cancel')}
|
||||||
|
tone="ghost"
|
||||||
|
fullWidth={false}
|
||||||
|
onPress={() => setDeleteCandidate(null)}
|
||||||
|
/>
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action asChild>
|
||||||
|
<CTAButton
|
||||||
|
label={t('events.tasks.remove', 'Remove')}
|
||||||
|
tone="danger"
|
||||||
|
fullWidth={false}
|
||||||
|
onPress={() => confirmDeleteTask()}
|
||||||
|
/>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Portal>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<FloatingActionButton
|
||||||
|
onPress={() => setShowFabMenu(true)}
|
||||||
|
label={t('events.tasks.add', 'Add')}
|
||||||
|
icon={Plus}
|
||||||
|
/>
|
||||||
|
|
||||||
<MobileSheet
|
<MobileSheet
|
||||||
open={showFabMenu}
|
open={showFabMenu}
|
||||||
@@ -967,61 +1058,67 @@ export default function MobileEventTasksPage() {
|
|||||||
title={t('events.tasks.actions', 'Aktionen')}
|
title={t('events.tasks.actions', 'Aktionen')}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<YStack space="$1">
|
<YGroup borderWidth={1} borderColor={border} borderRadius="$4" overflow="hidden">
|
||||||
<ListItem
|
<YGroup.Item bordered>
|
||||||
hoverTheme
|
<ListItem
|
||||||
pressTheme
|
hoverTheme
|
||||||
title={
|
pressTheme
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
title={
|
||||||
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
</Text>
|
{t('events.tasks.addTask', 'Aufgabe hinzufügen')}
|
||||||
}
|
</Text>
|
||||||
onPress={() => {
|
}
|
||||||
setShowFabMenu(false);
|
onPress={() => {
|
||||||
setShowTaskSheet(true);
|
setShowFabMenu(false);
|
||||||
}}
|
setShowTaskSheet(true);
|
||||||
paddingVertical="$2"
|
}}
|
||||||
paddingHorizontal="$3"
|
paddingVertical="$2"
|
||||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
paddingHorizontal="$3"
|
||||||
/>
|
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||||
<ListItem
|
/>
|
||||||
hoverTheme
|
</YGroup.Item>
|
||||||
pressTheme
|
<YGroup.Item bordered>
|
||||||
title={
|
<ListItem
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
hoverTheme
|
||||||
{t('events.tasks.bulkAdd', 'Bulk add')}
|
pressTheme
|
||||||
</Text>
|
title={
|
||||||
}
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
onPress={() => {
|
{t('events.tasks.bulkAdd', 'Bulk add')}
|
||||||
setShowFabMenu(false);
|
</Text>
|
||||||
setShowBulkSheet(true);
|
}
|
||||||
}}
|
onPress={() => {
|
||||||
paddingVertical="$2"
|
setShowFabMenu(false);
|
||||||
paddingHorizontal="$3"
|
setShowBulkSheet(true);
|
||||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
}}
|
||||||
/>
|
paddingVertical="$2"
|
||||||
<ListItem
|
paddingHorizontal="$3"
|
||||||
hoverTheme
|
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||||
pressTheme
|
/>
|
||||||
title={
|
</YGroup.Item>
|
||||||
<Text fontSize={12.5} fontWeight="600" color={text}>
|
<YGroup.Item>
|
||||||
{t('events.tasks.manageEmotions', 'Manage emotions')}
|
<ListItem
|
||||||
</Text>
|
hoverTheme
|
||||||
}
|
pressTheme
|
||||||
subTitle={
|
title={
|
||||||
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
<Text fontSize={12.5} fontWeight="600" color={text}>
|
||||||
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
|
{t('events.tasks.manageEmotions', 'Manage emotions')}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
onPress={() => {
|
subTitle={
|
||||||
setShowFabMenu(false);
|
<Text fontSize={11.5} fontWeight="400" color={subtle}>
|
||||||
setShowEmotionSheet(true);
|
{t('events.tasks.manageEmotionsHint', 'Filter and keep your taxonomy tidy.')}
|
||||||
}}
|
</Text>
|
||||||
paddingVertical="$2"
|
}
|
||||||
paddingHorizontal="$3"
|
onPress={() => {
|
||||||
iconAfter={<ChevronRight size={14} color={subtle} />}
|
setShowFabMenu(false);
|
||||||
/>
|
setShowEmotionSheet(true);
|
||||||
</YStack>
|
}}
|
||||||
|
paddingVertical="$2"
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
iconAfter={<ChevronRight size={14} color={subtle} />}
|
||||||
|
/>
|
||||||
|
</YGroup.Item>
|
||||||
|
</YGroup>
|
||||||
</MobileSheet>
|
</MobileSheet>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
|
|||||||
94
resources/js/admin/mobile/__tests__/EventFormPage.test.tsx
Normal file
94
resources/js/admin/mobile/__tests__/EventFormPage.test.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
const backMock = vi.fn();
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('react-router-dom', () => ({
|
||||||
|
useNavigate: () => navigateMock,
|
||||||
|
useParams: () => ({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../hooks/useBackNavigation', () => ({
|
||||||
|
useBackNavigation: () => backMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../api', () => ({
|
||||||
|
createEvent: vi.fn(),
|
||||||
|
getEvent: vi.fn(),
|
||||||
|
updateEvent: vi.fn(),
|
||||||
|
getEventTypes: vi.fn().mockResolvedValue([]),
|
||||||
|
trackOnboarding: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-hot-toast', () => ({
|
||||||
|
default: {
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/MobileShell', () => ({
|
||||||
|
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/Primitives', () => ({
|
||||||
|
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||||
|
<button type="button" onClick={onPress}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/FormControls', () => ({
|
||||||
|
MobileField: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
MobileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||||
|
MobileSelect: ({ children, ...props }: { children: React.ReactNode }) => <select {...props}>{children}</select>,
|
||||||
|
MobileTextArea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/LegalConsentSheet', () => ({
|
||||||
|
LegalConsentSheet: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/stacks', () => ({
|
||||||
|
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/switch', () => ({
|
||||||
|
Switch: Object.assign(
|
||||||
|
({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
{ Thumb: () => <div /> },
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../theme', () => ({
|
||||||
|
useAdminTheme: () => ({
|
||||||
|
text: '#111827',
|
||||||
|
muted: '#6b7280',
|
||||||
|
subtle: '#94a3b8',
|
||||||
|
danger: '#b91c1c',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import MobileEventFormPage from '../EventFormPage';
|
||||||
|
|
||||||
|
describe('MobileEventFormPage', () => {
|
||||||
|
it('renders a save draft button when creating a new event', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<MobileEventFormPage />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveDraft = screen.getByText('eventForm.actions.saveDraft');
|
||||||
|
fireEvent.click(saveDraft);
|
||||||
|
|
||||||
|
expect(backMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
85
resources/js/admin/mobile/components/FormControls.test.tsx
Normal file
85
resources/js/admin/mobile/components/FormControls.test.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
vi.mock('@tamagui/core', () => ({
|
||||||
|
useTheme: () => ({
|
||||||
|
color: { val: '#111827' },
|
||||||
|
gray: { val: '#6b7280' },
|
||||||
|
borderColor: { val: '#e5e7eb' },
|
||||||
|
primary: { val: '#FF5A5F' },
|
||||||
|
surface: { val: '#ffffff' },
|
||||||
|
red10: { val: '#b91c1c' },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/stacks', () => ({
|
||||||
|
YStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||||
|
XStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('tamagui', () => ({
|
||||||
|
Input: ({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||||
|
TextArea: ({ ...props }: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/select', () => {
|
||||||
|
const SelectContext = React.createContext<{ onValueChange?: (value: string) => void } | null>(null);
|
||||||
|
const Select = ({ children, onValueChange }: { children: React.ReactNode; onValueChange?: (value: string) => void }) => (
|
||||||
|
<SelectContext.Provider value={{ onValueChange }}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</SelectContext.Provider>
|
||||||
|
);
|
||||||
|
const Trigger = ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>;
|
||||||
|
const Value = ({ placeholder }: { placeholder?: React.ReactNode }) => <span>{placeholder}</span>;
|
||||||
|
const Content = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
const Viewport = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
const Group = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
const Item = ({ children, value }: { children: React.ReactNode; value: string }) => {
|
||||||
|
const ctx = React.useContext(SelectContext);
|
||||||
|
return (
|
||||||
|
<button type="button" data-value={value} onClick={() => ctx?.onValueChange?.(value)}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const ItemText = ({ children }: { children: React.ReactNode }) => <span>{children}</span>;
|
||||||
|
|
||||||
|
Select.Trigger = Trigger;
|
||||||
|
Select.Value = Value;
|
||||||
|
Select.Content = Content;
|
||||||
|
Select.Viewport = Viewport;
|
||||||
|
Select.Group = Group;
|
||||||
|
Select.Item = Item;
|
||||||
|
Select.ItemText = ItemText;
|
||||||
|
|
||||||
|
return { Select };
|
||||||
|
});
|
||||||
|
|
||||||
|
import { MobileSelect } from './FormControls';
|
||||||
|
|
||||||
|
describe('MobileSelect', () => {
|
||||||
|
it('maps options and forwards selection changes', () => {
|
||||||
|
const handleChange = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MobileSelect value="" onChange={handleChange}>
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="one">One</option>
|
||||||
|
</MobileSelect>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = screen.getAllByRole('button');
|
||||||
|
fireEvent.click(items[1]);
|
||||||
|
|
||||||
|
expect(handleChange).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
target: expect.objectContaining({ value: 'one' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } 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 { Input, TextArea } from 'tamagui';
|
||||||
|
import { Select } from '@tamagui/select';
|
||||||
import { withAlpha } from './colors';
|
import { withAlpha } from './colors';
|
||||||
import { useAdminTheme } from '../theme';
|
import { useAdminTheme } from '../theme';
|
||||||
|
|
||||||
@@ -40,41 +42,41 @@ type ControlProps = {
|
|||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<'input'> & ControlProps>(
|
type MobileSelectProps = React.ComponentPropsWithoutRef<'select'> & ControlProps & {
|
||||||
function MobileInput({ hasError = false, compact = false, style, ...props }, ref) {
|
placeholder?: string;
|
||||||
const { border, surface, text, primary, danger } = useAdminTheme();
|
containerStyle?: React.CSSProperties;
|
||||||
const [focused, setFocused] = React.useState(false);
|
};
|
||||||
|
|
||||||
const height = compact ? 36 : 44;
|
export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPropsWithoutRef<'input'> & ControlProps>(
|
||||||
const borderColor = hasError ? danger : focused ? primary : border;
|
function MobileInput({ hasError = false, compact = false, style, onChange, ...props }, ref) {
|
||||||
|
const { border, surface, text, primary, danger } = useAdminTheme();
|
||||||
|
const borderColor = hasError ? danger : border;
|
||||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<Input
|
||||||
ref={ref}
|
ref={ref as React.Ref<any>}
|
||||||
{...props}
|
{...props}
|
||||||
onFocus={(event) => {
|
onChangeText={(value) => {
|
||||||
setFocused(true);
|
onChange?.({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
||||||
props.onFocus?.(event);
|
|
||||||
}}
|
}}
|
||||||
onBlur={(event) => {
|
size={compact ? '$3' : '$4'}
|
||||||
setFocused(false);
|
height={compact ? 36 : 44}
|
||||||
props.onBlur?.(event);
|
paddingHorizontal="$3"
|
||||||
|
borderRadius={12}
|
||||||
|
width="100%"
|
||||||
|
fontSize={compact ? 13 : 14}
|
||||||
|
backgroundColor={surface}
|
||||||
|
color={text}
|
||||||
|
borderColor={borderColor}
|
||||||
|
focusStyle={{
|
||||||
|
borderColor: hasError ? danger : primary,
|
||||||
|
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||||
}}
|
}}
|
||||||
style={{
|
hoverStyle={{
|
||||||
width: '100%',
|
borderColor,
|
||||||
height,
|
|
||||||
borderRadius: 12,
|
|
||||||
border: `1px solid ${borderColor}`,
|
|
||||||
padding: '0 12px',
|
|
||||||
fontSize: compact ? 13 : 14,
|
|
||||||
background: surface,
|
|
||||||
color: text,
|
|
||||||
outline: 'none',
|
|
||||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
|
||||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
|
||||||
...style,
|
|
||||||
}}
|
}}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -83,40 +85,35 @@ export const MobileInput = React.forwardRef<HTMLInputElement, React.ComponentPro
|
|||||||
export const MobileTextArea = React.forwardRef<
|
export const MobileTextArea = React.forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
React.ComponentPropsWithoutRef<'textarea'> & ControlProps
|
React.ComponentPropsWithoutRef<'textarea'> & ControlProps
|
||||||
>(function MobileTextArea({ hasError = false, compact = false, style, ...props }, ref) {
|
>(function MobileTextArea({ hasError = false, compact = false, style, onChange, ...props }, ref) {
|
||||||
const { border, surface, text, primary, danger } = useAdminTheme();
|
const { border, surface, text, primary, danger } = useAdminTheme();
|
||||||
const [focused, setFocused] = React.useState(false);
|
const borderColor = hasError ? danger : border;
|
||||||
|
|
||||||
const borderColor = hasError ? danger : focused ? primary : border;
|
|
||||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<TextArea
|
||||||
ref={ref}
|
ref={ref as React.Ref<any>}
|
||||||
{...props}
|
{...props}
|
||||||
onFocus={(event) => {
|
onChangeText={(value) => {
|
||||||
setFocused(true);
|
onChange?.({ target: { value } } as React.ChangeEvent<HTMLTextAreaElement>);
|
||||||
props.onFocus?.(event);
|
|
||||||
}}
|
}}
|
||||||
onBlur={(event) => {
|
size={compact ? '$3' : '$4'}
|
||||||
setFocused(false);
|
minHeight={compact ? 72 : 96}
|
||||||
props.onBlur?.(event);
|
borderRadius={12}
|
||||||
|
padding="$3"
|
||||||
|
width="100%"
|
||||||
|
fontSize={compact ? 13 : 14}
|
||||||
|
backgroundColor={surface}
|
||||||
|
color={text}
|
||||||
|
borderColor={borderColor}
|
||||||
|
focusStyle={{
|
||||||
|
borderColor: hasError ? danger : primary,
|
||||||
|
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||||
}}
|
}}
|
||||||
style={{
|
hoverStyle={{
|
||||||
width: '100%',
|
borderColor,
|
||||||
borderRadius: 12,
|
|
||||||
border: `1px solid ${borderColor}`,
|
|
||||||
padding: '10px 12px',
|
|
||||||
fontSize: compact ? 13 : 14,
|
|
||||||
background: surface,
|
|
||||||
color: text,
|
|
||||||
outline: 'none',
|
|
||||||
minHeight: compact ? 72 : 96,
|
|
||||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
|
||||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
|
||||||
resize: 'vertical',
|
|
||||||
...style,
|
|
||||||
}}
|
}}
|
||||||
|
style={{ resize: 'vertical', ...style }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -125,50 +122,92 @@ export function MobileSelect({
|
|||||||
children,
|
children,
|
||||||
hasError = false,
|
hasError = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
|
containerStyle,
|
||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithoutRef<'select'> & ControlProps) {
|
}: MobileSelectProps) {
|
||||||
const { border, surface, text, primary, danger, subtle } = useAdminTheme();
|
const { border, surface, text, primary, danger, subtle } = useAdminTheme();
|
||||||
const [focused, setFocused] = React.useState(false);
|
const borderColor = hasError ? danger : border;
|
||||||
|
|
||||||
const height = compact ? 36 : 44;
|
|
||||||
const borderColor = hasError ? danger : focused ? primary : border;
|
|
||||||
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18);
|
||||||
|
const hasSizing =
|
||||||
|
typeof containerStyle === 'object' &&
|
||||||
|
containerStyle !== null &&
|
||||||
|
('width' in containerStyle ||
|
||||||
|
'maxWidth' in containerStyle ||
|
||||||
|
'minWidth' in containerStyle ||
|
||||||
|
'flex' in containerStyle ||
|
||||||
|
'flexGrow' in containerStyle);
|
||||||
|
const options = React.Children.toArray(children).flatMap((child) => {
|
||||||
|
if (!React.isValidElement(child)) return [];
|
||||||
|
const { value, children: label, disabled } = child.props as {
|
||||||
|
value?: string | number;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: value === undefined ? '' : String(value),
|
||||||
|
label: label ?? '',
|
||||||
|
disabled: Boolean(disabled),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const emptyOption = options.find((option) => option.value === '');
|
||||||
|
const selectValue = props.value === undefined ? undefined : String(props.value ?? '');
|
||||||
|
const selectDefault = props.defaultValue === undefined ? undefined : String(props.defaultValue ?? '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<XStack position="relative" alignItems="center">
|
<XStack position="relative" alignItems="center" width={hasSizing ? undefined : '100%'} style={containerStyle}>
|
||||||
<select
|
<Select
|
||||||
{...props}
|
value={selectValue}
|
||||||
onFocus={(event) => {
|
defaultValue={selectValue === undefined ? selectDefault : undefined}
|
||||||
setFocused(true);
|
onValueChange={(next) => {
|
||||||
props.onFocus?.(event);
|
props.onChange?.({ target: { value: next } } as React.ChangeEvent<HTMLSelectElement>);
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
setFocused(false);
|
|
||||||
props.onBlur?.(event);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height,
|
|
||||||
borderRadius: 12,
|
|
||||||
border: `1px solid ${borderColor}`,
|
|
||||||
padding: '0 36px 0 12px',
|
|
||||||
fontSize: compact ? 13 : 14,
|
|
||||||
background: surface,
|
|
||||||
color: text,
|
|
||||||
outline: 'none',
|
|
||||||
appearance: 'none',
|
|
||||||
WebkitAppearance: 'none',
|
|
||||||
boxShadow: focused || hasError ? `0 0 0 3px ${ringColor}` : 'none',
|
|
||||||
transition: 'border-color 150ms ease, box-shadow 150ms ease',
|
|
||||||
...style,
|
|
||||||
}}
|
}}
|
||||||
|
size={compact ? '$3' : '$4'}
|
||||||
>
|
>
|
||||||
{children}
|
<Select.Trigger
|
||||||
</select>
|
width="100%"
|
||||||
<XStack position="absolute" right={12} pointerEvents="none">
|
borderRadius={12}
|
||||||
<ChevronDown size={16} color={subtle} />
|
borderWidth={1}
|
||||||
</XStack>
|
borderColor={borderColor}
|
||||||
|
backgroundColor={surface}
|
||||||
|
paddingVertical={compact ? 6 : 10}
|
||||||
|
paddingHorizontal="$3"
|
||||||
|
disabled={props.disabled}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
onBlur={props.onBlur}
|
||||||
|
iconAfter={<ChevronDown size={16} color={subtle} />}
|
||||||
|
focusStyle={{
|
||||||
|
borderColor: hasError ? danger : primary,
|
||||||
|
boxShadow: `0 0 0 3px ${ringColor}`,
|
||||||
|
}}
|
||||||
|
hoverStyle={{
|
||||||
|
borderColor,
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<Select.Value placeholder={props.placeholder ?? emptyOption?.label ?? ''} color={text} />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content
|
||||||
|
zIndex={200000}
|
||||||
|
borderRadius={14}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={border}
|
||||||
|
backgroundColor={surface}
|
||||||
|
>
|
||||||
|
<Select.Viewport padding="$2">
|
||||||
|
<Select.Group>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<Select.Item key={`${option.value}-${index}`} value={option.value} disabled={option.disabled}>
|
||||||
|
<Select.ItemText>{option.label}</Select.ItemText>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Group>
|
||||||
|
</Select.Viewport>
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
{props.name ? <input type="hidden" name={props.name} value={selectValue ?? selectDefault ?? ''} /> : null}
|
||||||
</XStack>
|
</XStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,14 +83,18 @@ export function CTAButton({
|
|||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
tone?: 'primary' | 'ghost';
|
tone?: 'primary' | 'ghost' | 'danger';
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { primary, surface, border, text } = useAdminTheme();
|
const { primary, surface, border, text, danger } = useAdminTheme();
|
||||||
const isPrimary = tone === 'primary';
|
const isPrimary = tone === 'primary';
|
||||||
|
const isDanger = tone === 'danger';
|
||||||
const isDisabled = disabled || loading;
|
const isDisabled = disabled || loading;
|
||||||
|
const backgroundColor = isDanger ? danger : isPrimary ? primary : surface;
|
||||||
|
const borderColor = isPrimary || isDanger ? 'transparent' : border;
|
||||||
|
const labelColor = isPrimary || isDanger ? 'white' : text;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={isDisabled ? undefined : onPress}
|
onPress={isDisabled ? undefined : onPress}
|
||||||
@@ -106,11 +110,11 @@ export function CTAButton({
|
|||||||
borderRadius={16}
|
borderRadius={16}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
backgroundColor={isPrimary ? primary : surface}
|
backgroundColor={backgroundColor}
|
||||||
borderWidth={isPrimary ? 0 : 1}
|
borderWidth={isPrimary || isDanger ? 0 : 1}
|
||||||
borderColor={isPrimary ? 'transparent' : border}
|
borderColor={borderColor}
|
||||||
>
|
>
|
||||||
<Text fontSize="$sm" fontWeight="800" color={isPrimary ? 'white' : text}>
|
<Text fontSize="$sm" fontWeight="800" color={labelColor}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|||||||
62
resources/js/admin/mobile/components/Sheet.test.tsx
Normal file
62
resources/js/admin/mobile/components/Sheet.test.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/core', () => ({
|
||||||
|
useTheme: () => ({
|
||||||
|
surface: { val: '#ffffff' },
|
||||||
|
color12: { val: '#111827' },
|
||||||
|
gray: { val: '#6b7280' },
|
||||||
|
gray12: { val: '#0f172a' },
|
||||||
|
borderColor: { val: '#e5e7eb' },
|
||||||
|
shadowColor: { val: 'rgba(0,0,0,0.1)' },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/stacks', () => ({
|
||||||
|
YStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||||
|
XStack: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||||
|
Pressable: ({ children, onPress, ...props }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||||
|
<button type="button" onClick={onPress} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/sheet', () => {
|
||||||
|
const Sheet = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Frame = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Overlay = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.ScrollView = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Handle = () => <div />;
|
||||||
|
return { Sheet };
|
||||||
|
});
|
||||||
|
|
||||||
|
import { MobileSheet } from './Sheet';
|
||||||
|
|
||||||
|
describe('MobileSheet', () => {
|
||||||
|
it('renders title and closes via the close action', () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MobileSheet open title="Test Sheet" onClose={onClose}>
|
||||||
|
<div>Body</div>
|
||||||
|
</MobileSheet>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Sheet')).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByText('Close'));
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import React from '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 { Sheet } from '@tamagui/sheet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAdminTheme } from '../theme';
|
import { useAdminTheme } from '../theme';
|
||||||
|
|
||||||
@@ -17,42 +18,64 @@ type SheetProps = {
|
|||||||
|
|
||||||
export function MobileSheet({ open, title, onClose, children, footer, bottomOffsetPx = 88 }: SheetProps) {
|
export function MobileSheet({ open, title, onClose, children, footer, bottomOffsetPx = 88 }: SheetProps) {
|
||||||
const { t } = useTranslation('mobile');
|
const { t } = useTranslation('mobile');
|
||||||
const { surface, textStrong, muted, overlay, shadow } = useAdminTheme();
|
const { surface, textStrong, muted, overlay, shadow, border } = useAdminTheme();
|
||||||
|
const bottomOffset = `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)`;
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-end justify-center backdrop-blur-sm" style={{ backgroundColor: `${overlay}66` }}>
|
<Sheet
|
||||||
<YStack
|
modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(next) => {
|
||||||
|
if (!next) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
snapPoints={[82]}
|
||||||
|
snapPointsMode="percent"
|
||||||
|
dismissOnOverlayPress
|
||||||
|
dismissOnSnapToBottom
|
||||||
|
zIndex={100000}
|
||||||
|
>
|
||||||
|
<Sheet.Overlay backgroundColor={`${overlay}66`} />
|
||||||
|
<Sheet.Frame
|
||||||
width="100%"
|
width="100%"
|
||||||
maxWidth={520}
|
maxWidth={520}
|
||||||
|
alignSelf="center"
|
||||||
borderTopLeftRadius={24}
|
borderTopLeftRadius={24}
|
||||||
borderTopRightRadius={24}
|
borderTopRightRadius={24}
|
||||||
backgroundColor={surface}
|
backgroundColor={surface}
|
||||||
padding="$4"
|
padding="$4"
|
||||||
paddingBottom="$7"
|
paddingBottom="$7"
|
||||||
space="$3"
|
|
||||||
shadowColor={shadow}
|
shadowColor={shadow}
|
||||||
shadowOpacity={0.12}
|
shadowOpacity={0.12}
|
||||||
shadowRadius={18}
|
shadowRadius={18}
|
||||||
shadowOffset={{ width: 0, height: -8 }}
|
shadowOffset={{ width: 0, height: -8 }}
|
||||||
maxHeight="82vh"
|
style={{ marginBottom: bottomOffset }}
|
||||||
overflow="auto"
|
|
||||||
// keep sheet above bottom nav / safe area
|
|
||||||
style={{ marginBottom: `max(env(safe-area-inset-bottom, 0px), ${bottomOffsetPx}px)` }}
|
|
||||||
>
|
>
|
||||||
<XStack alignItems="center" justifyContent="space-between">
|
<Sheet.Handle height={5} width={48} backgroundColor={border} borderRadius={999} marginBottom="$3" />
|
||||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
<Sheet.ScrollView
|
||||||
{title}
|
showsVerticalScrollIndicator={false}
|
||||||
</Text>
|
contentContainerStyle={{ paddingBottom: 6 }}
|
||||||
<Pressable onPress={onClose}>
|
>
|
||||||
<Text fontSize="$md" color={muted}>
|
<YStack space="$3">
|
||||||
{t('actions.close', 'Close')}
|
<XStack alignItems="center" justifyContent="space-between">
|
||||||
</Text>
|
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||||
</Pressable>
|
{title}
|
||||||
</XStack>
|
</Text>
|
||||||
{children}
|
<Pressable onPress={onClose}>
|
||||||
{footer ? footer : null}
|
<Text fontSize="$md" color={muted}>
|
||||||
</YStack>
|
{t('actions.close', 'Close')}
|
||||||
</div>
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</XStack>
|
||||||
|
{children}
|
||||||
|
{footer ? footer : null}
|
||||||
|
</YStack>
|
||||||
|
</Sheet.ScrollView>
|
||||||
|
</Sheet.Frame>
|
||||||
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ vi.mock('@tamagui/react-native-web-lite', () => ({
|
|||||||
Pressable: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
Pressable: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/sheet', () => {
|
||||||
|
const Sheet = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Frame = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Overlay = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.ScrollView = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Sheet.Handle = () => <div />;
|
||||||
|
return { Sheet };
|
||||||
|
});
|
||||||
|
|
||||||
import { LegalConsentSheet } from '../LegalConsentSheet';
|
import { LegalConsentSheet } from '../LegalConsentSheet';
|
||||||
|
|
||||||
describe('LegalConsentSheet', () => {
|
describe('LegalConsentSheet', () => {
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ describe('resolveOnboardingRedirect', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns null for event creation path', () => {
|
||||||
|
const result = resolveOnboardingRedirect({
|
||||||
|
hasEvents: false,
|
||||||
|
hasActivePackage: true,
|
||||||
|
selectedPackageId: null,
|
||||||
|
pathname: '/event-admin/mobile/events/new',
|
||||||
|
isWelcomePath: false,
|
||||||
|
isBillingPath: false,
|
||||||
|
isOnboardingDismissed: false,
|
||||||
|
isOnboardingCompleted: false,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('redirects to event setup when package active', () => {
|
it('redirects to event setup when package active', () => {
|
||||||
const result = resolveOnboardingRedirect({
|
const result = resolveOnboardingRedirect({
|
||||||
hasEvents: false,
|
hasEvents: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ADMIN_EVENT_CREATE_PATH,
|
||||||
ADMIN_WELCOME_BASE_PATH,
|
ADMIN_WELCOME_BASE_PATH,
|
||||||
ADMIN_WELCOME_EVENT_PATH,
|
ADMIN_WELCOME_EVENT_PATH,
|
||||||
ADMIN_WELCOME_SUMMARY_PATH,
|
ADMIN_WELCOME_SUMMARY_PATH,
|
||||||
@@ -37,6 +38,10 @@ export function resolveOnboardingRedirect({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith(ADMIN_EVENT_CREATE_PATH)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const shouldContinueSummary = Boolean(selectedPackageId && selectedPackageId > 0);
|
const shouldContinueSummary = Boolean(selectedPackageId && selectedPackageId > 0);
|
||||||
const target = hasActivePackage
|
const target = hasActivePackage
|
||||||
? ADMIN_WELCOME_EVENT_PATH
|
? ADMIN_WELCOME_EVENT_PATH
|
||||||
|
|||||||
Reference in New Issue
Block a user