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:
@@ -4,6 +4,7 @@ import { CheckSquare, GalleryHorizontal, Home, Trophy, Camera } from 'lucide-rea
|
||||
import { useEventData } from '../hooks/useEventData';
|
||||
import { useTranslation } from '../i18n/useTranslation';
|
||||
import { useEventBranding } from '../context/EventBrandingContext';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
|
||||
function TabLink({
|
||||
to,
|
||||
@@ -64,6 +65,7 @@ export default function BottomNav() {
|
||||
|
||||
const base = `/e/${encodeURIComponent(token)}`;
|
||||
const currentPath = location.pathname;
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
|
||||
const labels = {
|
||||
home: t('navigation.home'),
|
||||
@@ -102,19 +104,21 @@ export default function BottomNav() {
|
||||
<span>{labels.home}</span>
|
||||
</div>
|
||||
</TabLink>
|
||||
<TabLink
|
||||
to={`${base}/tasks`}
|
||||
isActive={isTasksActive}
|
||||
accentColor={branding.primaryColor}
|
||||
radius={radius}
|
||||
compact={compact}
|
||||
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<CheckSquare className="h-5 w-5" aria-hidden />
|
||||
<span>{labels.tasks}</span>
|
||||
</div>
|
||||
</TabLink>
|
||||
{tasksEnabled ? (
|
||||
<TabLink
|
||||
to={`${base}/tasks`}
|
||||
isActive={isTasksActive}
|
||||
accentColor={branding.primaryColor}
|
||||
radius={radius}
|
||||
compact={compact}
|
||||
style={buttonStyle === 'outline' ? { background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` } : undefined}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<CheckSquare className="h-5 w-5" aria-hidden />
|
||||
<span>{labels.tasks}</span>
|
||||
</div>
|
||||
</TabLink>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useOptionalNotificationCenter, type NotificationCenterValue } from '../
|
||||
import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress';
|
||||
import { usePushSubscription } from '../hooks/usePushSubscription';
|
||||
import { getContrastingTextColor, relativeLuminance } from '../lib/color';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
|
||||
const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
heart: Heart,
|
||||
@@ -150,6 +151,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
|
||||
const [statusFilter, setStatusFilter] = React.useState<'new' | 'read' | 'dismissed' | 'all'>('new');
|
||||
const taskProgress = useGuestTaskProgress(eventToken);
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
const panelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
React.useEffect(() => {
|
||||
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="font-semibold text-lg">{event.name}</div>
|
||||
<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">
|
||||
<User className="h-3 w-3" />
|
||||
@@ -244,7 +246,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
open={notificationsOpen}
|
||||
onToggle={() => setNotificationsOpen((prev) => !prev)}
|
||||
panelRef={panelRef}
|
||||
taskProgress={taskProgress?.hydrated ? taskProgress : undefined}
|
||||
taskProgress={tasksEnabled && taskProgress?.hydrated ? taskProgress : undefined}
|
||||
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 type { LocaleCode } from '../i18n/messages';
|
||||
import { localizeTaskLabel } from '../lib/localizeTaskLabel';
|
||||
import { useEventData } from '../hooks/useEventData';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
|
||||
const GENERIC_ERROR = 'GENERIC_ERROR';
|
||||
|
||||
@@ -311,9 +313,10 @@ function Highlights({ topPhoto, trendingEmotion, t, formatRelativeTime, locale,
|
||||
type PersonalActionsProps = {
|
||||
token: string;
|
||||
t: TranslateFn;
|
||||
tasksEnabled: boolean;
|
||||
};
|
||||
|
||||
function PersonalActions({ token, t }: PersonalActionsProps) {
|
||||
function PersonalActions({ token, t, tasksEnabled }: PersonalActionsProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button asChild>
|
||||
@@ -322,12 +325,14 @@ function PersonalActions({ token, t }: PersonalActionsProps) {
|
||||
{t('achievements.personal.actions.upload')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/e/${encodeURIComponent(token)}/tasks`} className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" aria-hidden />
|
||||
{t('achievements.personal.actions.tasks')}
|
||||
</Link>
|
||||
</Button>
|
||||
{tasksEnabled ? (
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/e/${encodeURIComponent(token)}/tasks`} className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" aria-hidden />
|
||||
{t('achievements.personal.actions.tasks')}
|
||||
</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -336,6 +341,8 @@ export default function AchievementsPage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const identity = useGuestIdentity();
|
||||
const { t, locale } = useTranslation();
|
||||
const { event } = useEventData();
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
const [data, setData] = useState<AchievementsPayload | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -453,15 +460,15 @@ export default function AchievementsPage() {
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{t('achievements.personal.greeting', { name: data.personal.guestName || identity.name || t('achievements.leaderboard.guestFallback') })}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('achievements.personal.stats', {
|
||||
photos: formatNumber(data.personal.photos),
|
||||
tasks: formatNumber(data.personal.tasks),
|
||||
likes: formatNumber(data.personal.likes),
|
||||
})}
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
{t('achievements.personal.stats', {
|
||||
photos: formatNumber(data.personal.photos),
|
||||
tasks: formatNumber(data.personal.tasks),
|
||||
likes: formatNumber(data.personal.likes),
|
||||
})}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<PersonalActions token={token} t={t} />
|
||||
<PersonalActions token={token} t={t} tasksEnabled={tasksEnabled} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getEmotionIcon, getEmotionTheme, type EmotionIdentity } from '../lib/em
|
||||
import { getDeviceId } from '../lib/device';
|
||||
import { useDirectUpload } from '../hooks/useDirectUpload';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
|
||||
export default function HomePage() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
@@ -72,6 +73,7 @@ export default function HomePage() {
|
||||
const secondaryAccent = branding.secondaryColor;
|
||||
const uploadsRequireApproval =
|
||||
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
|
||||
const [missionDeck, setMissionDeck] = React.useState<MissionPreview[]>([]);
|
||||
const [missionPool, setMissionPool] = React.useState<MissionPreview[]>([]);
|
||||
@@ -232,8 +234,9 @@ export default function HomePage() {
|
||||
}
|
||||
}
|
||||
poolIndexRef.current = restoredIndex;
|
||||
if (!tasksEnabled) return;
|
||||
fetchTasksPage(1, true);
|
||||
}, [fetchTasksPage, locale, sliderStateKey, token]);
|
||||
}, [fetchTasksPage, locale, sliderStateKey, tasksEnabled, token]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (missionPool.length === 0) return;
|
||||
@@ -279,6 +282,34 @@ export default function HomePage() {
|
||||
const introMessage =
|
||||
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 (
|
||||
<div className="space-y-0.5 pb-24" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
<section className="space-y-1 px-4">
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useEventBranding } from '../context/EventBrandingContext';
|
||||
import { compressPhoto, formatBytes } from '../lib/image';
|
||||
import { useGuestIdentity } from '../context/GuestIdentityContext';
|
||||
import { useEventData } from '../hooks/useEventData';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
|
||||
interface Task {
|
||||
id: number;
|
||||
@@ -118,6 +119,8 @@ export default function UploadPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { markCompleted } = useGuestTaskProgress(token);
|
||||
const identity = useGuestIdentity();
|
||||
const { event } = useEventData();
|
||||
const tasksEnabled = isTaskModeEnabled(event);
|
||||
const { t, locale } = useTranslation();
|
||||
const stats = useEventStats();
|
||||
const { branding } = useEventBranding();
|
||||
@@ -233,7 +236,7 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
|
||||
// Load task metadata
|
||||
useEffect(() => {
|
||||
if (!token || taskId === null) {
|
||||
if (!token || taskId === null || !tasksEnabled) {
|
||||
setLoadingTask(false);
|
||||
return;
|
||||
}
|
||||
@@ -249,7 +252,7 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
const fallbackInstructions = t('upload.taskInfo.fallbackInstructions');
|
||||
|
||||
try {
|
||||
setLoadingTask(true);
|
||||
setLoadingTask(true);
|
||||
|
||||
const res = await fetch(
|
||||
`/api/v1/events/${encodeURIComponent(eventKey)}/tasks?locale=${encodeURIComponent(locale)}`,
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { EventBranding } from './types/event-branding';
|
||||
import type { EventBrandingPayload, FetchEventErrorCode } from './services/eventApi';
|
||||
import { NotificationCenterProvider } from './context/NotificationCenterContext';
|
||||
import RouteErrorElement from '@/components/RouteErrorElement';
|
||||
import { isTaskModeEnabled } from './lib/engagement';
|
||||
|
||||
const LandingPage = React.lazy(() => import('./pages/LandingPage'));
|
||||
const ProfileSetupPage = React.lazy(() => import('./pages/ProfileSetupPage'));
|
||||
@@ -75,8 +76,8 @@ export const router = createBrowserRouter([
|
||||
errorElement: <RouteErrorElement />,
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: 'tasks', element: <TaskPickerPage /> },
|
||||
{ path: 'tasks/:taskId', element: <TaskDetailPage /> },
|
||||
{ path: 'tasks', element: <TaskGuard><TaskPickerPage /></TaskGuard> },
|
||||
{ path: 'tasks/:taskId', element: <TaskGuard><TaskDetailPage /></TaskGuard> },
|
||||
{ path: 'upload', element: <UploadPage /> },
|
||||
{ path: 'queue', element: <UploadQueuePage /> },
|
||||
{ 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() {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const { event } = useEventData();
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface EventData {
|
||||
slug: string;
|
||||
name: string;
|
||||
default_locale: string;
|
||||
engagement_mode?: 'tasks' | 'photo_only';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
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() !== ''
|
||||
? json.default_locale
|
||||
: DEFAULT_LOCALE,
|
||||
engagement_mode: (json?.engagement_mode as 'tasks' | 'photo_only' | undefined) ?? 'tasks',
|
||||
guest_upload_visibility:
|
||||
json?.guest_upload_visibility === 'immediate' ? 'immediate' : 'review',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user