From 4f3503e3f484d722f4141be32923f0b38e74631d Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 22 Jan 2026 16:13:22 +0100 Subject: [PATCH] Refactor mobile dashboard layout --- resources/js/admin/mobile/DashboardPage.tsx | 598 ++++++++++-------- .../mobile/__tests__/DashboardPage.test.tsx | 40 ++ 2 files changed, 370 insertions(+), 268 deletions(-) diff --git a/resources/js/admin/mobile/DashboardPage.tsx b/resources/js/admin/mobile/DashboardPage.tsx index 1888706..79f31bf 100644 --- a/resources/js/admin/mobile/DashboardPage.tsx +++ b/resources/js/admin/mobile/DashboardPage.tsx @@ -2,11 +2,16 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { Bell, CalendarDays, Camera, CheckCircle2, Download, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users, ArrowRight, AlertCircle } from 'lucide-react'; +import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Download, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; +import { Button } from '@tamagui/button'; +import { Card } from '@tamagui/card'; +import { YGroup } from '@tamagui/group'; +import { ListItem } from '@tamagui/list-item'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { Image } from '@tamagui/image'; +import { Separator } from 'tamagui'; import { isSameDay, isPast, parseISO, differenceInDays, startOfDay } from 'date-fns'; import { MobileShell } from './components/MobileShell'; @@ -20,7 +25,7 @@ import { buildLimitWarnings } from '../lib/limitWarnings'; import { withAlpha } from './components/colors'; import { useEventReadiness } from './hooks/useEventReadiness'; import { SetupChecklist } from './components/SetupChecklist'; -import { KpiStrip } from './components/Primitives'; +import { KpiStrip, PillBadge } from './components/Primitives'; // --- HELPERS --- @@ -28,89 +33,67 @@ function translateLimits(t: any) { return (key: string, options?: any) => t(`management:limits.${key}`, key, options); } -// --- MODERN PRIMITIVES --- +// --- TAMAGUI-ALIGNED PRIMITIVES --- -function ModernCard({ children, style, ...rest }: any) { +function DashboardCard({ children, style, ...rest }: React.ComponentProps) { const theme = useAdminTheme(); return ( - {children} + + ); +} + +function SectionHeader({ + title, + subtitle, + action, +}: { + title: string; + subtitle?: string; + action?: React.ReactNode; +}) { + const theme = useAdminTheme(); + return ( + + + + {title} + + {action ?? null} + + {subtitle ? ( + + {subtitle} + + ) : null} + ); } -function ModernButton({ label, onPress, tone = 'primary', fullWidth = true, icon, style }: any) { - const theme = useAdminTheme(); - const isPrimary = tone === 'primary'; - const isGhost = tone === 'ghost'; - const isAccent = tone === 'accent'; - - const bg = isPrimary ? theme.primary : isAccent ? theme.accent : isGhost ? 'transparent' : theme.surface; - const text = isPrimary || isAccent ? 'white' : isGhost ? theme.text : theme.textStrong; - const border = isPrimary || isAccent || isGhost ? 'transparent' : theme.border; - - return ( - - - {icon} - - {label} - - - - ); -} - function StatusBadge({ status }: { status: string }) { - const theme = useAdminTheme(); const { t } = useTranslation('management'); - - const config = { - published: { bg: '#DCFCE7', text: '#166534', label: t('events.status.published', 'Live') }, - draft: { bg: '#FEF3C7', text: '#92400E', label: t('events.status.draft', 'Draft') }, - archived: { bg: '#F1F5F9', text: '#475569', label: t('events.status.archived', 'Archived') }, - }[status] || { bg: theme.surfaceMuted, text: theme.muted, label: status }; + const config = + { + published: { tone: 'success', label: t('events.status.published', 'Live') }, + draft: { tone: 'warning', label: t('events.status.draft', 'Draft') }, + archived: { tone: 'muted', label: t('events.status.archived', 'Archived') }, + }[status] || { tone: 'muted', label: status }; - return ( - - - {config.label} - - - ); + return {config.label}; } // --- MAIN PAGE COMPONENT --- @@ -119,7 +102,7 @@ export default function MobileDashboardPage() { const navigate = useNavigate(); const { slug: slugParam } = useParams<{ slug?: string }>(); const { t, i18n } = useTranslation(['management', 'dashboard', 'mobile']); - const { events, activeEvent, hasEvents, hasMultipleEvents, isLoading, selectEvent } = useEventContext(); + const { events, activeEvent, hasEvents, isLoading, selectEvent } = useEventContext(); const { user } = useAuth(); const isMember = user?.role === 'member'; @@ -181,7 +164,7 @@ export default function MobileDashboardPage() { if (isLoading) { return ( - + ); } @@ -201,6 +184,10 @@ export default function MobileDashboardPage() { return ( + {/* 1. LIFECYCLE HERO */} setEventSwitcherOpen(true)} - canSwitch={hasMultipleEvents} readiness={readiness} /> @@ -228,6 +213,10 @@ export default function MobileDashboardPage() { {/* 4. UNIFIED COMMAND GRID */} + ( - - - - {formatEventDate(event.event_date, locale)} - - - - + + + + {formatEventDate(event.event_date, locale)} + + + + ); if (phase === 'live') { return ( - -
- + +
+ + - - - - - {t('dashboard:liveNow.status', 'Happening Now')} - - - - {pendingPhotos > 0 ? `${pendingPhotos} ${t('management:photos.filters.pending', 'Pending')}` : t('dashboard:liveNow.description', 'Event is Running')} + + + + + {t('dashboard:liveNow.status', 'Happening Now')} - - {pendingPhotos > 0 && ( - navigate(adminPath(`/mobile/events/${event.slug}/control-room`))}> - - - {t('management:photos.openModeration', 'Review')} - - - - )} + + + {pendingPhotos > 0 + ? `${pendingPhotos} ${t('management:photos.filters.pending', 'Pending')}` + : t('dashboard:liveNow.description', 'Event is Running')} + + + {pendingPhotos > 0 ? ( + + ) : null} - + + ); } @@ -321,33 +319,54 @@ function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch, re if (phase === 'post') { return ( - -
- + +
+ + - - - - - - {t('events.recap.completedTitle', 'Event completed')} - - {t('events.recap.galleryOpen', 'Gallery online')} - + + + + + + {t('events.recap.completedTitle', 'Event completed')} + + {t('events.recap.galleryOpen', 'Gallery online')} + - } - onPress={() => navigate(adminPath(`/mobile/exports`))} - /> - } + + + + + + ); } @@ -358,43 +377,54 @@ function LifecycleHero({ event, stats, locale, navigate, onSwitch, canSwitch, re const ctaAction = nextStep ? () => navigate(adminPath(nextStep.targetPath)) : undefined; return ( - -
- + +
+ + - - - {t('dashboard:upcoming.status.planning', 'Countdown')} + + + {t('dashboard:upcoming.status.planning', 'Countdown')} + + + {daysToGo}{' '} + + {t('management:galleryStatus.daysLabel', 'days')} - - {daysToGo} {t('management:galleryStatus.daysLabel', 'days')} - - - - - + + + + + - - - - {/* Main CTA if not ready */} - {!readiness.isReady && ( - } - onPress={ctaAction} - /> - )} - {readiness.isReady && ( - - - - {t('management:mobileDashboard.readyForLiftoff', 'Ready for Liftoff')} + + + + {!readiness.isReady ? ( + + ) : ( + + + + {t('management:mobileDashboard.readyForLiftoff', 'Ready for Liftoff')} + + )} - + + ); } @@ -407,7 +437,8 @@ function PulseStrip({ event, stats }: any) { const pendingCount = stats?.pending_photos ?? event?.pending_photo_count ?? 0; return ( - + + section.items.length > 0); return ( - + {sections.map((section) => ( - - - {section.title} - - - {section.items.map((item) => ( - navigate(adminPath(item.path))} - style={{ width: '48%', flexGrow: 1 }} - > - - - - - + + + + {section.title} + + + + + {section.items.map((item) => { + const iconColor = item.color || theme.textStrong; + return ( + + navigate(adminPath(item.path))} + title={ + + + + + + {item.label} + - - {item.label} - - - - ))} - - + } + iconAfter={} + /> + + ); + })} + + ))} ); @@ -528,45 +562,52 @@ function RecentPhotosSection({ photos, navigate, slug }: { photos: TenantPhoto[] if (!photos.length) return null; return ( - - - - {t('photos.recentTitle', 'Latest Uploads')} - - navigate(adminPath(`/mobile/events/${slug}/control-room`))}> - {t('common.all', 'See all')} - - - - + + + + + {t('photos.recentTitle', 'Latest Uploads')} + + + + + + + {photos.map((photo) => ( - navigate(adminPath(`/mobile/events/${slug}/control-room`))}> - - {photo.thumbnail_url ? ( - - ) : ( - - - - )} - - + navigate(adminPath(`/mobile/events/${slug}/control-room`))}> + + {photo.thumbnail_url ? ( + + ) : ( + + + + )} + + ))} - - + + + ); } @@ -577,33 +618,41 @@ function AlertsSection({ event, stats, t }: any) { if (!limitWarnings.length) return null; return ( - - {limitWarnings.map((w: any, idx: number) => { - const isDanger = w.tone === 'danger'; - const bg = isDanger ? theme.dangerBg : theme.warningBg; - const border = isDanger ? theme.dangerText : theme.warningBorder; - const text = isDanger ? theme.dangerText : theme.warningText; - const Icon = isDanger ? AlertCircle : Bell; + + + + {t('management:alertsTitle', 'Alerts')} + + + + {limitWarnings.map((w: any, idx: number) => { + const isDanger = w.tone === 'danger'; + const bg = isDanger ? theme.dangerBg : theme.warningBg; + const border = isDanger ? theme.dangerText : theme.warningBorder; + const text = isDanger ? theme.dangerText : theme.warningText; + const Icon = isDanger ? AlertCircle : Bell; - return ( - - - - {w.message} - - - ); - })} - + return ( + + + + {w.message} + + + ); + })} + + + ); } @@ -619,7 +668,20 @@ function EmptyState({ canManage, onCreate }: any) { {t('mobile:header.noEventsBody', 'Create your first event.')} - {canManage && } + {canManage ? ( + + ) : null} ); } diff --git a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx index 2f6e062..b4ea8e1 100644 --- a/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx +++ b/resources/js/admin/mobile/__tests__/DashboardPage.test.tsx @@ -194,6 +194,38 @@ vi.mock('@tamagui/stacks', () => ({ XStack: ({ children }: { children: React.ReactNode }) =>
{children}
, })); +vi.mock('@tamagui/button', () => ({ + Button: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => ( + + ), +})); + +vi.mock('@tamagui/list-item', () => ({ + ListItem: ({ + title, + subTitle, + iconAfter, + onPress, + }: { + title?: React.ReactNode; + subTitle?: React.ReactNode; + iconAfter?: React.ReactNode; + onPress?: () => void; + }) => ( + + ), +})); + +vi.mock('tamagui', () => ({ + Separator: ({ children }: { children?: React.ReactNode }) =>
{children}
, +})); + vi.mock('@tamagui/text', () => ({ SizableText: ({ children }: { children: React.ReactNode }) => {children}, })); @@ -226,6 +258,7 @@ vi.mock('../theme', () => ({ }, useAdminTheme: () => ({ textStrong: '#0f172a', + text: '#0f172a', muted: '#64748b', border: '#e2e8f0', surface: '#ffffff', @@ -233,6 +266,13 @@ vi.mock('../theme', () => ({ primary: '#ff5a5f', surfaceMuted: '#f8fafc', shadow: 'rgba(15,23,42,0.12)', + success: '#16a34a', + successText: '#166534', + dangerBg: '#fee2e2', + dangerText: '#b91c1c', + warningBg: '#fef9c3', + warningBorder: '#fef08a', + warningText: '#92400e', }), }));