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:
Maurice
2026-03-20 23:14:06 +01:00
parent 3edf65957b
commit 384d583628
35 changed files with 3841 additions and 82 deletions

View File

@@ -48,6 +48,9 @@ const de = {
'dashboard.subtitle.activeMany': '{count} aktive Reisen',
'dashboard.subtitle.archivedSuffix': ' · {count} archiviert',
'dashboard.newTrip': 'Neue Reise',
'dashboard.currency': 'Währung',
'dashboard.timezone': 'Zeitzonen',
'dashboard.localTime': 'Lokal',
'dashboard.emptyTitle': 'Noch keine Reisen',
'dashboard.emptyText': 'Erstelle deine erste Reise und beginne mit der Planung von Orten, Tagesabläufen und Packlisten.',
'dashboard.emptyButton': 'Erste Reise erstellen',
@@ -227,6 +230,7 @@ const de = {
'admin.allowRegistration': 'Registrierung erlauben',
'admin.allowRegistrationHint': 'Neue Benutzer können sich selbst registrieren',
'admin.apiKeys': 'API-Schlüssel',
'admin.apiKeysHint': 'Optional. Aktiviert erweiterte Ortsdaten wie Fotos und Wetter.',
'admin.mapsKey': 'Google Maps API Key',
'admin.mapsKeyHint': 'Für Ortsuche benötigt. Erstellen unter console.cloud.google.com',
'admin.mapsKeyHintLong': 'Ohne API Key wird OpenStreetMap für die Ortssuche genutzt. Mit Google API Key können zusätzlich Bilder, Bewertungen und Öffnungszeiten geladen werden. Erstellen unter console.cloud.google.com.',
@@ -244,11 +248,137 @@ const de = {
'admin.oidcIssuerHint': 'Die OpenID Connect Issuer URL des Anbieters. z.B. https://accounts.google.com',
'admin.oidcSaved': 'OIDC-Konfiguration gespeichert',
// Addons
'admin.tabs.addons': 'Addons',
'admin.addons.title': 'Addons',
'admin.addons.subtitle': 'Aktiviere oder deaktiviere Funktionen, um NOMAD nach deinen Wünschen anzupassen.',
'admin.addons.enabled': 'Aktiviert',
'admin.addons.disabled': 'Deaktiviert',
'admin.addons.type.trip': 'Trip',
'admin.addons.type.global': 'Global',
'admin.addons.tripHint': 'Verfügbar als Tab innerhalb jedes Trips',
'admin.addons.globalHint': 'Verfügbar als eigenständiger Bereich in der Navigation',
'admin.addons.toast.updated': 'Addon aktualisiert',
'admin.addons.toast.error': 'Addon konnte nicht aktualisiert werden',
'admin.addons.noAddons': 'Keine Addons verfügbar',
// Vacay addon
'vacay.subtitle': 'Urlaubstage planen und verwalten',
'vacay.settings': 'Einstellungen',
'vacay.year': 'Jahr',
'vacay.addYear': 'Jahr hinzufügen',
'vacay.removeYear': 'Jahr entfernen',
'vacay.removeYearConfirm': '{year} entfernen?',
'vacay.removeYearHint': 'Alle Urlaubseinträge und Betriebsferien für dieses Jahr werden unwiderruflich gelöscht.',
'vacay.remove': 'Entfernen',
'vacay.persons': 'Personen',
'vacay.noPersons': 'Keine Personen angelegt',
'vacay.addPerson': 'Person hinzufügen',
'vacay.editPerson': 'Person bearbeiten',
'vacay.removePerson': 'Person entfernen',
'vacay.removePersonConfirm': '{name} wirklich entfernen?',
'vacay.removePersonHint': 'Alle Urlaubseinträge dieser Person werden unwiderruflich gelöscht.',
'vacay.personName': 'Name',
'vacay.personNamePlaceholder': 'Name eingeben',
'vacay.color': 'Farbe',
'vacay.add': 'Hinzufügen',
'vacay.legend': 'Legende',
'vacay.publicHoliday': 'Feiertag',
'vacay.companyHoliday': 'Betriebsferien',
'vacay.weekend': 'Wochenende',
'vacay.modeVacation': 'Urlaub',
'vacay.modeCompany': 'Betriebsferien',
'vacay.entitlement': 'Urlaubsanspruch',
'vacay.entitlementDays': 'Tage',
'vacay.used': 'Weg',
'vacay.remaining': 'Rest',
'vacay.carriedOver': 'aus {year}',
'vacay.blockWeekends': 'Wochenenden sperren',
'vacay.blockWeekendsHint': 'Verhindert Urlaubseinträge an Samstagen und Sonntagen',
'vacay.publicHolidays': 'Feiertage',
'vacay.publicHolidaysHint': 'Feiertage im Kalender markieren',
'vacay.selectCountry': 'Land wählen',
'vacay.selectRegion': 'Region wählen (optional)',
'vacay.companyHolidays': 'Betriebsferien',
'vacay.companyHolidaysHint': 'Erlaubt das Markieren von unternehmensweiten Feiertagen',
'vacay.companyHolidaysNoDeduct': 'Betriebsferien werden nicht vom Urlaubskontingent abgezogen.',
'vacay.carryOver': 'Urlaubsmitnahme',
'vacay.carryOverHint': 'Resturlaub automatisch ins Folgejahr übertragen',
'vacay.sharing': 'Teilen',
'vacay.sharingHint': 'Teile deinen Urlaubsplan mit anderen NOMAD-Benutzern',
'vacay.owner': 'Besitzer',
'vacay.shareEmailPlaceholder': 'E-Mail des NOMAD-Benutzers',
'vacay.shareSuccess': 'Plan erfolgreich geteilt',
'vacay.shareError': 'Plan konnte nicht geteilt werden',
'vacay.dissolve': 'Fusion auflösen',
'vacay.dissolveHint': 'Kalender wieder trennen. Deine Einträge bleiben erhalten.',
'vacay.dissolveAction': 'Auflösen',
'vacay.dissolved': 'Kalender getrennt',
'vacay.fusedWith': 'Fusioniert mit',
'vacay.you': 'du',
'vacay.noData': 'Keine Daten',
'vacay.changeColor': 'Farbe ändern',
'vacay.inviteUser': 'Benutzer einladen',
'vacay.inviteHint': 'Lade einen anderen NOMAD-Benutzer ein, um einen gemeinsamen Urlaubskalender zu teilen.',
'vacay.selectUser': 'Benutzer wählen',
'vacay.sendInvite': 'Einladung senden',
'vacay.inviteSent': 'Einladung gesendet',
'vacay.inviteError': 'Einladung konnte nicht gesendet werden',
'vacay.pending': 'ausstehend',
'vacay.noUsersAvailable': 'Keine Benutzer verfügbar',
'vacay.accept': 'Annehmen',
'vacay.decline': 'Ablehnen',
'vacay.acceptFusion': 'Annehmen & Fusionieren',
'vacay.inviteTitle': 'Fusionsanfrage',
'vacay.inviteWantsToFuse': 'möchte einen Urlaubskalender mit dir teilen.',
'vacay.fuseInfo1': 'Beide sehen alle Urlaubseinträge in einem gemeinsamen Kalender.',
'vacay.fuseInfo2': 'Beide können Einträge für den jeweils anderen erstellen und bearbeiten.',
'vacay.fuseInfo3': 'Beide können Einträge löschen und den Urlaubsanspruch ändern.',
'vacay.fuseInfo4': 'Einstellungen wie Feiertage und Betriebsferien werden geteilt.',
'vacay.fuseInfo5': 'Die Fusion kann jederzeit von beiden Seiten aufgelöst werden. Einträge bleiben erhalten.',
'nav.myTrips': 'Meine Trips',
// Atlas addon
'atlas.subtitle': 'Dein Reise-Fußabdruck auf der Welt',
'atlas.countries': 'Länder',
'atlas.trips': 'Reisen',
'atlas.places': 'Orte',
'atlas.days': 'Tage',
'atlas.visitedCountries': 'Besuchte Länder',
'atlas.cities': 'Städte',
'atlas.noData': 'Noch keine Reisedaten',
'atlas.noDataHint': 'Erstelle einen Trip und füge Orte hinzu',
'atlas.lastTrip': 'Letzter Trip',
'atlas.nextTrip': 'Nächster Trip',
'atlas.daysLeft': 'Tage',
'atlas.streak': 'Streak',
'atlas.year': 'Jahr',
'atlas.years': 'Jahre',
'atlas.yearInRow': 'Jahr in Folge',
'atlas.yearsInRow': 'Jahre in Folge',
'atlas.tripIn': 'Reise in',
'atlas.tripsIn': 'Reisen in',
'atlas.since': 'seit',
'atlas.europe': 'Europa',
'atlas.asia': 'Asien',
'atlas.northAmerica': 'N-Amerika',
'atlas.southAmerica': 'S-Amerika',
'atlas.africa': 'Afrika',
'atlas.oceania': 'Ozeanien',
'atlas.other': 'Andere',
'atlas.firstVisit': 'Erste Reise',
'atlas.lastVisitLabel': 'Letzte Reise',
'atlas.tripSingular': 'Reise',
'atlas.tripPlural': 'Reisen',
'atlas.placeVisited': 'Ort besucht',
'atlas.placesVisited': 'Orte besucht',
// Trip Planner
'trip.tabs.plan': 'Planung',
'trip.tabs.plan': 'Karte',
'trip.tabs.reservations': 'Buchungen',
'trip.tabs.packing': 'Packliste',
'trip.tabs.packingShort': 'Packliste',
'trip.tabs.reservationsShort': 'Buchung',
'trip.tabs.packing': 'Liste',
'trip.tabs.packingShort': 'Liste',
'trip.tabs.budget': 'Budget',
'trip.tabs.files': 'Dateien',
'trip.loading': 'Reise wird geladen...',

View File

@@ -48,6 +48,9 @@ const en = {
'dashboard.subtitle.activeMany': '{count} active trips',
'dashboard.subtitle.archivedSuffix': ' · {count} archived',
'dashboard.newTrip': 'New Trip',
'dashboard.currency': 'Currency',
'dashboard.timezone': 'Timezones',
'dashboard.localTime': 'Local',
'dashboard.emptyTitle': 'No trips yet',
'dashboard.emptyText': 'Create your first trip and start planning!',
'dashboard.emptyButton': 'Create First Trip',
@@ -227,6 +230,7 @@ const en = {
'admin.allowRegistration': 'Allow Registration',
'admin.allowRegistrationHint': 'New users can register themselves',
'admin.apiKeys': 'API Keys',
'admin.apiKeysHint': 'Optional. Enables extended place data like photos and weather.',
'admin.mapsKey': 'Google Maps API Key',
'admin.mapsKeyHint': 'Required for place search. Get at console.cloud.google.com',
'admin.mapsKeyHintLong': 'Without an API key, OpenStreetMap is used for place search. With a Google API key, photos, ratings, and opening hours can be loaded as well. Get one at console.cloud.google.com.',
@@ -244,9 +248,135 @@ const en = {
'admin.oidcIssuerHint': 'The OpenID Connect Issuer URL of the provider. e.g. https://accounts.google.com',
'admin.oidcSaved': 'OIDC configuration saved',
// Addons
'admin.tabs.addons': 'Addons',
'admin.addons.title': 'Addons',
'admin.addons.subtitle': 'Enable or disable features to customize your NOMAD experience.',
'admin.addons.enabled': 'Enabled',
'admin.addons.disabled': 'Disabled',
'admin.addons.type.trip': 'Trip',
'admin.addons.type.global': 'Global',
'admin.addons.tripHint': 'Available as a tab within each trip',
'admin.addons.globalHint': 'Available as a standalone section in the main navigation',
'admin.addons.toast.updated': 'Addon updated',
'admin.addons.toast.error': 'Failed to update addon',
'admin.addons.noAddons': 'No addons available',
// Vacay addon
'vacay.subtitle': 'Plan and manage vacation days',
'vacay.settings': 'Settings',
'vacay.year': 'Year',
'vacay.addYear': 'Add year',
'vacay.removeYear': 'Remove year',
'vacay.removeYearConfirm': 'Remove {year}?',
'vacay.removeYearHint': 'All vacation entries and company holidays for this year will be permanently deleted.',
'vacay.remove': 'Remove',
'vacay.persons': 'Persons',
'vacay.noPersons': 'No persons added',
'vacay.addPerson': 'Add Person',
'vacay.editPerson': 'Edit Person',
'vacay.removePerson': 'Remove Person',
'vacay.removePersonConfirm': 'Remove {name}?',
'vacay.removePersonHint': 'All vacation entries for this person will be permanently deleted.',
'vacay.personName': 'Name',
'vacay.personNamePlaceholder': 'Enter name',
'vacay.color': 'Color',
'vacay.add': 'Add',
'vacay.legend': 'Legend',
'vacay.publicHoliday': 'Public Holiday',
'vacay.companyHoliday': 'Company Holiday',
'vacay.weekend': 'Weekend',
'vacay.modeVacation': 'Vacation',
'vacay.modeCompany': 'Company Holiday',
'vacay.entitlement': 'Entitlement',
'vacay.entitlementDays': 'Days',
'vacay.used': 'Used',
'vacay.remaining': 'Left',
'vacay.carriedOver': 'from {year}',
'vacay.blockWeekends': 'Block Weekends',
'vacay.blockWeekendsHint': 'Prevent vacation entries on Saturdays and Sundays',
'vacay.publicHolidays': 'Public Holidays',
'vacay.publicHolidaysHint': 'Mark public holidays in the calendar',
'vacay.selectCountry': 'Select country',
'vacay.selectRegion': 'Select region (optional)',
'vacay.companyHolidays': 'Company Holidays',
'vacay.companyHolidaysHint': 'Allow marking company-wide holiday days',
'vacay.companyHolidaysNoDeduct': 'Company holidays do not count towards vacation days.',
'vacay.carryOver': 'Carry Over',
'vacay.carryOverHint': 'Automatically carry remaining vacation days into the next year',
'vacay.sharing': 'Sharing',
'vacay.sharingHint': 'Share your vacation plan with other NOMAD users',
'vacay.owner': 'Owner',
'vacay.shareEmailPlaceholder': 'Email of NOMAD user',
'vacay.shareSuccess': 'Plan shared successfully',
'vacay.shareError': 'Could not share plan',
'vacay.dissolve': 'Dissolve Fusion',
'vacay.dissolveHint': 'Separate calendars again. Your entries will be kept.',
'vacay.dissolveAction': 'Dissolve',
'vacay.dissolved': 'Calendar separated',
'vacay.fusedWith': 'Fused with',
'vacay.you': 'you',
'vacay.noData': 'No data',
'vacay.changeColor': 'Change color',
'vacay.inviteUser': 'Invite User',
'vacay.inviteHint': 'Invite another NOMAD user to share a combined vacation calendar.',
'vacay.selectUser': 'Select user',
'vacay.sendInvite': 'Send Invite',
'vacay.inviteSent': 'Invite sent',
'vacay.inviteError': 'Could not send invite',
'vacay.pending': 'pending',
'vacay.noUsersAvailable': 'No users available',
'vacay.accept': 'Accept',
'vacay.decline': 'Decline',
'vacay.acceptFusion': 'Accept & Fuse',
'vacay.inviteTitle': 'Fusion Request',
'vacay.inviteWantsToFuse': 'wants to share a vacation calendar with you.',
'vacay.fuseInfo1': 'Both of you will see all vacation entries in one shared calendar.',
'vacay.fuseInfo2': 'Both parties can create and edit entries for each other.',
'vacay.fuseInfo3': 'Both parties can delete entries and change vacation entitlements.',
'vacay.fuseInfo4': 'Settings like public holidays and company holidays are shared.',
'vacay.fuseInfo5': 'The fusion can be dissolved at any time by either party. Your entries will be preserved.',
'nav.myTrips': 'My Trips',
// Atlas addon
'atlas.subtitle': 'Your travel footprint around the world',
'atlas.countries': 'Countries',
'atlas.trips': 'Trips',
'atlas.places': 'Places',
'atlas.days': 'Days',
'atlas.visitedCountries': 'Visited Countries',
'atlas.cities': 'Cities',
'atlas.noData': 'No travel data yet',
'atlas.noDataHint': 'Create a trip and add places to see your world map',
'atlas.lastTrip': 'Last trip',
'atlas.nextTrip': 'Next trip',
'atlas.daysLeft': 'days left',
'atlas.streak': 'Streak',
'atlas.year': 'year',
'atlas.years': 'years',
'atlas.yearInRow': 'year in a row',
'atlas.yearsInRow': 'years in a row',
'atlas.tripIn': 'trip in',
'atlas.tripsIn': 'trips in',
'atlas.since': 'since',
'atlas.europe': 'Europe',
'atlas.asia': 'Asia',
'atlas.northAmerica': 'N. America',
'atlas.southAmerica': 'S. America',
'atlas.africa': 'Africa',
'atlas.oceania': 'Oceania',
'atlas.other': 'Other',
'atlas.firstVisit': 'First trip',
'atlas.lastVisitLabel': 'Last trip',
'atlas.tripSingular': 'Trip',
'atlas.tripPlural': 'Trips',
'atlas.placeVisited': 'Place visited',
'atlas.placesVisited': 'Places visited',
// Trip Planner
'trip.tabs.plan': 'Plan',
'trip.tabs.reservations': 'Bookings',
'trip.tabs.reservationsShort': 'Book',
'trip.tabs.packing': 'Packing List',
'trip.tabs.packingShort': 'Packing',
'trip.tabs.budget': 'Budget',