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:
149
frontend/src/views/social/VocabLanguageView.vue
Normal file
149
frontend/src/views/social/VocabLanguageView.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<h2>{{ $t('socialnetwork.vocab.languageTitle', { name: language?.name || '' }) }}</h2>
|
||||
|
||||
<div class="box">
|
||||
<div v-if="loading">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="!language">{{ $t('socialnetwork.vocab.notFound') }}</div>
|
||||
<div v-else>
|
||||
<div class="row">
|
||||
<strong>{{ $t('socialnetwork.vocab.languageName') }}:</strong>
|
||||
<span>{{ language.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="language.isOwner && language.shareCode">
|
||||
<strong>{{ $t('socialnetwork.vocab.shareCode') }}:</strong>
|
||||
<code>{{ language.shareCode }}</code>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button @click="goSubscribe">{{ $t('socialnetwork.vocab.subscribeByCode') }}</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="row">
|
||||
<h3>{{ $t('socialnetwork.vocab.chapters') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="language.isOwner">
|
||||
<label>
|
||||
{{ $t('socialnetwork.vocab.newChapter') }}
|
||||
<input v-model="newChapterTitle" type="text" />
|
||||
</label>
|
||||
<button :disabled="creatingChapter || newChapterTitle.trim().length < 2" @click="createChapter">
|
||||
{{ creatingChapter ? $t('socialnetwork.vocab.saving') : $t('socialnetwork.vocab.createChapter') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="chaptersLoading">{{ $t('general.loading') }}</div>
|
||||
<div v-else>
|
||||
<div v-if="chapters.length === 0">{{ $t('socialnetwork.vocab.noChapters') }}</div>
|
||||
<ul v-else>
|
||||
<li v-for="c in chapters" :key="c.id">
|
||||
<span class="click" @click="openChapter(c.id)">
|
||||
{{ c.title }} <span class="count">({{ c.vocabCount }})</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'VocabLanguageView',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
language: null,
|
||||
chaptersLoading: false,
|
||||
chapters: [],
|
||||
newChapterTitle: '',
|
||||
creatingChapter: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goSubscribe() {
|
||||
this.$router.push('/socialnetwork/vocab/subscribe');
|
||||
},
|
||||
openChapter(chapterId) {
|
||||
this.$router.push(`/socialnetwork/vocab/${this.$route.params.languageId}/chapters/${chapterId}`);
|
||||
},
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/languages/${this.$route.params.languageId}`);
|
||||
this.language = res.data;
|
||||
await this.loadChapters();
|
||||
} catch (e) {
|
||||
console.error('Load vocab language failed:', e);
|
||||
this.language = null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadChapters() {
|
||||
this.chaptersLoading = true;
|
||||
try {
|
||||
const res = await apiClient.get(`/api/vocab/languages/${this.$route.params.languageId}/chapters`);
|
||||
this.chapters = res.data?.chapters || [];
|
||||
} catch (e) {
|
||||
console.error('Load chapters failed:', e);
|
||||
this.chapters = [];
|
||||
} finally {
|
||||
this.chaptersLoading = false;
|
||||
}
|
||||
},
|
||||
async createChapter() {
|
||||
this.creatingChapter = true;
|
||||
try {
|
||||
await apiClient.post(`/api/vocab/languages/${this.$route.params.languageId}/chapters`, {
|
||||
title: this.newChapterTitle,
|
||||
});
|
||||
this.newChapterTitle = '';
|
||||
await this.loadChapters();
|
||||
} catch (e) {
|
||||
console.error('Create chapter failed:', e);
|
||||
this.$root.$refs.messageDialog?.open(
|
||||
this.$t('socialnetwork.vocab.createChapterError'),
|
||||
this.$t('error.title')
|
||||
);
|
||||
} finally {
|
||||
this.creatingChapter = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.params.languageId'() {
|
||||
this.load();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box {
|
||||
background: #f6f6f6;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.click {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.count {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user