Refactor backend CORS settings to include default origins and improve error handling in chat services: Introduce dynamic CORS origin handling, enhance RabbitMQ message sending with fallback mechanisms, and update WebSocket service to manage pending messages. Update UI components for better accessibility and responsiveness, including adjustments to dialog and navigation elements. Enhance styling for improved user experience across various components.

This commit is contained in:
Torsten Schulz (local)
2026-03-19 14:44:04 +01:00
parent 4442937ebd
commit 9d44a265ca
67 changed files with 5426 additions and 1099 deletions

View File

@@ -1,11 +1,16 @@
<template>
<div class="vocab-course-view">
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-if="loading" class="surface-card course-state">{{ $t('general.loading') }}</div>
<div v-else-if="course">
<h2>{{ course.title }}</h2>
<p v-if="course.description">{{ course.description }}</p>
<section class="course-hero surface-card">
<div>
<span class="course-kicker">Lernkurs</span>
<h2>{{ course.title }}</h2>
<p v-if="course.description">{{ course.description }}</p>
</div>
</section>
<div class="course-info">
<div class="course-info surface-card">
<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">
@@ -18,7 +23,7 @@
<button @click="editCourse">{{ $t('socialnetwork.vocab.courses.edit') }}</button>
</div>
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list">
<div v-if="course.lessons && course.lessons.length > 0" class="lessons-list surface-card">
<div class="current-lesson-section" v-if="currentLesson">
<button @click="openLesson(currentLesson.id)" class="btn-current-lesson">
{{ $t('socialnetwork.vocab.courses.continueCurrentLesson') }}
@@ -75,7 +80,7 @@
</table>
</div>
<div v-else>
<p>{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
<p class="surface-card course-state">{{ $t('socialnetwork.vocab.courses.noLessons') }}</p>
</div>
</div>
@@ -84,28 +89,29 @@
<div class="dialog" @click.stop>
<h3>{{ $t('socialnetwork.vocab.courses.addLesson') }}</h3>
<form @submit.prevent="addLesson">
<div class="form-group">
<div class="form-group form-field">
<label>{{ $t('socialnetwork.vocab.courses.lessonNumber') }}</label>
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required />
<input type="number" v-model.number="newLesson.lessonNumber" min="1" required :class="{ 'field-error': lessonFormTouched && !isLessonNumberValid }" />
</div>
<div class="form-group">
<div class="form-group form-field">
<label>{{ $t('socialnetwork.vocab.courses.title') }}</label>
<input v-model="newLesson.title" required />
<input v-model="newLesson.title" required :class="{ 'field-error': lessonFormTouched && !isLessonTitleValid }" />
</div>
<div class="form-group">
<div class="form-group form-field">
<label>{{ $t('socialnetwork.vocab.courses.description') }}</label>
<textarea v-model="newLesson.description"></textarea>
</div>
<div class="form-group">
<div class="form-group form-field">
<label>{{ $t('socialnetwork.vocab.courses.chapter') }}</label>
<select v-model="newLesson.chapterId" required>
<select v-model="newLesson.chapterId" required :class="{ 'field-error': lessonFormTouched && !isLessonChapterValid }">
<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>
<span v-if="lessonFormTouched && !canCreateLesson" class="form-error">Bitte Nummer, Titel und Kapitel vollstaendig angeben.</span>
<div class="form-actions form-actions-row">
<button type="submit" :disabled="!canCreateLesson">{{ $t('general.create') }}</button>
<button type="button" @click="showAddLessonDialog = false" class="button-secondary">{{ $t('general.cancel') }}</button>
</div>
</form>
</div>
@@ -116,6 +122,7 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
import { showApiError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'VocabCourseView',
@@ -132,6 +139,7 @@ export default {
progress: [],
chapters: [],
showAddLessonDialog: false,
lessonFormTouched: false,
newLesson: {
lessonNumber: 1,
title: '',
@@ -163,6 +171,18 @@ export default {
// Alle Lektionen abgeschlossen - zeige die letzte Lektion
return sortedLessons[sortedLessons.length - 1];
},
isLessonNumberValid() {
return Number(this.newLesson.lessonNumber) > 0;
},
isLessonTitleValid() {
return this.newLesson.title.trim().length >= 3;
},
isLessonChapterValid() {
return Boolean(this.newLesson.chapterId);
},
canCreateLesson() {
return this.isLessonNumberValid && this.isLessonTitleValid && this.isLessonChapterValid;
}
},
watch: {
@@ -232,9 +252,14 @@ export default {
return false;
},
async addLesson() {
this.lessonFormTouched = true;
if (!this.canCreateLesson) {
return;
}
try {
await apiClient.post(`/api/vocab/courses/${this.courseId}/lessons`, this.newLesson);
this.showAddLessonDialog = false;
this.lessonFormTouched = false;
this.newLesson = {
lessonNumber: 1,
title: '',
@@ -242,9 +267,10 @@ export default {
chapterId: null
};
await this.loadCourse();
showSuccess(this, 'Lektion erfolgreich angelegt.');
} catch (e) {
console.error('Fehler beim Hinzufügen der Lektion:', e);
alert(e.response?.data?.error || 'Fehler beim Hinzufügen der Lektion');
showApiError(this, e, 'Fehler beim Hinzufuegen der Lektion');
}
},
async deleteLesson(lessonId) {
@@ -254,9 +280,10 @@ export default {
try {
await apiClient.delete(`/api/vocab/lessons/${lessonId}`);
await this.loadCourse();
showSuccess(this, 'Lektion erfolgreich geloescht.');
} catch (e) {
console.error('Fehler beim Löschen der Lektion:', e);
alert(e.response?.data?.error || 'Fehler beim Löschen der Lektion');
showApiError(this, e, 'Fehler beim Loeschen der Lektion');
}
},
openLesson(lessonId) {
@@ -278,15 +305,47 @@ export default {
<style scoped>
.vocab-course-view {
padding: 20px;
max-width: var(--content-max-width);
margin: 0 auto;
padding: 0 0 24px;
}
.course-hero,
.course-info,
.lessons-list,
.course-state {
margin-bottom: 16px;
}
.course-hero {
padding: 24px 26px;
}
.course-kicker {
display: inline-block;
margin-bottom: 10px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.14);
color: #8a5411;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.course-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.course-info {
display: flex;
gap: 15px;
margin: 15px 0;
margin: 0 0 16px;
color: #666;
flex-wrap: wrap;
padding: 16px 18px;
}
.share-code {
@@ -307,7 +366,14 @@ export default {
}
.lessons-list {
margin-top: 30px;
margin-top: 0;
padding: 20px;
}
.course-state {
padding: 18px;
text-align: center;
color: var(--color-text-secondary);
}
.current-lesson-section {