Implement vocab course and grammar exercise features in backend and frontend
- Added new course management functionalities in VocabController, including creating, updating, and deleting courses and lessons. - Implemented enrollment and progress tracking for courses, along with grammar exercise creation and management. - Updated database schema to include tables for courses, lessons, enrollments, and grammar exercises. - Enhanced frontend with new routes and views for course listing and details, including internationalization support for course-related texts. - Improved user experience by adding navigation to courses from the main vocab trainer view.
This commit is contained in:
@@ -319,6 +319,35 @@
|
||||
"search": "Suchen",
|
||||
"noResults": "Keine Treffer.",
|
||||
"error": "Suche fehlgeschlagen."
|
||||
},
|
||||
"courses": {
|
||||
"title": "Sprachlernkurse",
|
||||
"create": "Kurs erstellen",
|
||||
"myCourses": "Meine Kurse",
|
||||
"allCourses": "Alle Kurse",
|
||||
"none": "Keine Kurse gefunden.",
|
||||
"owner": "Besitzer",
|
||||
"enrolled": "Eingeschrieben",
|
||||
"public": "Öffentlich",
|
||||
"difficulty": "Schwierigkeit",
|
||||
"lessons": "Lektionen",
|
||||
"enroll": "Einschreiben",
|
||||
"continue": "Fortsetzen",
|
||||
"edit": "Bearbeiten",
|
||||
"addLesson": "Lektion hinzufügen",
|
||||
"completed": "Abgeschlossen",
|
||||
"score": "Punktzahl",
|
||||
"review": "Wiederholen",
|
||||
"start": "Starten",
|
||||
"noLessons": "Dieser Kurs hat noch keine Lektionen.",
|
||||
"lessonNumber": "Lektionsnummer",
|
||||
"chapter": "Kapitel",
|
||||
"selectChapter": "Kapitel auswählen",
|
||||
"selectLanguage": "Sprache auswählen",
|
||||
"confirmDelete": "Lektion wirklich löschen?",
|
||||
"titleLabel": "Titel",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"languageLabel": "Sprache"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,35 @@
|
||||
"search": "Search",
|
||||
"noResults": "No results.",
|
||||
"error": "Search failed."
|
||||
},
|
||||
"courses": {
|
||||
"title": "Language Learning Courses",
|
||||
"create": "Create Course",
|
||||
"myCourses": "My Courses",
|
||||
"allCourses": "All Courses",
|
||||
"none": "No courses found.",
|
||||
"owner": "Owner",
|
||||
"enrolled": "Enrolled",
|
||||
"public": "Public",
|
||||
"difficulty": "Difficulty",
|
||||
"lessons": "Lessons",
|
||||
"enroll": "Enroll",
|
||||
"continue": "Continue",
|
||||
"edit": "Edit",
|
||||
"addLesson": "Add Lesson",
|
||||
"completed": "Completed",
|
||||
"score": "Score",
|
||||
"review": "Review",
|
||||
"start": "Start",
|
||||
"noLessons": "This course has no lessons yet.",
|
||||
"lessonNumber": "Lesson Number",
|
||||
"chapter": "Chapter",
|
||||
"selectChapter": "Select Chapter",
|
||||
"selectLanguage": "Select Language",
|
||||
"confirmDelete": "Really delete lesson?",
|
||||
"titleLabel": "Title",
|
||||
"descriptionLabel": "Description",
|
||||
"languageLabel": "Language"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import VocabNewLanguageView from '../views/social/VocabNewLanguageView.vue';
|
||||
import VocabLanguageView from '../views/social/VocabLanguageView.vue';
|
||||
import VocabSubscribeView from '../views/social/VocabSubscribeView.vue';
|
||||
import VocabChapterView from '../views/social/VocabChapterView.vue';
|
||||
import VocabCourseListView from '../views/social/VocabCourseListView.vue';
|
||||
import VocabCourseView from '../views/social/VocabCourseView.vue';
|
||||
|
||||
const socialRoutes = [
|
||||
{
|
||||
@@ -84,6 +86,19 @@ const socialRoutes = [
|
||||
component: VocabChapterView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/vocab/courses',
|
||||
name: 'VocabCourses',
|
||||
component: VocabCourseListView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/vocab/courses/:courseId',
|
||||
name: 'VocabCourse',
|
||||
component: VocabCourseView,
|
||||
props: true,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
];
|
||||
|
||||
export default socialRoutes;
|
||||
|
||||
330
frontend/src/views/social/VocabCourseListView.vue
Normal file
330
frontend/src/views/social/VocabCourseListView.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<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>
|
||||
</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>{{ $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>
|
||||
|
||||
<!-- 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: [],
|
||||
showCreateDialog: false,
|
||||
newCourse: {
|
||||
title: '',
|
||||
description: '',
|
||||
languageId: null,
|
||||
difficultyLevel: 1,
|
||||
isPublic: false
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
methods: {
|
||||
async loadLanguages() {
|
||||
try {
|
||||
const res = await apiClient.get('/api/vocab/languages');
|
||||
this.languages = res.data?.languages || [];
|
||||
} catch (e) {
|
||||
console.error('Konnte Sprachen nicht laden:', e);
|
||||
}
|
||||
},
|
||||
async loadAllCourses() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await apiClient.get('/api/vocab/courses', { params: { includePublic: true, includeOwn: true } });
|
||||
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;
|
||||
}
|
||||
},
|
||||
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,
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
325
frontend/src/views/social/VocabCourseView.vue
Normal file
325
frontend/src/views/social/VocabCourseView.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div class="vocab-course-view">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="course">
|
||||
<h2>{{ course.title }}</h2>
|
||||
<p v-if="course.description">{{ course.description }}</p>
|
||||
|
||||
<div class="course-info">
|
||||
<span>{{ $t('socialnetwork.vocab.courses.difficulty') }}: {{ course.difficultyLevel }}</span>
|
||||
<span v-if="course.isPublic">{{ $t('socialnetwork.vocab.courses.public') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isOwner" class="owner-actions">
|
||||
<button @click="showAddLessonDialog = true">{{ $t('socialnetwork.vocab.courses.addLesson') }}</button>
|
||||
<button @click="editCourse">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list">
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.lessons') }}</h3>
|
||||
<div v-for="lesson in course.lessons" :key="lesson.id" class="lesson-item">
|
||||
<div class="lesson-header">
|
||||
<span class="lesson-number">{{ lesson.lessonNumber }}.</span>
|
||||
<h4 @click="openLesson(lesson.id)" class="lesson-title">{{ lesson.title }}</h4>
|
||||
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
|
||||
{{ $t('socialnetwork.vocab.courses.completed') }}
|
||||
</span>
|
||||
<span v-if="getLessonProgress(lesson.id)?.score" class="score">
|
||||
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="lesson.description" class="lesson-description">{{ lesson.description }}</p>
|
||||
<div class="lesson-actions">
|
||||
<button @click="openLesson(lesson.id)">
|
||||
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
|
||||
</button>
|
||||
<button v-if="isOwner" @click="editLesson(lesson.id)">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
|
||||
<button v-if="isOwner" @click="deleteLesson(lesson.id)">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Lesson Dialog -->
|
||||
<div v-if="showAddLessonDialog" class="dialog-overlay" @click="showAddLessonDialog = false">
|
||||
<div class="dialog" @click.stop>
|
||||
<h3>{{ $t('socialnetwork.vocab.courses.addLesson') }}</h3>
|
||||
<form @submit.prevent="addLesson">
|
||||
<div class="form-group">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</label>
|
||||
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.title') }}</label>
|
||||
<input v-model="newLesson.title" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.description') }}</label>
|
||||
<textarea v-model="newLesson.description"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('socialnetwork.vocab.courses.chapter') }}</label>
|
||||
<select v-model="newLesson.chapterId" required>
|
||||
<option value="">{{ $t('socialnetwork.vocab.courses.selectChapter') }}</option>
|
||||
<option v-for="chapter in chapters" :key="chapter.id" :value="chapter.id">{{ chapter.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit">{{ $t('general.create') }}</button>
|
||||
<button type="button" @click="showAddLessonDialog = 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: 'VocabCourseView',
|
||||
props: {
|
||||
courseId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
course: null,
|
||||
progress: [],
|
||||
chapters: [],
|
||||
showAddLessonDialog: false,
|
||||
newLesson: {
|
||||
lessonNumber: 1,
|
||||
title: '',
|
||||
description: '',
|
||||
chapterId: null
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
isOwner() {
|
||||
return this.course && this.course.ownerUserId === this.user?.id;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
courseId() {
|
||||
this.loadCourse();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadCourse() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/courses/${this.courseId}`);
|
||||
this.course = res.data;
|
||||
await this.loadProgress();
|
||||
if (this.course.languageId) {
|
||||
await this.loadChapters();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Konnte Kurs nicht laden:', e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadProgress() {
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/courses/${this.courseId}/progress`);
|
||||
this.progress = res.data || [];
|
||||
} catch (e) {
|
||||
// Nicht eingeschrieben? Progress ist leer
|
||||
this.progress = [];
|
||||
}
|
||||
},
|
||||
async loadChapters() {
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/languages/${this.course.languageId}/chapters`);
|
||||
this.chapters = res.data?.chapters || [];
|
||||
} catch (e) {
|
||||
console.error('Konnte Kapitel nicht laden:', e);
|
||||
}
|
||||
},
|
||||
getLessonProgress(lessonId) {
|
||||
return this.progress.find(p => p.lessonId === lessonId);
|
||||
},
|
||||
async addLesson() {
|
||||
try {
|
||||
await apiClient.post(`/api/vocab/courses/${this.courseId}/lessons`, this.newLesson);
|
||||
this.showAddLessonDialog = false;
|
||||
this.newLesson = {
|
||||
lessonNumber: 1,
|
||||
title: '',
|
||||
description: '',
|
||||
chapterId: null
|
||||
};
|
||||
await this.loadCourse();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Hinzufügen der Lektion:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Hinzufügen der Lektion');
|
||||
}
|
||||
},
|
||||
async deleteLesson(lessonId) {
|
||||
if (!confirm(this.$t('socialnetwork.vocab.courses.confirmDelete'))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
|
||||
await this.loadCourse();
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Löschen der Lektion:', e);
|
||||
alert(e.response?.data?.error || 'Fehler beim Löschen der Lektion');
|
||||
}
|
||||
},
|
||||
openLesson(lessonId) {
|
||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/lessons/${lessonId}`);
|
||||
},
|
||||
editCourse() {
|
||||
this.$router.push(`/socialnetwork/vocab/courses/${this.courseId}/edit`);
|
||||
},
|
||||
editLesson(lessonId) {
|
||||
// TODO: Implement edit lesson
|
||||
console.log('Edit lesson', lessonId);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadCourse();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vocab-course-view {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin: 15px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.owner-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.lessons-list {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.lesson-item {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.lesson-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.lesson-number {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.lesson-title {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: #0066cc;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.badge.completed {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.lesson-description {
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.lesson-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;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<div class="actions">
|
||||
<button @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
|
||||
<button @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
@@ -43,6 +44,9 @@ export default {
|
||||
goNewLanguage() {
|
||||
this.$router.push('/socialnetwork/vocab/new');
|
||||
},
|
||||
goCourses() {
|
||||
this.$router.push('/socialnetwork/vocab/courses');
|
||||
},
|
||||
openLanguage(id) {
|
||||
this.$router.push(`/socialnetwork/vocab/${id}`);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user