mitai-jinkendo/frontend/src/App.jsx
Lars 4191c52298
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
feat: implement Vitals module (Ruhepuls + HRV)
Backend:
- New router: vitals.py with CRUD endpoints
- GET /api/vitals (list)
- GET /api/vitals/by-date/{date}
- POST /api/vitals (upsert)
- PUT /api/vitals/{id}
- DELETE /api/vitals/{id}
- GET /api/vitals/stats (7d/30d averages, trends)
- Registered in main.py

Frontend:
- VitalsPage.jsx with manual entry form
- List with inline editing
- Stats overview (averages, trend indicators)
- Added to CaptureHub (❤️ icon)
- Route /vitals in App.jsx

API:
- Added vitals methods to api.js

v9d Phase 2d - Vitals tracking complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 14:52:09 +01:00

206 lines
7.9 KiB
JavaScript

import { useEffect } from 'react'
import { BrowserRouter, Routes, Route, NavLink, useNavigate } from 'react-router-dom'
import { LayoutDashboard, PlusSquare, TrendingUp, BarChart2, Settings, LogOut } from 'lucide-react'
import { ProfileProvider, useProfile } from './context/ProfileContext'
import { AuthProvider, useAuth } from './context/AuthContext'
import { setProfileId } from './utils/api'
import { Avatar } from './pages/ProfileSelect'
import SetupScreen from './pages/SetupScreen'
import { ResetPassword } from './pages/PasswordRecovery'
import LoginScreen from './pages/LoginScreen'
import Register from './pages/Register'
import Verify from './pages/Verify'
import Dashboard from './pages/Dashboard'
import CaptureHub from './pages/CaptureHub'
import WeightScreen from './pages/WeightScreen'
import CircumScreen from './pages/CircumScreen'
import CaliperScreen from './pages/CaliperScreen'
import MeasureWizard from './pages/MeasureWizard'
import History from './pages/History'
import NutritionPage from './pages/NutritionPage'
import ActivityPage from './pages/ActivityPage'
import Analysis from './pages/Analysis'
import SettingsPage from './pages/SettingsPage'
import GuidePage from './pages/GuidePage'
import AdminTierLimitsPage from './pages/AdminTierLimitsPage'
import AdminFeaturesPage from './pages/AdminFeaturesPage'
import AdminTiersPage from './pages/AdminTiersPage'
import AdminCouponsPage from './pages/AdminCouponsPage'
import AdminUserRestrictionsPage from './pages/AdminUserRestrictionsPage'
import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
import SubscriptionPage from './pages/SubscriptionPage'
import SleepPage from './pages/SleepPage'
import RestDaysPage from './pages/RestDaysPage'
import VitalsPage from './pages/VitalsPage'
import './app.css'
function Nav() {
const links = [
{ to:'/', icon:<LayoutDashboard size={20}/>, label:'Übersicht' },
{ to:'/capture', icon:<PlusSquare size={20}/>, label:'Erfassen' },
{ to:'/history', icon:<TrendingUp size={20}/>, label:'Verlauf' },
{ to:'/analysis', icon:<BarChart2 size={20}/>, label:'Analyse' },
{ to:'/settings', icon:<Settings size={20}/>, label:'Einst.' },
]
return (
<nav className="bottom-nav">
{links.map(l=>(
<NavLink key={l.to} to={l.to} end={l.to==='/'} className={({isActive})=>'nav-item'+(isActive?' active':'')}>
{l.icon}<span>{l.label}</span>
</NavLink>
))}
</nav>
)
}
function AppShell() {
const { session, loading: authLoading, needsSetup, logout } = useAuth()
const { activeProfile, loading: profileLoading } = useProfile()
const nav = useNavigate()
const handleLogout = () => {
if (confirm('Wirklich abmelden?')) {
logout()
window.location.href = '/'
}
}
useEffect(()=>{
if (session?.profile_id) {
setProfileId(session.profile_id)
localStorage.setItem('mitai-jinkendo_active_profile', session.profile_id)
}
}, [session?.profile_id])
// Handle public pages (register, verify, reset-password)
const urlParams = new URLSearchParams(window.location.search)
const currentPath = window.location.pathname
// Register page
if (currentPath === '/register') return (
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
<Register/>
</div>
)
// Verify email page
if (currentPath === '/verify') return (
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
<Verify/>
</div>
)
// Password reset page
const resetToken = urlParams.get('reset-password') || (currentPath === '/reset-password' ? urlParams.get('token') : null)
if (resetToken) return (
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center',
background:'var(--bg)',padding:24}}>
<div style={{width:'100%',maxWidth:360}}>
<div style={{textAlign:'center',marginBottom:20}}>
<div style={{fontSize:28,fontWeight:800,color:'var(--accent)'}}>Mitai Jinkendo</div>
</div>
<div className="card" style={{padding:20}}>
<ResetPassword token={resetToken} onDone={()=>window.location.href='/'}/>
</div>
</div>
</div>
)
// Auth loading
if (authLoading) return (
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center'}}>
<div className="spinner" style={{width:32,height:32}}/>
</div>
)
// First run
if (needsSetup) return <SetupScreen/>
// Need to log in
if (!session) return <LoginScreen/>
// Profile loading
if (profileLoading) return (
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center'}}>
<div className="spinner" style={{width:32,height:32}}/>
</div>
)
return (
<div className="app-shell">
<header className="app-header">
<span className="app-logo">Mitai Jinkendo</span>
<div style={{display:'flex', gap:12, alignItems:'center'}}>
<button
onClick={handleLogout}
title="Abmelden"
style={{
background:'none',
border:'none',
cursor:'pointer',
padding:6,
display:'flex',
alignItems:'center',
color:'var(--text2)',
transition:'color 0.15s'
}}
onMouseEnter={e => e.currentTarget.style.color = '#D85A30'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text2)'}
>
<LogOut size={18}/>
</button>
<NavLink to="/settings" style={{textDecoration:'none'}}>
{activeProfile
? <Avatar profile={activeProfile} size={30}/>
: <div style={{width:30,height:30,borderRadius:'50%',background:'var(--accent)'}}/>
}
</NavLink>
</div>
</header>
<main className="app-main">
<Routes>
<Route path="/" element={<Dashboard/>}/>
<Route path="/capture" element={<CaptureHub/>}/>
<Route path="/wizard" element={<MeasureWizard/>}/>
<Route path="/weight" element={<WeightScreen/>}/>
<Route path="/circum" element={<CircumScreen/>}/>
<Route path="/caliper" element={<CaliperScreen/>}/>
<Route path="/history" element={<History/>}/>
<Route path="/sleep" element={<SleepPage/>}/>
<Route path="/rest-days" element={<RestDaysPage/>}/>
<Route path="/vitals" element={<VitalsPage/>}/>
<Route path="/nutrition" element={<NutritionPage/>}/>
<Route path="/activity" element={<ActivityPage/>}/>
<Route path="/analysis" element={<Analysis/>}/>
<Route path="/settings" element={<SettingsPage/>}/>
<Route path="/guide" element={<GuidePage/>}/>
<Route path="/admin/tier-limits" element={<AdminTierLimitsPage/>}/>
<Route path="/admin/features" element={<AdminFeaturesPage/>}/>
<Route path="/admin/tiers" element={<AdminTiersPage/>}/>
<Route path="/admin/coupons" element={<AdminCouponsPage/>}/>
<Route path="/admin/user-restrictions" element={<AdminUserRestrictionsPage/>}/>
<Route path="/admin/training-types" element={<AdminTrainingTypesPage/>}/>
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
<Route path="/subscription" element={<SubscriptionPage/>}/>
</Routes>
</main>
<Nav/>
</div>
)
}
export default function App() {
return (
<AuthProvider>
<ProfileProvider>
<BrowserRouter>
<AppShell/>
</BrowserRouter>
</ProfileProvider>
</AuthProvider>
)
}