shinkan-jinkendo/frontend/src/App.jsx
Lars 2148d0aa7f
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
Update AI Prompt System and Admin API
- Incremented version to 1.1 and updated the status to reflect the implementation of core features including `ai_prompts`, `prompt_resolver`, and the Superadmin HTTP API.
- Documented the current API endpoints for managing AI prompts, including CRUD operations and preview functionality.
- Introduced a new placeholder catalog and preview capabilities for the Superadmin interface.
- Enhanced the backend with new functions for handling AI prompt templates and integrated them into the API.
- Updated frontend components to include navigation and routing for the new Admin AI Prompts page.
- Incremented application version to 0.8.158 and updated changelog to reflect these changes.
2026-05-22 11:02:02 +02:00

340 lines
12 KiB
JavaScript

import React, { Suspense, lazy } from 'react'
import {
RouterProvider,
createBrowserRouter,
Navigate,
NavLink,
useLocation,
Outlet,
} from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import { FormEditorActionsProvider, FormEditorBottomSlot } from './context/FormEditorActionsContext'
import { ToastProvider } from './context/ToastContext'
import { OrgInboxProvider, useOrgInbox } from './context/OrgInboxContext'
import DesktopSidebar from './components/DesktopSidebar'
import { getMainNavItems } from './config/appNav'
import AdminHomeRedirect from './components/AdminHomeRedirect'
import PlatformAdminRoute from './components/PlatformAdminRoute'
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
import './app.css'
const LoginPage = lazy(() => import('./pages/LoginPage'))
const VerifyPage = lazy(() => import('./pages/VerifyPage'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const AccountSettingsPage = lazy(() => import('./pages/AccountSettingsPage'))
const SettingsSystemInfoPage = lazy(() => import('./pages/SettingsSystemInfoPage'))
const ExercisesListPage = lazy(() => import('./pages/ExercisesListPage'))
const ExerciseDetailPage = lazy(() => import('./pages/ExerciseDetailPage'))
const ExerciseFormPage = lazy(() => import('./pages/ExerciseFormPage'))
const ClubsPage = lazy(() => import('./pages/ClubsPage'))
const InboxPage = lazy(() => import('./pages/InboxPage'))
const SkillsPage = lazy(() => import('./pages/SkillsPage'))
const TrainingPlanningPage = lazy(() => import('./pages/TrainingPlanningPage'))
const TrainingPlanTemplatesListPage = lazy(() => import('./pages/TrainingPlanTemplatesListPage'))
const TrainingPlanTemplateEditPage = lazy(() => import('./pages/TrainingPlanTemplateEditPage'))
const PlanningLayout = lazy(() => import('./layouts/PlanningLayout'))
const TrainingFrameworkProgramsListPage = lazy(() =>
import('./pages/TrainingFrameworkProgramsListPage'),
)
const TrainingFrameworkProgramEditPage = lazy(() =>
import('./pages/TrainingFrameworkProgramEditPage'),
)
const TrainingModulesListPage = lazy(() => import('./pages/TrainingModulesListPage'))
const TrainingModuleEditPage = lazy(() => import('./pages/TrainingModuleEditPage'))
const TrainingUnitRunPage = lazy(() => import('./pages/TrainingUnitRunPage'))
const TrainingUnitEditPage = lazy(() => import('./pages/TrainingUnitEditPage'))
const TrainingCoachPage = lazy(() => import('./pages/TrainingCoachPage'))
const AdminCatalogsPage = lazy(() => import('./pages/AdminCatalogsPage'))
const AdminHierarchyPage = lazy(() => import('./pages/AdminHierarchyPage'))
const AdminMaturityModelsPage = lazy(() => import('./pages/AdminMaturityModelsPage'))
const TrainerContextsPage = lazy(() => import('./pages/TrainerContextsPage'))
const MediaWikiImportPage = lazy(() => import('./pages/MediaWikiImportPage'))
const AdminUsersPage = lazy(() => import('./pages/AdminUsersPage'))
const MediaLibraryPage = lazy(() => import('./pages/MediaLibraryPage'))
const LegalPage = lazy(() => import('./pages/LegalPage'))
const AdminLegalDocumentsPage = lazy(() => import('./pages/AdminLegalDocumentsPage'))
const AdminAiSkillRetrievalPage = lazy(() => import('./pages/AdminAiSkillRetrievalPage'))
const AdminAiPromptsPage = lazy(() => import('./pages/AdminAiPromptsPage'))
const SettingsLegalPage = lazy(() => import('./pages/SettingsLegalPage'))
/** Shield „Admin“: nur Super-Admin (global). Vereinsorga: Vereine → Mitglieder. */
function computeShowAdminNav(currentUser) {
return currentUser?.role === 'superadmin'
}
function AppRouteFallback() {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)',
}}
>
<div className="spinner"></div>
</div>
)
}
// Bottom Navigation (Mobile)
function Nav({ showAdminNav }) {
const { canAccessOrgInbox, inboxCount } = useOrgInbox()
const items = getMainNavItems(showAdminNav, { showInbox: canAccessOrgInbox })
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={26} strokeWidth={2} />
{item.to === '/inbox' && inboxCount > 0 ? (
<span className="nav-item__badge" aria-label={`${inboxCount} offen`}>
{inboxCount > 99 ? '99+' : inboxCount}
</span>
) : null}
<span>{item.shortLabel || item.label}</span>
</NavLink>
))}
</nav>
)
}
function ProtectedLayout() {
const { isAuthenticated, loading, user, logout } = useAuth()
const handleLogout = () => {
if (confirm('Wirklich abmelden?')) {
logout()
window.location.href = '/'
}
}
if (loading) {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)',
}}
>
<div className="spinner"></div>
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
const showAdminNav = computeShowAdminNav(user)
return (
<OrgInboxProvider user={user}>
<FormEditorActionsProvider>
<DesktopSidebar showAdminNav={showAdminNav} user={user} onLogout={handleLogout} />
<div className="app-shell">
<div className="app-shell__column">
<div className="app-header app-header--mobile app-header--mobile-stack">
<div className="app-header-mobile__top">
<div className="app-logo">🥋 Shinkan</div>
</div>
<ActiveClubSwitcher variant="mobile" />
</div>
<div className="app-main">
<InactiveMembershipBanner />
<Outlet />
</div>
<FormEditorBottomSlot>
<Nav showAdminNav={showAdminNav} />
</FormEditorBottomSlot>
</div>
</div>
</FormEditorActionsProvider>
</OrgInboxProvider>
)
}
function PublicRoute({ children }) {
const { isAuthenticated, loading } = useAuth()
if (loading) {
return (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)',
}}
>
<div className="spinner"></div>
</div>
)
}
return !isAuthenticated ? children : <Navigate to="/" replace />
}
/**
* Data Router — erforderlich für `useBlocker` (ungespeicherte Änderungen).
* Klassisches `BrowserRouter` stellt keinen DataRouterContext bereit; ohne Migration
* werfen Seiten mit `useUnsavedChangesBlocker` beim Rendern eine Invariante.
*/
const appRouter = createBrowserRouter([
{ path: '/verify', element: <VerifyPage /> },
{
path: '/login',
element: (
<PublicRoute>
<LoginPage />
</PublicRoute>
),
},
{ path: '/impressum', element: <LegalPage type="impressum" /> },
{ path: '/datenschutz', element: <LegalPage type="datenschutz" /> },
{ path: '/nutzungsbedingungen', element: <LegalPage type="nutzungsbedingungen" /> },
{ path: '/medienrichtlinie', element: <LegalPage type="medienrichtlinie" /> },
{
element: <ProtectedLayout />,
children: [
{ index: true, element: <Dashboard /> },
{ path: 'profile', element: <Navigate to="/settings" replace /> },
{ path: 'settings', element: <AccountSettingsPage /> },
{ path: 'settings/system', element: <SettingsSystemInfoPage /> },
{ path: 'settings/legal', element: <SettingsLegalPage /> },
{ path: 'media', element: <MediaLibraryPage /> },
{
path: 'exercises',
children: [
{ index: true, element: <ExercisesListPage /> },
{ path: 'new', element: <ExerciseFormPage /> },
{ path: ':id/edit', element: <ExerciseFormPage /> },
{ path: ':id', element: <ExerciseDetailPage /> },
],
},
{ path: 'clubs', element: <ClubsPage /> },
{ path: 'inbox', element: <InboxPage /> },
{ path: 'skills', element: <SkillsPage /> },
{
path: 'planning',
element: <PlanningLayout />,
children: [
{ index: true, element: <TrainingPlanningPage /> },
{ path: 'framework-programs', element: <TrainingFrameworkProgramsListPage /> },
{ path: 'training-modules', element: <TrainingModulesListPage /> },
{ path: 'plan-templates', element: <TrainingPlanTemplatesListPage /> },
{ path: 'units/new', element: <TrainingUnitEditPage /> },
{ path: 'units/:id/edit', element: <TrainingUnitEditPage /> },
],
},
{ path: 'planning/framework-programs/new', element: <TrainingFrameworkProgramEditPage /> },
{ path: 'planning/framework-programs/:id', element: <TrainingFrameworkProgramEditPage /> },
{ path: 'planning/training-modules/new', element: <TrainingModuleEditPage /> },
{ path: 'planning/training-modules/:id', element: <TrainingModuleEditPage /> },
{ path: 'planning/plan-templates/:id', element: <TrainingPlanTemplateEditPage /> },
{ path: 'planning/run/:unitId/coach', element: <TrainingCoachPage /> },
{ path: 'planning/run/:unitId', element: <TrainingUnitRunPage /> },
{ path: 'admin', element: <AdminHomeRedirect /> },
{
path: 'admin/users',
element: (
<PlatformAdminRoute>
<AdminUsersPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/hierarchy',
element: (
<PlatformAdminRoute>
<AdminHierarchyPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/maturity-models',
element: (
<PlatformAdminRoute>
<AdminMaturityModelsPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/catalogs',
element: (
<PlatformAdminRoute>
<AdminCatalogsPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/mediawiki-import',
element: (
<PlatformAdminRoute>
<MediaWikiImportPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/legal-documents',
element: (
<PlatformAdminRoute>
<AdminLegalDocumentsPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/ai-skill-retrieval',
element: (
<PlatformAdminRoute>
<AdminAiSkillRetrievalPage />
</PlatformAdminRoute>
),
},
{
path: 'admin/ai-prompts',
element: (
<PlatformAdminRoute>
<AdminAiPromptsPage />
</PlatformAdminRoute>
),
},
{ path: 'trainer-contexts', element: <TrainerContextsPage /> },
],
},
{ path: '*', element: <Navigate to="/" replace /> },
])
function App() {
return (
<AuthProvider>
<ToastProvider>
<Suspense fallback={<AppRouteFallback />}>
<RouterProvider router={appRouter} />
</Suspense>
</ToastProvider>
</AuthProvider>
)
}
export default App