Add course retrieval by share code feature and enhance course search functionality

- Implemented a new endpoint in VocabController to retrieve courses using a share code.
- Updated VocabService to include logic for validating share codes and checking course access permissions.
- Enhanced course listing functionality with search and language filtering options in the frontend.
- Added a dialog for users to input share codes and search for courses, improving user experience.
- Updated internationalization files to include new strings for share code functionality and search features.
This commit is contained in:
Torsten Schulz (local)
2026-01-19 11:33:20 +01:00
parent e1b3dfb00a
commit 714e144329
7 changed files with 211 additions and 9 deletions

View File

@@ -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) {