BREAKING: Reservations have been completely rebuilt. Existing place-level reservations are no longer used. All reservations must be re-created via the Bookings tab. Your trips, places, and other data are unaffected. Reservation System (rebuilt from scratch): - Reservations now link to specific day assignments instead of places - Same place on different days can have independent reservations - New assignment picker in booking modal (grouped by day, searchable) - Removed day/place dropdowns from booking form - Reservation badges in day plan sidebar with type-specific icons - Reservation details in place inspector (only for selected assignment) - Reservation summary in day detail panel Day Detail Panel (new): - Opens on day click in the sidebar - Detailed weather: hourly forecast, precipitation, wind, sunrise/sunset - Historical climate averages for dates beyond 16 days - Accommodation management with check-in/check-out, confirmation number - Hotel assignment across multiple days with day range picker - Reservation overview for the day Places: - Places can now be assigned to the same day multiple times - Start time + end time fields (replaces single time field) - Map badges show multiple position numbers (e.g. "1 · 4") - Route optimization fixed for duplicate places - File attachments during place editing (not just creation) - Cover image upload during trip creation (not just editing) - Paste support (Ctrl+V) for images in trip, place, and file forms Internationalization: - 200+ hardcoded German strings translated to i18n (EN + DE) - Server error messages in English - Category seeds in English for new installations - All planner, register, photo, packing components translated UI/UX: - Auto dark mode (follows system preference, configurable in settings) - Navbar toggle switches light/dark (overrides auto) - Sidebar minimize buttons z-index fixed - Transport mode selector removed from day plan - CustomSelect supports grouped headers (isHeader option) - Optimistic updates for day notes (instant feedback) - Booking cards redesigned with type-colored headers and structured details Weather: - Wind speed in mph when using Fahrenheit setting - Weather description language matches app language Admin: - Weather info panel replaces OpenWeatherMap key input - "Recommended" badge styling updated
170 lines
4.9 KiB
JavaScript
170 lines
4.9 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, useTranslation } from './i18n'
|
|
import DemoBanner from './components/Layout/DemoBanner'
|
|
import { authApi } from './api/client'
|
|
|
|
function ProtectedRoute({ children, adminRequired = false }) {
|
|
const { isAuthenticated, user, isLoading } = useAuthStore()
|
|
const { t } = useTranslation()
|
|
|
|
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">{t('common.loading')}</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> + update PWA theme-color
|
|
useEffect(() => {
|
|
const mode = settings.dark_mode
|
|
const applyDark = (isDark) => {
|
|
document.documentElement.classList.toggle('dark', isDark)
|
|
const meta = document.querySelector('meta[name="theme-color"]')
|
|
if (meta) meta.setAttribute('content', isDark ? '#09090b' : '#ffffff')
|
|
}
|
|
|
|
if (mode === 'auto') {
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
applyDark(mq.matches)
|
|
const handler = (e) => applyDark(e.matches)
|
|
mq.addEventListener('change', handler)
|
|
return () => mq.removeEventListener('change', handler)
|
|
}
|
|
// Support legacy boolean + new string values
|
|
applyDark(mode === true || mode === '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>
|
|
)
|
|
}
|