Files
yourpart3/frontend/src/views/blog/BlogEditorView.vue
Torsten Schulz (local) 004a22983e feat(bisaya-course): enhance German course content and localization support
- Updated the create-german-for-bisaya-course-content.js script to improve lesson pattern retrieval by introducing a new function for generating a lesson pattern pool.
- Added new exercises for various topics including 'Wohnung & Nachbarn', 'Besuch empfangen', 'Arzt, Apotheke, Termin', and 'Amt, Dokumente, Anmeldung', enhancing practical language skills for learners.
- Improved localization by integrating translation keys for various UI elements and error messages across multiple components, ensuring a consistent user experience in both German and Bisaya.
- Enhanced the main.js file to recognize Bisaya language preferences in browser settings, improving accessibility for users.
2026-03-31 17:40:03 +02:00

203 lines
8.1 KiB
Vue

<template>
<div class="blog-editor">
<h1>{{ isEdit ? $t('blog.editor.editTitle') : $t('blog.editor.createTitle') }}</h1>
<form @submit.prevent="save">
<div>
<label>{{ $t('blog.title') }}</label>
<input v-model="form.title" required />
</div>
<div>
<label>{{ $t('blog.editor.description') }}</label>
<textarea v-model="form.description"></textarea>
</div>
<div>
<label>{{ $t('blog.editor.visibility') }}</label>
<select v-model="form.visibility">
<option value="public">{{ $t('blog.editor.visibilityPublic') }}</option>
<option value="logged_in">{{ $t('blog.editor.visibilityLoggedIn') }}</option>
</select>
</div>
<div v-if="form.visibility === 'logged_in'">
<label>{{ $t('blog.editor.ageRange') }}</label>
<div class="row">
<input type="number" min="0" v-model.number="form.ageMin" placeholder="min" />
<input type="number" min="0" v-model.number="form.ageMax" placeholder="max" />
</div>
<label>{{ $t('blog.editor.gender') }}</label>
<div class="row">
<label><input type="checkbox" value="m" v-model="genderSel"> {{ $t('blog.editor.genderMale') }}</label>
<label><input type="checkbox" value="f" v-model="genderSel"> {{ $t('blog.editor.genderFemale') }}</label>
</div>
</div>
<button class="btn" type="submit">{{ $t('blog.editor.save') }}</button>
</form>
<div v-if="isEdit" class="post-editor">
<h2>{{ $t('blog.editor.newPostTitle') }}</h2>
<form @submit.prevent="addPost">
<input v-model="post.title" :placeholder="$t('blog.title')" required />
<RichTextEditor v-model="post.content" :blog-id="$route.params.id" />
<button class="btn" type="submit">{{ $t('blog.editor.addPost') }}</button>
</form>
</div>
<div v-if="isEdit" class="share-section">
<h2>{{ $t('blog.editor.shareTitle') }}</h2>
<div class="share-url">
<label>{{ $t('blog.editor.url') }}</label>
<input :value="currentShareUrl" readonly @focus="$event.target.select()" />
<button class="btn" type="button" @click="copyUrl">{{ $t('blog.editor.copyLink') }}</button>
</div>
<div class="share-actions">
<button class="btn" type="button" @click="shareToFriends">{{ $t('blog.editor.shareToFriends') }}</button>
</div>
<div class="share-email">
<label>{{ $t('blog.editor.emailAddresses') }}</label>
<input v-model="emailInput" placeholder="name@example.com, second@example.org" />
<button class="btn" type="button" @click="shareToEmails">{{ $t('blog.editor.send') }}</button>
<p v-if="form.visibility !== 'public'" class="hint">{{ $t('blog.editor.restrictedHint') }}</p>
</div>
<p v-if="shareStatus" class="status">{{ shareStatus }}</p>
</div>
</div>
</template>
<script>
import { createBlog, updateBlog, getBlog, createPost, shareBlog } from '@/api/blogApi.js';
import RichTextEditor from './components/RichTextEditor.vue';
import { showError } from '@/utils/feedback.js';
export default {
name: 'BlogEditorView',
components: { RichTextEditor },
computed: {
isEdit() { return !!this.$route.params.id; },
isOwner() {
const u = this.$store.getters.user;
return !!(u && this.ownerHashedId && this.ownerHashedId === u.id);
}
},
data: () => ({
form: { title: '', description: '', visibility: 'public', ageMin: null, ageMax: null },
genderSel: [],
post: { title: '', content: '' },
ownerHashedId: null,
emailInput: '',
shareStatus: '',
currentShareUrl: '',
ownerUsername: '',
}),
async mounted() {
if (this.isEdit) {
const b = await getBlog(this.$route.params.id).catch(() => null);
if (!b) return this.$router.replace('/blogs');
this.ownerHashedId = b.owner?.hashedId || null;
this.ownerUsername = b.owner?.username || '';
if (!this.isOwner) return this.$router.replace(`/blogs/${this.$route.params.id}`);
this.form = {
title: b.title,
description: b.description,
visibility: b.visibility,
ageMin: b.ageMin,
ageMax: b.ageMax,
};
this.genderSel = (b.genders ? b.genders.split(',').filter(Boolean) : []);
this.currentShareUrl = this.buildSlugUrl(b.title);
}
},
methods: {
async save() {
if (this.form.visibility === 'logged_in') {
if (this.form.ageMin != null && this.form.ageMax != null && this.form.ageMin > this.form.ageMax) {
showError(this, 'tr:blog.editor.invalidAgeRange');
return;
}
}
const payload = { ...this.form, genders: this.genderSel };
if (this.isEdit) {
await updateBlog(this.$route.params.id, payload);
this.$router.push(`/blogs/${this.$route.params.id}`);
} else {
const b = await createBlog(payload);
this.$router.push(`/blogs/${b.id}`);
}
},
async addPost() {
if (!this.isEdit) return;
await createPost(this.$route.params.id, this.post);
this.post = { title: '', content: '' };
// optional: navigate to view; keep simple for now
},
blogAbsoluteUrl() {
try {
const origin = window.location.origin;
const uname = (this.ownerUsername || this.$store.getters.user?.username || '').toString();
const titlePart = (this.form.title||'').toString().replace(/\s+/g, '').replace(/[^a-zA-Z0-9_-]/g, '');
const slug = `${uname}${titlePart}`.replace(/[^a-zA-Z0-9_-]/g, '');
return `${origin}/blogs/${encodeURIComponent(slug)}`;
} catch {
const uname = (this.ownerUsername || this.$store.getters.user?.username || '').toString();
const titlePart = (this.form.title||'').toString().replace(/\s+/g, '').replace(/[^a-zA-Z0-9_-]/g, '');
const slug = `${uname}${titlePart}`.replace(/[^a-zA-Z0-9_-]/g, '');
return `/blogs/${encodeURIComponent(slug)}`;
}
},
buildSlugUrl(title) {
const origin = window.location.origin;
const uname = (this.ownerUsername || this.$store.getters.user?.username || '').toString();
const titlePart = (title || '').toString().replace(/\s+/g, '').replace(/[^a-zA-Z0-9_-]/g, '');
const base = `${uname}${titlePart}`.replace(/[^a-zA-Z0-9_-]/g, '');
return `${origin}/blogs/${encodeURIComponent(base)}`;
},
async copyUrl() {
const url = this.currentShareUrl || this.blogAbsoluteUrl();
try {
await navigator.clipboard.writeText(url);
this.shareStatus = this.$t('blog.editor.copySuccess');
} catch {
this.shareStatus = this.$t('blog.editor.copyError');
}
setTimeout(() => (this.shareStatus = ''), 2000);
},
async shareToFriends() {
try {
const res = await shareBlog(this.$route.params.id, { toFriends: true });
if (res.url) this.currentShareUrl = res.url;
this.shareStatus = this.$t('blog.editor.friendsSent', { count: res.notifiedFriends || 0 });
} catch (e) {
this.shareStatus = this.$t('blog.editor.shareError');
}
setTimeout(() => (this.shareStatus = ''), 3000);
},
async shareToEmails() {
const emails = this.emailInput.split(',').map(s => s.trim()).filter(Boolean);
if (!emails.length) return;
try {
const res = await shareBlog(this.$route.params.id, { emails });
if (res.url) this.currentShareUrl = res.url;
this.shareStatus = this.$t('blog.editor.emailsSent', { count: res.emailsSent || 0 });
} catch (e) {
this.shareStatus = this.$t('blog.editor.emailError');
}
setTimeout(() => (this.shareStatus = ''), 3000);
}
}
,
watch: {
'form.title'(t) {
if (this.isEdit) this.currentShareUrl = this.buildSlugUrl(t);
}
}
}
</script>
<style scoped>
.row { display: flex; gap: .5rem; }
.btn { margin-top: .5rem; }
.post-editor, .share-section { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #ddd; }
.share-url { display: flex; align-items: center; gap: .5rem; }
.share-url input { flex: 1; }
.share-email { margin-top: .5rem; }
.hint { color: #a66; font-size: .9em; }
.status { color: #2a6; font-size: .95em; margin-top: .5rem; }
</style>