Files
yourpart3/frontend/src/views/social/VocabCourseView.vue
Torsten Schulz (local) 175a61c81c Enhance VocabService and VocabCourseView for improved multiple choice handling and table layout
- Updated VocabService to support multiple correct answers in multiple choice exercises, allowing for better answer validation and user feedback.
- Enhanced the extraction of correct answers and alternatives to accommodate both single and multiple correct indices.
- Improved CSS styles in VocabCourseView for better table layout, including adjustments for overflow handling and vertical alignment, enhancing overall user experience.
2026-01-20 14:46:07 +01:00

492 lines
12 KiB
Vue

<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>
<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">
<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>
<table class="lessons-table">
<thead>
<tr>
<th class="col-number">{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</th>
<th class="col-title">{{ $t('socialnetwork.vocab.courses.title') }}</th>
<th class="col-status">Status</th>
<th class="col-actions">Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="lesson in course.lessons" :key="lesson.id" class="lesson-row">
<td class="lesson-number">{{ lesson.lessonNumber }}</td>
<td class="lesson-title">
<span class="title-label">{{ lesson.title }}</span>
<span v-if="lesson.description" class="lesson-description">{{ lesson.description }}</span>
</td>
<td class="lesson-status">
<span v-if="getLessonProgress(lesson.id)?.completed" class="badge completed">
{{ $t('socialnetwork.vocab.courses.completed') }}
</span>
<span v-else-if="getLessonProgress(lesson.id)?.score" class="score">
{{ $t('socialnetwork.vocab.courses.score') }}: {{ getLessonProgress(lesson.id).score }}%
</span>
<span v-else class="status-new">
{{ $t('socialnetwork.vocab.courses.notStarted') }}
</span>
</td>
<td class="lesson-actions">
<button @click="openLesson(lesson.id)" class="btn-start">
{{ getLessonProgress(lesson.id)?.completed ? $t('socialnetwork.vocab.courses.review') : $t('socialnetwork.vocab.courses.start') }}
</button>
<button v-if="isOwner" @click="editLesson(lesson.id)" class="btn-edit">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
<button v-if="isOwner" @click="deleteLesson(lesson.id)" class="btn-delete">{{ $t('general.delete') }}</button>
</td>
</tr>
</tbody>
</table>
</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;
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 {
display: flex;
gap: 10px;
margin: 15px 0;
}
.lessons-list {
margin-top: 30px;
}
.lessons-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
.lessons-table th,
.lessons-table td {
box-sizing: border-box;
}
.lessons-table thead {
background: #f8f9fa;
}
.lessons-table th {
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #333;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.lessons-table th.col-number,
.lessons-table td.lesson-number {
width: 80px;
min-width: 80px;
max-width: 80px;
overflow: visible;
}
.lessons-table th.col-title,
.lessons-table td.lesson-title {
width: auto;
min-width: 200px;
overflow: visible;
}
.lessons-table th.col-status,
.lessons-table td.lesson-status {
width: 200px;
min-width: 200px;
max-width: 200px;
overflow: visible;
word-wrap: break-word;
}
.lessons-table th.col-actions,
.lessons-table td.lesson-actions {
width: 250px;
min-width: 250px;
max-width: 250px;
overflow: visible;
}
.lessons-table tbody tr {
transition: background-color 0.2s ease;
}
.lessons-table tbody tr:hover {
background-color: #f8f9fa;
}
.lessons-table td {
padding: 15px;
vertical-align: top;
}
.lesson-number {
font-weight: 600;
color: #666;
font-size: 0.95em;
vertical-align: top;
padding-top: 15px;
}
.lesson-title {
display: flex;
flex-direction: column;
gap: 5px;
}
.title-label {
font-weight: 500;
color: #333;
font-size: 1em;
}
.lesson-description {
color: #666;
font-size: 0.85em;
line-height: 1.4;
}
.lesson-status {
display: flex;
flex-direction: column;
gap: 5px;
align-items: flex-start;
justify-content: flex-start;
white-space: nowrap;
overflow: visible;
}
.badge.completed {
background: #4CAF50;
color: white;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
white-space: nowrap;
}
.score {
color: #666;
font-size: 0.85em;
}
.status-new {
color: #999;
font-size: 0.85em;
font-style: italic;
}
.lesson-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
}
.btn-start {
padding: 8px 16px;
background: #F9A22C;
color: #000000;
border: 1px solid #F9A22C;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
transition: background 0.05s;
}
.btn-start:hover {
background: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
}
.btn-edit {
padding: 6px 12px;
background: #F9A22C;
color: #000000;
border: 1px solid #F9A22C;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
transition: background 0.05s;
}
.btn-edit:hover {
background: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
}
.btn-delete {
padding: 6px 12px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
transition: background-color 0.2s ease;
}
.btn-delete:hover {
background: #c82333;
}
.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>