127 lines
3.9 KiB
Markdown
127 lines
3.9 KiB
Markdown
|
|
# Frontend-Architektur
|
|||
|
|
|
|||
|
|
## Struktur
|
|||
|
|
```
|
|||
|
|
frontend/src/
|
|||
|
|
├── App.jsx # Root: Auth-Gates, Navigation, Routing
|
|||
|
|
├── app.css # CSS-Variablen + globale Klassen
|
|||
|
|
├── main.jsx # Vite Entry Point
|
|||
|
|
├── context/
|
|||
|
|
│ ├── AuthContext.jsx # Session, Login, Logout, getToken()
|
|||
|
|
│ └── ProfileContext.jsx # Aktives Profil, Profile-Liste
|
|||
|
|
├── pages/ # Eine Datei pro Screen
|
|||
|
|
├── utils/
|
|||
|
|
│ ├── api.js # Alle API-Calls (Token automatisch injiziert)
|
|||
|
|
│ ├── calc.js # Körperfett-Formeln (Jackson/Pollock etc.)
|
|||
|
|
│ ├── interpret.js # Regelbasierte Auswertungen
|
|||
|
|
│ ├── Markdown.jsx # Eigener Markdown-Renderer
|
|||
|
|
│ └── guideData.js # Messanleitungen (statisch)
|
|||
|
|
└── public/ # Icons (Jinkendo Ensō-Logo)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## API-Calls – IMMER über api.js
|
|||
|
|
```javascript
|
|||
|
|
// ✅ Richtig – Token wird automatisch injiziert:
|
|||
|
|
import { api } from '../utils/api'
|
|||
|
|
const data = await api.listWeight()
|
|||
|
|
await api.upsertWeight(date, weight, note)
|
|||
|
|
|
|||
|
|
// ❌ Falsch – kein Token, gibt 401:
|
|||
|
|
const r = await fetch('/api/weight')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Neue API-Methode hinzufügen
|
|||
|
|
In `frontend/src/utils/api.js`:
|
|||
|
|
```javascript
|
|||
|
|
export const api = {
|
|||
|
|
// ...bestehende Methoden...
|
|||
|
|
meinEndpoint: (param) => req(`/mein-endpoint?p=${param}`),
|
|||
|
|
createItem: (data) => req('/items', json(data)),
|
|||
|
|
updateItem: (id, d) => req(`/items/${id}`, jput(d)),
|
|||
|
|
deleteItem: (id) => req(`/items/${id}`, {method:'DELETE'}),
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Navigation (Haupt-App & Admin)
|
|||
|
|
- **Hauptmenü (Mobile + Desktop):** `frontend/src/config/appNav.js` (`getMainNavItems`) – in `App.jsx` (Bottom-Nav) und `DesktopSidebar.jsx` nutzen.
|
|||
|
|
- **Admin-Bereich:** `frontend/src/config/adminNav.js` + `layouts/AdminShell.jsx` + `layouts/RequireAdmin.jsx`; Shell wie Analyse (`.analysis-split*`).
|
|||
|
|
- **Bottom-Nav / Safe Area (PWA):** `--nav-h`, `.bottom-nav`, `.app-main` in `app.css`.
|
|||
|
|
- **Agent-Doku:** `docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`
|
|||
|
|
|
|||
|
|
## Neue Seite hinzufügen
|
|||
|
|
1. `frontend/src/pages/MeineSeite.jsx` erstellen
|
|||
|
|
2. In `App.jsx` importieren und Route hinzufügen
|
|||
|
|
3. Navigation: Eintrag in **`config/appNav.js`** (und ggf. Admin in **`adminNav.js`**) – nicht mehr nur in `App.jsx` duplizieren
|
|||
|
|
|
|||
|
|
## Komponenten-Pattern
|
|||
|
|
```jsx
|
|||
|
|
export default function MeineSeite() {
|
|||
|
|
const [data, setData] = useState([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [error, setError] = useState(null)
|
|||
|
|
|
|||
|
|
useEffect(() => { load() }, [])
|
|||
|
|
|
|||
|
|
const load = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true)
|
|||
|
|
setData(await api.meinEndpoint())
|
|||
|
|
} catch(e) { setError(e.message) }
|
|||
|
|
finally { setLoading(false) }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (loading) return <div style={{display:'flex',justifyContent:'center',padding:40}}><div className="spinner"/></div>
|
|||
|
|
if (error) return <div style={{color:'var(--danger)',padding:16}}>{error}</div>
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={{padding:'0 0 80px'}}>
|
|||
|
|
{/* Inhalt */}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## CSS-Variablen (Kurzreferenz)
|
|||
|
|
```css
|
|||
|
|
--accent: #1D9E75 --accent-dark: #085041 --accent-light: #E1F5EE
|
|||
|
|
--danger: #D85A30
|
|||
|
|
--bg · --surface · --surface2 · --border · --text1 · --text2 · --text3
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## CSS-Klassen
|
|||
|
|
```
|
|||
|
|
.card Weißer Container, border-radius 12px
|
|||
|
|
.btn Basis-Button
|
|||
|
|
.btn-primary Grüner Button
|
|||
|
|
.btn-secondary Grauer Button
|
|||
|
|
.btn-full 100% Breite
|
|||
|
|
.form-input Eingabefeld
|
|||
|
|
.form-label Label (klein, uppercase)
|
|||
|
|
.spinner Ladekreis
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Bekannte Fallstricke
|
|||
|
|
|
|||
|
|
### dayjs.week() – NIEMALS verwenden
|
|||
|
|
```javascript
|
|||
|
|
// ❌ Falsch:
|
|||
|
|
dayjs(date).week()
|
|||
|
|
|
|||
|
|
// ✅ Richtig (ISO 8601):
|
|||
|
|
const weekNum = (() => {
|
|||
|
|
const dt = new Date(date)
|
|||
|
|
dt.setHours(0,0,0,0)
|
|||
|
|
dt.setDate(dt.getDate()+4-(dt.getDay()||7))
|
|||
|
|
return Math.ceil(((dt-new Date(dt.getFullYear(),0,1))/86400000+1)/7)
|
|||
|
|
})()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Recharts Bar fill
|
|||
|
|
```jsx
|
|||
|
|
// ❌ Falsch:
|
|||
|
|
<Bar fill={(entry) => entry.color}/>
|
|||
|
|
|
|||
|
|
// ✅ Richtig:
|
|||
|
|
<Bar fill="#1D9E75"/>
|
|||
|
|
```
|