# 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 `` 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`.