import React, { useEffect, useState, useRef } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { tripsApi } from '../api/client' import { tripRepo } from '../repo/tripRepo' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { useTranslation } from '../i18n' import { getApiErrorMessage } from '../types' import Navbar from '../components/Layout/Navbar' import DemoBanner from '../components/Layout/DemoBanner' import CurrencyWidget from '../components/Dashboard/CurrencyWidget' import TimezoneWidget from '../components/Dashboard/TimezoneWidget' import TripFormModal from '../components/Trips/TripFormModal' import ConfirmDialog from '../components/shared/ConfirmDialog' import CopyTripDialog from '../components/shared/CopyTripDialog' import { useToast } from '../components/shared/Toast' import { useCountUp } from '../hooks/useCountUp' import { Plus, Calendar, Trash2, Edit2, Map, ChevronDown, ChevronUp, Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft, Users, LayoutGrid, List, Copy, Bell, CheckCircle2, } from 'lucide-react' import { useCanDo } from '../store/permissionsStore' interface DashboardTrip { id: number title: string description?: string | null start_date?: string | null end_date?: string | null cover_image?: string | null is_archived?: boolean is_owner?: boolean owner_username?: string day_count?: number place_count?: number shared_count?: number [key: string]: string | number | boolean | null | undefined } const font: React.CSSProperties = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" } const MS_PER_DAY = 86400000 function daysUntil(dateStr: string | null | undefined): number | null { if (!dateStr) return null const today = new Date(); today.setHours(0, 0, 0, 0) const d = new Date(dateStr + 'T00:00:00'); d.setHours(0, 0, 0, 0) return Math.round((d - today) / MS_PER_DAY) } function getTripStatus(trip: DashboardTrip): string | null { const today = new Date().toISOString().split('T')[0] if (trip.start_date && trip.end_date && trip.start_date <= today && trip.end_date >= today) return 'ongoing' const until = daysUntil(trip.start_date) if (until === null) return null if (until === 0) return 'today' if (until === 1) return 'tomorrow' if (until > 1) return 'future' return 'past' } function formatDate(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }) } function formatDateShort(dateStr: string | null | undefined, locale: string = 'en-US'): string | null { if (!dateStr) return null return new Date(dateStr + 'T00:00:00Z').toLocaleDateString(locale, { day: 'numeric', month: 'short', timeZone: 'UTC' }) } function sortTrips(trips: DashboardTrip[]): DashboardTrip[] { const today = new Date().toISOString().split('T')[0] function rank(t) { if (t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) return 0 // ongoing if (t.start_date && t.start_date >= today) return 1 // upcoming return 2 // past } return [...trips].sort((a, b) => { const ra = rank(a), rb = rank(b) if (ra !== rb) return ra - rb const ad = a.start_date || '', bd = b.start_date || '' if (ra <= 1) return ad.localeCompare(bd) return bd.localeCompare(ad) }) } // Gradient backgrounds when no cover image const GRADIENTS = [ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', 'linear-gradient(135deg, #96fbc4 0%, #f9f586 100%)', ] function tripGradient(id: number): string { return GRADIENTS[id % GRADIENTS.length] } // ── Liquid Glass hover effect ──────────────────────────────────────────────── interface LiquidGlassProps { children: React.ReactNode dark: boolean style?: React.CSSProperties className?: string onClick?: () => void } function LiquidGlass({ children, dark, style, className = '', onClick }: LiquidGlassProps): React.ReactElement { const ref = useRef(null) const glareRef = useRef(null) const borderRef = useRef(null) const onMove = (e: React.MouseEvent): void => { if (!ref.current || !glareRef.current || !borderRef.current) return const rect = ref.current.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top glareRef.current.style.background = `radial-gradient(circle 250px at ${x}px ${y}px, ${dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)'} 0%, transparent 70%)` glareRef.current.style.opacity = '1' borderRef.current.style.opacity = '1' borderRef.current.style.maskImage = `radial-gradient(circle 120px at ${x}px ${y}px, black 0%, transparent 100%)` borderRef.current.style.WebkitMaskImage = `radial-gradient(circle 120px at ${x}px ${y}px, black 0%, transparent 100%)` } const onLeave = () => { if (glareRef.current) glareRef.current.style.opacity = '0' if (borderRef.current) borderRef.current.style.opacity = '0' } return (
{children}
) } // ── Spotlight Card (next upcoming trip) ───────────────────────────────────── interface TripCardProps { trip: DashboardTrip onEdit?: (trip: DashboardTrip) => void onCopy?: (trip: DashboardTrip) => void onDelete?: (trip: DashboardTrip) => void onArchive?: (id: number) => void onClick: (trip: DashboardTrip) => void t: (key: string, params?: Record) => string locale: string dark?: boolean } function SpotlightStats({ trip, totalDays, t }: { trip: DashboardTrip; totalDays: number; t: TripCardProps['t'] }): React.ReactElement { const days = useCountUp(trip.day_count || totalDays) const places = useCountUp(trip.place_count || 0) const buddies = useCountUp(trip.shared_count || 0) return (

{days}

{t('dashboard.mobile.days')}

{places}

{t('dashboard.mobile.places')}

{buddies}

{t('dashboard.mobile.buddies')}

) } function SpotlightCard({ trip, onEdit, onCopy, onDelete, onArchive, onClick, t, locale }: TripCardProps): React.ReactElement { const status = getTripStatus(trip) const isLive = status === 'ongoing' const today = new Date().toISOString().split('T')[0] const startDate = trip.start_date || today const endDate = trip.end_date || today const totalDays = Math.max(1, Math.ceil((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000) + 1) const currentDay = Math.min(totalDays, Math.ceil((new Date(today).getTime() - new Date(startDate).getTime()) / 86400000) + 1) const daysLeft = Math.max(0, totalDays - currentDay) const progress = Math.round((currentDay / totalDays) * 100) const badgeText = isLive ? t('dashboard.mobile.liveNow') : status === 'today' ? t('dashboard.mobile.startsToday') : status === 'tomorrow' ? t('dashboard.mobile.tomorrow') : status === 'future' ? t('dashboard.status.daysLeft', { count: daysUntil(trip.start_date) }) : status === 'past' ? t('dashboard.mobile.completed') : null return (
onClick(trip)} className="group relative rounded-3xl overflow-hidden cursor-pointer mb-8 transition-[transform,box-shadow] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-1 hover:shadow-[0_16px_60px_rgba(0,0,0,0.22)] active:scale-[0.995]" style={{ minHeight: 340, boxShadow: '0 8px 40px rgba(0,0,0,0.13)', isolation: 'isolate' }} > {/* Background */}
{trip.cover_image && ( <>
)}
{/* Content */}
{/* Top: badge + actions */}
{badgeText ? ( {isLive ? ( ) : ( )} {badgeText} ) : }
e.stopPropagation()}> {onEdit && } {onCopy && } {onArchive && } {onDelete && }
{/* Title area — pushed to bottom */}
{!trip.is_owner && ( {t('dashboard.sharedBy', { name: trip.owner_username })} )}

{trip.title}

{formatDateShort(trip.start_date, locale)} — {formatDateShort(trip.end_date, locale)} {isLive && <> · {t('journey.pdf.day')} {currentDay} / {totalDays}}

{/* Progress bar — only for live trips */} {isLive && (
{t('dashboard.mobile.tripProgress')} {t('dashboard.mobile.daysLeft', { count: daysLeft })}
)} {/* Stats */}
) } // ── Mobile Trip Card (upcoming style) ──────────────────────────────────────── function MobileTripCard({ trip, onEdit, onCopy, onDelete, onArchive, onClick, t, locale }: Omit): React.ReactElement { const status = getTripStatus(trip) const until = daysUntil(trip.start_date) const duration = trip.start_date && trip.end_date ? Math.ceil((new Date(trip.end_date).getTime() - new Date(trip.start_date).getTime()) / 86400000) + 1 : trip.day_count || null const badgeText = status === 'ongoing' ? t('dashboard.mobile.ongoing') : status === 'today' ? t('dashboard.mobile.startsToday') : status === 'tomorrow' ? t('dashboard.mobile.tomorrow') : until && until > 0 ? (until < 30 ? t('dashboard.mobile.inDays', { count: until }) : until < 365 ? t('dashboard.mobile.inMonths', { count: Math.round(until / 30) }) : `In ${Math.round(until / 365)}y`) : status === 'past' ? t('dashboard.mobile.completed') : null return (
onClick?.(trip)} className="group rounded-2xl border border-zinc-200 dark:border-zinc-700 overflow-hidden cursor-pointer transition-[transform,box-shadow,border-color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-md" style={{ background: 'var(--bg-card)', isolation: 'isolate' }} > {/* Cover */}
{trip.cover_image && ( )}
{/* Action buttons top-right */}
{onEdit && } {onCopy && } {onArchive && } {onDelete && }
{/* Countdown badge */} {badgeText && (
{status === 'ongoing' ? ( ) : status === 'past' ? ( ) : ( )} {badgeText}
)} {/* Title on cover */}

{trip.title}

{trip.description && (

{trip.description}

)}
{/* Bottom stats */}
{trip.start_date && (
{formatDateShort(trip.start_date, locale)} {t('dashboard.mobile.starts')}
)} {duration && (
{duration} {duration === 1 ? t('dashboard.mobile.day') : t('dashboard.mobile.days')} {t('dashboard.mobile.duration')}
)}
{trip.place_count || 0} {t('dashboard.mobile.places')}
{(trip.shared_count || 0) > 0 && (
{trip.shared_count} {t('dashboard.mobile.buddies')}
)}
) } // ── Regular Trip Card (matches mobile card design) ────────────────────────── function TripCard({ trip, onEdit, onCopy, onDelete, onArchive, onClick, t, locale }: Omit): React.ReactElement { const status = getTripStatus(trip) const until = daysUntil(trip.start_date) const duration = trip.start_date && trip.end_date ? Math.ceil((new Date(trip.end_date).getTime() - new Date(trip.start_date).getTime()) / 86400000) + 1 : trip.day_count || null const badgeText = status === 'ongoing' ? t('dashboard.mobile.ongoing') : status === 'today' ? t('dashboard.mobile.startsToday') : status === 'tomorrow' ? t('dashboard.mobile.tomorrow') : until && until > 0 ? (until < 30 ? t('dashboard.mobile.inDays', { count: until }) : until < 365 ? t('dashboard.mobile.inMonths', { count: Math.round(until / 30) }) : `In ${Math.round(until / 365)}y`) : status === 'past' ? t('dashboard.mobile.completed') : null return (
onClick(trip)} className="group rounded-2xl border border-zinc-200 dark:border-zinc-700 overflow-hidden cursor-pointer transition-[transform,box-shadow,border-color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] hover:-translate-y-0.5 hover:shadow-lg hover:border-zinc-300 dark:hover:border-zinc-600" style={{ background: 'var(--bg-card)', isolation: 'isolate' }} > {/* Cover */}
{trip.cover_image && ( )}
{/* Action buttons top-right — visible on hover */}
{onEdit && } {onCopy && } {onArchive && } {onDelete && }
{/* Status badge top-left */} {badgeText && (
{status === 'ongoing' ? ( ) : status === 'past' ? ( ) : ( )} {badgeText}
)} {/* Shared badge */} {!trip.is_owner && (
{t('dashboard.shared')}
)} {/* Title on cover */}

{trip.title}

{trip.description && (

{trip.description}

)}
{/* Bottom stats */}
{trip.start_date && (
{formatDateShort(trip.start_date, locale)} {t('dashboard.mobile.starts')}
)} {duration && (
{duration} {duration === 1 ? t('dashboard.mobile.day') : t('dashboard.mobile.days')} {t('dashboard.mobile.duration')}
)}
{trip.place_count || 0} {t('dashboard.mobile.places')}
{(trip.shared_count || 0) > 0 && (
{trip.shared_count} {t('dashboard.mobile.buddies')}
)}
) } // ── List View Item ────────────────────────────────────────────────────────── function TripListItem({ trip, onEdit, onCopy, onDelete, onArchive, onClick, t, locale }: Omit): React.ReactElement { const status = getTripStatus(trip) const [hovered, setHovered] = useState(false) const coverBg = trip.cover_image ? `url(${trip.cover_image}) center/cover no-repeat` : tripGradient(trip.id) return (
setHovered(true)} onMouseLeave={() => setHovered(false)} onClick={() => onClick(trip)} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 16px', background: hovered ? 'var(--bg-tertiary)' : 'var(--bg-card)', borderRadius: 14, border: `1px solid ${hovered ? 'var(--text-faint)' : 'var(--border-primary)'}`, cursor: 'pointer', transition: 'all 0.15s', boxShadow: hovered ? '0 4px 16px rgba(0,0,0,0.08)' : '0 1px 3px rgba(0,0,0,0.03)', }} > {/* Cover thumbnail */}
{status === 'ongoing' && ( )}
{/* Title & description */}
{trip.title} {!trip.is_owner && ( {t('dashboard.shared')} )} {status && ( {status === 'ongoing' ? t('dashboard.status.ongoing') : status === 'today' ? t('dashboard.status.today') : status === 'tomorrow' ? t('dashboard.status.tomorrow') : status === 'future' ? t('dashboard.status.daysLeft', { count: daysUntil(trip.start_date) }) : t('dashboard.mobile.completed')} )}
{trip.description && (

{trip.description}

)}
{/* Date & stats */}
{trip.start_date && (
{formatDateShort(trip.start_date, locale)} {trip.end_date && <> — {formatDateShort(trip.end_date, locale)}}
)}
{trip.day_count || 0}
{trip.place_count || 0}
{trip.shared_count || 0}
{/* Actions */} {(onEdit || onCopy || onArchive || onDelete) && (
e.stopPropagation()}> {onEdit && onEdit(trip)} icon={} label="" />} {onCopy && onCopy(trip)} icon={} label="" />} {onArchive && onArchive(trip.id)} icon={} label="" />} {onDelete && onDelete(trip)} icon={} label="" danger />}
)}
) } // ── Archived Trip Row ──────────────────────────────────────────────────────── interface ArchivedRowProps { trip: DashboardTrip onEdit?: (trip: DashboardTrip) => void onCopy?: (trip: DashboardTrip) => void onUnarchive?: (id: number) => void onDelete?: (trip: DashboardTrip) => void onClick: (trip: DashboardTrip) => void t: (key: string, params?: Record) => string locale: string } function ArchivedRow({ trip, onEdit, onCopy, onUnarchive, onDelete, onClick, t, locale }: ArchivedRowProps): React.ReactElement { return (
onClick(trip)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 16px', borderRadius: 12, border: '1px solid var(--border-faint)', background: 'var(--bg-card)', cursor: 'pointer', transition: 'border-color 0.12s', }} onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--border-primary)'} onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border-faint)'}> {/* Mini cover */}
{trip.title} {!trip.is_owner && {t('dashboard.shared')}}
{trip.start_date && (
{formatDateShort(trip.start_date, locale)}{trip.end_date ? ` — ${formatDateShort(trip.end_date, locale)}` : ''}
)}
{(onEdit || onCopy || onUnarchive || onDelete) && (
e.stopPropagation()}> {onCopy && } {onUnarchive && } {onDelete && }
)}
) } // ── Helpers ────────────────────────────────────────────────────────────────── function Stat({ value, label }: { value: number | string; label: string }): React.ReactElement { return (
{value} {label}
) } function CardAction({ onClick, icon, label, danger }: { onClick: () => void; icon: React.ReactNode; label: string; danger?: boolean }): React.ReactElement { return ( ) } function IconBtn({ onClick, title, danger, loading, children }: { onClick: () => void; title: string; danger?: boolean; loading?: boolean; children: React.ReactNode }): React.ReactElement { return ( ) } // ── Skeleton ───────────────────────────────────────────────────────────────── function SkeletonCard(): React.ReactElement { return (
) } // ── Main Page ──────────────────────────────────────────────────────────────── export default function DashboardPage(): React.ReactElement { const [trips, setTrips] = useState([]) const [archivedTrips, setArchivedTrips] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showForm, setShowForm] = useState(false) const [editingTrip, setEditingTrip] = useState(null) const [showArchived, setShowArchived] = useState(false) const [showWidgetSettings, setShowWidgetSettings] = useState(false) const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => (localStorage.getItem('trek_dashboard_view') as 'grid' | 'list') || 'grid') const [deleteTrip, setDeleteTrip] = useState(null) const [copyTrip, setCopyTrip] = useState(null) const toggleViewMode = () => { setViewMode(prev => { const next = prev === 'grid' ? 'list' : 'grid' localStorage.setItem('trek_dashboard_view', next) return next }) } const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const toast = useToast() const { t, locale } = useTranslation() const { demoMode, user } = useAuthStore() const { settings, updateSetting } = useSettingsStore() const can = useCanDo() const dm = settings.dark_mode const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) const showCurrency = settings.dashboard_currency !== 'off' const showTimezone = settings.dashboard_timezone !== 'off' const showSidebar = showCurrency || showTimezone useEffect(() => { if (showWidgetSettings === 'mobile' || showWidgetSettings === 'mobile-currency' || showWidgetSettings === 'mobile-timezone') { document.body.style.overflow = 'hidden' } else { document.body.style.overflow = '' } return () => { document.body.style.overflow = '' } }, [showWidgetSettings]) useEffect(() => { if (searchParams.get('create') === '1') { setShowForm(true) setSearchParams({}, { replace: true }) } }, [searchParams]) useEffect(() => { loadTrips() }, []) const loadTrips = async () => { setIsLoading(true) try { const { trips, archivedTrips } = await tripRepo.list() setTrips(sortTrips(trips)) setArchivedTrips(sortTrips(archivedTrips)) } catch { toast.error(t('dashboard.toast.loadError')) } finally { setIsLoading(false) } } const handleCreate = async (tripData) => { try { const data = await tripsApi.create(tripData) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.created')) return data } catch (err: unknown) { throw new Error(getApiErrorMessage(err, t('dashboard.toast.createError'))) } } const handleUpdate = async (tripData) => { try { const data = await tripsApi.update(editingTrip.id, tripData) setTrips(prev => sortTrips(prev.map(t => t.id === editingTrip.id ? data.trip : t))) toast.success(t('dashboard.toast.updated')) } catch (err: unknown) { throw new Error(getApiErrorMessage(err, t('dashboard.toast.updateError'))) } } const handleDelete = (trip) => setDeleteTrip(trip) const confirmDelete = async () => { if (!deleteTrip) return try { await tripsApi.delete(deleteTrip.id) setTrips(prev => prev.filter(t => t.id !== deleteTrip.id)) setArchivedTrips(prev => prev.filter(t => t.id !== deleteTrip.id)) toast.success(t('dashboard.toast.deleted')) } catch { toast.error(t('dashboard.toast.deleteError')) } setDeleteTrip(null) } const handleArchive = async (id) => { try { const data = await tripsApi.archive(id) setTrips(prev => prev.filter(t => t.id !== id)) setArchivedTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.archived')) } catch { toast.error(t('dashboard.toast.archiveError')) } } const handleUnarchive = async (id) => { try { const data = await tripsApi.unarchive(id) setArchivedTrips(prev => prev.filter(t => t.id !== id)) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.restored')) } catch { toast.error(t('dashboard.toast.restoreError')) } } const handleCoverUpdate = (tripId: number, coverImage: string | null): void => { const update = (t: DashboardTrip) => t.id === tripId ? { ...t, cover_image: coverImage } : t setTrips(prev => prev.map(update)) setArchivedTrips(prev => prev.map(update)) } const handleCopy = (trip: DashboardTrip) => setCopyTrip(trip) const confirmCopy = async () => { if (!copyTrip) return try { const data = await tripsApi.copy(copyTrip.id, { title: `${copyTrip.title} (${t('dashboard.copySuffix')})` }) setTrips(prev => sortTrips([data.trip, ...prev])) toast.success(t('dashboard.toast.copied')) } catch { toast.error(t('dashboard.toast.copyError')) } setCopyTrip(null) } const today = new Date().toISOString().split('T')[0] const spotlight = trips.find(t => t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) || trips.find(t => t.start_date && t.start_date >= today) || trips[0] || null const rest = spotlight ? trips.filter(t => t.id !== spotlight.id) : trips return (
{demoMode && }
{/* Mobile greeting header */}

{new Date().getHours() < 12 ? t('dashboard.greeting.morning') : new Date().getHours() < 18 ? t('dashboard.greeting.afternoon') : t('dashboard.greeting.evening')}

{user?.username || t('nav.profile')}

{/* Mobile: Hero Trip (spotlight — ongoing or next upcoming) */} {!isLoading && spotlight && (
{ setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onDelete={can('trip_delete', spotlight) ? handleDelete : undefined} onArchive={can('trip_archive', spotlight) ? handleArchive : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} />
)} {/* Mobile: Quick Actions */}
{can('trip_create') && ( )}
{/* Desktop header — unified toolbar */}

{t('dashboard.title')}

{isLoading ? t('common.loading') : trips.length > 0 ? `${t(trips.length !== 1 ? 'dashboard.subtitle.activeMany' : 'dashboard.subtitle.activeOne', { count: trips.length })}${archivedTrips.length > 0 ? t('dashboard.subtitle.archivedSuffix', { count: archivedTrips.length }) : ''}` : t('dashboard.subtitle.empty')}
{can('trip_create') && ( )}
{/* Widget settings dropdown */} {showWidgetSettings && (
Widgets:
)} {/* Mobile widgets button — replaced by Quick Actions */}
{/* Main content */}
{/* Loading skeletons */} {isLoading && ( <>
{[1, 2, 3].map(i => )}
)} {/* Empty state */} {!isLoading && trips.length === 0 && (

{t('dashboard.emptyTitle')}

{t('dashboard.emptyText')}

{can('trip_create') && }
)} {/* Spotlight (grid mode, desktop only — mobile has Live Hero) */} {!isLoading && spotlight && viewMode === 'grid' && (
{ setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onDelete={can('trip_delete', spotlight) ? handleDelete : undefined} onArchive={can('trip_archive', spotlight) ? handleArchive : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} />
)} {/* Trips — mobile cards */} {!isLoading && rest.length > 0 && (
{rest.some(t => getTripStatus(t) === 'future' || getTripStatus(t) === 'tomorrow') ? t('dashboard.mobile.upcomingTrips') : t('dashboard.mobile.yourTrips')} {rest.length} {t('dashboard.mobile.trips')}
{rest.map(trip => ( { setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onDelete={can('trip_delete', trip) ? handleDelete : undefined} onArchive={can('trip_archive', trip) ? handleArchive : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} /> ))}
)} {/* Trips — desktop grid or list */} {!isLoading && (viewMode === 'grid' ? rest : trips).length > 0 && ( viewMode === 'grid' ? (
{rest.map(trip => ( { setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onDelete={can('trip_delete', trip) ? handleDelete : undefined} onArchive={can('trip_archive', trip) ? handleArchive : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} /> ))}
) : (
{trips.map(trip => ( { setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onDelete={can('trip_delete', trip) ? handleDelete : undefined} onArchive={can('trip_archive', trip) ? handleArchive : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} /> ))}
) )} {/* Archived section */} {!isLoading && archivedTrips.length > 0 && (
{showArchived && (
{archivedTrips.map(trip => ( { setEditingTrip(tr); setShowForm(true) } : undefined} onCopy={can('trip_create') ? handleCopy : undefined} onUnarchive={can('trip_archive', trip) ? handleUnarchive : undefined} onDelete={can('trip_delete', trip) ? handleDelete : undefined} onClick={tr => navigate(`/trips/${tr.id}`)} /> ))}
)}
)}
{/* Widgets sidebar */} {showSidebar && (
{showCurrency && } {showTimezone && }
)}
{/* Mobile widgets bottom sheet */} {(showWidgetSettings === 'mobile' || showWidgetSettings === 'mobile-currency' || showWidgetSettings === 'mobile-timezone') && (
setShowWidgetSettings(false)}>
e.stopPropagation()}>
{showWidgetSettings === 'mobile-currency' ? t('dashboard.mobile.currencyConverter') : showWidgetSettings === 'mobile-timezone' ? t('dashboard.mobile.timezone') : t('common.settings')}
{(showWidgetSettings === 'mobile' || showWidgetSettings === 'mobile-currency') && } {(showWidgetSettings === 'mobile' || showWidgetSettings === 'mobile-timezone') && }
)} { setShowForm(false); setEditingTrip(null) }} onSave={editingTrip ? handleUpdate : handleCreate} trip={editingTrip} onCoverUpdate={handleCoverUpdate} /> setDeleteTrip(null)} onConfirm={confirmDelete} title={t('common.delete')} message={t('dashboard.confirm.delete', { title: deleteTrip?.title || '' })} /> setCopyTrip(null)} onConfirm={confirmCopy} />
) }