Das Abschalten des Aufgaben-Modus wird nun sauber in der App reflektiert- die UI passt sich an und der Admin erhält einen Hinweis, dass die Aufgabenverwaltung nicht verfügbar ist
This commit is contained in:
@@ -659,6 +659,25 @@ class EventPublicController extends BaseController
|
|||||||
return $trimmed;
|
return $trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function normalizeSettings(array|string|null $settings): array
|
||||||
|
{
|
||||||
|
if (is_array($settings)) {
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($settings)) {
|
||||||
|
$decoded = json_decode($settings, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
private function buildDeterministicSeed(?string $identifier): int
|
private function buildDeterministicSeed(?string $identifier): int
|
||||||
{
|
{
|
||||||
if ($identifier === null || trim($identifier) === '') {
|
if ($identifier === null || trim($identifier) === '') {
|
||||||
@@ -1757,6 +1776,8 @@ class EventPublicController extends BaseController
|
|||||||
];
|
];
|
||||||
|
|
||||||
$branding = $this->buildGalleryBranding($event);
|
$branding = $this->buildGalleryBranding($event);
|
||||||
|
$settings = $this->normalizeSettings($event->settings ?? []);
|
||||||
|
$engagementMode = $settings['engagement_mode'] ?? 'tasks';
|
||||||
|
|
||||||
if ($joinToken) {
|
if ($joinToken) {
|
||||||
$this->joinTokenService->incrementUsage($joinToken);
|
$this->joinTokenService->incrementUsage($joinToken);
|
||||||
@@ -1774,6 +1795,7 @@ class EventPublicController extends BaseController
|
|||||||
'photobooth_enabled' => (bool) $event->photobooth_enabled,
|
'photobooth_enabled' => (bool) $event->photobooth_enabled,
|
||||||
'branding' => $branding,
|
'branding' => $branding,
|
||||||
'guest_upload_visibility' => Arr::get($event->settings ?? [], 'guest_upload_visibility', 'review'),
|
'guest_upload_visibility' => Arr::get($event->settings ?? [], 'guest_upload_visibility', 'review'),
|
||||||
|
'engagement_mode' => $engagementMode,
|
||||||
])->header('Cache-Control', 'no-store');
|
])->header('Cache-Control', 'no-store');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2365,7 +2387,7 @@ class EventPublicController extends BaseController
|
|||||||
|
|
||||||
public function stats(Request $request, string $token)
|
public function stats(Request $request, string $token)
|
||||||
{
|
{
|
||||||
$result = $this->resolvePublishedEvent($request, $token, ['id']);
|
$result = $this->resolvePublishedEvent($request, $token, ['id', 'settings']);
|
||||||
|
|
||||||
if ($result instanceof JsonResponse) {
|
if ($result instanceof JsonResponse) {
|
||||||
return $result;
|
return $result;
|
||||||
@@ -2374,6 +2396,8 @@ class EventPublicController extends BaseController
|
|||||||
[$event, $joinToken] = $result;
|
[$event, $joinToken] = $result;
|
||||||
$eventId = $event->id;
|
$eventId = $event->id;
|
||||||
$eventModel = Event::with('storageAssignments.storageTarget')->findOrFail($eventId);
|
$eventModel = Event::with('storageAssignments.storageTarget')->findOrFail($eventId);
|
||||||
|
$settings = $this->normalizeSettings($event->settings ?? null);
|
||||||
|
$engagementMode = $settings['engagement_mode'] ?? 'tasks';
|
||||||
|
|
||||||
// Approximate online guests as distinct recent uploaders in last 10 minutes.
|
// Approximate online guests as distinct recent uploaders in last 10 minutes.
|
||||||
$tenMinutesAgo = CarbonImmutable::now()->subMinutes(10);
|
$tenMinutesAgo = CarbonImmutable::now()->subMinutes(10);
|
||||||
@@ -2384,7 +2408,9 @@ class EventPublicController extends BaseController
|
|||||||
->count('guest_name');
|
->count('guest_name');
|
||||||
|
|
||||||
// Tasks solved as number of photos linked to a task (proxy metric).
|
// Tasks solved as number of photos linked to a task (proxy metric).
|
||||||
$tasksSolved = DB::table('photos')->where('event_id', $eventId)->whereNotNull('task_id')->count();
|
$tasksSolved = $engagementMode === 'photo_only'
|
||||||
|
? 0
|
||||||
|
: DB::table('photos')->where('event_id', $eventId)->whereNotNull('task_id')->count();
|
||||||
|
|
||||||
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
$latestPhotoAt = DB::table('photos')->where('event_id', $eventId)->max('created_at');
|
||||||
|
|
||||||
@@ -2392,6 +2418,7 @@ class EventPublicController extends BaseController
|
|||||||
'online_guests' => $onlineGuests,
|
'online_guests' => $onlineGuests,
|
||||||
'tasks_solved' => $tasksSolved,
|
'tasks_solved' => $tasksSolved,
|
||||||
'latest_photo_at' => $latestPhotoAt,
|
'latest_photo_at' => $latestPhotoAt,
|
||||||
|
'engagement_mode' => $engagementMode,
|
||||||
];
|
];
|
||||||
|
|
||||||
$etag = sha1(json_encode($payload));
|
$etag = sha1(json_encode($payload));
|
||||||
@@ -2472,7 +2499,7 @@ class EventPublicController extends BaseController
|
|||||||
|
|
||||||
public function tasks(Request $request, string $token)
|
public function tasks(Request $request, string $token)
|
||||||
{
|
{
|
||||||
$result = $this->resolvePublishedEvent($request, $token, ['id', 'default_locale']);
|
$result = $this->resolvePublishedEvent($request, $token, ['id', 'default_locale', 'settings']);
|
||||||
|
|
||||||
if ($result instanceof JsonResponse) {
|
if ($result instanceof JsonResponse) {
|
||||||
return $result;
|
return $result;
|
||||||
@@ -2480,8 +2507,35 @@ class EventPublicController extends BaseController
|
|||||||
|
|
||||||
[$event, $joinToken] = $result;
|
[$event, $joinToken] = $result;
|
||||||
|
|
||||||
|
$settings = $this->normalizeSettings($event->settings ?? null);
|
||||||
|
$engagementMode = $settings['engagement_mode'] ?? 'tasks';
|
||||||
|
|
||||||
[$resolvedLocale] = $this->resolveGuestLocale($request, $event);
|
[$resolvedLocale] = $this->resolveGuestLocale($request, $event);
|
||||||
|
|
||||||
|
$page = max(1, (int) $request->query('page', 1));
|
||||||
|
$perPage = max(1, min(100, (int) $request->query('per_page', 20)));
|
||||||
|
|
||||||
|
if ($engagementMode === 'photo_only') {
|
||||||
|
$payload = [
|
||||||
|
'data' => [],
|
||||||
|
'meta' => [
|
||||||
|
'total' => 0,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'current_page' => $page,
|
||||||
|
'last_page' => 1,
|
||||||
|
'has_more' => false,
|
||||||
|
'seed' => null,
|
||||||
|
],
|
||||||
|
'engagement_mode' => $engagementMode,
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($payload)
|
||||||
|
->header('Cache-Control', 'public, max-age=120')
|
||||||
|
->header('Vary', 'Accept-Language, X-Locale')
|
||||||
|
->header('X-Content-Locale', $resolvedLocale)
|
||||||
|
->header('X-Engagement-Mode', $engagementMode);
|
||||||
|
}
|
||||||
|
|
||||||
$cached = $this->eventTasksCache->remember((int) $event->id, $resolvedLocale, function () use ($event, $resolvedLocale) {
|
$cached = $this->eventTasksCache->remember((int) $event->id, $resolvedLocale, function () use ($event, $resolvedLocale) {
|
||||||
return $this->buildLocalizedTasksPayload((int) $event->id, $resolvedLocale, $event->default_locale ?? null);
|
return $this->buildLocalizedTasksPayload((int) $event->id, $resolvedLocale, $event->default_locale ?? null);
|
||||||
});
|
});
|
||||||
@@ -2489,9 +2543,6 @@ class EventPublicController extends BaseController
|
|||||||
$tasks = $cached['tasks'];
|
$tasks = $cached['tasks'];
|
||||||
$baseHash = $cached['hash'] ?? sha1(json_encode($tasks));
|
$baseHash = $cached['hash'] ?? sha1(json_encode($tasks));
|
||||||
|
|
||||||
$page = max(1, (int) $request->query('page', 1));
|
|
||||||
$perPage = max(1, min(100, (int) $request->query('per_page', 20)));
|
|
||||||
|
|
||||||
// Shuffle per request for unpredictability; stable when seeded by guest/device or explicit seed.
|
// Shuffle per request for unpredictability; stable when seeded by guest/device or explicit seed.
|
||||||
$seedParam = $request->query('seed');
|
$seedParam = $request->query('seed');
|
||||||
$guestIdentifier = $this->determineGuestIdentifier($request);
|
$guestIdentifier = $this->determineGuestIdentifier($request);
|
||||||
|
|||||||
@@ -1699,6 +1699,11 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Event sofort veröffentlichen",
|
"label": "Event sofort veröffentlichen",
|
||||||
"help": "Aktiviere diese Option, wenn Gäste das Event direkt sehen sollen. Du kannst den Status später ändern."
|
"help": "Aktiviere diese Option, wenn Gäste das Event direkt sehen sollen. Du kannst den Status später ändern."
|
||||||
|
},
|
||||||
|
"tasksMode": {
|
||||||
|
"label": "Tasks & Challenges",
|
||||||
|
"helpOn": "Gäste sehen Aufgaben, Challenges und Achievements.",
|
||||||
|
"helpOff": "Task-Modus aus: Gäste sehen nur den Fotofeed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -1781,6 +1786,8 @@
|
|||||||
"saveFailed": "Task konnte nicht gespeichert werden."
|
"saveFailed": "Task konnte nicht gespeichert werden."
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"disabledTitle": "Task-Modus ist für dieses Event aus",
|
||||||
|
"disabledBody": "Gäste sehen nur den Fotofeed. Aktiviere Tasks in den Event-Einstellungen, um sie wieder anzuzeigen.",
|
||||||
"title": "Tasks & Checklisten",
|
"title": "Tasks & Checklisten",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"assigned": "Task hinzugefügt",
|
"assigned": "Task hinzugefügt",
|
||||||
@@ -1859,6 +1866,7 @@
|
|||||||
"photosDesc": "Uploads und Highlights moderieren",
|
"photosDesc": "Uploads und Highlights moderieren",
|
||||||
"tasksLabel": "Tasks & Challenges verwalten",
|
"tasksLabel": "Tasks & Challenges verwalten",
|
||||||
"tasksDesc": "Zuweisen und Fortschritt verfolgen",
|
"tasksDesc": "Zuweisen und Fortschritt verfolgen",
|
||||||
|
"tasksDisabledDesc": "Tasks werden Gästen nicht angezeigt (Task-Modus aus)",
|
||||||
"qrLabel": "QR-Code anzeigen/teilen",
|
"qrLabel": "QR-Code anzeigen/teilen",
|
||||||
"qrDesc": "Poster, Karten und Links",
|
"qrDesc": "Poster, Karten und Links",
|
||||||
"shortcutsTitle": "Shortcuts",
|
"shortcutsTitle": "Shortcuts",
|
||||||
|
|||||||
@@ -1719,6 +1719,11 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Publish immediately",
|
"label": "Publish immediately",
|
||||||
"help": "Enable if guests should see the event right away. You can change the status later."
|
"help": "Enable if guests should see the event right away. You can change the status later."
|
||||||
|
},
|
||||||
|
"tasksMode": {
|
||||||
|
"label": "Tasks & challenges",
|
||||||
|
"helpOn": "Guests can see tasks, challenges and achievements.",
|
||||||
|
"helpOff": "Task mode is off: guests only see the photo feed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -1801,6 +1806,8 @@
|
|||||||
"saveFailed": "Task could not be saved."
|
"saveFailed": "Task could not be saved."
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"disabledTitle": "Task mode is off for this event",
|
||||||
|
"disabledBody": "Guests only see the photo feed. Enable tasks in the event settings to show them again.",
|
||||||
"title": "Tasks & checklists",
|
"title": "Tasks & checklists",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"assigned": "Task added",
|
"assigned": "Task added",
|
||||||
@@ -1879,6 +1886,7 @@
|
|||||||
"photosDesc": "Moderate uploads and highlights",
|
"photosDesc": "Moderate uploads and highlights",
|
||||||
"tasksLabel": "Manage tasks & challenges",
|
"tasksLabel": "Manage tasks & challenges",
|
||||||
"tasksDesc": "Assign and track progress",
|
"tasksDesc": "Assign and track progress",
|
||||||
|
"tasksDisabledDesc": "Guests do not see tasks (task mode off)",
|
||||||
"qrLabel": "Show / share QR code",
|
"qrLabel": "Show / share QR code",
|
||||||
"qrDesc": "Posters, cards, and links",
|
"qrDesc": "Posters, cards, and links",
|
||||||
"shortcutsTitle": "Shortcuts",
|
"shortcutsTitle": "Shortcuts",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge } from './compone
|
|||||||
import { adminPath } from '../constants';
|
import { adminPath } from '../constants';
|
||||||
import { useEventContext } from '../context/EventContext';
|
import { useEventContext } from '../context/EventContext';
|
||||||
import { getEventStats, EventStats, TenantEvent, getEvents } from '../api';
|
import { getEventStats, EventStats, TenantEvent, getEvents } from '../api';
|
||||||
import { formatEventDate, resolveEventDisplayName } from '../lib/events';
|
import { formatEventDate, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
||||||
import { useTheme } from '@tamagui/core';
|
import { useTheme } from '@tamagui/core';
|
||||||
|
|
||||||
export default function MobileDashboardPage() {
|
export default function MobileDashboardPage() {
|
||||||
@@ -37,6 +37,8 @@ export default function MobileDashboardPage() {
|
|||||||
return await getEventStats(activeEvent.slug);
|
return await getEventStats(activeEvent.slug);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const tasksEnabled =
|
||||||
|
resolveEngagementMode(activeEvent ?? undefined) !== 'photo_only';
|
||||||
|
|
||||||
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||||
const { data: dashboardEvents } = useQuery<TenantEvent[]>({
|
const { data: dashboardEvents } = useQuery<TenantEvent[]>({
|
||||||
@@ -107,6 +109,7 @@ export default function MobileDashboardPage() {
|
|||||||
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
|
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
|
||||||
>
|
>
|
||||||
<FeaturedActions
|
<FeaturedActions
|
||||||
|
tasksEnabled={tasksEnabled}
|
||||||
onReviewPhotos={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/photos`))}
|
onReviewPhotos={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/photos`))}
|
||||||
onManageTasks={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/tasks`))}
|
onManageTasks={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/tasks`))}
|
||||||
onShowQr={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/qr`))}
|
onShowQr={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/qr`))}
|
||||||
@@ -120,9 +123,15 @@ export default function MobileDashboardPage() {
|
|||||||
onSettings={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}`))}
|
onSettings={() => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}`))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<KpiStrip event={activeEvent} stats={stats} loading={statsLoading} locale={locale} />
|
<KpiStrip
|
||||||
|
event={activeEvent}
|
||||||
|
stats={stats}
|
||||||
|
loading={statsLoading}
|
||||||
|
locale={locale}
|
||||||
|
tasksEnabled={tasksEnabled}
|
||||||
|
/>
|
||||||
|
|
||||||
<AlertsAndHints event={activeEvent} stats={stats} />
|
<AlertsAndHints event={activeEvent} stats={stats} tasksEnabled={tasksEnabled} />
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -370,10 +379,12 @@ function EventPickerList({ events, locale, text, muted, border }: { events: Tena
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FeaturedActions({
|
function FeaturedActions({
|
||||||
|
tasksEnabled,
|
||||||
onReviewPhotos,
|
onReviewPhotos,
|
||||||
onManageTasks,
|
onManageTasks,
|
||||||
onShowQr,
|
onShowQr,
|
||||||
}: {
|
}: {
|
||||||
|
tasksEnabled: boolean;
|
||||||
onReviewPhotos: () => void;
|
onReviewPhotos: () => void;
|
||||||
onManageTasks: () => void;
|
onManageTasks: () => void;
|
||||||
onShowQr: () => void;
|
onShowQr: () => void;
|
||||||
@@ -394,7 +405,9 @@ function FeaturedActions({
|
|||||||
{
|
{
|
||||||
key: 'tasks',
|
key: 'tasks',
|
||||||
label: t('mobileDashboard.tasksLabel', 'Manage tasks & challenges'),
|
label: t('mobileDashboard.tasksLabel', 'Manage tasks & challenges'),
|
||||||
desc: t('mobileDashboard.tasksDesc', 'Assign and track progress'),
|
desc: tasksEnabled
|
||||||
|
? t('mobileDashboard.tasksDesc', 'Assign and track progress')
|
||||||
|
: t('mobileDashboard.tasksDisabledDesc', 'Guests do not see tasks (task mode off)'),
|
||||||
icon: ListTodo,
|
icon: ListTodo,
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
action: onManageTasks,
|
action: onManageTasks,
|
||||||
@@ -522,7 +535,19 @@ function SecondaryGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null; stats: EventStats | null | undefined; loading: boolean; locale: string }) {
|
function KpiStrip({
|
||||||
|
event,
|
||||||
|
stats,
|
||||||
|
loading,
|
||||||
|
locale,
|
||||||
|
tasksEnabled,
|
||||||
|
}: {
|
||||||
|
event: TenantEvent | null;
|
||||||
|
stats: EventStats | null | undefined;
|
||||||
|
loading: boolean;
|
||||||
|
locale: string;
|
||||||
|
tasksEnabled: boolean;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
||||||
@@ -530,11 +555,6 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
|
|||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
|
|
||||||
const kpis = [
|
const kpis = [
|
||||||
{
|
|
||||||
label: t('mobileDashboard.kpiTasks', 'Open tasks'),
|
|
||||||
value: event.tasks_count ?? '—',
|
|
||||||
icon: ListTodo,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('mobileDashboard.kpiPhotos', 'Photos'),
|
label: t('mobileDashboard.kpiPhotos', 'Photos'),
|
||||||
value: stats?.uploads_total ?? event.photo_count ?? '—',
|
value: stats?.uploads_total ?? event.photo_count ?? '—',
|
||||||
@@ -547,6 +567,14 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (tasksEnabled) {
|
||||||
|
kpis.unshift({
|
||||||
|
label: t('mobileDashboard.kpiTasks', 'Open tasks'),
|
||||||
|
value: event.tasks_count ?? '—',
|
||||||
|
icon: ListTodo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack space="$2">
|
<YStack space="$2">
|
||||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||||
@@ -572,7 +600,7 @@ function KpiStrip({ event, stats, loading, locale }: { event: TenantEvent | null
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertsAndHints({ event, stats }: { event: TenantEvent | null; stats: EventStats | null | undefined }) {
|
function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | null; stats: EventStats | null | undefined; tasksEnabled: boolean }) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
const text = String(theme.color12?.val ?? theme.color?.val ?? '#f8fafc');
|
||||||
@@ -585,7 +613,7 @@ function AlertsAndHints({ event, stats }: { event: TenantEvent | null; stats: Ev
|
|||||||
if (stats?.pending_photos) {
|
if (stats?.pending_photos) {
|
||||||
alerts.push(t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos }));
|
alerts.push(t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos }));
|
||||||
}
|
}
|
||||||
if (event.tasks_count) {
|
if (tasksEnabled && event.tasks_count) {
|
||||||
alerts.push(t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count }));
|
alerts.push(t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { isAuthError } from '../auth/tokens';
|
|||||||
import { getApiErrorMessage } from '../lib/apiError';
|
import { getApiErrorMessage } from '../lib/apiError';
|
||||||
import { MobileSheet } from './components/Sheet';
|
import { MobileSheet } from './components/Sheet';
|
||||||
import { useEventContext } from '../context/EventContext';
|
import { useEventContext } from '../context/EventContext';
|
||||||
import { formatEventDate, resolveEventDisplayName } from '../lib/events';
|
import { formatEventDate, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
||||||
import { isPastEvent } from './eventDate';
|
import { isPastEvent } from './eventDate';
|
||||||
|
|
||||||
export default function MobileEventDetailPage() {
|
export default function MobileEventDetailPage() {
|
||||||
@@ -66,12 +66,9 @@ export default function MobileEventDetailPage() {
|
|||||||
})();
|
})();
|
||||||
}, [slug, t]);
|
}, [slug, t]);
|
||||||
|
|
||||||
|
const tasksEnabled = resolveEngagementMode(event ?? activeEvent ?? null) !== 'photo_only';
|
||||||
|
|
||||||
const kpis = [
|
const kpis = [
|
||||||
{
|
|
||||||
label: t('events.detail.kpi.tasks', 'Active Tasks'),
|
|
||||||
value: event?.tasks_count ?? toolkit?.tasks?.summary?.total ?? '—',
|
|
||||||
icon: Sparkles,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('events.detail.kpi.guests', 'Guests Registered'),
|
label: t('events.detail.kpi.guests', 'Guests Registered'),
|
||||||
value: toolkit?.invites?.summary.total ?? event?.active_invites_count ?? '—',
|
value: toolkit?.invites?.summary.total ?? event?.active_invites_count ?? '—',
|
||||||
@@ -84,6 +81,14 @@ export default function MobileEventDetailPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (tasksEnabled) {
|
||||||
|
kpis.unshift({
|
||||||
|
label: t('events.detail.kpi.tasks', 'Active Tasks'),
|
||||||
|
value: event?.tasks_count ?? toolkit?.tasks?.summary?.total ?? '—',
|
||||||
|
icon: Sparkles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell
|
<MobileShell
|
||||||
activeTab="home"
|
activeTab="home"
|
||||||
@@ -217,7 +222,11 @@ export default function MobileEventDetailPage() {
|
|||||||
<XStack flexWrap="wrap" space="$2">
|
<XStack flexWrap="wrap" space="$2">
|
||||||
<ActionTile
|
<ActionTile
|
||||||
icon={Sparkles}
|
icon={Sparkles}
|
||||||
label={t('events.quick.tasks', 'Tasks & Checklists')}
|
label={
|
||||||
|
tasksEnabled
|
||||||
|
? t('events.quick.tasks', 'Tasks & Checklists')
|
||||||
|
: `${t('events.quick.tasks', 'Tasks & Checklists')} (${t('common:states.disabled', 'Disabled')})`
|
||||||
|
}
|
||||||
color="#60a5fa"
|
color="#60a5fa"
|
||||||
onPress={() => navigate(adminPath(`/mobile/events/${slug ?? ''}/tasks`))}
|
onPress={() => navigate(adminPath(`/mobile/events/${slug ?? ''}/tasks`))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type FormState = {
|
|||||||
location: string;
|
location: string;
|
||||||
published: boolean;
|
published: boolean;
|
||||||
autoApproveUploads: boolean;
|
autoApproveUploads: boolean;
|
||||||
|
tasksEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MobileEventFormPage() {
|
export default function MobileEventFormPage() {
|
||||||
@@ -37,6 +38,7 @@ export default function MobileEventFormPage() {
|
|||||||
location: '',
|
location: '',
|
||||||
published: false,
|
published: false,
|
||||||
autoApproveUploads: true,
|
autoApproveUploads: true,
|
||||||
|
tasksEnabled: true,
|
||||||
});
|
});
|
||||||
const [eventTypes, setEventTypes] = React.useState<TenantEventType[]>([]);
|
const [eventTypes, setEventTypes] = React.useState<TenantEventType[]>([]);
|
||||||
const [typesLoading, setTypesLoading] = React.useState(false);
|
const [typesLoading, setTypesLoading] = React.useState(false);
|
||||||
@@ -59,6 +61,9 @@ export default function MobileEventFormPage() {
|
|||||||
published: data.status === 'published',
|
published: data.status === 'published',
|
||||||
autoApproveUploads:
|
autoApproveUploads:
|
||||||
(data.settings?.guest_upload_visibility as string | undefined) === 'immediate',
|
(data.settings?.guest_upload_visibility as string | undefined) === 'immediate',
|
||||||
|
tasksEnabled:
|
||||||
|
(data.settings?.engagement_mode as string | undefined) !== 'photo_only' &&
|
||||||
|
(data.engagement_mode as string | undefined) !== 'photo_only',
|
||||||
});
|
});
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -102,6 +107,7 @@ export default function MobileEventFormPage() {
|
|||||||
settings: {
|
settings: {
|
||||||
location: form.location,
|
location: form.location,
|
||||||
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
|
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
|
||||||
|
engagement_mode: form.tasksEnabled ? 'tasks' : 'photo_only',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
navigate(adminPath(`/mobile/events/${slug}`));
|
navigate(adminPath(`/mobile/events/${slug}`));
|
||||||
@@ -115,6 +121,7 @@ export default function MobileEventFormPage() {
|
|||||||
settings: {
|
settings: {
|
||||||
location: form.location,
|
location: form.location,
|
||||||
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
|
guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review',
|
||||||
|
engagement_mode: form.tasksEnabled ? 'tasks' : 'photo_only',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const { event } = await createEvent(payload as any);
|
const { event } = await createEvent(payload as any);
|
||||||
@@ -228,6 +235,37 @@ export default function MobileEventFormPage() {
|
|||||||
<Text fontSize="$xs" color="#6b7280">{t('eventForm.fields.publish.help', 'Enable if guests should see the event right away. You can change the status later.')}</Text>
|
<Text fontSize="$xs" color="#6b7280">{t('eventForm.fields.publish.help', 'Enable if guests should see the event right away. You can change the status later.')}</Text>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label={t('eventForm.fields.tasksMode.label', 'Tasks & challenges')}>
|
||||||
|
<XStack alignItems="center" space="$2">
|
||||||
|
<Switch
|
||||||
|
checked={form.tasksEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) }))
|
||||||
|
}
|
||||||
|
size="$3"
|
||||||
|
aria-label={t('eventForm.fields.tasksMode.label', 'Tasks & challenges')}
|
||||||
|
>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch>
|
||||||
|
<Text fontSize="$sm" color="#111827">
|
||||||
|
{form.tasksEnabled
|
||||||
|
? t('common:states.enabled', 'Enabled')
|
||||||
|
: t('common:states.disabled', 'Disabled')}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
<Text fontSize="$xs" color="#6b7280">
|
||||||
|
{form.tasksEnabled
|
||||||
|
? t(
|
||||||
|
'eventForm.fields.tasksMode.helpOn',
|
||||||
|
'Guests can see tasks, challenges and achievements.',
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'eventForm.fields.tasksMode.helpOff',
|
||||||
|
'Task mode is off: guests only see the photo feed.',
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}>
|
<Field label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')}>
|
||||||
<XStack alignItems="center" space="$2">
|
<XStack alignItems="center" space="$2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Pressable } from '@tamagui/react-native-web-lite';
|
|||||||
import { MobileShell } from './components/MobileShell';
|
import { MobileShell } from './components/MobileShell';
|
||||||
import { MobileCard, CTAButton } from './components/Primitives';
|
import { MobileCard, CTAButton } from './components/Primitives';
|
||||||
import { useEventContext } from '../context/EventContext';
|
import { useEventContext } from '../context/EventContext';
|
||||||
import { formatEventDate, resolveEventDisplayName } from '../lib/events';
|
import { formatEventDate, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
|
||||||
import { adminPath } from '../constants';
|
import { adminPath } from '../constants';
|
||||||
import { useTheme } from '@tamagui/core';
|
import { useTheme } from '@tamagui/core';
|
||||||
|
|
||||||
@@ -20,11 +20,31 @@ export default function MobileTasksTabPage() {
|
|||||||
const muted = String(theme.gray?.val ?? '#4b5563');
|
const muted = String(theme.gray?.val ?? '#4b5563');
|
||||||
const border = String(theme.borderColor?.val ?? '#e5e7eb');
|
const border = String(theme.borderColor?.val ?? '#e5e7eb');
|
||||||
const primary = String(theme.primary?.val ?? '#007AFF');
|
const primary = String(theme.primary?.val ?? '#007AFF');
|
||||||
|
const tasksEnabled = resolveEngagementMode(activeEvent ?? null) !== 'photo_only';
|
||||||
|
|
||||||
if (activeEvent?.slug) {
|
if (activeEvent?.slug && tasksEnabled) {
|
||||||
return <Navigate to={adminPath(`/mobile/events/${activeEvent.slug}/tasks`)} replace />;
|
return <Navigate to={adminPath(`/mobile/events/${activeEvent.slug}/tasks`)} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeEvent?.slug && !tasksEnabled) {
|
||||||
|
return (
|
||||||
|
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Tasks')}>
|
||||||
|
<MobileCard alignItems="flex-start" space="$3">
|
||||||
|
<Text fontSize="$lg" fontWeight="800" color={text}>
|
||||||
|
{t('events.tasks.disabledTitle', 'Task mode is off for this event')}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$sm" color={muted}>
|
||||||
|
{t('events.tasks.disabledBody', 'Guests see only the photo feed. Enable tasks in the event settings to show them again.')}
|
||||||
|
</Text>
|
||||||
|
<CTAButton
|
||||||
|
label={t('events.actions.settings', 'Event settings')}
|
||||||
|
onPress={() => navigate(adminPath(`/mobile/events/${activeEvent.slug}/edit`))}
|
||||||
|
/>
|
||||||
|
</MobileCard>
|
||||||
|
</MobileShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasEvents) {
|
if (!hasEvents) {
|
||||||
return (
|
return (
|
||||||
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Tasks')}>
|
<MobileShell activeTab="tasks" title={t('events.tasks.title', 'Tasks')}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CheckSquare, GalleryHorizontal, Home, Trophy, Camera } from 'lucide-rea
|
|||||||
import { useEventData } from '../hooks/useEventData';
|
import { useEventData } from '../hooks/useEventData';
|
||||||
import { useTranslation } from '../i18n/useTranslation';
|
import { useTranslation } from '../i18n/useTranslation';
|
||||||
import { useEventBranding } from '../context/EventBrandingContext';
|
import { useEventBranding } from '../context/EventBrandingContext';
|
||||||
|
import { isTaskModeEnabled } from '../lib/engagement';
|
||||||
|
|
||||||
function TabLink({
|
function TabLink({
|
||||||
to,
|
to,
|
||||||
@@ -64,6 +65,7 @@ export default function BottomNav() {
|
|||||||
|
|
||||||
const base = `/e/${encodeURIComponent(token)}`;
|
const base = `/e/${encodeURIComponent(token)}`;
|
||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
|
const tasksEnabled = isTaskModeEnabled(event);
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
home: t('navigation.home'),
|
home: t('navigation.home'),
|
||||||
@@ -102,6 +104,7 @@ export default function BottomNav() {
|
|||||||
<span>{labels.home}</span>
|
<span>{labels.home}</span>
|
||||||
</div>
|
</div>
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
{tasksEnabled ? (
|
||||||
<TabLink
|
<TabLink
|
||||||
to={`${base}/tasks`}
|
to={`${base}/tasks`}
|
||||||
isActive={isTasksActive}
|
isActive={isTasksActive}
|
||||||
@@ -115,6 +118,7 @@ export default function BottomNav() {
|
|||||||
<span>{labels.tasks}</span>
|
<span>{labels.tasks}</span>
|
||||||
</div>
|
</div>
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useOptionalNotificationCenter, type NotificationCenterValue } from '../
|
|||||||
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
|
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
|
||||||
import { usePushSubscription } from '../hooks/usePushSubscription';
|
import { usePushSubscription } from '../hooks/usePushSubscription';
|
||||||
import { getContrastingTextColor, relativeLuminance } from '../lib/color';
|
import { getContrastingTextColor, relativeLuminance } from '../lib/color';
|
||||||
|
import { isTaskModeEnabled } from '../lib/engagement';
|
||||||
|
|
||||||
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
|
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
heart: Heart,
|
heart: Heart,
|
||||||
@@ -150,6 +151,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
|
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
|
||||||
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
|
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
|
||||||
const taskProgress = useGuestTaskProgress(eventToken);
|
const taskProgress = useGuestTaskProgress(eventToken);
|
||||||
|
const tasksEnabled = isTaskModeEnabled(event);
|
||||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!notificationsOpen) {
|
if (!notificationsOpen) {
|
||||||
@@ -220,7 +222,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
<div className="flex flex-col" style={headerFont ? { fontFamily: headerFont } : undefined}>
|
<div className="flex flex-col" style={headerFont ? { fontFamily: headerFont } : undefined}>
|
||||||
<div className="font-semibold text-lg">{event.name}</div>
|
<div className="font-semibold text-lg">{event.name}</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-white/70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
<div className="flex items-center gap-2 text-xs text-white/70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||||
{stats && (
|
{stats && tasksEnabled && (
|
||||||
<>
|
<>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<User className="h-3 w-3" />
|
<User className="h-3 w-3" />
|
||||||
@@ -244,7 +246,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
open={notificationsOpen}
|
open={notificationsOpen}
|
||||||
onToggle={() => setNotificationsOpen((prev) => !prev)}
|
onToggle={() => setNotificationsOpen((prev) => !prev)}
|
||||||
panelRef={panelRef}
|
panelRef={panelRef}
|
||||||
taskProgress={taskProgress?.hydrated ? taskProgress : undefined}
|
taskProgress={tasksEnabled && taskProgress?.hydrated ? taskProgress : undefined}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
8
resources/js/guest/lib/engagement.ts
Normal file
8
resources/js/guest/lib/engagement.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import { Sparkles, Award, Trophy, Camera, Users, BarChart2, Flame } from 'lucide
|
|||||||
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
||||||
import type { LocaleCode } from '../i18n/messages';
|
import type { LocaleCode } from '../i18n/messages';
|
||||||
import { localizeTaskLabel } from '../lib/localizeTaskLabel';
|
import { localizeTaskLabel } from '../lib/localizeTaskLabel';
|
||||||
|
import { useEventData } from '../hooks/useEventData';
|
||||||
|
import { isTaskModeEnabled } from '../lib/engagement';
|
||||||
|
|
||||||
const GENERIC_ERROR = 'GENERIC_ERROR';
|
const GENERIC_ERROR = 'GENERIC_ERROR';
|
||||||
|
|
||||||
@@ -311,9 +313,10 @@ function Highlights({ topPhoto, trendingEmotion, t, formatRelativeTime, locale,
|
|||||||
type PersonalActionsProps = {
|
type PersonalActionsProps = {
|
||||||
token: string;
|
token: string;
|
||||||
t: TranslateFn;
|
t: TranslateFn;
|
||||||
|
tasksEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function PersonalActions({ token, t }: PersonalActionsProps) {
|
function PersonalActions({ token, t, tasksEnabled }: PersonalActionsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
@@ -322,12 +325,14 @@ function PersonalActions({ token, t }: PersonalActionsProps) {
|
|||||||
{t('achievements.personal.actions.upload')}
|
{t('achievements.personal.actions.upload')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
{tasksEnabled ? (
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link to={`/e/${encodeURIComponent(token)}/tasks`} className="flex items-center gap-2">
|
<Link to={`/e/${encodeURIComponent(token)}/tasks`} className="flex items-center gap-2">
|
||||||
<Sparkles className="h-4 w-4" aria-hidden />
|
<Sparkles className="h-4 w-4" aria-hidden />
|
||||||
{t('achievements.personal.actions.tasks')}
|
{t('achievements.personal.actions.tasks')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -336,6 +341,8 @@ export default function AchievementsPage() {
|
|||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
const identity = useGuestIdentity();
|
const identity = useGuestIdentity();
|
||||||
const { t, locale } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
|
const { event } = useEventData();
|
||||||
|
const tasksEnabled = isTaskModeEnabled(event);
|
||||||
const [data, setData] = useState<AchievementsPayload | null>(null);
|
const [data, setData] = useState<AchievementsPayload | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -461,7 +468,7 @@ export default function AchievementsPage() {
|
|||||||
})}
|
})}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<PersonalActions token={token} t={t} />
|
<PersonalActions token={token} t={t} tasksEnabled={tasksEnabled} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { getEmotionIcon, getEmotionTheme, type EmotionIdentity } from '../lib/em
|
|||||||
import { getDeviceId } from '../lib/device';
|
import { getDeviceId } from '../lib/device';
|
||||||
import { useDirectUpload } from '../hooks/useDirectUpload';
|
import { useDirectUpload } from '../hooks/useDirectUpload';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { isTaskModeEnabled } from '../lib/engagement';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
@@ -72,6 +73,7 @@ export default function HomePage() {
|
|||||||
const secondaryAccent = branding.secondaryColor;
|
const secondaryAccent = branding.secondaryColor;
|
||||||
const uploadsRequireApproval =
|
const uploadsRequireApproval =
|
||||||
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
|
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
|
||||||
|
const tasksEnabled = isTaskModeEnabled(event);
|
||||||
|
|
||||||
const [missionDeck, setMissionDeck] = React.useState<MissionPreview[]>([]);
|
const [missionDeck, setMissionDeck] = React.useState<MissionPreview[]>([]);
|
||||||
const [missionPool, setMissionPool] = React.useState<MissionPreview[]>([]);
|
const [missionPool, setMissionPool] = React.useState<MissionPreview[]>([]);
|
||||||
@@ -232,8 +234,9 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
poolIndexRef.current = restoredIndex;
|
poolIndexRef.current = restoredIndex;
|
||||||
|
if (!tasksEnabled) return;
|
||||||
fetchTasksPage(1, true);
|
fetchTasksPage(1, true);
|
||||||
}, [fetchTasksPage, locale, sliderStateKey, token]);
|
}, [fetchTasksPage, locale, sliderStateKey, tasksEnabled, token]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (missionPool.length === 0) return;
|
if (missionPool.length === 0) return;
|
||||||
@@ -279,6 +282,34 @@ export default function HomePage() {
|
|||||||
const introMessage =
|
const introMessage =
|
||||||
introArray.length > 0 ? introArray[Math.floor(Math.random() * introArray.length)] : '';
|
introArray.length > 0 ? introArray[Math.floor(Math.random() * introArray.length)] : '';
|
||||||
|
|
||||||
|
if (!tasksEnabled) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 pb-24" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||||
|
<section className="space-y-1 px-4">
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{t('home.welcomeLine').replace('{name}', displayName)}
|
||||||
|
</p>
|
||||||
|
{introMessage && <p className="text-xs text-muted-foreground">{introMessage}</p>}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-2 px-4">
|
||||||
|
<UploadActionCard
|
||||||
|
token={token}
|
||||||
|
accentColor={accentColor}
|
||||||
|
secondaryAccent={secondaryAccent}
|
||||||
|
radius={radius}
|
||||||
|
bodyFont={bodyFont}
|
||||||
|
requiresApproval={uploadsRequireApproval}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<GalleryPreview token={token} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0.5 pb-24" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
<div className="space-y-0.5 pb-24" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||||
<section className="space-y-1 px-4">
|
<section className="space-y-1 px-4">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { useEventBranding } from '../context/EventBrandingContext';
|
|||||||
import { compressPhoto, formatBytes } from '../lib/image';
|
import { compressPhoto, formatBytes } from '../lib/image';
|
||||||
import { useGuestIdentity } from '../context/GuestIdentityContext';
|
import { useGuestIdentity } from '../context/GuestIdentityContext';
|
||||||
import { useEventData } from '../hooks/useEventData';
|
import { useEventData } from '../hooks/useEventData';
|
||||||
|
import { isTaskModeEnabled } from '../lib/engagement';
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -118,6 +119,8 @@ export default function UploadPage() {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { markCompleted } = useGuestTaskProgress(token);
|
const { markCompleted } = useGuestTaskProgress(token);
|
||||||
const identity = useGuestIdentity();
|
const identity = useGuestIdentity();
|
||||||
|
const { event } = useEventData();
|
||||||
|
const tasksEnabled = isTaskModeEnabled(event);
|
||||||
const { t, locale } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const stats = useEventStats();
|
const stats = useEventStats();
|
||||||
const { branding } = useEventBranding();
|
const { branding } = useEventBranding();
|
||||||
@@ -233,7 +236,7 @@ const [canUpload, setCanUpload] = useState(true);
|
|||||||
|
|
||||||
// Load task metadata
|
// Load task metadata
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token || taskId === null) {
|
if (!token || taskId === null || !tasksEnabled) {
|
||||||
setLoadingTask(false);
|
setLoadingTask(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { EventBranding } from './types/event-branding';
|
|||||||
import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi';
|
import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi';
|
||||||
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
||||||
import RouteErrorElement from '@/components/RouteErrorElement';
|
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||||
|
import { isTaskModeEnabled } from './lib/engagement';
|
||||||
|
|
||||||
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
||||||
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
||||||
@@ -75,8 +76,8 @@ export const router = createBrowserRouter([
|
|||||||
errorElement: <RouteErrorElement />,
|
errorElement: <RouteErrorElement />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <HomePage /> },
|
{ index: true, element: <HomePage /> },
|
||||||
{ path: 'tasks', element: <TaskPickerPage /> },
|
{ path: 'tasks', element: <TaskGuard><TaskPickerPage /></TaskGuard> },
|
||||||
{ path: 'tasks/:taskId', element: <TaskDetailPage /> },
|
{ path: 'tasks/:taskId', element: <TaskGuard><TaskDetailPage /></TaskGuard> },
|
||||||
{ path: 'upload', element: <UploadPage /> },
|
{ path: 'upload', element: <UploadPage /> },
|
||||||
{ path: 'queue', element: <UploadQueuePage /> },
|
{ path: 'queue', element: <UploadQueuePage /> },
|
||||||
{ path: 'gallery', element: <GalleryPage /> },
|
{ path: 'gallery', element: <GalleryPage /> },
|
||||||
@@ -133,6 +134,21 @@ function EventBoundary({ token }: { token: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TaskGuard({ children }: { children: React.ReactNode }) {
|
||||||
|
const { token } = useParams<{ token: string }>();
|
||||||
|
const { event, status } = useEventData();
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return <EventLoadingView />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event && !isTaskModeEnabled(event)) {
|
||||||
|
return <Navigate to={`/e/${encodeURIComponent(token ?? '')}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
function SetupLayout() {
|
function SetupLayout() {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
const { event } = useEventData();
|
const { event } = useEventData();
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export interface EventData {
|
|||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
|
engagement_mode?: 'tasks' | 'photo_only';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
join_token?: string | null;
|
join_token?: string | null;
|
||||||
@@ -266,6 +267,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',
|
||||||
guest_upload_visibility:
|
guest_upload_visibility:
|
||||||
json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review',
|
json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user