Files
yourpart3/frontend/src/views/social/VocabCourseListView.vue
Torsten Schulz (local) c13cb40c7b Add lesson retrieval functionality in VocabController and VocabService
- Introduced a new method in VocabService to fetch lesson details, including access control based on user ownership and lesson visibility.
- Updated VocabController to wrap the new method for user access.
- Added a new route in VocabRouter to handle requests for specific lessons.
- Enhanced VocabCourseListView to support navigation to individual lesson views, improving user experience in accessing lesson content.
2026-01-19 15:07:52 +01:00

592 lines
17 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-content">
<div class="course-info">
<div class="course-title-row">
<span class="course-title">{{ course.title }}</span>
<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>
</div>
<div class="course-meta">
<span v-if="course.languageName" class="meta-item">{{ course.languageName }}</span>
<span v-if="course.nativeLanguageName" class="meta-item">{{ course.nativeLanguageName }}</span>
<span class="meta-item">{{ $t('socialnetwork.vocab.courses.difficulty') }} {{ course.difficultyLevel }}</span>
<span v-if="course.lessons" class="meta-item">{{ course.lessons.length }} {{ $t('socialnetwork.vocab.courses.lessons') }}</span>
</div>
</div>
<div class="course-actions">
<button v-if="!course.enrolledAt && (course.isPublic || course.isOwner)" @click="enroll(course.id)" class="btn-enroll">
{{ $t('socialnetwork.vocab.courses.enroll') }}
</button>
<button v-if="course.enrolledAt" @click="openCourse(course.id)" class="btn-continue">
{{ $t('socialnetwork.vocab.courses.continue') }}
</button>
<button v-if="course.isOwner" @click="editCourse(course.id)" class="btn-edit">
{{ $t('socialnetwork.vocab.courses.edit') }}
</button>
</div>
</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 {
// Verwende /languages/all für die Kursliste, um alle Sprachen anzuzeigen
const res = await apiClient.get('/api/vocab/languages/all');
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;
// Setze die eigene Muttersprache als Standardauswahl
if (!this.selectedNativeLanguageId) {
this.selectedNativeLanguageId = 'my';
}
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 checkIfHasCourses() {
// Prüfe, ob der Benutzer bereits Kurse hat
try {
const res = await apiClient.get('/api/vocab/courses/my');
const courses = res.data || [];
return courses.length > 0;
} catch (e) {
return 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`);
// Nach dem Einschreiben sofort zum Kurs navigieren
this.openCourse(courseId);
} 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();
// Wenn der Benutzer bereits Kurse hat, zeige "Meine Kurse" als Standard
const hasCourses = await this.checkIfHasCourses();
if (hasCourses) {
await this.loadMyCourses();
} else {
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: 8px;
margin-top: 15px;
}
.course-item {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
transition: box-shadow 0.2s;
}
.course-item:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.course-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 15px;
gap: 15px;
}
.course-info {
flex: 1;
min-width: 0;
}
.course-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.course-title {
font-weight: 600;
font-size: 1em;
color: #333;
margin: 0;
}
.badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 0.75em;
white-space: nowrap;
}
.badge.owner {
background: #4CAF50;
color: white;
}
.badge.enrolled {
background: #2196F3;
color: white;
}
.badge.public {
background: #FF9800;
color: white;
}
.course-meta {
display: flex;
gap: 12px;
font-size: 0.85em;
color: #666;
flex-wrap: wrap;
}
.meta-item {
white-space: nowrap;
}
.course-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.course-actions button {
padding: 6px 14px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 0.9em;
white-space: nowrap;
transition: background-color 0.2s, border-color 0.2s;
}
.course-actions button:hover {
background: #f5f5f5;
border-color: #bbb;
}
.btn-enroll {
background: #4CAF50 !important;
color: white !important;
border-color: #4CAF50 !important;
}
.btn-enroll:hover {
background: #45a049 !important;
border-color: #45a049 !important;
}
.btn-continue {
background: #2196F3 !important;
color: white !important;
border-color: #2196F3 !important;
}
.btn-continue:hover {
background: #0b7dda !important;
border-color: #0b7dda !important;
}
.btn-edit {
background: #FF9800 !important;
color: white !important;
border-color: #FF9800 !important;
}
.btn-edit:hover {
background: #e68900 !important;
border-color: #e68900 !important;
}
.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>