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,27 +1,101 @@
<template>
<h2>{{ $t('socialnetwork.vocab.title') }}</h2>
<div class="vocab-view">
<section class="vocab-hero surface-card">
<div>
<span class="vocab-kicker">Sprachenlernen</span>
<h2>{{ $t('socialnetwork.vocab.title') }}</h2>
<p>{{ $t('socialnetwork.vocab.description') }}</p>
</div>
<div class="actions">
<button @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
<button @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
</div>
</section>
<div class="box">
<p>{{ $t('socialnetwork.vocab.description') }}</p>
<section class="vocab-summary-grid">
<article class="summary-card surface-card">
<span class="summary-card__label">Sprachen gesamt</span>
<strong>{{ languages.length }}</strong>
<p>Alle aktiven Sprachbereiche, in denen du Inhalte nutzt oder verwaltest.</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Eigene Bereiche</span>
<strong>{{ ownedLanguages.length }}</strong>
<p>Hier legst du Inhalte, Kapitel und Lernmaterial aktiv selbst an.</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Abonniert</span>
<strong>{{ subscribedLanguages.length }}</strong>
<p>Diese Bereiche sind eher fuer Lernen und Fortschritt statt Verwaltung gedacht.</p>
</article>
</section>
<div class="actions">
<button @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
<button @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
</div>
<section class="vocab-task-grid">
<article class="task-card surface-card">
<span class="task-card__eyebrow">Schnellstart</span>
<h3>Neue Sprache anlegen</h3>
<p>Der beste Einstieg, wenn du Inhalte selbst strukturieren und pflegen willst.</p>
<button type="button" @click="goNewLanguage">{{ $t('socialnetwork.vocab.newLanguage') }}</button>
</article>
<article class="task-card surface-card">
<span class="task-card__eyebrow">Weiterlernen</span>
<h3>Kurse und Kapitel oeffnen</h3>
<p>Springe direkt in bestehende Lernpfade und arbeite mit vorhandenen Kursen weiter.</p>
<button type="button" class="button-secondary" @click="goCourses">{{ $t('socialnetwork.vocab.courses.title') }}</button>
</article>
</section>
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-else>
<div v-if="languages.length === 0">
<section class="vocab-box surface-card">
<div v-if="loading" class="vocab-state">{{ $t('general.loading') }}</div>
<div v-else-if="languages.length === 0" class="vocab-state">
{{ $t('socialnetwork.vocab.none') }}
</div>
<ul v-else>
<li v-for="l in languages" :key="l.id">
<span class="langname" @click="openLanguage(l.id)">{{ l.name }}</span>
<span class="role" v-if="l.isOwner">({{ $t('socialnetwork.vocab.owner') }})</span>
<span class="role" v-else>({{ $t('socialnetwork.vocab.subscribed') }})</span>
</li>
</ul>
</div>
<div v-else class="language-sections">
<section class="language-section">
<div class="language-section__header">
<div>
<h3>Eigene Sprachen</h3>
<p>Direkter Einstieg in Bearbeitung, Kapitel und Kursverwaltung.</p>
</div>
<span class="language-section__count">{{ ownedLanguages.length }}</span>
</div>
<ul v-if="ownedLanguages.length" class="language-list">
<li v-for="l in ownedLanguages" :key="l.id" class="language-card">
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
<div class="language-card__info">
<span class="langname">{{ l.name }}</span>
<span class="language-card__hint">Verwalten und Inhalte pflegen</span>
</div>
<span class="role">{{ $t('socialnetwork.vocab.owner') }}</span>
</button>
</li>
</ul>
<p v-else class="language-empty">Noch keine eigenen Sprachbereiche vorhanden.</p>
</section>
<section class="language-section">
<div class="language-section__header">
<div>
<h3>Abonnierte Sprachen</h3>
<p>Gut fuer schnellen Wiedereinstieg ins Lernen ohne Verwaltungsaufwand.</p>
</div>
<span class="language-section__count">{{ subscribedLanguages.length }}</span>
</div>
<ul v-if="subscribedLanguages.length" class="language-list">
<li v-for="l in subscribedLanguages" :key="l.id" class="language-card">
<button type="button" class="language-card__main" @click="openLanguage(l.id)">
<div class="language-card__info">
<span class="langname">{{ l.name }}</span>
<span class="language-card__hint">Lernen, ueben und Fortschritt ansehen</span>
</div>
<span class="role">{{ $t('socialnetwork.vocab.subscribed') }}</span>
</button>
</li>
</ul>
<p v-else class="language-empty">Keine abonnierten Sprachen vorhanden.</p>
</section>
</div>
</section>
</div>
</template>
@@ -39,6 +113,12 @@ export default {
},
computed: {
...mapGetters(['user']),
ownedLanguages() {
return this.languages.filter((language) => language.isOwner);
},
subscribedLanguages() {
return this.languages.filter((language) => !language.isOwner);
},
},
methods: {
goNewLanguage() {
@@ -69,22 +149,197 @@ export default {
</script>
<style scoped>
.box {
background: #f6f6f6;
padding: 12px;
border: 1px solid #ccc;
.vocab-view {
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.vocab-hero {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 18px;
padding: 24px 26px;
margin-bottom: 16px;
}
.vocab-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;
}
.vocab-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.vocab-box {
padding: 20px;
}
.vocab-summary-grid,
.vocab-task-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 16px;
}
.vocab-task-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.summary-card,
.task-card {
padding: 18px;
}
.summary-card strong {
display: block;
margin: 6px 0 10px;
font-size: 1.8rem;
line-height: 1;
}
.summary-card p,
.task-card p {
margin: 0;
color: var(--color-text-secondary);
}
.summary-card__label,
.task-card__eyebrow {
display: inline-flex;
margin-bottom: 4px;
color: var(--color-text-muted);
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.task-card h3 {
margin: 0 0 8px;
}
.task-card button {
margin-top: 14px;
}
.actions {
margin: 10px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.vocab-state {
text-align: center;
color: var(--color-text-secondary);
padding: 18px;
}
.language-sections {
display: grid;
gap: 20px;
}
.language-section {
display: grid;
gap: 14px;
}
.language-section__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.language-section__header h3 {
margin: 0 0 4px;
}
.language-section__header p,
.language-empty {
margin: 0;
color: var(--color-text-secondary);
}
.language-section__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.12);
color: #8a5411;
font-weight: 700;
}
.language-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.language-card__main {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--color-border);
box-shadow: none;
padding: 14px 16px;
border-radius: var(--radius-lg);
color: var(--color-text-primary);
}
.language-card__info {
display: grid;
gap: 3px;
text-align: left;
}
.langname {
cursor: pointer;
text-decoration: underline;
font-weight: 700;
}
.language-card__hint {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.role {
margin-left: 6px;
color: #666;
color: var(--color-text-muted);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (max-width: 960px) {
.vocab-hero {
flex-direction: column;
align-items: flex-start;
}
.vocab-summary-grid,
.vocab-task-grid {
grid-template-columns: 1fr;
}
}
</style>