Backend (v9d Phase 1b): - Migration 006: Add abilities JSONB column + descriptions - admin_training_types.py: Full CRUD endpoints for training types - List, Get, Create, Update, Delete - Abilities taxonomy endpoint (5 dimensions: koordinativ, konditionell, kognitiv, psychisch, taktisch) - Validation: Cannot delete types in use - Register admin_training_types router in main.py Frontend: - AdminTrainingTypesPage: Full CRUD UI - Create/edit form with all fields (category, subcategory, names, icon, descriptions, sort_order) - List grouped by category with color coding - Delete with usage check - Note about abilities mapping coming in v9f - Add TrainingTypeDistribution to ActivityPage stats tab - Add admin link in AdminPanel (v9d section) - Update api.js with admin training types methods Notes: - Abilities mapping UI deferred to v9f (flexible prompt system) - Placeholders (abilities column) in place for future AI analysis Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
196 lines
7.3 KiB
JavaScript
196 lines
7.3 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 SubscriptionPage from './pages/SubscriptionPage'
|
|
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="/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="/subscription" element={<SubscriptionPage/>}/>
|
|
</Routes>
|
|
</main>
|
|
<Nav/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<ProfileProvider>
|
|
<BrowserRouter>
|
|
<AppShell/>
|
|
</BrowserRouter>
|
|
</ProfileProvider>
|
|
</AuthProvider>
|
|
)
|
|
}
|