Handle no-tasks mode in guest v2
This commit is contained in:
@@ -57,4 +57,14 @@ describe('TaskDetailScreen', () => {
|
|||||||
|
|
||||||
expect(await screen.findByText('Capture the dancefloor')).toBeInTheDocument();
|
expect(await screen.findByText('Capture the dancefloor')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows disabled state when tasks are disabled', () => {
|
||||||
|
render(
|
||||||
|
<EventDataProvider token="token" tasksEnabledFallback={false}>
|
||||||
|
<TaskDetailScreen />
|
||||||
|
</EventDataProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Tasks are disabled')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -497,7 +497,9 @@ export default function AchievementsScreen() {
|
|||||||
<XStack gap="$2" flexWrap="nowrap" overflow="hidden">
|
<XStack gap="$2" flexWrap="nowrap" overflow="hidden">
|
||||||
{[
|
{[
|
||||||
{ label: t('achievements.summary.photosShared', 'Photos shared'), value: summary.totalPhotos },
|
{ 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.likesCollected', 'Likes collected'), value: summary.likesTotal },
|
||||||
{ label: t('achievements.summary.uniqueGuests', 'Guests involved'), value: summary.uniqueGuests },
|
{ label: t('achievements.summary.uniqueGuests', 'Guests involved'), value: summary.uniqueGuests },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function getTaskList(task: TaskItem, key: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskDetailScreen() {
|
export default function TaskDetailScreen() {
|
||||||
const { token } = useEventData();
|
const { token, tasksEnabled } = useEventData();
|
||||||
const { taskId } = useParams<{ taskId: string }>();
|
const { taskId } = useParams<{ taskId: string }>();
|
||||||
const { t, locale } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -45,6 +45,37 @@ export default function TaskDetailScreen() {
|
|||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!tasksEnabled) {
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<YStack gap="$4">
|
||||||
|
<SurfaceCard>
|
||||||
|
<Text fontSize="$4" fontWeight="$7">
|
||||||
|
{t('tasks.disabled.title', 'Tasks are disabled')}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$2" color={mutedText} marginTop="$2">
|
||||||
|
{t('tasks.disabled.subtitle', 'This event is set to photo-only mode.')}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="$4"
|
||||||
|
borderRadius="$pill"
|
||||||
|
backgroundColor="$primary"
|
||||||
|
marginTop="$4"
|
||||||
|
onPress={() => navigate(buildEventPath(token, '/upload'))}
|
||||||
|
>
|
||||||
|
<XStack alignItems="center" justifyContent="center" gap="$2" width="100%">
|
||||||
|
<Camera size={18} color="white" />
|
||||||
|
<Text fontSize="$3" fontWeight="$7" color="white">
|
||||||
|
{t('homeV2.captureReady.cta', 'Upload / Take photo')}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
</Button>
|
||||||
|
</SurfaceCard>
|
||||||
|
</YStack>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
if (!token || !taskId) {
|
if (!token || !taskId) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function getTaskValue(task: TaskItem, key: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadScreen() {
|
export default function UploadScreen() {
|
||||||
const { token, event } = useEventData();
|
const { token, event, tasksEnabled } = useEventData();
|
||||||
const identity = useOptionalGuestIdentity();
|
const identity = useOptionalGuestIdentity();
|
||||||
const { items, add } = useUploadQueue();
|
const { items, add } = useUploadQueue();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -73,7 +73,7 @@ export default function UploadScreen() {
|
|||||||
const sendingCount = items.filter((item) => item.status === 'uploading').length;
|
const sendingCount = items.filter((item) => item.status === 'uploading').length;
|
||||||
const taskIdParam = searchParams.get('taskId');
|
const taskIdParam = searchParams.get('taskId');
|
||||||
const parsedTaskId = taskIdParam ? Number(taskIdParam) : NaN;
|
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<TaskItem | null>(null);
|
const [task, setTask] = React.useState<TaskItem | null>(null);
|
||||||
const [taskLoading, setTaskLoading] = React.useState(false);
|
const [taskLoading, setTaskLoading] = React.useState(false);
|
||||||
const [taskError, setTaskError] = React.useState<string | null>(null);
|
const [taskError, setTaskError] = React.useState<string | null>(null);
|
||||||
@@ -128,7 +128,7 @@ export default function UploadScreen() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
if (!token || !taskId) {
|
if (!token || !taskId || !tasksEnabled) {
|
||||||
setTask(null);
|
setTask(null);
|
||||||
setTaskLoading(false);
|
setTaskLoading(false);
|
||||||
setTaskError(null);
|
setTaskError(null);
|
||||||
@@ -158,7 +158,7 @@ export default function UploadScreen() {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [locale, t, taskId, token]);
|
}, [locale, t, taskId, tasksEnabled, token]);
|
||||||
|
|
||||||
const enqueueFile = React.useCallback(
|
const enqueueFile = React.useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import type { EventData } from '../services/eventApi';
|
|||||||
export function isTaskModeEnabled(event?: EventData | null): boolean {
|
export function isTaskModeEnabled(event?: EventData | null): boolean {
|
||||||
if (!event) return true;
|
if (!event) return true;
|
||||||
const mode = event.engagement_mode;
|
const mode = event.engagement_mode;
|
||||||
if (mode === 'photo_only') return false;
|
if (!mode) return true;
|
||||||
return true;
|
return mode === 'tasks';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export interface EventData {
|
|||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
engagement_mode?: 'tasks' | 'photo_only';
|
engagement_mode?: 'tasks' | 'photo_only' | 'no_tasks';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
join_token?: string | null;
|
join_token?: string | null;
|
||||||
@@ -283,7 +283,7 @@ export async function fetchEvent(eventKey: string): Promise<EventData> {
|
|||||||
default_locale: typeof json?.default_locale === 'string' && json.default_locale.trim() !== ''
|
default_locale: typeof json?.default_locale === 'string' && json.default_locale.trim() !== ''
|
||||||
? json.default_locale
|
? json.default_locale
|
||||||
: 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:
|
guest_upload_visibility:
|
||||||
json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review',
|
json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review',
|
||||||
live_show: {
|
live_show: {
|
||||||
|
|||||||
@@ -1,6 +1,29 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { vi } from 'vitest';
|
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 () => {
|
vi.mock('react-i18next', async () => {
|
||||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user