feat: Implement CaptureShell layout with sub-navigation for capture section
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s

This commit is contained in:
Lars 2026-04-05 10:17:47 +02:00
parent a639d08037
commit a4376d1cd8
6 changed files with 274 additions and 94 deletions

View File

@ -11,6 +11,7 @@ import LoginScreen from './pages/LoginScreen'
import Register from './pages/Register'
import Verify from './pages/Verify'
import Dashboard from './pages/Dashboard'
import CaptureShell from './layouts/CaptureShell'
import CaptureHub from './pages/CaptureHub'
import WeightScreen from './pages/WeightScreen'
import CircumScreen from './pages/CircumScreen'
@ -42,10 +43,12 @@ import CustomGoalsPage from './pages/CustomGoalsPage'
import WorkflowEditorPage from './pages/WorkflowEditorPage'
import DesktopSidebar from './components/DesktopSidebar'
import { getMainNavItems } from './config/appNav'
import { isCaptureSectionPath } from './config/captureNav'
import './app.css'
function navItemActive(pathname, item, routerIsActive) {
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
if (item.to === '/capture' && isCaptureSectionPath(pathname)) return true
return routerIsActive
}
@ -198,22 +201,24 @@ function AppShell() {
<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 element={<CaptureShell />}>
<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="/sleep" element={<SleepPage />} />
<Route path="/rest-days" element={<RestDaysPage />} />
<Route path="/vitals" element={<VitalsPage />} />
<Route path="/custom-goals" element={<CustomGoalsPage />} />
<Route path="/nutrition" element={<NutritionPage />} />
<Route path="/activity" element={<ActivityPage />} />
<Route path="/guide" element={<GuidePage />} />
</Route>
<Route path="/history" element={<History/>}/>
<Route path="/sleep" element={<SleepPage/>}/>
<Route path="/rest-days" element={<RestDaysPage/>}/>
<Route path="/vitals" element={<VitalsPage/>}/>
<Route path="/goals" element={<GoalsPage/>}/>
<Route path="/custom-goals" element={<CustomGoalsPage/>}/>
<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/>}/>

View File

@ -363,6 +363,120 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
}
}
/* Erfassung: Sub-Navigation (Mobil = Chips, Desktop = linke Spalte) */
.capture-shell {
width: 100%;
}
.capture-shell__layout {
display: flex;
flex-direction: column;
gap: 16px;
}
.capture-shell__nav {
display: flex;
flex-direction: row;
gap: 6px;
overflow-x: auto;
padding-bottom: 6px;
-ms-overflow-style: none;
scrollbar-width: none;
}
.capture-shell__nav::-webkit-scrollbar {
display: none;
}
.capture-shell__nav-item {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
border-radius: 20px;
border: 1.5px solid var(--border2);
background: var(--surface);
color: var(--text2);
font-family: var(--font);
font-size: 13px;
font-weight: 500;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
box-sizing: border-box;
}
.capture-shell__nav-item:hover {
border-color: var(--accent);
color: var(--text1);
}
.capture-shell__nav-item--active {
border-color: var(--accent);
background: var(--accent);
color: white;
}
.capture-shell__nav-item--active:hover {
color: white;
}
.capture-shell__nav-item--highlight:not(.capture-shell__nav-item--active) {
border-color: #7f77dd88;
background: #7f77dd14;
}
.capture-shell__nav-icon {
font-size: 15px;
line-height: 1;
}
.capture-shell__nav-label {
line-height: 1.2;
}
.capture-shell__main {
min-width: 0;
}
@media (min-width: 1024px) {
.capture-shell__layout {
flex-direction: row;
align-items: flex-start;
gap: 24px;
}
.capture-shell__nav-wrap {
flex: 0 0 260px;
max-width: 280px;
position: sticky;
top: 16px;
align-self: flex-start;
}
.capture-shell__nav {
flex-direction: column;
overflow-x: visible;
overflow-y: auto;
max-height: calc(100vh - 140px);
padding-bottom: 0;
gap: 8px;
}
.capture-shell__nav-item {
width: 100%;
justify-content: flex-start;
border-radius: 10px;
white-space: normal;
padding: 9px 12px;
}
.capture-shell__main {
flex: 1;
}
}
.muted { color: var(--text3); font-size: 13px; }
.empty-state { text-align: center; padding: 48px 16px; color: var(--text3); }
.empty-state h3 { font-size: 16px; color: var(--text2); margin-bottom: 6px; }

View File

@ -2,9 +2,11 @@ import { NavLink, useLocation } from 'react-router-dom'
import { LogOut } from 'lucide-react'
import { Avatar } from '../pages/ProfileSelect'
import { getMainNavItems } from '../config/appNav'
import { isCaptureSectionPath } from '../config/captureNav'
function sidebarLinkActive(pathname, item, routerIsActive) {
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
if (item.to === '/capture' && isCaptureSectionPath(pathname)) return true
return routerIsActive
}

View File

@ -0,0 +1,103 @@
/**
* Erfassungs-Routen: Kachel-Hub + Sub-Navigation (Chip / Seitenleiste).
* Pfade müssen mit den Routes in App.jsx unter CaptureShell übereinstimmen.
*/
export const CAPTURE_HUB_TILES = [
{
icon: '⚖️',
label: 'Gewicht',
sub: 'Tägliche Gewichtseingabe',
to: '/weight',
color: '#378ADD',
},
{
icon: '🪄',
label: 'Assistent',
sub: 'Schritt-für-Schritt Messung (Umfänge & Caliper)',
to: '/wizard',
color: '#7F77DD',
highlight: true,
},
{
icon: '📏',
label: 'Umfänge',
sub: 'Hals, Brust, Taille, Bauch, Hüfte, Oberschenkel, Wade, Arm',
to: '/circum',
color: '#1D9E75',
},
{
icon: '📐',
label: 'Caliper',
sub: 'Körperfett per Hautfaltenmessung',
to: '/caliper',
color: '#D85A30',
},
{
icon: '🍽️',
label: 'Ernährung',
sub: 'FDDB CSV importieren',
to: '/nutrition',
color: '#EF9F27',
},
{
icon: '🏋️',
label: 'Aktivität',
sub: 'Training manuell oder Apple Health importieren',
to: '/activity',
color: '#D4537E',
},
{
icon: '🌙',
label: 'Schlaf',
sub: 'Schlafdaten erfassen oder Apple Health importieren',
to: '/sleep',
color: '#7B68EE',
},
{
icon: '🛌',
label: 'Ruhetage',
sub: 'Kraft-, Cardio-, oder Entspannungs-Ruhetag erfassen',
to: '/rest-days',
color: '#9B59B6',
},
{
icon: '❤️',
label: 'Vitalwerte',
sub: 'Ruhepuls und HRV morgens erfassen',
to: '/vitals',
color: '#E74C3C',
},
{
icon: '🎯',
label: 'Eigene Ziele',
sub: 'Fortschritte für individuelle Ziele erfassen',
to: '/custom-goals',
color: '#1D9E75',
},
{
icon: '📖',
label: 'Messanleitung',
sub: 'Wie und wo genau messen?',
to: '/guide',
color: '#888780',
},
]
/** Erster Eintrag: zurück zur Kachel-Übersicht */
const OVERVIEW_ENTRY = {
icon: '📋',
label: 'Übersicht',
sub: 'Alle Erfassungsarten',
to: '/capture',
color: 'var(--accent)',
}
/** Reihenfolge für Chip- / Seitenleiste (inkl. Übersicht) */
export const CAPTURE_SHELL_NAV_ITEMS = [OVERVIEW_ENTRY, ...CAPTURE_HUB_TILES]
export const CAPTURE_SECTION_PATHS = CAPTURE_SHELL_NAV_ITEMS.map((e) => e.to)
export function isCaptureSectionPath(pathname) {
return CAPTURE_SECTION_PATHS.includes(pathname)
}

View File

@ -0,0 +1,36 @@
import { Outlet, NavLink } from 'react-router-dom'
import { CAPTURE_SHELL_NAV_ITEMS } from '../config/captureNav'
/**
* Erfassung: Mobil Chip-Leiste, Desktop linke Spalte Wechsel zwischen Masken ohne Hub.
*/
export default function CaptureShell() {
return (
<div className="capture-shell">
<div className="capture-shell__layout">
<nav className="capture-shell__nav-wrap" aria-label="Erfassungsbereiche">
<div className="capture-shell__nav">
{CAPTURE_SHELL_NAV_ITEMS.map((e) => (
<NavLink
key={e.to}
to={e.to}
end={e.to === '/capture'}
className={({ isActive }) =>
'capture-shell__nav-item' +
(isActive ? ' capture-shell__nav-item--active' : '') +
(e.highlight ? ' capture-shell__nav-item--highlight' : '')
}
>
<span className="capture-shell__nav-icon" aria-hidden>{e.icon}</span>
<span className="capture-shell__nav-label">{e.label}</span>
</NavLink>
))}
</div>
</nav>
<div className="capture-shell__main">
<Outlet />
</div>
</div>
</div>
)
}

View File

@ -1,86 +1,6 @@
import { useNavigate } from 'react-router-dom'
import { ChevronRight } from 'lucide-react'
const ENTRIES = [
{
icon: '⚖️',
label: 'Gewicht',
sub: 'Tägliche Gewichtseingabe',
to: '/weight',
color: '#378ADD',
},
{
icon: '🪄',
label: 'Assistent',
sub: 'Schritt-für-Schritt Messung (Umfänge & Caliper)',
to: '/wizard',
color: '#7F77DD',
highlight: true,
},
{
icon: '📏',
label: 'Umfänge',
sub: 'Hals, Brust, Taille, Bauch, Hüfte, Oberschenkel, Wade, Arm',
to: '/circum',
color: '#1D9E75',
},
{
icon: '📐',
label: 'Caliper',
sub: 'Körperfett per Hautfaltenmessung',
to: '/caliper',
color: '#D85A30',
},
{
icon: '🍽️',
label: 'Ernährung',
sub: 'FDDB CSV importieren',
to: '/nutrition',
color: '#EF9F27',
},
{
icon: '🏋️',
label: 'Aktivität',
sub: 'Training manuell oder Apple Health importieren',
to: '/activity',
color: '#D4537E',
},
{
icon: '🌙',
label: 'Schlaf',
sub: 'Schlafdaten erfassen oder Apple Health importieren',
to: '/sleep',
color: '#7B68EE',
},
{
icon: '🛌',
label: 'Ruhetage',
sub: 'Kraft-, Cardio-, oder Entspannungs-Ruhetag erfassen',
to: '/rest-days',
color: '#9B59B6',
},
{
icon: '❤️',
label: 'Vitalwerte',
sub: 'Ruhepuls und HRV morgens erfassen',
to: '/vitals',
color: '#E74C3C',
},
{
icon: '🎯',
label: 'Eigene Ziele',
sub: 'Fortschritte für individuelle Ziele erfassen',
to: '/custom-goals',
color: '#1D9E75',
},
{
icon: '📖',
label: 'Messanleitung',
sub: 'Wie und wo genau messen?',
to: '/guide',
color: '#888780',
},
]
import { CAPTURE_HUB_TILES } from '../config/captureNav'
export default function CaptureHub() {
const nav = useNavigate()
@ -88,7 +8,7 @@ export default function CaptureHub() {
<div className="capture-page">
<h1 className="page-title">Erfassen</h1>
<div style={{display:'flex',flexDirection:'column',gap:10}}>
{ENTRIES.map(e => (
{CAPTURE_HUB_TILES.map(e => (
<button key={e.to} onClick={()=>nav(e.to)}
style={{
display:'flex', alignItems:'center', gap:14,