Add Vocab Trainer feature with routing, database schema, and translations

- Introduced Vocab Trainer functionality, including new routes for managing languages and chapters.
- Implemented database schema for vocab-related tables to ensure data integrity.
- Updated navigation and UI components to include Vocab Trainer in the social network menu.
- Added translations for Vocab Trainer in both German and English locales, enhancing user accessibility.
This commit is contained in:
Torsten Schulz (local)
2025-12-30 18:34:32 +01:00
parent a09220b881
commit 83597d9e02
24 changed files with 2135 additions and 3 deletions

View File

@@ -0,0 +1,153 @@
<template>
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
<div class="box">
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-else-if="!chapter">{{ $t('socialnetwork.vocab.notFound') }}</div>
<div v-else>
<div class="row">
<button @click="back">{{ $t('general.back') }}</button>
<button v-if="vocabs.length" @click="openPractice">{{ $t('socialnetwork.vocab.practice.open') }}</button>
</div>
<div class="row" v-if="chapter.isOwner">
<h3>{{ $t('socialnetwork.vocab.addVocab') }}</h3>
<div class="grid">
<label>
{{ $t('socialnetwork.vocab.learningWord') }}
<input v-model="learning" type="text" />
</label>
<label>
{{ $t('socialnetwork.vocab.referenceWord') }}
<input v-model="reference" type="text" />
</label>
</div>
<button :disabled="saving || !canSave" @click="add">
{{ saving ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.add') }}
</button>
</div>
<hr />
<div v-if="vocabs.length === 0">{{ $t('socialnetwork.vocab.noVocabs') }}</div>
<table v-else class="tbl">
<thead>
<tr>
<th>{{ $t('socialnetwork.vocab.learningWord') }}</th>
<th>{{ $t('socialnetwork.vocab.referenceWord') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="v in vocabs" :key="v.id">
<td>{{ v.learning }}</td>
<td>{{ v.reference }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<VocabPracticeDialog ref="practiceDialog" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import VocabPracticeDialog from '@/dialogues/socialnetwork/VocabPracticeDialog.vue';
export default {
name: 'VocabChapterView',
components: { VocabPracticeDialog },
data() {
return {
loading: false,
saving: false,
chapter: null,
vocabs: [],
learning: '',
reference: '',
};
},
computed: {
canSave() {
return this.learning.trim().length > 0 && this.reference.trim().length > 0;
},
},
methods: {
back() {
this.$router.push(`/socialnetwork/vocab/${this.$route.params.languageId}`);
},
openPractice() {
this.$refs.practiceDialog?.open?.({
languageId: this.$route.params.languageId,
chapterId: this.$route.params.chapterId,
});
},
async load() {
this.loading = true;
try {
const res = await apiClient.get(`/api/vocab/chapters/${this.$route.params.chapterId}/vocabs`);
this.chapter = res.data?.chapter || null;
this.vocabs = res.data?.vocabs || [];
} catch (e) {
console.error('Load chapter vocabs failed:', e);
this.chapter = null;
this.vocabs = [];
} finally {
this.loading = false;
}
},
async add() {
this.saving = true;
try {
await apiClient.post(`/api/vocab/chapters/${this.$route.params.chapterId}/vocabs`, {
learning: this.learning,
reference: this.reference,
});
this.learning = '';
this.reference = '';
await this.load();
} catch (e) {
console.error('Add vocab failed:', e);
this.$root.$refs.messageDialog?.open(
this.$t('socialnetwork.vocab.addVocabError'),
this.$t('error.title')
);
} finally {
this.saving = false;
}
},
},
mounted() {
this.load();
},
};
</script>
<style scoped>
.box {
background: #f6f6f6;
padding: 12px;
border: 1px solid #ccc;
display: inline-block;
}
.row {
margin-bottom: 10px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 8px;
}
.tbl {
width: 100%;
border-collapse: collapse;
}
.tbl th,
.tbl td {
border: 1px solid #ccc;
padding: 6px;
}
</style>