From da7905957649884daf1c51abde185eebe613dc0e Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 19 Mar 2026 14:09:12 +0100 Subject: [PATCH] Replace demo banner with dismissable popup modal Shows once per session, no layout interference with navbar/map. Uses sessionStorage so it reappears on next visit. --- client/src/App.jsx | 10 +- client/src/components/Layout/DemoBanner.jsx | 202 ++++++++------------ client/src/store/authStore.js | 8 +- 3 files changed, 90 insertions(+), 130 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 60c8dfa..e3f7ce8 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -13,6 +13,7 @@ import SettingsPage from './pages/SettingsPage' 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() @@ -61,12 +62,9 @@ export default function App() { if (token) { loadUser() } - // Check if demo mode is active - import('./api/client').then(({ authApi }) => { - authApi.getAppConfig?.().then(config => { - if (config?.demo_mode) setDemoMode(true) - }).catch(() => {}) - }) + authApi.getAppConfig().then(config => { + if (config?.demo_mode) setDemoMode(true) + }).catch(() => {}) }, []) const { settings } = useSettingsStore() diff --git a/client/src/components/Layout/DemoBanner.jsx b/client/src/components/Layout/DemoBanner.jsx index ef215f4..68da3be 100644 --- a/client/src/components/Layout/DemoBanner.jsx +++ b/client/src/components/Layout/DemoBanner.jsx @@ -1,16 +1,12 @@ -import React, { useState, useEffect } from 'react' -import { Info, Github, Shield, Key, Users, Database, ChevronDown, ChevronUp } from 'lucide-react' +import React, { useState } from 'react' +import { Info, Github, Shield, Key, Users, Database, X } from 'lucide-react' import { useTranslation } from '../../i18n' const texts = { de: { - title: 'Demo-Modus', - resetInfo: 'Aenderungen werden stuendlich zurueckgesetzt', - nextReset: 'naechster Reset in ~{min} Min.', - moreInfo: 'Mehr Info', - lessInfo: 'Weniger', - description: 'Du nutzt die NOMAD Demo. Du kannst Reisen ansehen, bearbeiten und eigene erstellen — alles wird jede Stunde automatisch zurueckgesetzt.', - fullVersionTitle: 'Diese Funktionen sind in der Vollversion verfuegbar:', + title: 'Willkommen zur NOMAD Demo', + description: 'Du kannst Reisen ansehen, bearbeiten und eigene erstellen. Alle Aenderungen werden jede Stunde automatisch zurueckgesetzt.', + fullVersionTitle: 'In der Vollversion zusaetzlich verfuegbar:', features: [ 'API-Schluessel verwalten (Google Maps, Wetter)', 'Benutzer & Rechte verwalten', @@ -19,15 +15,12 @@ const texts = { ], selfHost: 'NOMAD ist Open Source — ', selfHostLink: 'selbst hosten', + close: 'Verstanden', }, en: { - title: 'Demo Mode', - resetInfo: 'Changes are reset every hour', - nextReset: 'next reset in ~{min} min.', - moreInfo: 'More info', - lessInfo: 'Less', - description: 'You are using the NOMAD demo. You can view, edit and create trips — everything is automatically reset every hour.', - fullVersionTitle: 'These features are available in the full version:', + title: 'Welcome to the NOMAD Demo', + description: 'You can view, edit and create trips. All changes are automatically reset every hour.', + fullVersionTitle: 'Additionally available in the full version:', features: [ 'API key management (Google Maps, Weather)', 'User & permission management', @@ -36,128 +29,93 @@ const texts = { ], selfHost: 'NOMAD is open source — ', selfHostLink: 'self-host it', + close: 'Got it', }, } const featureIcons = [Key, Users, Database, Shield] export default function DemoBanner() { - const [expanded, setExpanded] = useState(false) - const [minutesLeft, setMinutesLeft] = useState(null) + const [dismissed, setDismissed] = useState(() => sessionStorage.getItem('demo_dismissed') === 'true') const { language } = useTranslation() const t = texts[language] || texts.en - useEffect(() => { - const update = () => setMinutesLeft(59 - new Date().getMinutes()) - update() - const interval = setInterval(update, 30000) - return () => clearInterval(interval) - }, []) + if (dismissed) return null - const bannerHeight = expanded ? undefined : 36 + const handleClose = () => { + sessionStorage.setItem('demo_dismissed', 'true') + setDismissed(true) + } return ( -
- {/* Main banner bar */} +
- - - {t.title} - · - {t.resetInfo} - {minutesLeft !== null && ( - - ({t.nextReset.replace('{min}', minutesLeft)}) - - )} - - -
+ background: 'white', borderRadius: 20, padding: '32px 28px 24px', + maxWidth: 440, width: '100%', + boxShadow: '0 20px 60px rgba(0,0,0,0.3)', + }} onClick={e => e.stopPropagation()}> - {/* Expanded info panel */} - {expanded && ( -
-
-

- {t.description} -

- -

- {t.fullVersionTitle} -

- -
- {t.features.map((text, i) => { - const Icon = featureIcons[i] - return ( -
- - {text} -
- ) - })} -
- -
- - {t.selfHost} - - {t.selfHostLink} → - -
+
+
+
+

+ {t.title} +

- )} + +

+ {t.description} +

+ +

+ {t.fullVersionTitle} +

+ +
+ {t.features.map((text, i) => { + const Icon = featureIcons[i] + return ( +
+ + {text} +
+ ) + })} +
+ +
+
+ + {t.selfHost} + + {t.selfHostLink} + +
+ + +
+
) } diff --git a/client/src/store/authStore.js b/client/src/store/authStore.js index 17cde02..2d310b6 100644 --- a/client/src/store/authStore.js +++ b/client/src/store/authStore.js @@ -8,7 +8,7 @@ export const useAuthStore = create((set, get) => ({ isAuthenticated: !!localStorage.getItem('auth_token'), isLoading: false, error: null, - demoMode: false, + demoMode: localStorage.getItem('demo_mode') === 'true', login: async (email, password) => { set({ isLoading: true, error: null }) @@ -131,7 +131,11 @@ export const useAuthStore = create((set, get) => ({ set(state => ({ user: { ...state.user, avatar_url: null } })) }, - setDemoMode: (val) => set({ demoMode: val }), + setDemoMode: (val) => { + if (val) localStorage.setItem('demo_mode', 'true') + else localStorage.removeItem('demo_mode') + set({ demoMode: val }) + }, demoLogin: async () => { set({ isLoading: true, error: null })