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
158 lines
4.4 KiB
JavaScript
158 lines
4.4 KiB
JavaScript
import React, { useEffect } from 'react'
|
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
|
|
import { useAuthStore } from './store/authStore'
|
|
import { useSettingsStore } from './store/settingsStore'
|
|
import LoginPage from './pages/LoginPage'
|
|
import RegisterPage from './pages/RegisterPage'
|
|
import DashboardPage from './pages/DashboardPage'
|
|
import TripPlannerPage from './pages/TripPlannerPage'
|
|
// PhotosPage removed - replaced by Finanzplan
|
|
import FilesPage from './pages/FilesPage'
|
|
import AdminPage from './pages/AdminPage'
|
|
import SettingsPage from './pages/SettingsPage'
|
|
import VacayPage from './pages/VacayPage'
|
|
import AtlasPage from './pages/AtlasPage'
|
|
import { ToastContainer } from './components/shared/Toast'
|
|
import { TranslationProvider } from './i18n'
|
|
import DemoBanner from './components/Layout/DemoBanner'
|
|
import { authApi } from './api/client'
|
|
|
|
function ProtectedRoute({ children, adminRequired = false }) {
|
|
const { isAuthenticated, user, isLoading } = useAuthStore()
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-10 h-10 border-4 border-slate-200 border-t-slate-900 rounded-full animate-spin"></div>
|
|
<p className="text-slate-500 text-sm">Wird geladen...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Navigate to="/login" replace />
|
|
}
|
|
|
|
if (adminRequired && user && user.role !== 'admin') {
|
|
return <Navigate to="/dashboard" replace />
|
|
}
|
|
|
|
return children
|
|
}
|
|
|
|
function RootRedirect() {
|
|
const { isAuthenticated, isLoading } = useAuthStore()
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
<div className="w-10 h-10 border-4 border-slate-200 border-t-slate-900 rounded-full animate-spin"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return <Navigate to={isAuthenticated ? '/dashboard' : '/login'} replace />
|
|
}
|
|
|
|
export default function App() {
|
|
const { loadUser, token, isAuthenticated, demoMode, setDemoMode, setHasMapsKey } = useAuthStore()
|
|
const { loadSettings } = useSettingsStore()
|
|
|
|
useEffect(() => {
|
|
if (token) {
|
|
loadUser()
|
|
}
|
|
authApi.getAppConfig().then(config => {
|
|
if (config?.demo_mode) setDemoMode(true)
|
|
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
|
}).catch(() => {})
|
|
}, [])
|
|
|
|
const { settings } = useSettingsStore()
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadSettings()
|
|
}
|
|
}, [isAuthenticated])
|
|
|
|
// Apply dark mode class to <html>
|
|
useEffect(() => {
|
|
if (settings.dark_mode) {
|
|
document.documentElement.classList.add('dark')
|
|
} else {
|
|
document.documentElement.classList.remove('dark')
|
|
}
|
|
}, [settings.dark_mode])
|
|
|
|
return (
|
|
<TranslationProvider>
|
|
<ToastContainer />
|
|
<Routes>
|
|
<Route path="/" element={<RootRedirect />} />
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route path="/register" element={<Navigate to="/login" replace />} />
|
|
<Route
|
|
path="/dashboard"
|
|
element={
|
|
<ProtectedRoute>
|
|
<DashboardPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/trips/:id"
|
|
element={
|
|
<ProtectedRoute>
|
|
<TripPlannerPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/trips/:id/files"
|
|
element={
|
|
<ProtectedRoute>
|
|
<FilesPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<ProtectedRoute adminRequired>
|
|
<AdminPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/settings"
|
|
element={
|
|
<ProtectedRoute>
|
|
<SettingsPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/vacay"
|
|
element={
|
|
<ProtectedRoute>
|
|
<VacayPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/atlas"
|
|
element={
|
|
<ProtectedRoute>
|
|
<AtlasPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</TranslationProvider>
|
|
)
|
|
}
|