From 384d583628b4e6624206e5c2cb58ec79980adbca Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 20 Mar 2026 23:14:06 +0100 Subject: [PATCH] =?UTF-8?q?v2.5.0=20=E2=80=94=20Addon=20System,=20Vacay,?= =?UTF-8?q?=20Atlas,=20Dashboard=20Widgets=20&=20Mobile=20Overhaul?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 6 +- client/package.json | 2 +- client/src/App.jsx | 20 +- client/src/api/client.js | 6 + client/src/components/Admin/AddonManager.jsx | 158 +++++ client/src/components/Admin/BackupPanel.jsx | 24 +- .../src/components/Admin/CategoryManager.jsx | 9 +- .../components/Dashboard/CurrencyWidget.jsx | 89 +++ .../components/Dashboard/TimezoneWidget.jsx | 126 ++++ client/src/components/Layout/Navbar.jsx | 61 +- .../src/components/Planner/PlaceFormModal.jsx | 6 +- client/src/components/Vacay/VacayCalendar.jsx | 96 +++ .../src/components/Vacay/VacayMonthCard.jsx | 118 ++++ client/src/components/Vacay/VacayPersons.jsx | 190 ++++++ client/src/components/Vacay/VacaySettings.jsx | 213 +++++++ client/src/components/Vacay/VacayStats.jsx | 124 ++++ client/src/components/Vacay/holidays.js | 146 +++++ client/src/components/shared/Modal.jsx | 6 +- client/src/i18n/translations/de.js | 136 +++- client/src/i18n/translations/en.js | 130 ++++ client/src/index.css | 93 +++ client/src/pages/AdminPage.jsx | 25 +- client/src/pages/AtlasPage.jsx | 482 +++++++++++++++ client/src/pages/DashboardPage.jsx | 129 +++- client/src/pages/TripPlannerPage.jsx | 63 +- client/src/pages/VacayPage.jsx | 282 +++++++++ client/src/store/vacayStore.js | 188 ++++++ server/package-lock.json | 4 +- server/package.json | 2 +- server/src/db/database.js | 95 +++ server/src/index.js | 15 + server/src/routes/admin.js | 26 +- server/src/routes/atlas.js | 247 ++++++++ server/src/routes/vacay.js | 582 ++++++++++++++++++ server/src/websocket.js | 24 +- 35 files changed, 3841 insertions(+), 82 deletions(-) create mode 100644 client/src/components/Admin/AddonManager.jsx create mode 100644 client/src/components/Dashboard/CurrencyWidget.jsx create mode 100644 client/src/components/Dashboard/TimezoneWidget.jsx create mode 100644 client/src/components/Vacay/VacayCalendar.jsx create mode 100644 client/src/components/Vacay/VacayMonthCard.jsx create mode 100644 client/src/components/Vacay/VacayPersons.jsx create mode 100644 client/src/components/Vacay/VacaySettings.jsx create mode 100644 client/src/components/Vacay/VacayStats.jsx create mode 100644 client/src/components/Vacay/holidays.js create mode 100644 client/src/pages/AtlasPage.jsx create mode 100644 client/src/pages/VacayPage.jsx create mode 100644 client/src/store/vacayStore.js create mode 100644 server/src/routes/atlas.js create mode 100644 server/src/routes/vacay.js diff --git a/README.md b/README.md index 3ee3189..d725ece 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,11 @@ A self-hosted, real-time collaborative travel planner for organizing trips with - **Document Manager** — Attach documents, tickets, and PDFs to trips, places, or reservations (up to 50 MB per file) - **PDF Export** — Export complete trip plans as PDF with images and notes - **Multi-User** — Invite members to collaborate on shared trips with role-based access -- **Admin Panel** — User management, create users, global categories, API key configuration, and backups +- **Addon System** — Modular features that admins can enable/disable: Packing Lists, Budget, Documents, and global addons +- **Vacay** — Personal vacation day planner with calendar view, public holidays (100+ countries), company holidays, user fusion with WebSocket live sync, and carry-over tracking +- **Atlas** — Interactive world map showing visited countries with travel stats, continent breakdown, streak tracking, and country details on click +- **Dashboard Widgets** — Currency converter and timezone clock, toggleable per user +- **Admin Panel** — User management with online status, global categories, addon management, API key configuration, and backups - **Auto-Backups** — Scheduled backups with configurable interval and retention - **Route Optimization** — Auto-optimize place order and export to Google Maps - **Day Notes** — Add timestamped notes to individual days diff --git a/client/package.json b/client/package.json index 727484e..aaea969 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "nomad-client", - "version": "2.0.0", + "version": "2.5.0", "private": true, "type": "module", "scripts": { diff --git a/client/src/App.jsx b/client/src/App.jsx index 1e5beea..619e76a 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,6 +10,8 @@ import TripPlannerPage from './pages/TripPlannerPage' 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' @@ -33,7 +35,7 @@ function ProtectedRoute({ children, adminRequired = false }) { return } - if (adminRequired && user?.role !== 'admin') { + if (adminRequired && user && user.role !== 'admin') { return } @@ -132,6 +134,22 @@ export default function App() { } /> + + + + } + /> + + + + } + /> } /> diff --git a/client/src/api/client.js b/client/src/api/client.js index 179c5d8..dffb805 100644 --- a/client/src/api/client.js +++ b/client/src/api/client.js @@ -127,6 +127,12 @@ export const adminApi = { saveDemoBaseline: () => apiClient.post('/admin/save-demo-baseline').then(r => r.data), getOidc: () => apiClient.get('/admin/oidc').then(r => r.data), updateOidc: (data) => apiClient.put('/admin/oidc', data).then(r => r.data), + addons: () => apiClient.get('/admin/addons').then(r => r.data), + updateAddon: (id, data) => apiClient.put(`/admin/addons/${id}`, data).then(r => r.data), +} + +export const addonsApi = { + enabled: () => apiClient.get('/addons').then(r => r.data), } export const mapsApi = { diff --git a/client/src/components/Admin/AddonManager.jsx b/client/src/components/Admin/AddonManager.jsx new file mode 100644 index 0000000..4751193 --- /dev/null +++ b/client/src/components/Admin/AddonManager.jsx @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from 'react' +import { adminApi } from '../../api/client' +import { useTranslation } from '../../i18n' +import { useToast } from '../shared/Toast' +import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase } from 'lucide-react' + +const ICON_MAP = { + ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, +} + +function AddonIcon({ name, size = 20 }) { + const Icon = ICON_MAP[name] || Puzzle + return +} + +export default function AddonManager() { + const { t } = useTranslation() + const toast = useToast() + const [addons, setAddons] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + loadAddons() + }, []) + + const loadAddons = async () => { + setLoading(true) + try { + const data = await adminApi.addons() + setAddons(data.addons) + } catch (err) { + toast.error(t('admin.addons.toast.error')) + } finally { + setLoading(false) + } + } + + const handleToggle = async (addon) => { + const newEnabled = !addon.enabled + // Optimistic update + setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: newEnabled } : a)) + try { + await adminApi.updateAddon(addon.id, { enabled: newEnabled }) + window.dispatchEvent(new Event('addons-changed')) + toast.success(t('admin.addons.toast.updated')) + } catch (err) { + // Rollback + setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: !newEnabled } : a)) + toast.error(t('admin.addons.toast.error')) + } + } + + const tripAddons = addons.filter(a => a.type === 'trip') + const globalAddons = addons.filter(a => a.type === 'global') + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

{t('admin.addons.title')}

+

{t('admin.addons.subtitle')}

+
+ + {addons.length === 0 ? ( +
+ {t('admin.addons.noAddons')} +
+ ) : ( +
+ {/* Trip Addons */} + {tripAddons.length > 0 && ( +
+
+ + + {t('admin.addons.type.trip')} — {t('admin.addons.tripHint')} + +
+ {tripAddons.map(addon => ( + + ))} +
+ )} + + {/* Global Addons */} + {globalAddons.length > 0 && ( +
+
+ + + {t('admin.addons.type.global')} — {t('admin.addons.globalHint')} + +
+ {globalAddons.map(addon => ( + + ))} +
+ )} +
+ )} +
+
+ ) +} + +function AddonRow({ addon, onToggle, t }) { + return ( +
+ {/* Icon */} +
+ +
+ + {/* Info */} +
+
+ {addon.name} + + {addon.type === 'global' ? t('admin.addons.type.global') : t('admin.addons.type.trip')} + +
+

{addon.description}

+
+ + {/* Toggle */} +
+ + {addon.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} + + +
+
+ ) +} diff --git a/client/src/components/Admin/BackupPanel.jsx b/client/src/components/Admin/BackupPanel.jsx index ac08ec4..47c3f8b 100644 --- a/client/src/components/Admin/BackupPanel.jsx +++ b/client/src/components/Admin/BackupPanel.jsx @@ -153,8 +153,8 @@ export default function BackupPanel() {
-

{t('backup.title')}

-

{t('backup.subtitle')}

+

{t('backup.title')}

+

{t('backup.subtitle')}

@@ -179,26 +179,28 @@ export default function BackupPanel() { onClick={() => fileInputRef.current?.click()} disabled={isUploading} className="flex items-center gap-2 border border-gray-200 text-gray-700 px-3 py-2 rounded-lg hover:bg-gray-50 text-sm font-medium disabled:opacity-60" + title={isUploading ? t('backup.uploading') : t('backup.upload')} > {isUploading ? (
) : ( )} - {isUploading ? t('backup.uploading') : t('backup.upload')} + {isUploading ? t('backup.uploading') : t('backup.upload')}
@@ -275,23 +277,23 @@ export default function BackupPanel() {
-

{t('backup.auto.title')}

-

{t('backup.auto.subtitle')}

+

{t('backup.auto.title')}

+

{t('backup.auto.subtitle')}

{/* Enable toggle */} -