Add native language support in vocab course management
- Introduced a new field for native language in the VocabCourse model to allow learners to specify their native language. - Updated the VocabService to handle native language during course creation and retrieval, including filtering options. - Enhanced the database schema to include foreign key constraints for native language. - Updated frontend components to support native language selection and display in course listings. - Added internationalization strings for native language features in both German and English.
This commit is contained in:
@@ -27,6 +27,12 @@ VocabCourse.init({
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
field: 'language_id'
|
field: 'language_id'
|
||||||
},
|
},
|
||||||
|
nativeLanguageId: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'native_language_id',
|
||||||
|
comment: 'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".'
|
||||||
|
},
|
||||||
difficultyLevel: {
|
difficultyLevel: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
452
backend/scripts/create-language-courses.js
Executable file
452
backend/scripts/create-language-courses.js
Executable file
@@ -0,0 +1,452 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Script zum Erstellen von Sprachkursen für verschiedene Sprachen
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* node backend/scripts/create-language-courses.js <ownerHashedId>
|
||||||
|
*
|
||||||
|
* Erstellt Kurse für: Bisaya, Französisch, Spanisch, Latein, Italienisch, Portugiesisch, Tagalog
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sequelize } from '../utils/sequelize.js';
|
||||||
|
import VocabCourse from '../models/community/vocab_course.js';
|
||||||
|
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
|
||||||
|
import User from '../models/community/user.js';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
// Kursstruktur für alle Sprachen (4 Wochen, 40 Lektionen)
|
||||||
|
const LESSON_TEMPLATE = [
|
||||||
|
// WOCHE 1: Grundlagen & Aussprache
|
||||||
|
{ week: 1, day: 1, num: 1, type: 'conversation', title: 'Begrüßungen & Höflichkeit',
|
||||||
|
desc: 'Lerne die wichtigsten Begrüßungen und Höflichkeitsformeln',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Höflichkeit ist wichtig. Lächeln hilft!' },
|
||||||
|
|
||||||
|
{ week: 1, day: 1, num: 2, type: 'vocab', title: 'Überlebenssätze - Teil 1',
|
||||||
|
desc: 'Die 10 wichtigsten Sätze für den Alltag',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: 'Diese Sätze helfen dir sofort im Alltag weiter.' },
|
||||||
|
|
||||||
|
{ week: 1, day: 2, num: 3, type: 'vocab', title: 'Familienwörter',
|
||||||
|
desc: 'Mama, Papa, Geschwister, Großeltern und mehr',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: 'Familienwörter sind wichtig für echte Gespräche.' },
|
||||||
|
|
||||||
|
{ week: 1, day: 2, num: 4, type: 'conversation', title: 'Familien-Gespräche',
|
||||||
|
desc: 'Einfache Gespräche mit Familienmitgliedern',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Familienkonversationen sind herzlicher als formelle Gespräche.' },
|
||||||
|
|
||||||
|
{ week: 1, day: 3, num: 5, type: 'conversation', title: 'Gefühle & Zuneigung',
|
||||||
|
desc: 'Wie man Gefühle ausdrückt',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Gefühle auszudrücken ist wichtig für echte Verbindung.' },
|
||||||
|
|
||||||
|
{ week: 1, day: 3, num: 6, type: 'vocab', title: 'Überlebenssätze - Teil 2',
|
||||||
|
desc: 'Weitere wichtige Alltagssätze',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 1, day: 4, num: 7, type: 'conversation', title: 'Essen & Fürsorge',
|
||||||
|
desc: 'Gespräche rund ums Essen',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Essen verbindet Menschen überall auf der Welt.' },
|
||||||
|
|
||||||
|
{ week: 1, day: 4, num: 8, type: 'vocab', title: 'Essen & Trinken',
|
||||||
|
desc: 'Wichtige Wörter rund ums Essen',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 1, day: 5, num: 9, type: 'review', title: 'Woche 1 - Wiederholung',
|
||||||
|
desc: 'Wiederhole alle Inhalte der ersten Woche',
|
||||||
|
targetMin: 30, targetScore: 80, review: false,
|
||||||
|
cultural: 'Wiederholung ist der Schlüssel zum Erfolg!' },
|
||||||
|
|
||||||
|
{ week: 1, day: 5, num: 10, type: 'vocab', title: 'Woche 1 - Vokabeltest',
|
||||||
|
desc: 'Teste dein Wissen aus Woche 1',
|
||||||
|
targetMin: 15, targetScore: 80, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
// WOCHE 2: Alltag & Familie
|
||||||
|
{ week: 2, day: 1, num: 11, type: 'conversation', title: 'Alltagsgespräche - Teil 1',
|
||||||
|
desc: 'Wie war dein Tag? Was machst du?',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Alltagsgespräche sind wichtig für echte Kommunikation.' },
|
||||||
|
|
||||||
|
{ week: 2, day: 1, num: 12, type: 'vocab', title: 'Haus & Familie',
|
||||||
|
desc: 'Wörter für Haus, Zimmer, Familie',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 2, num: 13, type: 'conversation', title: 'Alltagsgespräche - Teil 2',
|
||||||
|
desc: 'Wohin gehst du? Was machst du heute?',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 2, num: 14, type: 'vocab', title: 'Ort & Richtung',
|
||||||
|
desc: 'Wo, hier, dort, gehen zu',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 3, num: 15, type: 'grammar', title: 'Zeitformen - Grundlagen',
|
||||||
|
desc: 'Vergangenheit, Gegenwart, Zukunft',
|
||||||
|
targetMin: 25, targetScore: 75, review: true,
|
||||||
|
cultural: 'Zeitformen sind wichtig für präzise Kommunikation.' },
|
||||||
|
|
||||||
|
{ week: 2, day: 3, num: 16, type: 'vocab', title: 'Zeit & Datum',
|
||||||
|
desc: 'Jetzt, morgen, gestern, heute',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 4, num: 17, type: 'conversation', title: 'Einkaufen & Preise',
|
||||||
|
desc: 'Wie viel kostet das? Kann es billiger sein?',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Einkaufen ist eine wichtige Alltagssituation.' },
|
||||||
|
|
||||||
|
{ week: 2, day: 4, num: 18, type: 'vocab', title: 'Zahlen & Preise',
|
||||||
|
desc: 'Zahlen 1-100, Preise, Mengen',
|
||||||
|
targetMin: 25, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 5, num: 19, type: 'review', title: 'Woche 2 - Wiederholung',
|
||||||
|
desc: 'Wiederhole alle Inhalte der zweiten Woche',
|
||||||
|
targetMin: 30, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 2, day: 5, num: 20, type: 'vocab', title: 'Woche 2 - Vokabeltest',
|
||||||
|
desc: 'Teste dein Wissen aus Woche 2',
|
||||||
|
targetMin: 15, targetScore: 80, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
// WOCHE 3: Vertiefung
|
||||||
|
{ week: 3, day: 1, num: 21, type: 'conversation', title: 'Gefühle & Emotionen',
|
||||||
|
desc: 'Wie man verschiedene Gefühle ausdrückt',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Emotionen auszudrücken ist wichtig für echte Verbindung.' },
|
||||||
|
|
||||||
|
{ week: 3, day: 1, num: 22, type: 'vocab', title: 'Gefühle & Emotionen',
|
||||||
|
desc: 'Wörter für verschiedene Gefühle',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 2, num: 23, type: 'conversation', title: 'Gesundheit & Wohlbefinden',
|
||||||
|
desc: 'Gespräche über Gesundheit',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 2, num: 24, type: 'vocab', title: 'Körper & Gesundheit',
|
||||||
|
desc: 'Wörter rund um den Körper und Gesundheit',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 3, num: 25, type: 'grammar', title: 'Höflichkeitsformen',
|
||||||
|
desc: 'Wie man höflich spricht',
|
||||||
|
targetMin: 20, targetScore: 75, review: true,
|
||||||
|
cultural: 'Höflichkeit ist extrem wichtig in jeder Kultur.' },
|
||||||
|
|
||||||
|
{ week: 3, day: 3, num: 26, type: 'conversation', title: 'Bitten & Fragen',
|
||||||
|
desc: 'Wie man höflich fragt und bittet',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 4, num: 27, type: 'conversation', title: 'Kinder & Familie',
|
||||||
|
desc: 'Gespräche mit und über Kinder',
|
||||||
|
targetMin: 15, targetScore: 80, review: false,
|
||||||
|
cultural: 'Kinder sind sehr wichtig in Familien.' },
|
||||||
|
|
||||||
|
{ week: 3, day: 4, num: 28, type: 'vocab', title: 'Kinder & Spiel',
|
||||||
|
desc: 'Wörter für Kinder und Spielsachen',
|
||||||
|
targetMin: 20, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 5, num: 29, type: 'review', title: 'Woche 3 - Wiederholung',
|
||||||
|
desc: 'Wiederhole alle Inhalte der dritten Woche',
|
||||||
|
targetMin: 30, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 3, day: 5, num: 30, type: 'vocab', title: 'Woche 3 - Vokabeltest',
|
||||||
|
desc: 'Teste dein Wissen aus Woche 3',
|
||||||
|
targetMin: 15, targetScore: 80, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
// WOCHE 4: Freies Sprechen
|
||||||
|
{ week: 4, day: 1, num: 31, type: 'conversation', title: 'Freies Gespräch - Thema 1',
|
||||||
|
desc: 'Übe freies Sprechen zu verschiedenen Themen',
|
||||||
|
targetMin: 20, targetScore: 75, review: false,
|
||||||
|
cultural: 'Fehler sind okay! Muttersprachler schätzen das Bemühen.' },
|
||||||
|
|
||||||
|
{ week: 4, day: 1, num: 32, type: 'vocab', title: 'Wiederholung - Woche 1 & 2',
|
||||||
|
desc: 'Wiederhole wichtige Vokabeln aus den ersten beiden Wochen',
|
||||||
|
targetMin: 25, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 2, num: 33, type: 'conversation', title: 'Freies Gespräch - Thema 2',
|
||||||
|
desc: 'Weitere Übung im freien Sprechen',
|
||||||
|
targetMin: 20, targetScore: 75, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 2, num: 34, type: 'vocab', title: 'Wiederholung - Woche 3',
|
||||||
|
desc: 'Wiederhole wichtige Vokabeln aus Woche 3',
|
||||||
|
targetMin: 25, targetScore: 85, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 3, num: 35, type: 'conversation', title: 'Komplexere Gespräche',
|
||||||
|
desc: 'Längere Gespräche zu verschiedenen Themen',
|
||||||
|
targetMin: 25, targetScore: 75, review: false,
|
||||||
|
cultural: 'Je mehr du sprichst, desto besser wirst du!' },
|
||||||
|
|
||||||
|
{ week: 4, day: 3, num: 36, type: 'review', title: 'Gesamtwiederholung',
|
||||||
|
desc: 'Wiederhole alle wichtigen Inhalte des Kurses',
|
||||||
|
targetMin: 30, targetScore: 80, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 4, num: 37, type: 'conversation', title: 'Praktische Übung',
|
||||||
|
desc: 'Simuliere echte Gesprächssituationen',
|
||||||
|
targetMin: 25, targetScore: 75, review: false,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 4, num: 38, type: 'vocab', title: 'Abschlusstest - Vokabeln',
|
||||||
|
desc: 'Finaler Vokabeltest über den gesamten Kurs',
|
||||||
|
targetMin: 20, targetScore: 80, review: true,
|
||||||
|
cultural: null },
|
||||||
|
|
||||||
|
{ week: 4, day: 5, num: 39, type: 'review', title: 'Abschlussprüfung',
|
||||||
|
desc: 'Finale Prüfung über alle Kursinhalte',
|
||||||
|
targetMin: 30, targetScore: 80, review: false,
|
||||||
|
cultural: 'Gratulation zum Abschluss des Kurses!' },
|
||||||
|
|
||||||
|
{ week: 4, day: 5, num: 40, type: 'culture', title: 'Kulturelle Tipps & Tricks',
|
||||||
|
desc: 'Wichtige kulturelle Hinweise für den Alltag',
|
||||||
|
targetMin: 15, targetScore: 0, review: false,
|
||||||
|
cultural: 'Kulturelles Verständnis ist genauso wichtig wie die Sprache selbst.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Zielsprachen (die zu lernenden Sprachen)
|
||||||
|
const TARGET_LANGUAGES = [
|
||||||
|
'Bisaya',
|
||||||
|
'Französisch',
|
||||||
|
'Spanisch',
|
||||||
|
'Latein',
|
||||||
|
'Italienisch',
|
||||||
|
'Portugiesisch',
|
||||||
|
'Tagalog'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Muttersprachen (für die Kurse erstellt werden)
|
||||||
|
const NATIVE_LANGUAGES = [
|
||||||
|
'Deutsch',
|
||||||
|
'Englisch',
|
||||||
|
'Spanisch',
|
||||||
|
'Französisch',
|
||||||
|
'Italienisch',
|
||||||
|
'Portugiesisch'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generiere Kurskonfigurationen für alle Kombinationen
|
||||||
|
function generateCourseConfigs() {
|
||||||
|
const configs = [];
|
||||||
|
|
||||||
|
for (const targetLang of TARGET_LANGUAGES) {
|
||||||
|
for (const nativeLang of NATIVE_LANGUAGES) {
|
||||||
|
// Überspringe, wenn Zielsprache = Muttersprache
|
||||||
|
if (targetLang === nativeLang) continue;
|
||||||
|
|
||||||
|
const title = `${targetLang} für ${nativeLang}sprachige - Schnellstart in 4 Wochen`;
|
||||||
|
let description = `Lerne ${targetLang} schnell und praktisch für den Alltag. `;
|
||||||
|
|
||||||
|
if (targetLang === 'Latein') {
|
||||||
|
description = `Lerne ${targetLang} systematisch mit Fokus auf Grammatik und Vokabular. `;
|
||||||
|
} else if (targetLang === 'Bisaya') {
|
||||||
|
description = `Lerne ${targetLang} (Cebuano) schnell und praktisch für den Familienalltag. `;
|
||||||
|
}
|
||||||
|
|
||||||
|
description += `Fokus auf Sprechen & Hören mit strukturiertem 4-Wochen-Plan.`;
|
||||||
|
|
||||||
|
configs.push({
|
||||||
|
targetLanguageName: targetLang,
|
||||||
|
nativeLanguageName: nativeLang,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
difficultyLevel: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LANGUAGE_COURSES = generateCourseConfigs();
|
||||||
|
|
||||||
|
async function findOrCreateLanguage(languageName, ownerUserId) {
|
||||||
|
// Suche zuerst nach vorhandener Sprache
|
||||||
|
const [existing] = await sequelize.query(
|
||||||
|
`SELECT id FROM community.vocab_language WHERE name = :name LIMIT 1`,
|
||||||
|
{
|
||||||
|
replacements: { name: languageName },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
console.log(` ✅ Sprache "${languageName}" bereits vorhanden (ID: ${existing.id})`);
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle neue Sprache
|
||||||
|
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||||
|
const [created] = await sequelize.query(
|
||||||
|
`INSERT INTO community.vocab_language (owner_user_id, name, share_code)
|
||||||
|
VALUES (:ownerUserId, :name, :shareCode)
|
||||||
|
RETURNING id`,
|
||||||
|
{
|
||||||
|
replacements: { ownerUserId, name: languageName, shareCode },
|
||||||
|
type: sequelize.QueryTypes.SELECT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(` ✅ Sprache "${languageName}" erstellt (ID: ${created.id})`);
|
||||||
|
return created.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCourseForLanguage(targetLanguageId, nativeLanguageId, languageConfig, ownerUserId) {
|
||||||
|
const shareCode = crypto.randomBytes(8).toString('hex');
|
||||||
|
|
||||||
|
const course = await VocabCourse.create({
|
||||||
|
ownerUserId,
|
||||||
|
title: languageConfig.title,
|
||||||
|
description: languageConfig.description,
|
||||||
|
languageId: Number(targetLanguageId),
|
||||||
|
nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
|
||||||
|
difficultyLevel: languageConfig.difficultyLevel || 1,
|
||||||
|
isPublic: true,
|
||||||
|
shareCode
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` ✅ Kurs erstellt: "${course.title}" (ID: ${course.id}, Share-Code: ${shareCode})`);
|
||||||
|
|
||||||
|
// Erstelle Lektionen
|
||||||
|
for (const lessonData of LESSON_TEMPLATE) {
|
||||||
|
await VocabCourseLesson.create({
|
||||||
|
courseId: course.id,
|
||||||
|
chapterId: null,
|
||||||
|
lessonNumber: lessonData.num,
|
||||||
|
title: lessonData.title,
|
||||||
|
description: lessonData.desc,
|
||||||
|
weekNumber: lessonData.week,
|
||||||
|
dayNumber: lessonData.day,
|
||||||
|
lessonType: lessonData.type,
|
||||||
|
culturalNotes: lessonData.cultural,
|
||||||
|
targetMinutes: lessonData.targetMin,
|
||||||
|
targetScorePercent: lessonData.targetScore,
|
||||||
|
requiresReview: lessonData.review
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ✅ ${LESSON_TEMPLATE.length} Lektionen erstellt`);
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAllLanguageCourses(ownerHashedId) {
|
||||||
|
try {
|
||||||
|
// Finde User
|
||||||
|
const user = await User.findOne({ where: { hashedId: ownerHashedId } });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error(`User mit hashedId ${ownerHashedId} nicht gefunden`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🚀 Erstelle Sprachkurse für Benutzer: ${user.id}\n`);
|
||||||
|
|
||||||
|
const createdCourses = [];
|
||||||
|
|
||||||
|
// Stelle sicher, dass alle benötigten Sprachen existieren
|
||||||
|
console.log(`\n🌍 Stelle sicher, dass alle Sprachen existieren...`);
|
||||||
|
const allLanguages = [...new Set([...TARGET_LANGUAGES, ...NATIVE_LANGUAGES])];
|
||||||
|
const languageMap = new Map();
|
||||||
|
|
||||||
|
for (const langName of allLanguages) {
|
||||||
|
const langId = await findOrCreateLanguage(langName, user.id);
|
||||||
|
languageMap.set(langName, langId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const langConfig of LANGUAGE_COURSES) {
|
||||||
|
console.log(`\n📚 Verarbeite: ${langConfig.targetLanguageName} für ${langConfig.nativeLanguageName}sprachige`);
|
||||||
|
|
||||||
|
const targetLanguageId = languageMap.get(langConfig.targetLanguageName);
|
||||||
|
const nativeLanguageId = languageMap.get(langConfig.nativeLanguageName);
|
||||||
|
|
||||||
|
// Prüfe, ob Kurs bereits existiert
|
||||||
|
const existingCourse = await VocabCourse.findOne({
|
||||||
|
where: {
|
||||||
|
languageId: targetLanguageId,
|
||||||
|
nativeLanguageId: nativeLanguageId,
|
||||||
|
ownerUserId: user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingCourse) {
|
||||||
|
console.log(` ⚠️ Kurs "${langConfig.title}" existiert bereits (ID: ${existingCourse.id})`);
|
||||||
|
createdCourses.push({
|
||||||
|
...langConfig,
|
||||||
|
courseId: existingCourse.id,
|
||||||
|
targetLanguageId,
|
||||||
|
nativeLanguageId,
|
||||||
|
skipped: true
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle Kurs
|
||||||
|
const course = await createCourseForLanguage(targetLanguageId, nativeLanguageId, langConfig, user.id);
|
||||||
|
createdCourses.push({
|
||||||
|
...langConfig,
|
||||||
|
courseId: course.id,
|
||||||
|
targetLanguageId,
|
||||||
|
nativeLanguageId,
|
||||||
|
shareCode: course.shareCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n\n🎉 Zusammenfassung:\n`);
|
||||||
|
console.log(` Gesamt: ${LANGUAGE_COURSES.length} Sprachen`);
|
||||||
|
console.log(` Erstellt: ${createdCourses.filter(c => !c.skipped).length} Kurse`);
|
||||||
|
console.log(` Übersprungen: ${createdCourses.filter(c => c.skipped).length} Kurse`);
|
||||||
|
|
||||||
|
console.log(`\n📋 Erstellte Kurse:\n`);
|
||||||
|
for (const course of createdCourses) {
|
||||||
|
if (course.skipped) {
|
||||||
|
console.log(` ⚠️ ${course.languageName}: Bereits vorhanden (ID: ${course.courseId})`);
|
||||||
|
} else {
|
||||||
|
console.log(` ✅ ${course.languageName}: ${course.title}`);
|
||||||
|
console.log(` Share-Code: ${course.shareCode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n💡 Nächste Schritte:`);
|
||||||
|
console.log(` 1. Füge Vokabeln zu den Vokabel-Lektionen hinzu`);
|
||||||
|
console.log(` 2. Erstelle Grammatik-Übungen für die Grammatik-Lektionen`);
|
||||||
|
console.log(` 3. Teile die Kurse mit anderen (Share-Codes oben)`);
|
||||||
|
|
||||||
|
return createdCourses;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Erstellen der Kurse:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI-Aufruf
|
||||||
|
const ownerHashedId = process.argv[2];
|
||||||
|
|
||||||
|
if (!ownerHashedId) {
|
||||||
|
console.error('Verwendung: node create-language-courses.js <ownerHashedId>');
|
||||||
|
console.error('Beispiel: node create-language-courses.js abc123def456');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAllLanguageCourses(ownerHashedId)
|
||||||
|
.then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -538,7 +538,7 @@ export default class VocabService {
|
|||||||
|
|
||||||
// ========== COURSE METHODS ==========
|
// ========== COURSE METHODS ==========
|
||||||
|
|
||||||
async createCourse(hashedUserId, { title, description, languageId, difficultyLevel = 1, isPublic = false }) {
|
async createCourse(hashedUserId, { title, description, languageId, nativeLanguageId, difficultyLevel = 1, isPublic = false }) {
|
||||||
const user = await this._getUserByHashedId(hashedUserId);
|
const user = await this._getUserByHashedId(hashedUserId);
|
||||||
|
|
||||||
// Prüfe Zugriff auf Sprache
|
// Prüfe Zugriff auf Sprache
|
||||||
@@ -551,6 +551,7 @@ export default class VocabService {
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
languageId: Number(languageId),
|
languageId: Number(languageId),
|
||||||
|
nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
|
||||||
difficultyLevel: Number(difficultyLevel) || 1,
|
difficultyLevel: Number(difficultyLevel) || 1,
|
||||||
isPublic: Boolean(isPublic),
|
isPublic: Boolean(isPublic),
|
||||||
shareCode
|
shareCode
|
||||||
@@ -559,7 +560,7 @@ export default class VocabService {
|
|||||||
return course.get({ plain: true });
|
return course.get({ plain: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, search } = {}) {
|
async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, nativeLanguageId, search } = {}) {
|
||||||
const user = await this._getUserByHashedId(hashedUserId);
|
const user = await this._getUserByHashedId(hashedUserId);
|
||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
@@ -579,11 +580,21 @@ export default class VocabService {
|
|||||||
where.isPublic = true;
|
where.isPublic = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter nach Sprache
|
// Filter nach Zielsprache (die zu lernende Sprache)
|
||||||
if (languageId) {
|
if (languageId) {
|
||||||
where.languageId = Number(languageId);
|
where.languageId = Number(languageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter nach Muttersprache (die Sprache des Lerners)
|
||||||
|
if (nativeLanguageId !== undefined) {
|
||||||
|
if (nativeLanguageId === null) {
|
||||||
|
// NULL bedeutet "für alle Sprachen" - zeige Kurse ohne native_language_id
|
||||||
|
where.nativeLanguageId = null;
|
||||||
|
} else {
|
||||||
|
where.nativeLanguageId = Number(nativeLanguageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Suche nach Titel oder Beschreibung
|
// Suche nach Titel oder Beschreibung
|
||||||
if (search && search.trim()) {
|
if (search && search.trim()) {
|
||||||
const searchTerm = `%${search.trim()}%`;
|
const searchTerm = `%${search.trim()}%`;
|
||||||
@@ -698,7 +709,7 @@ export default class VocabService {
|
|||||||
return courseData;
|
return courseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCourse(hashedUserId, courseId, { title, description, difficultyLevel, isPublic }) {
|
async updateCourse(hashedUserId, courseId, { title, description, languageId, nativeLanguageId, difficultyLevel, isPublic }) {
|
||||||
const user = await this._getUserByHashedId(hashedUserId);
|
const user = await this._getUserByHashedId(hashedUserId);
|
||||||
const course = await VocabCourse.findByPk(courseId);
|
const course = await VocabCourse.findByPk(courseId);
|
||||||
|
|
||||||
@@ -717,6 +728,8 @@ export default class VocabService {
|
|||||||
const updates = {};
|
const updates = {};
|
||||||
if (title !== undefined) updates.title = title;
|
if (title !== undefined) updates.title = title;
|
||||||
if (description !== undefined) updates.description = description;
|
if (description !== undefined) updates.description = description;
|
||||||
|
if (languageId !== undefined) updates.languageId = Number(languageId);
|
||||||
|
if (nativeLanguageId !== undefined) updates.nativeLanguageId = nativeLanguageId ? Number(nativeLanguageId) : null;
|
||||||
if (difficultyLevel !== undefined) updates.difficultyLevel = Number(difficultyLevel);
|
if (difficultyLevel !== undefined) updates.difficultyLevel = Number(difficultyLevel);
|
||||||
if (isPublic !== undefined) {
|
if (isPublic !== undefined) {
|
||||||
updates.isPublic = Boolean(isPublic);
|
updates.isPublic = Boolean(isPublic);
|
||||||
|
|||||||
24
backend/sql/add-native-language-to-courses.sql
Normal file
24
backend/sql/add-native-language-to-courses.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Füge native_language_id zu vocab_course hinzu
|
||||||
|
-- ============================================
|
||||||
|
-- Dieses Feld speichert die Muttersprache des Lerners
|
||||||
|
-- z.B. "Bisaya für Deutschsprachige" -> language_id = Bisaya, native_language_id = Deutsch
|
||||||
|
|
||||||
|
-- Spalte hinzufügen
|
||||||
|
ALTER TABLE community.vocab_course
|
||||||
|
ADD COLUMN IF NOT EXISTS native_language_id INTEGER;
|
||||||
|
|
||||||
|
-- Foreign Key Constraint hinzufügen
|
||||||
|
ALTER TABLE community.vocab_course
|
||||||
|
ADD CONSTRAINT vocab_course_native_language_fk
|
||||||
|
FOREIGN KEY (native_language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Index für bessere Performance
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
|
||||||
|
ON community.vocab_course(native_language_id);
|
||||||
|
|
||||||
|
-- Kommentar hinzufügen
|
||||||
|
COMMENT ON COLUMN community.vocab_course.native_language_id IS
|
||||||
|
'Muttersprache des Lerners (z.B. Deutsch, Englisch). NULL bedeutet "für alle Sprachen".';
|
||||||
@@ -152,6 +152,7 @@ const syncDatabase = async () => {
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
language_id INTEGER NOT NULL,
|
language_id INTEGER NOT NULL,
|
||||||
|
native_language_id INTEGER,
|
||||||
difficulty_level INTEGER DEFAULT 1,
|
difficulty_level INTEGER DEFAULT 1,
|
||||||
is_public BOOLEAN DEFAULT false,
|
is_public BOOLEAN DEFAULT false,
|
||||||
share_code TEXT,
|
share_code TEXT,
|
||||||
@@ -165,6 +166,10 @@ const syncDatabase = async () => {
|
|||||||
FOREIGN KEY (language_id)
|
FOREIGN KEY (language_id)
|
||||||
REFERENCES community.vocab_language(id)
|
REFERENCES community.vocab_language(id)
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT vocab_course_native_language_fk
|
||||||
|
FOREIGN KEY (native_language_id)
|
||||||
|
REFERENCES community.vocab_language(id)
|
||||||
|
ON DELETE SET NULL,
|
||||||
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,6 +244,8 @@ const syncDatabase = async () => {
|
|||||||
ON community.vocab_course(owner_user_id);
|
ON community.vocab_course(owner_user_id);
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
CREATE INDEX IF NOT EXISTS vocab_course_language_idx
|
||||||
ON community.vocab_course(language_id);
|
ON community.vocab_course(language_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx
|
||||||
|
ON community.vocab_course(native_language_id);
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
CREATE INDEX IF NOT EXISTS vocab_course_public_idx
|
||||||
ON community.vocab_course(is_public);
|
ON community.vocab_course(is_public);
|
||||||
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx
|
||||||
|
|||||||
@@ -352,6 +352,11 @@
|
|||||||
"shareCode": "Share-Code",
|
"shareCode": "Share-Code",
|
||||||
"searchPlaceholder": "Kurs suchen...",
|
"searchPlaceholder": "Kurs suchen...",
|
||||||
"allLanguages": "Alle Sprachen",
|
"allLanguages": "Alle Sprachen",
|
||||||
|
"targetLanguage": "Zielsprache",
|
||||||
|
"nativeLanguage": "Muttersprache",
|
||||||
|
"allNativeLanguages": "Alle Muttersprachen",
|
||||||
|
"forAllLanguages": "Für alle Sprachen",
|
||||||
|
"optional": "Optional",
|
||||||
"invalidCode": "Ungültiger Code",
|
"invalidCode": "Ungültiger Code",
|
||||||
"courseNotFound": "Kurs nicht gefunden"
|
"courseNotFound": "Kurs nicht gefunden"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,6 +352,11 @@
|
|||||||
"shareCode": "Share Code",
|
"shareCode": "Share Code",
|
||||||
"searchPlaceholder": "Search courses...",
|
"searchPlaceholder": "Search courses...",
|
||||||
"allLanguages": "All Languages",
|
"allLanguages": "All Languages",
|
||||||
|
"targetLanguage": "Target Language",
|
||||||
|
"nativeLanguage": "Native Language",
|
||||||
|
"allNativeLanguages": "All Native Languages",
|
||||||
|
"forAllLanguages": "For All Languages",
|
||||||
|
"optional": "Optional",
|
||||||
"invalidCode": "Invalid code",
|
"invalidCode": "Invalid code",
|
||||||
"courseNotFound": "Course not found"
|
"courseNotFound": "Course not found"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-box">
|
<div class="filter-box">
|
||||||
|
<label>{{ $t('socialnetwork.vocab.courses.targetLanguage') }}:</label>
|
||||||
<select v-model="selectedLanguageId" @change="loadAllCourses">
|
<select v-model="selectedLanguageId" @change="loadAllCourses">
|
||||||
<option value="">{{ $t('socialnetwork.vocab.courses.allLanguages') }}</option>
|
<option value="">{{ $t('socialnetwork.vocab.courses.allLanguages') }}</option>
|
||||||
<option v-for="lang in languages" :key="lang.id" :value="lang.id">{{ lang.name }}</option>
|
<option v-for="lang in languages" :key="lang.id" :value="lang.id">{{ lang.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-box">
|
||||||
|
<label>{{ $t('socialnetwork.vocab.courses.nativeLanguage') }}:</label>
|
||||||
|
<select v-model="selectedNativeLanguageId" @change="loadAllCourses">
|
||||||
|
<option value="">{{ $t('socialnetwork.vocab.courses.allNativeLanguages') }}</option>
|
||||||
|
<option value="null">{{ $t('socialnetwork.vocab.courses.forAllLanguages') }}</option>
|
||||||
|
<option v-for="lang in languages" :key="lang.id" :value="lang.id">{{ lang.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||||
@@ -42,7 +51,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="course.description" class="course-description">{{ course.description }}</p>
|
<p v-if="course.description" class="course-description">{{ course.description }}</p>
|
||||||
<div class="course-meta">
|
<div class="course-meta">
|
||||||
<span v-if="course.languageName">{{ $t('socialnetwork.vocab.courses.language') }}: {{ course.languageName }}</span>
|
<span v-if="course.languageName">{{ $t('socialnetwork.vocab.courses.targetLanguage') }}: {{ course.languageName }}</span>
|
||||||
|
<span v-if="course.nativeLanguageName">{{ $t('socialnetwork.vocab.courses.nativeLanguage') }}: {{ course.nativeLanguageName }}</span>
|
||||||
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
||||||
<span v-if="course.lessons">{{ $t('socialnetwork.vocab.courses.lessons') }}: {{ course.lessons.length }}</span>
|
<span v-if="course.lessons">{{ $t('socialnetwork.vocab.courses.lessons') }}: {{ course.lessons.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,6 +172,9 @@ export default {
|
|||||||
if (this.selectedLanguageId) {
|
if (this.selectedLanguageId) {
|
||||||
params.languageId = this.selectedLanguageId;
|
params.languageId = this.selectedLanguageId;
|
||||||
}
|
}
|
||||||
|
if (this.selectedNativeLanguageId !== '') {
|
||||||
|
params.nativeLanguageId = this.selectedNativeLanguageId === 'null' ? null : this.selectedNativeLanguageId;
|
||||||
|
}
|
||||||
if (this.searchTerm.trim()) {
|
if (this.searchTerm.trim()) {
|
||||||
params.search = this.searchTerm.trim();
|
params.search = this.searchTerm.trim();
|
||||||
}
|
}
|
||||||
@@ -230,6 +243,7 @@ export default {
|
|||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
languageId: null,
|
languageId: null,
|
||||||
|
nativeLanguageId: null,
|
||||||
difficultyLevel: 1,
|
difficultyLevel: 1,
|
||||||
isPublic: false
|
isPublic: false
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user