mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
v2.5.0 — Addon System, Vacay, Atlas, Dashboard Widgets & Mobile Overhaul
The biggest NOMAD update yet. Introduces a modular addon architecture and three major new features. Addon System: - Admin panel addon management with enable/disable toggles - Trip addons (Packing List, Budget, Documents) dynamically show/hide in trip tabs - Global addons appear in the main navigation for all users Vacay — Vacation Day Planner (Global Addon): - Monthly calendar view with international public holidays (100+ countries via Nager.Date API) - Company holidays with auto-cleanup of conflicting entries - User-based system: each NOMAD user is a person in the calendar - Fusion system: invite other users to share a combined calendar with real-time WebSocket sync - Vacation entitlement tracking with automatic carry-over to next year - Full settings: block weekends, public holidays, company holidays, carry-over toggle - Invite/accept/decline flow with forced confirmation modal - Color management per user with collision detection on fusion - Dissolve fusion with preserved entries Atlas — Travel World Map (Global Addon): - Fullscreen Leaflet world map with colored country polygons (GeoJSON) - Glass-effect bottom panel with stats, continent breakdown, streak tracking - Country tooltips with trip count, places visited, first/last visit dates - Liquid glass hover effect on the stats panel - Canvas renderer with tile preloading for maximum performance - Responsive: mobile stats bars, no zoom controls on touch Dashboard Widgets: - Currency converter with 50 currencies, CustomSelect dropdowns, localStorage persistence - Timezone widget with customizable city list, live updating clock - Per-user toggle via settings button, bottom sheet on mobile Admin Panel: - Consistent dark mode across all tabs (CSS variable overrides) - Online/offline status badges on user list via WebSocket - Unified heading sizes and subtitles across all sections - Responsive tab grid on mobile Mobile Improvements: - Vacay: slide-in sidebar drawer, floating toolbar, responsive calendar grid - Atlas: top/bottom glass stat bars, no popups - Trip Planner: fixed position content container prevents overscroll, portal-based sidebar buttons - Dashboard: fixed viewport container, mobile widget bottom sheet - Admin: responsive tab grid, compact buttons - Global: overscroll-behavior fixes, modal scroll containment Other: - Trip tab labels: Planung→Karte, Packliste→Liste, Buchungen→Buchung (DE mobile) - Reservation form responsive layout - Backup panel responsive buttons
This commit is contained in:
@@ -2,15 +2,17 @@ import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { tripsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { useSettingsStore } from '../store/settingsStore'
|
||||
import { useTranslation } from '../i18n'
|
||||
import Navbar from '../components/Layout/Navbar'
|
||||
import DemoBanner from '../components/Layout/DemoBanner'
|
||||
import TravelStats from '../components/Dashboard/TravelStats'
|
||||
import CurrencyWidget from '../components/Dashboard/CurrencyWidget'
|
||||
import TimezoneWidget from '../components/Dashboard/TimezoneWidget'
|
||||
import TripFormModal from '../components/Trips/TripFormModal'
|
||||
import { useToast } from '../components/shared/Toast'
|
||||
import {
|
||||
Plus, Calendar, Trash2, Edit2, Map, ChevronDown, ChevronUp,
|
||||
Archive, ArchiveRestore, Clock, MapPin,
|
||||
Archive, ArchiveRestore, Clock, MapPin, Settings, X, ArrowRightLeft,
|
||||
} from 'lucide-react'
|
||||
|
||||
const font = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }
|
||||
@@ -345,11 +347,25 @@ export default function DashboardPage() {
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [editingTrip, setEditingTrip] = useState(null)
|
||||
const [showArchived, setShowArchived] = useState(false)
|
||||
const [showWidgetSettings, setShowWidgetSettings] = useState(false)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toast = useToast()
|
||||
const { t, locale } = useTranslation()
|
||||
const { demoMode } = useAuthStore()
|
||||
const { settings, updateSetting } = useSettingsStore()
|
||||
const showCurrency = settings.dashboard_currency !== 'off'
|
||||
const showTimezone = settings.dashboard_timezone !== 'off'
|
||||
const showSidebar = showCurrency || showTimezone
|
||||
|
||||
useEffect(() => {
|
||||
if (showWidgetSettings === 'mobile') {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [showWidgetSettings])
|
||||
|
||||
useEffect(() => { loadTrips() }, [])
|
||||
|
||||
@@ -437,10 +453,10 @@ export default function DashboardPage() {
|
||||
const rest = spotlight ? trips.filter(t => t.id !== spotlight.id) : trips
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg-secondary)', ...font }}>
|
||||
<div style={{ position: 'fixed', inset: 0, display: 'flex', flexDirection: 'column', background: 'var(--bg-secondary)', ...font }}>
|
||||
<Navbar />
|
||||
{demoMode && <DemoBanner />}
|
||||
<div style={{ paddingTop: 56 }}>
|
||||
<div style={{ flex: 1, overflow: 'auto', overscrollBehavior: 'contain', marginTop: 56 }}>
|
||||
<div style={{ maxWidth: 1300, margin: '0 auto', padding: '32px 20px 60px' }}>
|
||||
|
||||
{/* Header */}
|
||||
@@ -453,21 +469,75 @@ export default function DashboardPage() {
|
||||
: t('dashboard.subtitle.empty')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setEditingTrip(null); setShowForm(true) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7, padding: '9px 18px',
|
||||
background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', borderRadius: 12,
|
||||
fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.opacity = '0.85'}
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch' }}>
|
||||
{/* Widget settings */}
|
||||
<button
|
||||
onClick={() => setShowWidgetSettings(s => s ? false : true)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '0 14px',
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 12,
|
||||
cursor: 'pointer', color: 'var(--text-faint)', fontFamily: 'inherit',
|
||||
transition: 'background 0.15s, border-color 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)'; e.currentTarget.style.borderColor = 'var(--text-faint)' }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'var(--bg-card)'; e.currentTarget.style.borderColor = 'var(--border-primary)' }}
|
||||
>
|
||||
<Settings size={15} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditingTrip(null); setShowForm(true) }}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7, padding: '9px 18px',
|
||||
background: 'var(--accent)', color: 'var(--accent-text)', border: 'none', borderRadius: 12,
|
||||
fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.opacity = '0.85'}
|
||||
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
|
||||
>
|
||||
<Plus size={15} /> {t('dashboard.newTrip')}
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Widget settings dropdown */}
|
||||
{showWidgetSettings && (
|
||||
<div className="rounded-xl border p-3 mb-4 flex items-center gap-4" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)' }}>
|
||||
<span className="text-xs font-semibold" style={{ color: 'var(--text-muted)' }}>Widgets:</span>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<button onClick={() => updateSetting('dashboard_currency', showCurrency ? 'off' : 'on')}
|
||||
className="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||
style={{ background: showCurrency ? 'var(--text-primary)' : 'var(--border-primary)' }}>
|
||||
<span className="absolute left-0.5 h-4 w-4 rounded-full transition-transform duration-200"
|
||||
style={{ background: 'var(--bg-card)', transform: showCurrency ? 'translateX(16px)' : 'translateX(0)' }} />
|
||||
</button>
|
||||
<span className="text-xs" style={{ color: 'var(--text-primary)' }}>{t('dashboard.currency')}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<button onClick={() => updateSetting('dashboard_timezone', showTimezone ? 'off' : 'on')}
|
||||
className="relative inline-flex h-5 w-9 items-center rounded-full transition-colors"
|
||||
style={{ background: showTimezone ? 'var(--text-primary)' : 'var(--border-primary)' }}>
|
||||
<span className="absolute left-0.5 h-4 w-4 rounded-full transition-transform duration-200"
|
||||
style={{ background: 'var(--bg-card)', transform: showTimezone ? 'translateX(16px)' : 'translateX(0)' }} />
|
||||
</button>
|
||||
<span className="text-xs" style={{ color: 'var(--text-primary)' }}>{t('dashboard.timezone')}</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile widgets button */}
|
||||
{showSidebar && (
|
||||
<button
|
||||
onClick={() => setShowWidgetSettings('mobile')}
|
||||
className="lg:hidden flex items-center justify-center gap-2 w-full py-2.5 rounded-xl text-xs font-semibold mb-4"
|
||||
style={{ background: 'var(--bg-card)', border: '1px solid var(--border-primary)', color: 'var(--text-primary)' }}
|
||||
>
|
||||
<ArrowRightLeft size={13} style={{ color: 'var(--text-faint)' }} />
|
||||
{showCurrency && showTimezone ? `${t('dashboard.currency')} & ${t('dashboard.timezone')}` : showCurrency ? t('dashboard.currency') : t('dashboard.timezone')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 24, alignItems: 'flex-start' }}>
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
@@ -562,14 +632,37 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats sidebar */}
|
||||
<div className="hidden lg:block" style={{ position: 'sticky', top: 80, flexShrink: 0 }}>
|
||||
<TravelStats />
|
||||
</div>
|
||||
{/* Widgets sidebar */}
|
||||
{showSidebar && (
|
||||
<div className="hidden lg:flex flex-col gap-4" style={{ position: 'sticky', top: 80, flexShrink: 0, width: 280 }}>
|
||||
{showCurrency && <CurrencyWidget />}
|
||||
{showTimezone && <TimezoneWidget />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile widgets bottom sheet */}
|
||||
{showWidgetSettings === 'mobile' && (
|
||||
<div className="lg:hidden fixed inset-0 z-50" style={{ background: 'rgba(0,0,0,0.3)', touchAction: 'none' }} onClick={() => setShowWidgetSettings(false)}>
|
||||
<div className="absolute bottom-0 left-0 right-0 flex flex-col overflow-hidden"
|
||||
style={{ maxHeight: '80vh', background: 'var(--bg-card)', borderRadius: '20px 20px 0 0', overscrollBehavior: 'contain' }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-4 py-3" style={{ borderBottom: '1px solid var(--border-secondary)' }}>
|
||||
<span className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>Widgets</span>
|
||||
<button onClick={() => setShowWidgetSettings(false)} className="w-7 h-7 rounded-full flex items-center justify-center" style={{ background: 'var(--bg-secondary)' }}>
|
||||
<X size={14} style={{ color: 'var(--text-primary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4 space-y-4">
|
||||
{showCurrency && <CurrencyWidget />}
|
||||
{showTimezone && <TimezoneWidget />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TripFormModal
|
||||
isOpen={showForm}
|
||||
onClose={() => { setShowForm(false); setEditingTrip(null) }}
|
||||
|
||||
Reference in New Issue
Block a user