376 lines
14 KiB
Markdown
376 lines
14 KiB
Markdown
# Directus Datenbankstruktur — Language App
|
||
|
||
**Domain:** https://db.hejyou.com
|
||
**Stand:** 2026-04-23
|
||
**Eigene Collections:** 10 (inkl. Junction `questions_words`)
|
||
**Frontend:** React 19 + Vite, kein Router-Package (State-basiertes Routing)
|
||
|
||
---
|
||
|
||
## App-Konzept (Kurzfassung)
|
||
|
||
Social-Media-Feed-Style-Sprachlern-App mit Fokus auf "Language Parenting" (Kontext & Immersion statt Grammatikdrill).
|
||
|
||
**Datenfluss:**
|
||
1. Ein Bild wird in `pictures` hochgeladen
|
||
2. KI-Analyse erkennt Objekte → werden in `objects` gespeichert (mit bbox, confidence, Label-Wort)
|
||
3. Zum Bild werden `questions` generiert (z. B. "Kannst du die Möwe fliegen sehen?")
|
||
4. Jedes Wort der Frage wird in `words` gespeichert und per M2M (`questions_words`) verknüpft
|
||
|
||
**Lern-Flow in der App:**
|
||
- User scrollt durch Feed-Kacheln
|
||
- Kacheln existieren **nicht als DB-Einträge**, sondern werden dynamisch aus `words` / `questions` generiert
|
||
- Kachel-Typen: `listen`, `speak`, `write`, `multiple_choice`, `image_match`, `sentence_fill`
|
||
- 1 EP pro gelöster Kachel
|
||
- Erst alle Wörter einer Frage auf Ziellevel → dann Satzfrage freischalten
|
||
- Danach gemischter Feed
|
||
|
||
---
|
||
|
||
## Sprachrichtung
|
||
|
||
- **Zielsprachen:** Alle drei gegenseitig (DE/EN/SE × DE/EN/SE)
|
||
- Gespeichert in `directus_users.language_native` + `language_target` (M2O → `language_options`)
|
||
- Zusätzlich `learning_pairs` pro aktive Sprachrichtung (mit eigenem Level + Punkten)
|
||
|
||
---
|
||
|
||
## App-Architektur (Frontend)
|
||
|
||
### Navigation
|
||
4 Seiten, über `BottomNav` (Tabs) gesteuerter `useState`-Router in `App.jsx`:
|
||
|
||
| Tab | Seite | Status |
|
||
|-----|-------|--------|
|
||
| Feed | `pages/Feed.jsx` | Implementiert, Directus-Daten |
|
||
| Game | `pages/Game.jsx` | Placeholder |
|
||
| Pro | `pages/Pro.jsx` | Placeholder |
|
||
| Profil | `pages/Profil.jsx` | Implementiert, Directus-Daten |
|
||
|
||
`App.jsx` zeigt `<AuthScreen />` wenn `!user.username || !user.language_native || !user.language_target`.
|
||
|
||
### Auth-Flow (2 Schritte)
|
||
|
||
```
|
||
RegisterStep1 (E-Mail + Passwort)
|
||
→ registerUser() + login() → Token in localStorage (hejyou_token)
|
||
→ onSuccess(userId, token) → RegisterStep2
|
||
|
||
RegisterStep2 (Username + Muttersprache + Zielsprache)
|
||
→ checkUsername() → createProfile()
|
||
→ POST /items/users_language
|
||
→ PATCH /users/:id (username, language_native, language_target)
|
||
→ PATCH /items/users_language/:id (user-Verknüpfung)
|
||
→ POST /items/learning_pairs
|
||
→ setUser() → AuthScreen zeigt Erfolgsscreen
|
||
|
||
AuthContext überwacht Token, ruft getMe() beim Start
|
||
```
|
||
|
||
Token-Key im localStorage: `hejyou_token`
|
||
|
||
### Frontend API-Funktionen (`src/api/directus.js`)
|
||
|
||
| Funktion | Endpoint | Zweck |
|
||
|----------|----------|-------|
|
||
| `login(email, pw)` | POST `/auth/login` | Token holen |
|
||
| `getMe(token)` | GET `/users/me` | Basisfelder für Auth-Check |
|
||
| `getProfilData(token)` | GET `/users/me` (erweitert) | Profil inkl. username-Objekt, points_total, streak_days |
|
||
| `registerUser(email, pw)` | POST `/users` | Neuen Directus-User anlegen |
|
||
| `checkUsername(name)` | GET `/items/users_language` (filter) | Verfügbarkeit prüfen |
|
||
| `createProfile(...)` | 4 PATCH/POST-Calls | Vollständiges Onboarding |
|
||
| `getActiveLearningPair(profileId)` | GET `/items/learning_pairs` (filter active) | Aktives Lernpaar des Nutzers |
|
||
| `getWords(limit)` | GET `/items/words` | Wörter für Feed-Karten |
|
||
| `langById(id)` | lokal | LANGUAGE_OPTIONS-Lookup per UUID |
|
||
|
||
### LANGUAGE_OPTIONS (Frontend-Konstante)
|
||
|
||
```js
|
||
// Felder pro Sprache:
|
||
{ id, label, flag, suffix, speech }
|
||
```
|
||
|
||
| Sprache | suffix | speech |
|
||
|---------|--------|--------|
|
||
| Deutsch | `de` | `de-DE` |
|
||
| Englisch | `en` | `en-US` |
|
||
| Schwedisch | `se` | `sv-SE` |
|
||
|
||
`suffix` wird genutzt um DB-Felder dynamisch zu bauen: `title_${suffix}` (z.B. `title_de`).
|
||
`speech` wird an die Web Speech API (SpeechRecognition) übergeben.
|
||
|
||
### Feed-Karten-Generierung
|
||
|
||
`buildCards(words, fromLang, toLang)` erzeugt pro Wort bis zu 3 Kacheln:
|
||
|
||
| Kachel-Typ | Komponente | Bedingung |
|
||
|------------|-----------|-----------|
|
||
| `text` | `NewWordTextCard` | immer |
|
||
| `voice` | `NewWordVoiceCard` | immer |
|
||
| `letter` | `LetterOrderCard` | `word.length >= 4` |
|
||
|
||
Felder aus DB: `word = words.title_${to.suffix}`, `translation = words.title_${from.suffix}`
|
||
|
||
Zusätzlich werden Demo-Karten (`DEMO_EXTRA`) an den echten Feed angehängt, solange Bilder/Audio noch nicht in der DB vorhanden sind (ImagePickCard, AudioQuizCard, ImageQuizCard).
|
||
|
||
> **Noch nicht implementiert:** Nach Kartenabschluss wird aktuell **kein** `user_progress`-Eintrag angelegt und `learning_pairs.points` wird nicht aktualisiert. Das ist der nächste Schritt.
|
||
|
||
---
|
||
|
||
## Collections im Überblick
|
||
|
||
| Collection | Zweck |
|
||
|---|---|
|
||
| `categories` | Kategorien für Wörter/Objekte (Farben, Zuhause, Tiere, …) |
|
||
| `language_options` | Verfügbare Sprachen (DE/EN/SE) |
|
||
| `learning_pairs` | Aktive Sprachrichtungen pro Nutzer, mit Level & Punkten |
|
||
| `objects` | Von KI erkannte Objekte in Bildern (bbox, confidence) |
|
||
| `pictures` | Bilder, Ausgangspunkt für Fragen |
|
||
| `questions` | Fragen/Sätze für den Lernfeed |
|
||
| `questions_words` | Junction: M2M Fragen ↔ Wörter |
|
||
| `user_progress` | Lernfortschritt pro Nutzer × Kachel |
|
||
| `users_language` | Öffentliches Profil (Username, Avatar) |
|
||
| `words` | Vokabeln in DE/EN/SE mit Level & Kategorie |
|
||
|
||
---
|
||
|
||
## Collection-Details
|
||
|
||
### `categories`
|
||
Kategorien für Wörter und Objekte.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `status` | string | draft/published/archived |
|
||
| `sort` | integer | Sortierreihenfolge |
|
||
| `Icon` | string | Material-Icon-Name |
|
||
| `type` | string | Dropdown, Werte undefiniert |
|
||
| `title_en/de/se` | string | Titel in 3 Sprachen |
|
||
|
||
---
|
||
|
||
### `language_options`
|
||
Verfügbare Sprachen.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `status` | string | |
|
||
| `short` | string | Kurzcode: `de`, `en`, `se` |
|
||
| `title_en/de/se` | string | |
|
||
|
||
> Frontend-Mapping: `LANGUAGE_OPTIONS` in `directus.js` ergänzt die DB-Werte um `suffix` (= `short`), `speech` (Web Speech API Code), `flag` (Emoji) und `label` (Anzeigename auf Deutsch).
|
||
|
||
**Aktuelle Einträge:**
|
||
- DE: `88053026-3d7e-4799-b10d-67187f7c1709`
|
||
- EN: `99fbaa9d-3cac-48cb-a5e2-dcb320e913e4`
|
||
- SE: `25350b32-e9ab-4fec-946e-c0f11eff70dd`
|
||
|
||
---
|
||
|
||
### `learning_pairs`
|
||
Welche Sprachrichtungen ein Nutzer aktiv lernt.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `user` | uuid M2O → `users_language` | |
|
||
| `language_from` | uuid M2O → `language_options` | Ausgangssprache |
|
||
| `language_to` | uuid M2O → `language_options` | Zielsprache |
|
||
| `active` | boolean | Aktuell aktiv? |
|
||
| `current_level` | integer (1–10) | Aktuelles Level in dieser Richtung |
|
||
| `points` | integer | Punkte in dieser Richtung |
|
||
|
||
---
|
||
|
||
### `words`
|
||
Vokabeln in 3 Sprachen.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `status` | string | |
|
||
| `title_de/en/se` | string | Wort in 3 Sprachen |
|
||
| `categories` | uuid M2O → `categories` | Ein Wort gehört zu einer Kategorie |
|
||
| `image` | uuid file → `directus_files` | Bild zum Wort (optional) |
|
||
| `audio_de/en/se` | uuid file → `directus_files` | Aussprache pro Sprache |
|
||
| `level` | integer (1–10) | Schwierigkeitslevel |
|
||
| `times_learned` | integer | Global, wie oft insgesamt gelernt |
|
||
| `related_questions` | alias M2M → `questions` | Alle Fragen, in denen das Wort vorkommt |
|
||
|
||
> `getWords()` fetcht aktuell nur `id, title_de, title_en, title_se, level`. `image` und `audio_de/en/se` werden noch nicht geladen → ImagePickCard und AudioQuizCard laufen noch mit Demo-Daten.
|
||
> **Hinweis (2026-04-23):** Felder `categories`, `image`, `audio_de`, `audio_en`, `audio_se` wurden mit korrekten FK-Relationen und `special`-Markierungen angelegt.
|
||
|
||
---
|
||
|
||
### `questions`
|
||
Fragen/Sätze, die im Feed erscheinen.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `status` | string | |
|
||
| `question_de/en/se` | string | Frage in 3 Sprachen |
|
||
| `answer_de/en/se` | string | Korrekte Antwort |
|
||
| `level` | integer (1–10) | Schwierigkeitslevel |
|
||
| `related_words` | alias M2M → `words` | Alle Wörter der Frage |
|
||
|
||
> Hinweis: `questions.type` (Kachel-Typen) wurde entfernt – Kacheln werden dynamisch in der App generiert, nicht pro Frage gespeichert.
|
||
|
||
---
|
||
|
||
### `questions_words` (Junction-Collection für M2M)
|
||
Verknüpft Fragen und Wörter (M:N).
|
||
|
||
| Feld | Typ |
|
||
|---|---|
|
||
| `id` | integer (PK, auto) |
|
||
| `questions_id` | uuid → `questions` (CASCADE) |
|
||
| `words_id` | uuid → `words` (CASCADE) |
|
||
|
||
---
|
||
|
||
### `pictures`
|
||
Bilder als Ausgangspunkt für Fragen.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `media` | uuid → directus_files | Das eigentliche Bild |
|
||
| `type` | string | `simple_object` / `multi_object` |
|
||
| `season` | string | `spring`/`summer`/`fall`/`winter` |
|
||
| `objects` | uuid M2O → `objects` | Legacy-Feld (zeigt auf ein einzelnes Objekt, nicht mehr nutzen) |
|
||
| `objects` (O2M alias) | alias O2M ← `objects.picture` | Alle Objekte dieses Bildes — über `objects.picture` Relation |
|
||
|
||
> **Hinweis:** Die korrekte Beziehung läuft über `objects.picture` (M2O von objects zu pictures). Das alte `pictures.objects`-Feld ist ein Legacy-FK und wird nicht mehr befüllt.
|
||
|
||
---
|
||
|
||
### `objects`
|
||
Von der KI erkannte Objekte innerhalb eines Bildes.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `picture` | uuid M2O → `pictures` | Bild, zu dem dieses Objekt gehört (O2M-Seite: `pictures.objects`) |
|
||
| `media` | uuid → directus_files | Optionaler Zuschnitt des Objekts im Bild |
|
||
| `categories` | uuid M2O → `categories` | |
|
||
| `label` | uuid M2O → `words` | Name des Objekts als Wort |
|
||
| `color` | uuid M2O → `words` | Farbe des Objekts (optional) |
|
||
| `action` | uuid M2O → `words` | Aktion des Objekts (z. B. "fliegen") |
|
||
| `questions` | uuid M2O → `questions` | Generierte Frage zum Objekt |
|
||
| `resolution` | string | z. B. `1920x1080` |
|
||
| `confidence` | float | KI-Konfidenz 0–1 |
|
||
| `bbox` | json | `{x, y, w, h}` |
|
||
| `polygon` | json | Polygon-Koordinaten |
|
||
| `parent` | uuid M2O → `objects` | Hierarchie (Szene → Teilobjekte) |
|
||
|
||
> **Hinweis (2026-04-23):** `objects.picture` wurde ergänzt, um die O2M-Beziehung zu `pictures` herzustellen. Ein Bild kann nun mehrere Objekte haben. Bestehende Testdaten (Möwen-Objekt) wurden verknüpft.
|
||
|
||
---
|
||
|
||
### `users_language`
|
||
Öffentliches Nutzerprofil (gekoppelt mit `directus_users`).
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `username_public` | string | Angezeigter Name |
|
||
| `username_lowercases` | string | Für Suche/Eindeutigkeit |
|
||
| `user` | uuid M2O → `directus_users` | Verknüpfung zum System-User |
|
||
|
||
---
|
||
|
||
### `directus_users` (System-Collection, custom fields)
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `app_type` | string | `socialmedia` / `language` |
|
||
| `language_native` | uuid M2O → `language_options` | Muttersprache |
|
||
| `language_target` | uuid M2O → `language_options` | Zielsprache |
|
||
| `username` | uuid M2O → `users_language` | Verknüpfung zum Profil |
|
||
| `points_total` | integer | Gesamtpunkte über alle Richtungen |
|
||
| `streak_days` | integer | Aktuelle Lernserie (Tage) |
|
||
| `streak_last_activity` | date | Für Streak-Berechnung |
|
||
|
||
---
|
||
|
||
### `user_progress`
|
||
Trackt jede gelöste/übersprungene Kachel pro Nutzer.
|
||
|
||
> **Status:** Schema definiert, aber noch **nicht in der App implementiert**. Karten-Ergebnisse werden aktuell nur lokal angezeigt, nicht in die DB geschrieben. Nächster Schritt: `saveProgress()`-Funktion in `directus.js` + Aufruf nach Kartenabschluss.
|
||
|
||
| Feld | Typ | Notiz |
|
||
|---|---|---|
|
||
| `id` | uuid (PK) | |
|
||
| `user` | uuid M2O → `users_language` | Nutzerprofil |
|
||
| `question` | uuid M2O → `questions` | Wenn Kachel zu einer Frage gehört |
|
||
| `word` | uuid M2O → `words` | Wenn Kachel zu einem Einzelwort gehört |
|
||
| `card_type` | string | `listen` / `speak` / `write` / `multiple_choice` / `image_match` / `sentence_fill` |
|
||
| `result` | string | `correct` / `wrong` / `skipped` |
|
||
| `points_earned` | integer | Verdiente EP (1 pro gelöster Kachel) |
|
||
| `language_from` | uuid M2O → `language_options` | Ausgangssprache |
|
||
| `language_to` | uuid M2O → `language_options` | Zielsprache |
|
||
|
||
> Entweder `word` ODER `question` ist gesetzt, je nachdem ob die Kachel ein Einzelwort oder eine Satzfrage trainiert.
|
||
|
||
---
|
||
|
||
## Beziehungs-Diagramm
|
||
|
||
```
|
||
users_language ──┬── learning_pairs (pro Sprachrichtung)
|
||
├── user_progress (pro Kachel)
|
||
└── directus_users (System-User)
|
||
|
||
pictures ──── objects (O2M via objects.picture) ──── questions
|
||
│ │ │
|
||
└── media ├── picture → pictures (M2O) ├── related_words (M2M via questions_words)
|
||
├── label → words └── answer (de/en/se)
|
||
├── color → words
|
||
└── action → words
|
||
|
||
words ──── categories (M2O)
|
||
├── image → directus_files
|
||
└── audio_de/en/se → directus_files
|
||
```
|
||
|
||
---
|
||
|
||
## Testdaten (Tim, Stand 2026-04-22)
|
||
|
||
- **User (users_language):** `24f9a499-e36d-4df8-91f7-6a7abc6f42ce` (Tim1505)
|
||
- **3 Kategorien:** Farben, Zuhause, Tiere
|
||
- **12 Wörter** (Rot, Blau, Grün, Tisch, Stuhl, Fenster, Hund, Katze, Vogel, Möwe, fliegen, sehen)
|
||
- **8 Fragen** (7 Single-Word + 1 komplexe Möwen-Frage mit 3 verknüpften Wörtern)
|
||
- **1 Bild** im Herbst (`multi_object`) mit erkanntem Möwen-Objekt (94% confidence)
|
||
- **2 learning_pairs:** DE→SE (aktiv, 30P), DE→EN (inaktiv, 10P)
|
||
- **6 user_progress-Einträge** mit verschiedenen card_types (listen/speak/write/multiple_choice/image_match)
|
||
|
||
---
|
||
|
||
## Offene Punkte / Nächste Schritte
|
||
|
||
| Priorität | Aufgabe |
|
||
|-----------|---------|
|
||
| 🔴 | `user_progress` schreiben nach Kartenabschluss |
|
||
| 🔴 | `learning_pairs.points` + `directus_users.points_total` updaten |
|
||
| 🟠 | `words.image` + `words.audio_*` in `getWords()` laden → echte ImagePickCard / AudioQuizCard |
|
||
| 🟠 | `directus_users.streak_days` + `streak_last_activity` serverseitig aktualisieren |
|
||
| 🟡 | `questions` Collection in Feed integrieren (Satzlevel-Kacheln) |
|
||
| 🟡 | Game und Pro Pages implementieren |
|
||
|
||
---
|
||
|
||
## Kachel-Logik (für die App)
|
||
|
||
Die App generiert Kacheln dynamisch aus den vorhandenen Daten eines Wortes:
|
||
|
||
- `audio_de/en/se` vorhanden → `listen`, `speak` möglich
|
||
- `image` vorhanden → `image_match` möglich
|
||
- Mindestens 3 Wörter in gleicher Kategorie → `multiple_choice` möglich
|
||
- Zielsprache setzen via `learning_pairs.language_to`
|
||
|
||
Pro Abschluss: +1 EP in `user_progress.points_earned` + Update in `learning_pairs.points`.
|