feat: Complete design foundation with responsive navigation
Some checks failed
Deploy Development / deploy (push) Failing after 26s

Design System:
- app.css already exists with full design tokens, dark mode, responsive breakpoints
- CSS variables for colors, spacing, typography
- Mobile-first layout with safe-area support (iOS notch)

Navigation System:
- config/appNav.js: Single source of truth for navigation items
- Bottom-Nav for mobile (<1024px) with horizontal scrolling
- DesktopSidebar for desktop (≥1024px) with fixed left sidebar
- Role-based navigation (isAdmin adds Admin link)

App.jsx Restructure:
- Responsive layout: Bottom-Nav (mobile) + DesktopSidebar (desktop)
- Protected/Public routes with loading states
- Logout handler with confirmation

PWA Setup:
- viewport-fit=cover for notch support
- apple-mobile-web-app meta tags
- Icon links prepared (icons pending)

Architecture:
- Follows Mitai design patterns
- Responsive breakpoint at 1024px
- Single source of truth for navigation config

Next: Icons, Exercise CRUD, Clubs/Groups Management
This commit is contained in:
Lars 2026-04-22 16:25:36 +02:00
parent edb33b8fc3
commit 46a90ae910
4 changed files with 211 additions and 9 deletions

View File

@ -2,9 +2,20 @@
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="Shinkan Jinkendo - Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung" />
<!-- PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Shinkan">
<link rel="apple-touch-icon" href="/icon-192.png">
<!-- Icons -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
<link rel="shortcut icon" href="/favicon.ico">
<title>Shinkan Jinkendo</title>
</head>
<body>

View File

@ -1,16 +1,55 @@
import React from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { BrowserRouter as Router, Routes, Route, Navigate, NavLink, useLocation } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import Navigation from './components/Navigation'
import DesktopSidebar from './components/DesktopSidebar'
import { getMainNavItems } from './config/appNav'
import LoginPage from './pages/LoginPage'
import Dashboard from './pages/Dashboard'
import ProfilePage from './pages/ProfilePage'
import ExercisesPage from './pages/ExercisesPage'
import ClubsPage from './pages/ClubsPage'
import './app.css'
// Bottom Navigation (Mobile)
function Nav({ isAdmin }) {
const items = getMainNavItems(isAdmin)
const loc = useLocation()
const navItemActive = (pathname, item, routerIsActive) => {
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
return routerIsActive
}
return (
<nav className="bottom-nav">
{items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={!!item.end}
className={({ isActive }) =>
'nav-item' +
(navItemActive(loc.pathname, item, isActive) ? ' active' : '')
}
>
<item.Icon size={20} strokeWidth={2} />
<span>{item.shortLabel || item.label}</span>
</NavLink>
))}
</nav>
)
}
// Protected Route Component
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth()
const { isAuthenticated, loading, user, logout } = useAuth()
const handleLogout = () => {
if (confirm('Wirklich abmelden?')) {
logout()
window.location.href = '/'
}
}
if (loading) {
return (
@ -26,12 +65,30 @@ function ProtectedRoute({ children }) {
)
}
return isAuthenticated ? (
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'
return (
<>
<Navigation />
{children}
<DesktopSidebar
isAdmin={isAdmin}
user={user}
onLogout={handleLogout}
/>
<div className="app-shell">
<div className="app-shell__column">
<div className="app-header app-header--mobile">
<div className="app-logo">🥋 Shinkan</div>
</div>
<div className="app-main">{children}</div>
<Nav isAdmin={isAdmin} />
</div>
</div>
</>
) : <Navigate to="/login" replace />
)
}
// Public Route Component (redirect to dashboard if already logged in)

View File

@ -0,0 +1,86 @@
import { NavLink, useLocation } from 'react-router-dom'
import { LogOut } from 'lucide-react'
import { getMainNavItems } from '../config/appNav'
function sidebarLinkActive(pathname, item, routerIsActive) {
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
return routerIsActive
}
/**
* Desktop-Sidebar (1024px) Sichtbarkeit via CSS (.desktop-sidebar).
*/
export default function DesktopSidebar({
isAdmin,
user,
onLogout
}) {
const loc = useLocation()
const items = getMainNavItems(isAdmin)
const tier = user?.tier || ''
return (
<aside className="desktop-sidebar" aria-label="Hauptnavigation">
<div className="desktop-sidebar__brand">
<div className="desktop-sidebar__logo" aria-hidden />
<div className="desktop-sidebar__title">Shinkan Jinkendo</div>
</div>
<nav className="desktop-sidebar__nav">
{items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={!!item.end}
className={({ isActive }) =>
'desktop-sidebar__link' +
(sidebarLinkActive(loc.pathname, item, isActive)
? ' desktop-sidebar__link--active'
: '')
}
>
<item.Icon size={20} strokeWidth={2} />
<span>{item.label}</span>
</NavLink>
))}
</nav>
<div className="desktop-sidebar__footer">
<div className="desktop-sidebar__user">
<div
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: 'var(--accent)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 600,
fontSize: '14px'
}}
>
{user?.name?.charAt(0) || 'U'}
</div>
<div className="desktop-sidebar__user-text">
<span className="desktop-sidebar__user-name">
{user?.name || user?.email || 'User'}
</span>
{tier ? (
<span className="desktop-sidebar__user-tier">{tier}</span>
) : null}
</div>
</div>
<button
type="button"
className="desktop-sidebar__logout"
onClick={onLogout}
title="Abmelden"
>
<LogOut size={18} />
</button>
</div>
</aside>
)
}

View File

@ -0,0 +1,48 @@
import {
LayoutDashboard,
BookOpen,
Calendar,
Building2,
Settings,
Shield
} from 'lucide-react'
/**
* Shinkan Navigation Configuration
* Single source of truth für Bottom-Nav (Mobile) + Desktop-Sidebar
*
* @typedef {{ to: string, label: string, shortLabel?: string, end?: boolean, Icon: import('react').ForwardRefExoticComponent }} AppNavItem
*/
/** @returns {Omit<AppNavItem, 'Icon'>[]} */
function baseItems() {
return [
{ to: '/', label: 'Übersicht', end: true },
{ to: '/exercises', label: 'Übungen', shortLabel: 'Übungen' },
{ to: '/planning', label: 'Planung' },
{ to: '/clubs', label: 'Vereine' },
{ to: '/settings', label: 'Einstellungen', shortLabel: 'Einst.' }
]
}
/** @param {boolean} isAdmin */
export function getMainNavItems(isAdmin) {
const icons = [
LayoutDashboard,
BookOpen,
Calendar,
Building2,
Settings
]
const raw = baseItems().map((item, i) => ({
...item,
Icon: icons[i]
}))
if (isAdmin) {
raw.push({ to: '/admin', label: 'Admin', end: false, Icon: Shield })
}
return raw
}