Files
yourpart3/frontend/src/views/social/VocabChapterView.vue

273 lines
6.9 KiB
Vue

<template>
<div class="vocab-chapter-view">
<section class="vocab-chapter-hero surface-card">
<span class="vocab-chapter-hero__eyebrow">Vokabeltrainer</span>
<h2>{{ $t('socialnetwork.vocab.chapterTitle', { title: chapter?.title || '' }) }}</h2>
<p>Kapitelinhalt durchsuchen, Vokabeln pflegen und direkt in die Uebung wechseln.</p>
</section>
<section class="box surface-card">
<div v-if="loading">{{ $t('general.loading') }}</div>
<div v-else-if="!chapter">{{ $t('socialnetwork.vocab.notFound') }}</div>
<div v-else>
<div v-show="!practiceOpen">
<div class="row row--actions">
<button @click="back">{{ $t('general.back') }}</button>
<button v-if="vocabs.length" @click="openPractice">{{ $t('socialnetwork.vocab.practice.open') }}</button>
<button @click="openSearch">{{ $t('socialnetwork.vocab.search.open') }}</button>
</div>
<div class="editor-card" v-if="chapter.isOwner">
<h3>{{ $t('socialnetwork.vocab.addVocab') }}</h3>
<div class="grid">
<label>
<span>{{ $t('socialnetwork.vocab.learningWord') }}</span>
<input v-model="learning" type="text" />
</label>
<label>
<span>{{ $t('socialnetwork.vocab.referenceWord') }}</span>
<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>
<div v-if="vocabs.length === 0" class="empty-state">{{ $t('socialnetwork.vocab.noVocabs') }}</div>
<div v-else class="table-wrap">
<table 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>
</div>
</section>
</div>
<VocabPracticeDialog ref="practiceDialog" />
<VocabSearchDialog ref="searchDialog" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import VocabPracticeDialog from '@/dialogues/socialnetwork/VocabPracticeDialog.vue';
import VocabSearchDialog from '@/dialogues/socialnetwork/VocabSearchDialog.vue';
export default {
name: 'VocabChapterView',
components: { VocabPracticeDialog, VocabSearchDialog },
data() {
return {
loading: false,
saving: false,
practiceOpen: false,
chapter: null,
languageName: '',
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.practiceOpen = true;
this.$refs.practiceDialog?.open?.({
languageId: this.$route.params.languageId,
chapterId: this.$route.params.chapterId,
onClose: () => {
this.practiceOpen = false;
},
});
},
openSearch() {
this.$refs.searchDialog?.open?.({
languageId: this.$route.params.languageId,
languageName: this.languageName || '',
});
},
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 || [];
try {
const langRes = await apiClient.get(`/api/vocab/languages/${this.$route.params.languageId}`);
this.languageName = langRes.data?.name || '';
} catch (_) {
this.languageName = '';
}
} 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>
.vocab-chapter-view {
display: grid;
gap: 18px;
}
.vocab-chapter-hero,
.box {
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
}
.vocab-chapter-hero,
.box {
padding: 22px 24px;
}
.vocab-chapter-hero__eyebrow {
display: inline-flex;
margin-bottom: 8px;
padding: 4px 10px;
border-radius: var(--radius-pill);
background: var(--color-secondary-soft);
color: var(--color-text-secondary);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.vocab-chapter-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.row {
margin-bottom: 10px;
}
.row--actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.editor-card {
display: grid;
gap: 14px;
margin: 18px 0 20px;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.62);
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 8px;
}
.grid label {
display: grid;
gap: 8px;
}
.grid span {
font-weight: 600;
color: var(--color-text-secondary);
}
.empty-state {
padding: 18px;
border: 1px dashed var(--color-border-strong);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
background: rgba(255, 255, 255, 0.5);
}
.table-wrap {
overflow-x: auto;
}
.tbl {
width: 100%;
border-collapse: collapse;
background: rgba(255, 255, 255, 0.68);
border-radius: var(--radius-md);
overflow: hidden;
}
.tbl th,
.tbl td {
border: 1px solid var(--color-border);
padding: 10px 12px;
text-align: left;
}
.tbl th {
background: rgba(248, 162, 43, 0.12);
color: var(--color-text-secondary);
font-weight: 700;
}
@media (max-width: 760px) {
.vocab-chapter-hero,
.box {
padding: 18px;
}
.grid {
grid-template-columns: 1fr;
}
}
</style>