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 {