- Renamed loadMyNativeLanguage to loadMyNativeLanguageId for better context. - Enhanced error handling to log warnings when the languages list is empty or when the native language is not found. - Improved debug logging to provide clearer insights into the native language loading process.
503 lines
15 KiB
Vue
503 lines
15 KiB
Vue
<template>
|
|
<div class="vocab-course-list">
|
|
<h2>{{ $t('socialnetwork.vocab.courses.title') }}</h2>
|
|
|
|
<div class="box">
|
|
<div class="actions">
|
|
<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">
|
|
<label>{{ $t('socialnetwork.vocab.courses.targetLanguage') }}:</label>
|
|
<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 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 v-if="myNativeLanguageId" value="my">{{ $t('socialnetwork.vocab.courses.myNativeLanguage') }}</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 v-if="loading">{{ $t('general.loading') }}</div>
|
|
<div v-else>
|
|
<div v-if="courses.length === 0">
|
|
{{ $t('socialnetwork.vocab.courses.none') }}
|
|
</div>
|
|
<div v-else class="course-list">
|
|
<div v-for="course in courses" :key="course.id" class="course-item">
|
|
<div class="course-header">
|
|
<h3 @click="openCourse(course.id)" class="course-title">{{ course.title }}</h3>
|
|
<span v-if="course.isOwner" class="badge owner">{{ $t('socialnetwork.vocab.courses.owner') }}</span>
|
|
<span v-else-if="course.enrolledAt" class="badge enrolled">{{ $t('socialnetwork.vocab.courses.enrolled') }}</span>
|
|
<span v-if="course.isPublic" class="badge public">{{ $t('socialnetwork.vocab.courses.public') }}</span>
|
|
</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.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 v-if="course.lessons">{{ $t('socialnetwork.vocab.courses.lessons') }}: {{ course.lessons.length }}</span>
|
|
</div>
|
|
<div class="course-actions">
|
|
<button v-if="!course.enrolledAt && (course.isPublic || course.isOwner)" @click="enroll(course.id)">
|
|
{{ $t('socialnetwork.vocab.courses.enroll') }}
|
|
</button>
|
|
<button v-if="course.enrolledAt" @click="openCourse(course.id)">
|
|
{{ $t('socialnetwork.vocab.courses.continue') }}
|
|
</button>
|
|
<button v-if="course.isOwner" @click="editCourse(course.id)">
|
|
{{ $t('socialnetwork.vocab.courses.edit') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
<h3>{{ $t('socialnetwork.vocab.courses.create') }}</h3>
|
|
<form @submit.prevent="createCourse">
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.title') }}</label>
|
|
<input v-model="newCourse.title" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.description') }}</label>
|
|
<textarea v-model="newCourse.description"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.language') }}</label>
|
|
<select v-model="newCourse.languageId" required>
|
|
<option value="">{{ $t('socialnetwork.vocab.courses.selectLanguage') }}</option>
|
|
<option v-for="lang in languages" :key="lang.id" :value="lang.id">{{ lang.name }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ $t('socialnetwork.vocab.courses.difficulty') }}</label>
|
|
<input type="number" v-model.number="newCourse.difficultyLevel" min="1" max="10" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" v-model="newCourse.isPublic" />
|
|
{{ $t('socialnetwork.vocab.courses.public') }}
|
|
</label>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit">{{ $t('general.create') }}</button>
|
|
<button type="button" @click="showCreateDialog = false">{{ $t('general.cancel') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import apiClient from '@/utils/axios.js';
|
|
|
|
export default {
|
|
name: 'VocabCourseListView',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
courses: [],
|
|
languages: [],
|
|
myNativeLanguageId: null,
|
|
showCreateDialog: false,
|
|
showShareCodeDialog: false,
|
|
searchTerm: '',
|
|
selectedLanguageId: '',
|
|
selectedNativeLanguageId: '',
|
|
shareCode: '',
|
|
searchTimeout: null,
|
|
newCourse: {
|
|
title: '',
|
|
description: '',
|
|
languageId: null,
|
|
nativeLanguageId: null,
|
|
difficultyLevel: 1,
|
|
isPublic: false
|
|
}
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters(['user', 'language']),
|
|
},
|
|
methods: {
|
|
async loadLanguages() {
|
|
try {
|
|
const res = await apiClient.get('/api/vocab/languages');
|
|
this.languages = res.data?.languages || [];
|
|
|
|
// Lade die Muttersprache des Benutzers
|
|
await this.loadMyNativeLanguageId();
|
|
} catch (e) {
|
|
console.error('Konnte Sprachen nicht laden:', e);
|
|
}
|
|
},
|
|
async loadMyNativeLanguageId() {
|
|
try {
|
|
// Mappe UI-Sprache zu vocab_language Name
|
|
const languageMap = {
|
|
'de': 'Deutsch',
|
|
'en': 'Englisch',
|
|
'es': 'Spanisch',
|
|
'fr': 'Französisch',
|
|
'it': 'Italienisch',
|
|
'pt': 'Portugiesisch'
|
|
};
|
|
|
|
const uiLanguage = this.language || 'de';
|
|
const nativeLanguageName = languageMap[uiLanguage] || 'Deutsch';
|
|
|
|
// Finde die entsprechende vocab_language ID
|
|
if (this.languages && this.languages.length > 0) {
|
|
const nativeLang = this.languages.find(lang => lang.name === nativeLanguageName);
|
|
if (nativeLang) {
|
|
this.myNativeLanguageId = nativeLang.id;
|
|
console.log(`[loadMyNativeLanguageId] Gefunden: ${nativeLanguageName} (ID: ${nativeLang.id})`);
|
|
} else {
|
|
console.warn(`[loadMyNativeLanguageId] Sprache "${nativeLanguageName}" nicht in languages-Liste gefunden. Verfügbare Sprachen:`, this.languages.map(l => l.name).join(', '));
|
|
}
|
|
} else {
|
|
console.warn(`[loadMyNativeLanguageId] languages-Liste ist leer.`);
|
|
}
|
|
} catch (e) {
|
|
console.error('Konnte Muttersprache nicht laden:', e);
|
|
}
|
|
},
|
|
async loadAllCourses() {
|
|
this.loading = true;
|
|
try {
|
|
const params = {
|
|
includePublic: true,
|
|
includeOwn: true
|
|
};
|
|
if (this.selectedLanguageId) {
|
|
params.languageId = this.selectedLanguageId;
|
|
}
|
|
// Nur nativeLanguageId senden, wenn explizit eine Sprache ausgewählt wurde
|
|
// Leer bedeutet: zeige alle Kurse
|
|
// "null" bedeutet: zeige nur Kurse ohne Muttersprache
|
|
// "my" bedeutet: verwende die Muttersprache des Benutzers
|
|
// Eine ID bedeutet: zeige nur Kurse für diese Muttersprache
|
|
if (this.selectedNativeLanguageId !== '') {
|
|
if (this.selectedNativeLanguageId === 'null') {
|
|
// Explizit Kurse ohne Muttersprache anfordern
|
|
params.nativeLanguageId = null;
|
|
} else if (this.selectedNativeLanguageId === 'my') {
|
|
// Verwende die Muttersprache des Benutzers
|
|
params.nativeLanguageId = this.myNativeLanguageId;
|
|
} else {
|
|
// Spezifische Muttersprache
|
|
params.nativeLanguageId = this.selectedNativeLanguageId;
|
|
}
|
|
}
|
|
// Wenn selectedNativeLanguageId leer ist, wird nativeLanguageId nicht gesetzt
|
|
// und das Backend zeigt alle Kurse an
|
|
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 => ({
|
|
...c,
|
|
isOwner: c.ownerUserId === this.user?.id
|
|
}));
|
|
} catch (e) {
|
|
console.error('Konnte Kurse nicht laden:', e);
|
|
} finally {
|
|
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 {
|
|
const res = await apiClient.get('/api/vocab/courses/my');
|
|
const courses = res.data || [];
|
|
// Füge isOwner Flag hinzu
|
|
this.courses = courses.map(c => ({
|
|
...c,
|
|
isOwner: c.ownerUserId === this.user?.id
|
|
}));
|
|
} catch (e) {
|
|
console.error('Konnte meine Kurse nicht laden:', e);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async createCourse() {
|
|
try {
|
|
await apiClient.post('/api/vocab/courses', this.newCourse);
|
|
this.showCreateDialog = false;
|
|
this.newCourse = {
|
|
title: '',
|
|
description: '',
|
|
languageId: null,
|
|
nativeLanguageId: null,
|
|
difficultyLevel: 1,
|
|
isPublic: false
|
|
};
|
|
await this.loadAllCourses();
|
|
} catch (e) {
|
|
console.error('Fehler beim Erstellen des Kurses:', e);
|
|
alert(e.response?.data?.error || 'Fehler beim Erstellen des Kurses');
|
|
}
|
|
},
|
|
async enroll(courseId) {
|
|
try {
|
|
await apiClient.post(`/api/vocab/courses/${courseId}/enroll`);
|
|
await this.loadAllCourses();
|
|
} catch (e) {
|
|
console.error('Fehler beim Einschreiben:', e);
|
|
alert(e.response?.data?.error || 'Fehler beim Einschreiben');
|
|
}
|
|
},
|
|
openCourse(courseId) {
|
|
this.$router.push(`/socialnetwork/vocab/courses/${courseId}`);
|
|
},
|
|
editCourse(courseId) {
|
|
this.$router.push(`/socialnetwork/vocab/courses/${courseId}/edit`);
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.loadLanguages();
|
|
await this.loadAllCourses();
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.vocab-course-list {
|
|
padding: 20px;
|
|
}
|
|
|
|
.box {
|
|
background: #f6f6f6;
|
|
padding: 12px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.actions {
|
|
margin: 10px 0;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.course-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.course-item {
|
|
background: white;
|
|
padding: 15px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.course-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.course-title {
|
|
cursor: pointer;
|
|
margin: 0;
|
|
flex: 1;
|
|
color: #0066cc;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.badge {
|
|
padding: 4px 8px;
|
|
border-radius: 3px;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.badge.owner {
|
|
background: #4CAF50;
|
|
color: white;
|
|
}
|
|
|
|
.badge.enrolled {
|
|
background: #2196F3;
|
|
color: white;
|
|
}
|
|
|
|
.badge.public {
|
|
background: #FF9800;
|
|
color: white;
|
|
}
|
|
|
|
.course-description {
|
|
color: #666;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.course-meta {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin: 10px 0;
|
|
font-size: 0.9em;
|
|
color: #888;
|
|
}
|
|
|
|
.course-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.dialog {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.form-group {
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.form-group textarea {
|
|
min-height: 80px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
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>
|