diff --git a/resources/js/guest-v2/__tests__/TaskDetailScreen.test.tsx b/resources/js/guest-v2/__tests__/TaskDetailScreen.test.tsx index 79a9b5a1..bf56f5eb 100644 --- a/resources/js/guest-v2/__tests__/TaskDetailScreen.test.tsx +++ b/resources/js/guest-v2/__tests__/TaskDetailScreen.test.tsx @@ -57,4 +57,14 @@ describe('TaskDetailScreen', () => { expect(await screen.findByText('Capture the dancefloor')).toBeInTheDocument(); }); + + it('shows disabled state when tasks are disabled', () => { + render( + + + + ); + + expect(screen.getByText('Tasks are disabled')).toBeInTheDocument(); + }); }); diff --git a/resources/js/guest-v2/screens/AchievementsScreen.tsx b/resources/js/guest-v2/screens/AchievementsScreen.tsx index 11325ae8..cf3c9365 100644 --- a/resources/js/guest-v2/screens/AchievementsScreen.tsx +++ b/resources/js/guest-v2/screens/AchievementsScreen.tsx @@ -497,7 +497,9 @@ export default function AchievementsScreen() { {[ { label: t('achievements.summary.photosShared', 'Photos shared'), value: summary.totalPhotos }, - { label: t('achievements.summary.tasksCompleted', 'Tasks completed'), value: summary.tasksSolved }, + ...(tasksEnabled + ? [{ label: t('achievements.summary.tasksCompleted', 'Tasks completed'), value: summary.tasksSolved }] + : []), { label: t('achievements.summary.likesCollected', 'Likes collected'), value: summary.likesTotal }, { label: t('achievements.summary.uniqueGuests', 'Guests involved'), value: summary.uniqueGuests }, ].map((item) => ( diff --git a/resources/js/guest-v2/screens/TaskDetailScreen.tsx b/resources/js/guest-v2/screens/TaskDetailScreen.tsx index b144c71d..050ca8a7 100644 --- a/resources/js/guest-v2/screens/TaskDetailScreen.tsx +++ b/resources/js/guest-v2/screens/TaskDetailScreen.tsx @@ -35,7 +35,7 @@ function getTaskList(task: TaskItem, key: string): string[] { } export default function TaskDetailScreen() { - const { token } = useEventData(); + const { token, tasksEnabled } = useEventData(); const { taskId } = useParams<{ taskId: string }>(); const { t, locale } = useTranslation(); const navigate = useNavigate(); @@ -45,6 +45,37 @@ export default function TaskDetailScreen() { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); + if (!tasksEnabled) { + return ( + + + + + {t('tasks.disabled.title', 'Tasks are disabled')} + + + {t('tasks.disabled.subtitle', 'This event is set to photo-only mode.')} + + + + + + ); + } + React.useEffect(() => { let active = true; if (!token || !taskId) { diff --git a/resources/js/guest-v2/screens/UploadScreen.tsx b/resources/js/guest-v2/screens/UploadScreen.tsx index 1ad9c75b..a3016ee2 100644 --- a/resources/js/guest-v2/screens/UploadScreen.tsx +++ b/resources/js/guest-v2/screens/UploadScreen.tsx @@ -31,7 +31,7 @@ function getTaskValue(task: TaskItem, key: string): string | undefined { } export default function UploadScreen() { - const { token, event } = useEventData(); + const { token, event, tasksEnabled } = useEventData(); const identity = useOptionalGuestIdentity(); const { items, add } = useUploadQueue(); const navigate = useNavigate(); @@ -73,7 +73,7 @@ export default function UploadScreen() { const sendingCount = items.filter((item) => item.status === 'uploading').length; const taskIdParam = searchParams.get('taskId'); const parsedTaskId = taskIdParam ? Number(taskIdParam) : NaN; - const taskId = Number.isFinite(parsedTaskId) ? parsedTaskId : undefined; + const taskId = tasksEnabled && Number.isFinite(parsedTaskId) ? parsedTaskId : undefined; const [task, setTask] = React.useState(null); const [taskLoading, setTaskLoading] = React.useState(false); const [taskError, setTaskError] = React.useState(null); @@ -128,7 +128,7 @@ export default function UploadScreen() { React.useEffect(() => { let active = true; - if (!token || !taskId) { + if (!token || !taskId || !tasksEnabled) { setTask(null); setTaskLoading(false); setTaskError(null); @@ -158,7 +158,7 @@ export default function UploadScreen() { return () => { active = false; }; - }, [locale, t, taskId, token]); + }, [locale, t, taskId, tasksEnabled, token]); const enqueueFile = React.useCallback( async (file: File) => { diff --git a/resources/js/guest/lib/engagement.ts b/resources/js/guest/lib/engagement.ts index a5e38fe6..ba3d0258 100644 --- a/resources/js/guest/lib/engagement.ts +++ b/resources/js/guest/lib/engagement.ts @@ -3,6 +3,6 @@ import type { EventData } from '../services/eventApi'; export function isTaskModeEnabled(event?: EventData | null): boolean { if (!event) return true; const mode = event.engagement_mode; - if (mode === 'photo_only') return false; - return true; + if (!mode) return true; + return mode === 'tasks'; } diff --git a/resources/js/guest/services/eventApi.ts b/resources/js/guest/services/eventApi.ts index e87c7d14..8d2aef94 100644 --- a/resources/js/guest/services/eventApi.ts +++ b/resources/js/guest/services/eventApi.ts @@ -55,7 +55,7 @@ export interface EventData { slug: string; name: string; default_locale: string; - engagement_mode?: 'tasks' | 'photo_only'; + engagement_mode?: 'tasks' | 'photo_only' | 'no_tasks'; created_at: string; updated_at: string; join_token?: string | null; @@ -283,7 +283,7 @@ export async function fetchEvent(eventKey: string): Promise { default_locale: typeof json?.default_locale === 'string' && json.default_locale.trim() !== '' ? json.default_locale : DEFAULT_LOCALE, - engagement_mode: (json?.engagement_mode as 'tasks' | 'photo_only' | undefined) ?? 'tasks', + engagement_mode: (json?.engagement_mode as 'tasks' | 'photo_only' | 'no_tasks' | undefined) ?? 'tasks', guest_upload_visibility: json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review', live_show: { diff --git a/resources/js/setupTests.ts b/resources/js/setupTests.ts index ba7daeb1..71f1f806 100644 --- a/resources/js/setupTests.ts +++ b/resources/js/setupTests.ts @@ -1,6 +1,29 @@ import '@testing-library/jest-dom'; import { vi } from 'vitest'; +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; +const suppressedConsolePatterns = [ + /React does not recognize the `.*` prop on a DOM element/i, + /Unknown event handler property `onPress`/i, + /non-boolean attribute/i, + /not wrapped in act/i, + /Lightbox photo load failed/i, +]; + +const shouldSuppressConsole = (message?: unknown) => + typeof message === 'string' && suppressedConsolePatterns.some((pattern) => pattern.test(message)); + +console.error = (...args: unknown[]) => { + if (shouldSuppressConsole(args[0])) return; + originalConsoleError(...args); +}; + +console.warn = (...args: unknown[]) => { + if (shouldSuppressConsole(args[0])) return; + originalConsoleWarn(...args); +}; + vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next'); return {