upgrade to tamagui v2 and guest pwa overhaul
This commit is contained in:
237
resources/js/guest-v2/screens/TaskDetailScreen.tsx
Normal file
237
resources/js/guest-v2/screens/TaskDetailScreen.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Button } from '@tamagui/button';
|
||||
import { Sparkles, ChevronLeft, Camera } from 'lucide-react';
|
||||
import AppShell from '../components/AppShell';
|
||||
import SurfaceCard from '../components/SurfaceCard';
|
||||
import { fetchTasks, type TaskItem } from '../services/tasksApi';
|
||||
import { useEventData } from '../context/EventDataContext';
|
||||
import { buildEventPath } from '../lib/routes';
|
||||
import { useTranslation } from '@/guest/i18n/useTranslation';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
|
||||
function getTaskValue(task: TaskItem, key: string): string | undefined {
|
||||
const value = task?.[key as keyof TaskItem];
|
||||
if (typeof value === 'string' && value.trim() !== '') return value;
|
||||
if (value && typeof value === 'object') {
|
||||
const obj = value as Record<string, unknown>;
|
||||
const candidate = Object.values(obj).find((item) => typeof item === 'string' && item.trim() !== '');
|
||||
if (typeof candidate === 'string') return candidate;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getTaskList(task: TaskItem, key: string): string[] {
|
||||
const value = task?.[key as keyof TaskItem];
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item) => typeof item === 'string' && item.trim() !== '') as string[];
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
return [value];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export default function TaskDetailScreen() {
|
||||
const { token } = useEventData();
|
||||
const { taskId } = useParams<{ taskId: string }>();
|
||||
const { t, locale } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { resolved } = useAppearance();
|
||||
const isDark = resolved === 'dark';
|
||||
const mutedText = isDark ? 'rgba(248, 250, 252, 0.7)' : 'rgba(15, 23, 42, 0.65)';
|
||||
const [task, setTask] = React.useState<TaskItem | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
if (!token || !taskId) {
|
||||
setLoading(false);
|
||||
setTask(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchTasks(token, { locale })
|
||||
.then((tasks) => {
|
||||
if (!active) return;
|
||||
const match = tasks.find((item) => {
|
||||
const id = item?.id ?? item?.task_id ?? item?.slug;
|
||||
return String(id) === String(taskId);
|
||||
}) ?? null;
|
||||
setTask(match);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!active) return;
|
||||
setError(err instanceof Error ? err.message : t('tasks.error', 'Tasks could not be loaded.'));
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [locale, taskId, t, token]);
|
||||
|
||||
const title = task ? (getTaskValue(task, 'title') ?? getTaskValue(task, 'name') ?? 'Task') : 'Task';
|
||||
const description = task ? (getTaskValue(task, 'description') ?? getTaskValue(task, 'prompt') ?? '') : '';
|
||||
const tips = task ? getTaskList(task, 'tips') : [];
|
||||
const steps = task ? getTaskList(task, 'steps') : [];
|
||||
const inspiration = task ? getTaskList(task, 'inspiration') : [];
|
||||
const duration = task ? (task.duration ?? task.time_limit ?? null) : null;
|
||||
const groupSize = task ? (task.group_size ?? task.groupSize ?? null) : null;
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<YStack gap="$4">
|
||||
<SurfaceCard glow>
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<Sparkles size={20} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
<Text fontSize="$4" fontWeight="$7">
|
||||
{t('tasks.page.title', 'Your next task')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Button
|
||||
size="$3"
|
||||
borderRadius="$pill"
|
||||
backgroundColor={isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)'}
|
||||
borderWidth={1}
|
||||
borderColor={isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)'}
|
||||
onPress={() => navigate(-1)}
|
||||
>
|
||||
<ChevronLeft size={16} color={isDark ? '#F8FAFF' : '#0F172A'} />
|
||||
</Button>
|
||||
</XStack>
|
||||
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||
{t('tasks.page.subtitle', 'Choose a mood or get surprised.')}
|
||||
</Text>
|
||||
</SurfaceCard>
|
||||
|
||||
{loading ? (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" color={mutedText}>
|
||||
{t('tasks.loading', 'Loading tasks...')}
|
||||
</Text>
|
||||
</SurfaceCard>
|
||||
) : error ? (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" fontWeight="$7">
|
||||
{t('tasks.page.emptyTitle', 'No matching task found')}
|
||||
</Text>
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{error}
|
||||
</Text>
|
||||
</SurfaceCard>
|
||||
) : task ? (
|
||||
<>
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$5" fontWeight="$8">
|
||||
{title}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text fontSize="$3" color={mutedText} marginTop="$2">
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
<XStack gap="$2" flexWrap="wrap" marginTop="$3">
|
||||
{duration ? (
|
||||
<Button size="$3" borderRadius="$pill" backgroundColor={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(15,23,42,0.06)'}>
|
||||
<Text fontSize="$2" fontWeight="$6">{String(duration)} min</Text>
|
||||
</Button>
|
||||
) : null}
|
||||
{groupSize ? (
|
||||
<Button size="$3" borderRadius="$pill" backgroundColor={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(15,23,42,0.06)'}>
|
||||
<Text fontSize="$2" fontWeight="$6">{String(groupSize)} guests</Text>
|
||||
</Button>
|
||||
) : null}
|
||||
</XStack>
|
||||
<Button
|
||||
size="$4"
|
||||
borderRadius="$pill"
|
||||
backgroundColor="$primary"
|
||||
marginTop="$4"
|
||||
width="100%"
|
||||
justifyContent="center"
|
||||
onPress={() => {
|
||||
if (!taskId) {
|
||||
navigate(buildEventPath(token, '/upload'));
|
||||
return;
|
||||
}
|
||||
const target = buildEventPath(token, `/upload?taskId=${encodeURIComponent(taskId)}`);
|
||||
navigate(target);
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center" justifyContent="center" gap="$2" width="100%">
|
||||
<Camera size={18} color="white" />
|
||||
<Text fontSize="$3" fontWeight="$7" color="white">
|
||||
{t('tasks.page.ctaStart', "Let's go!")}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Button>
|
||||
</SurfaceCard>
|
||||
{steps.length > 0 ? (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" fontWeight="$7">
|
||||
{t('tasks.page.checklist', 'Checklist')}
|
||||
</Text>
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{steps.map((step, index) => (
|
||||
<XStack key={`${step}-${index}`} alignItems="flex-start" gap="$2">
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
{index + 1}.
|
||||
</Text>
|
||||
<Text fontSize="$3">{step}</Text>
|
||||
</XStack>
|
||||
))}
|
||||
</YStack>
|
||||
</SurfaceCard>
|
||||
) : null}
|
||||
{tips.length > 0 ? (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" fontWeight="$7">
|
||||
{t('tasks.page.tips', 'Tips')}
|
||||
</Text>
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{tips.map((tip, index) => (
|
||||
<XStack key={`${tip}-${index}`} alignItems="flex-start" gap="$2">
|
||||
<Text fontSize="$2" color={mutedText}>
|
||||
•
|
||||
</Text>
|
||||
<Text fontSize="$3">{tip}</Text>
|
||||
</XStack>
|
||||
))}
|
||||
</YStack>
|
||||
</SurfaceCard>
|
||||
) : null}
|
||||
{inspiration.length > 0 ? (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" fontWeight="$7">
|
||||
{t('tasks.page.inspirationTitleSecondary', 'Inspiration')}
|
||||
</Text>
|
||||
<YStack gap="$2" marginTop="$2">
|
||||
{inspiration.map((item, index) => (
|
||||
<Text key={`${item}-${index}`} fontSize="$3" color={mutedText}>
|
||||
{item}
|
||||
</Text>
|
||||
))}
|
||||
</YStack>
|
||||
</SurfaceCard>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<SurfaceCard>
|
||||
<Text fontSize="$3" color={mutedText}>
|
||||
{t('tasks.page.emptyTitle', 'No matching task found')}
|
||||
</Text>
|
||||
</SurfaceCard>
|
||||
)}
|
||||
</YStack>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user