diff --git a/backend/controllers/vocabController.js b/backend/controllers/vocabController.js index cefa05a..6b2c666 100644 --- a/backend/controllers/vocabController.js +++ b/backend/controllers/vocabController.js @@ -26,6 +26,7 @@ class VocabController { this.createCourse = this._wrapWithUser((userId, req) => this.service.createCourse(userId, req.body), { successStatus: 201 }); this.getCourses = this._wrapWithUser((userId, req) => this.service.getCourses(userId, req.query)); this.getCourse = this._wrapWithUser((userId, req) => this.service.getCourse(userId, req.params.courseId)); + this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode)); this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body)); this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId)); diff --git a/backend/routers/vocabRouter.js b/backend/routers/vocabRouter.js index fdfb290..ac4b4e6 100644 --- a/backend/routers/vocabRouter.js +++ b/backend/routers/vocabRouter.js @@ -26,6 +26,7 @@ router.post('/chapters/:chapterId/vocabs', vocabController.addVocabToChapter); router.post('/courses', vocabController.createCourse); router.get('/courses', vocabController.getCourses); router.get('/courses/my', vocabController.getMyCourses); +router.post('/courses/find-by-code', vocabController.getCourseByShareCode); router.get('/courses/:courseId', vocabController.getCourse); router.put('/courses/:courseId', vocabController.updateCourse); router.delete('/courses/:courseId', vocabController.deleteCourse); diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index 2c5f7a7..1f773ec 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -559,27 +559,101 @@ export default class VocabService { return course.get({ plain: true }); } - async getCourses(hashedUserId, { includePublic = true, includeOwn = true } = {}) { + async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, search } = {}) { const user = await this._getUserByHashedId(hashedUserId); const where = {}; + const andConditions = []; + + // Zugriffsbedingungen if (includeOwn && includePublic) { - where[Op.or] = [ - { ownerUserId: user.id }, - { isPublic: true } - ]; + andConditions.push({ + [Op.or]: [ + { ownerUserId: user.id }, + { isPublic: true } + ] + }); } else if (includeOwn) { where.ownerUserId = user.id; } else if (includePublic) { where.isPublic = true; } + // Filter nach Sprache + if (languageId) { + where.languageId = Number(languageId); + } + + // Suche nach Titel oder Beschreibung + if (search && search.trim()) { + const searchTerm = `%${search.trim()}%`; + andConditions.push({ + [Op.or]: [ + { title: { [Op.iLike]: searchTerm } }, + { description: { [Op.iLike]: searchTerm } } + ] + }); + } + + // Kombiniere alle AND-Bedingungen + if (andConditions.length > 0) { + where[Op.and] = andConditions; + } + const courses = await VocabCourse.findAll({ where, order: [['createdAt', 'DESC']] }); - return courses.map(c => c.get({ plain: true })); + const coursesData = courses.map(c => c.get({ plain: true })); + + // Lade Sprachnamen für alle Kurse + const languageIds = [...new Set(coursesData.map(c => c.languageId))]; + if (languageIds.length > 0) { + const [languages] = await sequelize.query( + `SELECT id, name FROM community.vocab_language WHERE id IN (:languageIds)`, + { + replacements: { languageIds }, + type: sequelize.QueryTypes.SELECT + } + ); + const languageMap = new Map(languages.map(l => [l.id, l.name])); + coursesData.forEach(c => { + c.languageName = languageMap.get(c.languageId) || null; + }); + } + + return coursesData; + } + + async getCourseByShareCode(hashedUserId, shareCode) { + const user = await this._getUserByHashedId(hashedUserId); + const code = typeof shareCode === 'string' ? shareCode.trim() : ''; + + if (!code || code.length < 6 || code.length > 128) { + const err = new Error('Invalid share code'); + err.status = 400; + throw err; + } + + const course = await VocabCourse.findOne({ + where: { shareCode: code } + }); + + if (!course) { + const err = new Error('Course not found'); + err.status = 404; + throw err; + } + + // Prüfe Zugriff (öffentlich oder Besitzer) + if (course.ownerUserId !== user.id && !course.isPublic) { + const err = new Error('Course is not public'); + err.status = 403; + throw err; + } + + return course.get({ plain: true }); } async getCourse(hashedUserId, courseId) { diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index 30016eb..4f77f7f 100644 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -347,7 +347,13 @@ "confirmDelete": "Lektion wirklich löschen?", "titleLabel": "Titel", "descriptionLabel": "Beschreibung", - "languageLabel": "Sprache" + "languageLabel": "Sprache", + "findByCode": "Kurs per Code finden", + "shareCode": "Share-Code", + "searchPlaceholder": "Kurs suchen...", + "allLanguages": "Alle Sprachen", + "invalidCode": "Ungültiger Code", + "courseNotFound": "Kurs nicht gefunden" } } } diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 5275922..228086c 100644 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -347,7 +347,13 @@ "confirmDelete": "Really delete lesson?", "titleLabel": "Title", "descriptionLabel": "Description", - "languageLabel": "Language" + "languageLabel": "Language", + "findByCode": "Find Course by Code", + "shareCode": "Share Code", + "searchPlaceholder": "Search courses...", + "allLanguages": "All Languages", + "invalidCode": "Invalid code", + "courseNotFound": "Course not found" } } } diff --git a/frontend/src/views/social/VocabCourseListView.vue b/frontend/src/views/social/VocabCourseListView.vue index 1b22cde..adc167d 100644 --- a/frontend/src/views/social/VocabCourseListView.vue +++ b/frontend/src/views/social/VocabCourseListView.vue @@ -7,6 +7,24 @@ + + + + +
{{ course.description }}
@@ -43,6 +62,23 @@ + + +