feat: Implement blog and blog post models, routes, and services
- Added Blog and BlogPost models with necessary fields and relationships. - Created blogRouter for handling blog-related API endpoints including CRUD operations. - Developed BlogService for business logic related to blogs and posts, including sharing functionality. - Implemented API client methods for frontend to interact with blog-related endpoints. - Added internationalization support for blog-related text in English and German. - Created Vue components for blog editing, listing, and viewing, including a rich text editor for post content. - Enhanced user experience with form validations and dynamic visibility settings based on user input.
This commit is contained in:
109
frontend/src/views/blog/BlogView.vue
Normal file
109
frontend/src/views/blog/BlogView.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="blog-view">
|
||||
<div v-if="loading">Laden…</div>
|
||||
<div v-else>
|
||||
<h1>{{ blog.title }}</h1>
|
||||
<p v-if="blog.description">{{ blog.description }}</p>
|
||||
<div class="meta">von {{ blog.owner?.username }}</div>
|
||||
<div v-if="$store.getters.isLoggedIn" class="actions">
|
||||
<router-link class="editbutton" v-if="isOwner" :to="{ name: 'BlogEdit', params: { id: blog.id } }">Bearbeiten</router-link>
|
||||
</div>
|
||||
<div class="posts">
|
||||
<h2>{{ $t('blog.posts') }}</h2>
|
||||
<div v-if="!items.length">{{ $t('blog.noPosts') }}</div>
|
||||
<article v-for="p in items" :key="p.id" class="post">
|
||||
<h3>{{ p.title }}</h3>
|
||||
<div class="content" v-html="sanitize(p.content)" />
|
||||
</article>
|
||||
<div class="pagination" v-if="total > pageSize">
|
||||
<button :disabled="page===1" @click="go(page-1)">«</button>
|
||||
<span>{{ page }} / {{ pages }}</span>
|
||||
<button :disabled="page===pages" @click="go(page+1)">»</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isOwner" class="post-editor">
|
||||
<h3>{{ $t('blog.newPost') }}</h3>
|
||||
<form @submit.prevent="addPost">
|
||||
<input v-model="newPost.title" :placeholder="$t('blog.title')" required />
|
||||
<RichTextEditor v-model="newPost.content" :blog-id="blog.id" />
|
||||
<button class="btn" type="submit">{{ $t('blog.publish') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getBlog, listPosts, createPost } from '@/api/blogApi.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
import RichTextEditor from './components/RichTextEditor.vue';
|
||||
export default {
|
||||
name: 'BlogView',
|
||||
props: { id: String, slug: String },
|
||||
components: { RichTextEditor },
|
||||
data: () => ({ blog: null, items: [], page: 1, pageSize: 10, total: 0, loading: true, newPost: { title: '', content: '' }, resolvedId: null }),
|
||||
computed: {
|
||||
isOwner() {
|
||||
const u = this.$store.getters.user;
|
||||
return !!(u && this.blog && this.blog.owner && this.blog.owner.hashedId === u.id);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
let id = this.$route.params.id;
|
||||
// If we have a slug route param or the id is non-numeric, resolve to id
|
||||
if ((!id && this.$route.params.slug) || (id && isNaN(Number(id)))) {
|
||||
const slug = this.$route.params.slug || this.$route.params.id;
|
||||
// Resolve slug to id via backend resolver but keep slug URL
|
||||
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001'}/api/blog/blogs/slug/${encodeURIComponent(slug)}/id`);
|
||||
if (res.ok) {
|
||||
const { id: rid } = await res.json();
|
||||
this.resolvedId = rid;
|
||||
id = rid;
|
||||
} else {
|
||||
throw new Error('slug not found');
|
||||
}
|
||||
}
|
||||
const useId = id || this.resolvedId;
|
||||
this.blog = await getBlog(useId);
|
||||
await this.fetchPage(1);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// this.$router.replace('/blogs');
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
sanitize(html) {
|
||||
return DOMPurify.sanitize(html || '');
|
||||
},
|
||||
async fetchPage(p) {
|
||||
const id = this.$route.params.id || this.resolvedId;
|
||||
const res = await listPosts(id, { page: p, pageSize: this.pageSize });
|
||||
this.items = res.items;
|
||||
this.page = res.page;
|
||||
this.pageSize = res.pageSize;
|
||||
this.total = res.total;
|
||||
},
|
||||
get pages() { return Math.max(1, Math.ceil(this.total / this.pageSize)); },
|
||||
async go(p) { if (p>=1 && p<=this.pages) await this.fetchPage(p); },
|
||||
async addPost() {
|
||||
if (!this.newPost.title || !this.newPost.content) return;
|
||||
const id = this.$route.params.id || this.resolvedId;
|
||||
await createPost(id, this.newPost);
|
||||
this.newPost = { title: '', content: '' };
|
||||
await this.fetchPage(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editbutton {
|
||||
border: 1px solid #000;
|
||||
background-color: #f9a22c;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user