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.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ background: 'linear-gradient(135deg, #f59e0b, #d97706)', zIndex: 300 }}>
|
||||
{/* Main banner bar */}
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 9999,
|
||||
background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: 24,
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
}} onClick={handleClose}>
|
||||
<div style={{
|
||||
color: '#451a03',
|
||||
padding: '8px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
minHeight: 36,
|
||||
}}>
|
||||
<Info size={15} style={{ flexShrink: 0 }} />
|
||||
<span>
|
||||
{t.title}
|
||||
<span style={{ fontWeight: 400, margin: '0 6px' }}>·</span>
|
||||
{t.resetInfo}
|
||||
{minutesLeft !== null && (
|
||||
<span style={{ fontWeight: 400, opacity: 0.8, marginLeft: 4 }}>
|
||||
({t.nextReset.replace('{min}', minutesLeft)})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
style={{
|
||||
background: 'rgba(69, 26, 3, 0.15)',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
padding: '3px 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
color: '#451a03',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
fontFamily: 'inherit',
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
{expanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
|
||||
{expanded ? t.lessInfo : t.moreInfo}
|
||||
</button>
|
||||
</div>
|
||||
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 && (
|
||||
<div style={{
|
||||
background: '#fffbeb',
|
||||
borderBottom: '1px solid #fbbf24',
|
||||
padding: '16px 24px',
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif",
|
||||
}}>
|
||||
<div style={{ maxWidth: 640, margin: '0 auto' }}>
|
||||
<p style={{ fontSize: 13, color: '#92400e', margin: '0 0 12px', lineHeight: 1.6 }}>
|
||||
{t.description}
|
||||
</p>
|
||||
|
||||
<p style={{ fontSize: 12, fontWeight: 700, color: '#78350f', margin: '0 0 8px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
{t.fullVersionTitle}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 6 }}>
|
||||
{t.features.map((text, i) => {
|
||||
const Icon = featureIcons[i]
|
||||
return (
|
||||
<div key={text} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: '#92400e' }}>
|
||||
<Icon size={14} style={{ flexShrink: 0, opacity: 0.7 }} />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
marginTop: 14,
|
||||
paddingTop: 12,
|
||||
borderTop: '1px solid #fde68a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
fontSize: 12,
|
||||
color: '#92400e',
|
||||
}}>
|
||||
<Github size={14} />
|
||||
<span>{t.selfHost}</span>
|
||||
<a
|
||||
href="https://github.com/mauriceboe/NOMAD"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#78350f', fontWeight: 700, textDecoration: 'underline' }}
|
||||
>
|
||||
{t.selfHostLink} →
|
||||
</a>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #f59e0b, #d97706)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<Info size={20} style={{ color: 'white' }} />
|
||||
</div>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 700, color: '#111827' }}>
|
||||
{t.title}
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: 14, color: '#6b7280', lineHeight: 1.6, margin: '0 0 20px' }}>
|
||||
{t.description}
|
||||
</p>
|
||||
|
||||
<p style={{ fontSize: 12, fontWeight: 700, color: '#374151', margin: '0 0 10px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
{t.fullVersionTitle}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 20 }}>
|
||||
{t.features.map((text, i) => {
|
||||
const Icon = featureIcons[i]
|
||||
return (
|
||||
<div key={text} style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, color: '#4b5563' }}>
|
||||
<Icon size={15} style={{ flexShrink: 0, color: '#d97706' }} />
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
paddingTop: 16, borderTop: '1px solid #e5e7eb',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#9ca3af' }}>
|
||||
<Github size={14} />
|
||||
<span>{t.selfHost}</span>
|
||||
<a href="https://github.com/mauriceboe/NOMAD" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: '#d97706', fontWeight: 600, textDecoration: 'none' }}>
|
||||
{t.selfHostLink}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button onClick={handleClose} style={{
|
||||
background: '#111827', color: 'white', border: 'none',
|
||||
borderRadius: 10, padding: '8px 20px', fontSize: 13,
|
||||
fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>
|
||||
{t.close}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user