feat: Complete design foundation with responsive navigation
Some checks failed
Deploy Development / deploy (push) Failing after 26s
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:
parent
edb33b8fc3
commit
46a90ae910
|
|
@ -2,9 +2,20 @@
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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, viewport-fit=cover" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="description" content="Shinkan Jinkendo - Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung" />
|
<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>
|
<title>Shinkan Jinkendo</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,55 @@
|
||||||
import React from 'react'
|
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 { 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 LoginPage from './pages/LoginPage'
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import ProfilePage from './pages/ProfilePage'
|
import ProfilePage from './pages/ProfilePage'
|
||||||
import ExercisesPage from './pages/ExercisesPage'
|
import ExercisesPage from './pages/ExercisesPage'
|
||||||
import ClubsPage from './pages/ClubsPage'
|
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
|
// Protected Route Component
|
||||||
function ProtectedRoute({ children }) {
|
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) {
|
if (loading) {
|
||||||
return (
|
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 />
|
<DesktopSidebar
|
||||||
{children}
|
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)
|
// Public Route Component (redirect to dashboard if already logged in)
|
||||||
|
|
|
||||||
86
frontend/src/components/DesktopSidebar.jsx
Normal file
86
frontend/src/components/DesktopSidebar.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
frontend/src/config/appNav.js
Normal file
48
frontend/src/config/appNav.js
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user