diff --git a/scripts/seed-test-decks.js b/scripts/seed-test-decks.js new file mode 100644 index 0000000..f0eeb33 --- /dev/null +++ b/scripts/seed-test-decks.js @@ -0,0 +1,246 @@ +/** + * Test-Decks-Seed via Browser-Console. + * + * Einfachste Nutzung — keine Token-Konfiguration, keine ENV-Vars: + * + * 1. Cards-App im Browser öffnen (lokal http://localhost:3082 oder + * live https://cardecky.mana.how) und sicherstellen dass du + * eingeloggt bist (Decks-Seite ist erreichbar). + * 2. F12 → Console-Tab. + * 3. Den GESAMTEN Inhalt dieser Datei kopieren und einfügen. + * Browser fragt evtl. einmal "Wirklich Code einfügen?" → "Allow + * pasting" tippen. + * 4. Enter. + * + * Das Skript liest die Auth-Daten aus dem Browser (echtes JWT oder + * Dev-Stub-User-ID), zeigt einen Fortschritts-Log und legt 7 + * Test-Decks mit gemischten Größen + Farben an. Lauf dauert ~3-5s. + * + * Wiederholtes Ausführen erstellt zusätzliche Decks (gleiche Namen + * dürfen mehrfach vorkommen). Wenn du sauber starten willst: vorher + * vorhandene Test-Decks löschen oder ein frisches DB-Reset machen. + */ + +(async () => { + const TOKEN = localStorage.getItem('cards.auth.accessToken'); + const STUB = localStorage.getItem('cards.dev.userId'); + + const headers = { 'Content-Type': 'application/json' }; + if (TOKEN) headers['Authorization'] = `Bearer ${TOKEN}`; + else if (STUB) headers['X-User-Id'] = STUB; + else { + console.error('Kein Auth-Token gefunden. Bitte erst auf /einloggen.'); + return; + } + + const origin = location.origin; + const API = origin.includes('localhost:3082') + ? 'http://localhost:3081' + : origin.includes('cardecky.mana.how') + ? 'https://cardecky-api.mana.how' + : origin.replace(/cardecky/, 'cardecky-api'); + + console.log(`%cSeed-Skript läuft gegen ${API}`, 'color:#16a34a; font-weight:600'); + + async function api(path, body, method = 'POST') { + const r = await fetch(API + path, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + if (!r.ok) { + const text = await r.text(); + throw new Error(`${method} ${path} → ${r.status}: ${text}`); + } + return r.json(); + } + + // Verschiedene Größen + Farben → Stack-Hint, Fan-Spread, Empty-State alle testbar. + const DECKS = [ + { + name: 'Spanisch — Alltagsvokabular', + description: 'Wörter und kurze Wendungen für die ersten Reisetage.', + color: '#16a34a', + cards: makeBasic([ + ['hola', 'hallo'], + ['gracias', 'danke'], + ['por favor', 'bitte'], + ['¿dónde está…?', 'wo ist …?'], + ['la cuenta, por favor', 'die Rechnung, bitte'], + ['no hablo bien español', 'ich spreche nicht gut Spanisch'], + ['¿habla inglés?', 'sprechen Sie Englisch?'], + ['una mesa para dos', 'einen Tisch für zwei'], + ['agua sin gas', 'stilles Wasser'], + ['una cerveza', 'ein Bier'], + ['¿cuánto cuesta?', 'wie viel kostet das?'], + ['demasiado caro', 'zu teuer'], + ['está bien', 'ist gut'], + ['perdón', 'entschuldigung'], + ['lo siento', 'tut mir leid'], + ['ayuda', 'Hilfe'], + ['estoy perdido', 'ich habe mich verlaufen'], + ['la estación', 'der Bahnhof'], + ['el aeropuerto', 'der Flughafen'], + ['el hotel', 'das Hotel'], + ['la habitación', 'das Zimmer'], + ['la llave', 'der Schlüssel'], + ['mañana', 'morgen'], + ['ayer', 'gestern'], + ]), + }, + { + name: 'Deutsch ↔ Niederländisch', + description: 'Falsche Freunde und überraschend nahe Verwandte.', + color: '#FF6600', + cards: makeBasicReverse([ + ['Bahnhof', 'station'], + ['Käse', 'kaas'], + ['Fenster', 'raam'], + ['Tisch', 'tafel'], + ['Brot', 'brood'], + ['Buch', 'boek'], + ['Auto', 'auto'], + ['Haus', 'huis'], + ['Frau', 'vrouw'], + ['Mann', 'man'], + ['Kind', 'kind'], + ['Hund', 'hond'], + ]), + }, + { + name: 'JavaScript ES2026', + description: 'Neue Features — Pattern-Matching, Decorators, Pipe.', + color: '#6366F1', + cards: [ + ...makeBasic([ + ['Optional Chaining', '?.'], + ['Nullish Coalescing', '??'], + ['Logical Assignment', '||=, &&=, ??='], + ['Top-Level Await', 'await außerhalb von async-Funktion in ES-Modules'], + ['Pipeline Operator (Stage 2)', 'value |> fn()'], + ]), + ...makeCloze([ + 'Der `using`-Keyword aus Stage 3 ruft automatisch `Symbol.{{c1::dispose}}` auf, wenn der Scope verlassen wird.', + 'Mit Pattern-Matching schreibst du `{{c1::match}} (value) { when … -> … }` statt verschachtelter `if`-Ketten.', + '`Array.prototype.{{c1::with}}(index, value)` gibt eine neue Kopie zurück — immutable Update.', + ]), + ], + }, + { + name: 'Geschichte 19. Jh.', + description: 'Schlüsselereignisse 1815–1900.', + color: '#DC2626', + cards: makeBasic([ + ['1815', 'Wiener Kongress — Neuordnung Europas nach Napoleon'], + ['1848', 'Märzrevolution + Frankfurter Nationalversammlung'], + ['1861–1865', 'Amerikanischer Bürgerkrieg'], + ['1871', 'Deutsche Reichsgründung in Versailles'], + ['1884–1885', 'Berliner Kongokonferenz — Aufteilung Afrikas'], + ['1889', 'Pariser Weltausstellung + Eiffelturm'], + ['1895', 'Erste Filmvorführung der Brüder Lumière'], + ]), + }, + { + name: 'Gitarre — Akkord-Grundlagen', + description: 'Vier Akkorde, mit denen 80 % der Pop-Songs gehen.', + color: '#07D6FF', + cards: makeBasic([ + ['G-Dur', 'Tonika in vielen Singer-Songwriter-Stücken'], + ['D-Dur', 'helle Dominante zu G'], + ['Em', 'parallele Moll zu G — melancholische Färbung'], + ['C-Dur', 'Subdominante in G — auflösend'], + ]), + }, + { + name: 'Pflanzenkunde — heimische Bäume', + description: + 'Erkennungsmerkmale, Standort, Holz-Eigenschaften. Großer Stapel zum Testen.', + color: '#F8D62B', + cards: makeBasic( + [ + ['Eiche', 'Stieleiche oder Traubeneiche, gelappte Blätter, Eicheln'], + ['Buche', 'Rotbuche, ovale Blätter mit gewelltem Rand, Bucheckern'], + ['Linde', 'herzförmige Blätter, duftende Blüten im Juni'], + ['Birke', 'weiße Rinde, kleine dreieckige Blätter'], + ['Fichte', 'kurze Nadeln einzeln am Zweig, hängende Zapfen'], + ['Kiefer', 'lange Nadeln paarweise, rotbraune Rinde'], + ['Tanne', 'flache Nadeln mit zwei weißen Streifen unten, stehende Zapfen'], + ['Lärche', 'einziger heimischer Nadelbaum, der im Herbst Nadeln verliert'], + ['Esche', 'unpaarig gefiederte Blätter, schwarze Knospen'], + ['Ahorn', 'Spitzahorn, Bergahorn, Feldahorn — gelappte Blätter, Flügelfrüchte'], + ['Erle', 'eiförmige Blätter, Standort an Bachläufen'], + ['Hainbuche', 'doppelt gesägte Blätter, glatte graue Rinde'], + ['Pappel', 'dreieckige Blätter, sehr schnellwüchsig'], + ['Weide', 'lanzettliche Blätter, Standort an Wasserläufen'], + ['Ulme', 'asymmetrischer Blattgrund — Erkennungsmerkmal'], + ['Robinie', 'gefiederte Blätter, weiße Schmetterlingsblüten, sehr hartes Holz'], + ['Walnuss', 'gefiederte Blätter mit aromatischem Geruch'], + ['Edelkastanie', 'lange gesägte Blätter, stachelige Fruchthülle'], + ['Rosskastanie', 'handförmig gefiederte Blätter, glatte Kastanien'], + ['Vogelbeere', 'gefiederte Blätter, leuchtend rote Beeren-Doldentrauben'], + ['Holunder', 'gefiederte Blätter, weiße Doldenblüten, schwarze Beeren'], + ['Hasel', 'rundliche Blätter mit Spitze, Haselnüsse'], + ['Schlehe', 'dornig, weiße Blüten vor dem Laubaustrieb'], + ['Weißdorn', 'tief gelappte Blätter, weiße Blüten, rote Apfelfrüchte'], + ['Eberesche', 'andere Bezeichnung für Vogelbeere'], + ['Kirschbaum', 'glänzende Rinde mit horizontalen Streifen'], + ['Apfelbaum', 'eiförmige Blätter, kugelige Frucht'], + ['Birnbaum', 'eiförmige Blätter, längliche Frucht'], + ['Eibe', 'flache Nadeln, rote fleischige Samenmäntel — sonst alles giftig'], + ['Wacholder', 'spitze Nadeln, schwarzblaue Beerenzapfen für Gin'], + ].slice(0, 30), + ), + }, + { + name: 'Architekten der Moderne', + description: 'Leerer Stapel zum Testen des Empty-States.', + color: '#6B7280', + cards: [], + }, + ]; + + function makeBasic(pairs) { + return pairs.map(([front, back]) => ({ + type: 'basic', + fields: { front, back }, + })); + } + + function makeBasicReverse(pairs) { + return pairs.map(([front, back]) => ({ + type: 'basic-reverse', + fields: { front, back }, + })); + } + + function makeCloze(texts) { + return texts.map((text) => ({ + type: 'cloze', + fields: { text }, + })); + } + + let totalDecks = 0; + let totalCards = 0; + for (const spec of DECKS) { + const deck = await api('/api/v1/decks', { + name: spec.name, + description: spec.description, + color: spec.color, + }); + totalDecks++; + console.log(`%c✓ Deck: ${deck.name}`, 'color:#16a34a', `(${deck.id.slice(0, 8)})`); + for (const card of spec.cards) { + await api('/api/v1/cards', { deck_id: deck.id, ...card }); + totalCards++; + } + if (spec.cards.length > 0) { + console.log(` + ${spec.cards.length} Karten`); + } + } + + console.log( + `%c✓ Fertig — ${totalDecks} Decks, ${totalCards} Karten angelegt. Jetzt /decks öffnen.`, + 'color:#16a34a; font-weight:600; font-size:1.1em', + ); +})();