diff --git a/resources/js/guest-v2/__tests__/AppShell.test.tsx b/resources/js/guest-v2/__tests__/AppShell.test.tsx
new file mode 100644
index 00000000..fdbebdb5
--- /dev/null
+++ b/resources/js/guest-v2/__tests__/AppShell.test.tsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+
+const useEventDataMock = vi.fn();
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useLocation: () => ({ pathname: '/e/demo' }),
+}));
+
+vi.mock('../context/EventDataContext', () => ({
+ useEventData: () => useEventDataMock(),
+}));
+
+vi.mock('@/shared/guest/context/NotificationCenterContext', () => ({
+ useOptionalNotificationCenter: () => ({ unreadCount: 0 }),
+}));
+
+vi.mock('@/shared/guest/i18n/useTranslation', () => ({
+ useTranslation: () => ({
+ t: (_key: string, fallback?: string) => fallback ?? _key,
+ }),
+}));
+
+vi.mock('../lib/guestTheme', () => ({
+ useGuestThemeVariant: () => ({ isDark: false }),
+}));
+
+vi.mock('@tamagui/stacks', () => ({
+ XStack: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ YStack: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('@tamagui/button', () => ({
+ Button: ({ children, disabled, ...rest }: { children: React.ReactNode; disabled?: boolean }) => (
+
+ ),
+}));
+
+vi.mock('../components/TopBar', () => ({
+ default: () => topbar
,
+}));
+
+vi.mock('../components/FloatingActionButton', () => ({
+ default: () => fab
,
+}));
+
+vi.mock('../components/CompassHub', () => ({
+ default: ({ quadrants }: { quadrants: Array<{ key: string; label: string; disabled?: boolean }> }) => (
+
+ {quadrants.map((item) => (
+
+ ))}
+
+ ),
+}));
+
+vi.mock('../components/AmbientBackground', () => ({
+ default: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('../components/NotificationSheet', () => ({
+ default: () => null,
+}));
+
+vi.mock('../components/SettingsSheet', () => ({
+ default: () => null,
+}));
+
+vi.mock('../components/GuestAnalyticsNudge', () => ({
+ default: () => null,
+}));
+
+vi.mock('lucide-react', () => ({
+ Sparkles: () => sparkles,
+ Share2: () => share,
+ Image: () => image,
+ Camera: () => camera,
+ Settings: () => settings,
+ Home: () => home,
+ Menu: () => menu,
+}));
+
+import AppShell from '../components/AppShell';
+
+describe('AppShell', () => {
+ it('disables task link when no active tasks are available', () => {
+ useEventDataMock.mockReturnValue({
+ tasksEnabled: true,
+ hasActiveTasks: false,
+ event: { name: 'Demo Event' },
+ token: 'demo',
+ });
+
+ render(content
);
+
+ expect(screen.getByRole('button', { name: 'Tasks' })).toBeDisabled();
+ });
+
+ it('keeps task link enabled when active tasks exist', () => {
+ useEventDataMock.mockReturnValue({
+ tasksEnabled: true,
+ hasActiveTasks: true,
+ event: { name: 'Demo Event' },
+ token: 'demo',
+ });
+
+ render(content
);
+
+ expect(screen.getByRole('button', { name: 'Tasks' })).toBeEnabled();
+ });
+});
diff --git a/resources/js/guest-v2/__tests__/HomeScreen.test.tsx b/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
index b567d995..543c89b2 100644
--- a/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
+++ b/resources/js/guest-v2/__tests__/HomeScreen.test.tsx
@@ -135,6 +135,7 @@ describe('HomeScreen', () => {
fetchGalleryMock.mockResolvedValueOnce({ data: [] });
useEventDataMock.mockReturnValue({
tasksEnabled: true,
+ hasActiveTasks: true,
token: 'demo',
event: { name: 'Demo Event' },
});
@@ -149,6 +150,7 @@ describe('HomeScreen', () => {
fetchGalleryMock.mockResolvedValueOnce({ data: [] });
useEventDataMock.mockReturnValue({
tasksEnabled: false,
+ hasActiveTasks: false,
token: 'demo',
event: { name: 'Demo Event' },
});
@@ -165,6 +167,7 @@ describe('HomeScreen', () => {
});
useEventDataMock.mockReturnValue({
tasksEnabled: false,
+ hasActiveTasks: false,
token: 'demo',
event: { name: 'Demo Event' },
});
@@ -176,4 +179,34 @@ describe('HomeScreen', () => {
expect(navigateMock).toHaveBeenCalledWith('/e/demo/gallery?photo=42');
});
+
+ it('does not mention tasks when no active tasks are available', () => {
+ fetchGalleryMock.mockResolvedValueOnce({ data: [] });
+ useEventDataMock.mockReturnValue({
+ tasksEnabled: true,
+ hasActiveTasks: false,
+ token: 'demo',
+ event: { name: 'Demo Event' },
+ });
+
+ render();
+
+ expect(screen.getByText('Capture ready')).toBeInTheDocument();
+ expect(screen.queryByText("Let's go!")).not.toBeInTheDocument();
+ });
+
+ it('shows a friendly empty gallery message instead of placeholders', async () => {
+ fetchGalleryMock.mockResolvedValueOnce({ data: [] });
+ useEventDataMock.mockReturnValue({
+ tasksEnabled: false,
+ hasActiveTasks: false,
+ token: 'demo',
+ event: { name: 'Demo Event' },
+ });
+
+ render();
+
+ expect(await screen.findByText('No photos yet')).toBeInTheDocument();
+ expect(screen.getByText('You should start taking some and fill this gallery with moments.')).toBeInTheDocument();
+ });
});
diff --git a/resources/js/guest-v2/components/AppShell.tsx b/resources/js/guest-v2/components/AppShell.tsx
index 62625712..4a798a78 100644
--- a/resources/js/guest-v2/components/AppShell.tsx
+++ b/resources/js/guest-v2/components/AppShell.tsx
@@ -24,7 +24,7 @@ export default function AppShell({ children }: AppShellProps) {
const [compassOpen, setCompassOpen] = React.useState(false);
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
const [settingsOpen, setSettingsOpen] = React.useState(false);
- const { tasksEnabled, event, token } = useEventData();
+ const { tasksEnabled, hasActiveTasks, event, token } = useEventData();
const notificationCenter = useOptionalNotificationCenter();
const navigate = useNavigate();
const location = useLocation();
@@ -73,7 +73,8 @@ export default function AppShell({ children }: AppShellProps) {
key: 'tasks',
label: t('navigation.tasks', 'Tasks'),
icon: ,
- onPress: goTo('/tasks'),
+ onPress: hasActiveTasks ? goTo('/tasks') : undefined,
+ disabled: !hasActiveTasks,
}
: {
key: 'settings',
diff --git a/resources/js/guest-v2/components/CompassHub.tsx b/resources/js/guest-v2/components/CompassHub.tsx
index f8e9c47e..29fc416c 100644
--- a/resources/js/guest-v2/components/CompassHub.tsx
+++ b/resources/js/guest-v2/components/CompassHub.tsx
@@ -11,6 +11,7 @@ export type CompassAction = {
label: string;
icon?: React.ReactNode;
onPress?: () => void;
+ disabled?: boolean;
};
type CompassHubProps = {
@@ -128,7 +129,12 @@ export default function CompassHub({
{quadrants.map((action, index) => (