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

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -7,6 +7,24 @@
<button @click="showCreateDialog = true">{{ $t('socialnetwork.vocab.courses.create') }}</button>
<button @click="loadMyCourses">{{ $t('socialnetwork.vocab.courses.myCourses') }}</button>
<button @click="loadAllCourses">{{ $t('socialnetwork.vocab.courses.allCourses') }}</button>
<button @click="showShareCodeDialog = true">{{ $t('socialnetwork.vocab.courses.findByCode') }}</button>
</div>
<!-- Such- und Filter-Bereich -->
<div class="search-filter">
<div class="search-box">
<input
v-model="searchTerm"
:placeholder="$t('socialnetwork.vocab.courses.searchPlaceholder')"
@input="debouncedSearch"
/>
</div>
<div class="filter-box">
<select v-model="selectedLanguageId" @change="loadAllCourses">
<option value="">{{ $t('socialnetwork.vocab.courses.allLanguages') }}</option>
<option v-for="lang in languages" :key="lang.id" :value="lang.id">{{ lang.name }}</option>
</select>
</div>
</div>
<div v-if="loading">{{ $t('general.loading') }}</div>
@@ -24,6 +42,7 @@
</div>
<p v-if="course.description" class="course-description">{{ course.description }}</p>
<div class="course-meta">
<span v-if="course.languageName">{{ $t('socialnetwork.vocab.courses.language') }}: {{ course.languageName }}</span>
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
<span v-if="course.lessons">{{ $t('socialnetwork.vocab.courses.lessons') }}: {{ course.lessons.length }}</span>
</div>
@@ -43,6 +62,23 @@
</div>
</div>
<!-- Share Code Dialog -->
<div v-if="showShareCodeDialog" class="dialog-overlay" @click="showShareCodeDialog = false">
<div class="dialog" @click.stop>
<h3>{{ $t('socialnetwork.vocab.courses.findByCode') }}</h3>
<form @submit.prevent="findCourseByCode">
<div class="form-group">
<label>{{ $t('socialnetwork.vocab.courses.shareCode') }}</label>
<input v-model="shareCode" placeholder="z.B. abc123def456" required />
</div>
<div class="form-actions">
<button type="submit">{{ $t('general.search') }}</button>
<button type="button" @click="showShareCodeDialog = false">{{ $t('general.cancel') }}</button>
</div>
</form>
</div>
</div>
<!-- Create Course Dialog -->
<div v-if="showCreateDialog" class="dialog-overlay" @click="showCreateDialog = false">
<div class="dialog" @click.stop>
@@ -119,7 +155,17 @@ export default {
async loadAllCourses() {
this.loading = true;
try {
const res = await apiClient.get('/api/vocab/courses', { params: { includePublic: true, includeOwn: true } });
const params = {
includePublic: true,
includeOwn: true
};
if (this.selectedLanguageId) {
params.languageId = this.selectedLanguageId;
}
if (this.searchTerm.trim()) {
params.search = this.searchTerm.trim();
}
const res = await apiClient.get('/api/vocab/courses', { params });
const courses = res.data || [];
// Füge isOwner Flag hinzu
this.courses = courses.map(c => ({
@@ -132,6 +178,34 @@ export default {
this.loading = false;
}
},
debouncedSearch() {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.loadAllCourses();
}, 500);
},
async findCourseByCode() {
if (!this.shareCode.trim()) {
alert(this.$t('socialnetwork.vocab.courses.invalidCode'));
return;
}
this.loading = true;
try {
const res = await apiClient.post('/api/vocab/courses/find-by-code', { shareCode: this.shareCode });
const course = res.data;
this.showShareCodeDialog = false;
this.shareCode = '';
// Öffne den gefundenen Kurs
this.openCourse(course.id);
} catch (e) {
console.error('Fehler beim Suchen des Kurses:', e);
alert(e.response?.data?.error || this.$t('socialnetwork.vocab.courses.courseNotFound'));
} finally {
this.loading = false;
}
},
async loadMyCourses() {
this.loading = true;
try {
@@ -327,4 +401,29 @@ export default {
justify-content: flex-end;
margin-top: 20px;
}
.search-filter {
display: flex;
gap: 15px;
margin: 15px 0;
align-items: center;
}
.search-box {
flex: 1;
}
.search-box input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.filter-box select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 200px;
}
</style>

View File

@@ -8,6 +8,9 @@
<div class="course-info">
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
<span v-if="course.isPublic">{{ $t('socialnetwork.vocab.courses.public') }}</span>
<span v-if="course.shareCode && isOwner" class="share-code">
{{ $t('socialnetwork.vocab.courses.shareCode') }}: <code>{{ course.shareCode }}</code>
</span>
</div>
<div v-if="isOwner" class="owner-actions">
@@ -206,6 +209,18 @@ export default {
gap: 15px;
margin: 15px 0;
color: #666;
flex-wrap: wrap;
}
.share-code {
font-family: monospace;
}
.share-code code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
}
.owner-actions {