Files
yourpart3/frontend/src/views/blog/BlogListView.vue

128 lines
3.1 KiB
Vue

<template>
<div class="blog-list">
<section class="blog-list__hero surface-card">
<div>
<span class="blog-list__kicker">Community-Blogs</span>
<h1>Blogs</h1>
<p>Artikel, Projektstände und persönliche Einblicke aus der YourPart-Community.</p>
</div>
<div class="toolbar">
<router-link v-if="$store.getters.isLoggedIn" class="btn" to="/blogs/create">Neuen Blog erstellen</router-link>
</div>
</section>
<div v-if="loading" class="blog-list__state surface-card">Laden</div>
<div v-else-if="!blogs.length" class="blog-list__state surface-card">Keine Blogs gefunden.</div>
<div v-else class="blog-grid">
<article v-for="b in blogs" :key="b.id" class="blog-card surface-card">
<div class="blog-card__meta">von {{ b.owner?.username || 'Unbekannt' }}</div>
<h2><router-link :to="blogUrl(b)">{{ b.title }}</router-link></h2>
<p>{{ blogExcerpt(b) }}</p>
<router-link class="blog-card__link" :to="blogUrl(b)">Zum Blog</router-link>
</article>
</div>
</div>
</template>
<script>
import { listBlogs } from '@/api/blogApi.js';
import { createBlogSlug } from '@/utils/seo.js';
export default {
name: 'BlogListView',
data: () => ({ blogs: [], loading: true }),
async mounted() {
try { this.blogs = await listBlogs(); } finally { this.loading = false; }
},
methods: {
blogUrl(blog) {
const slug = createBlogSlug(blog?.owner?.username, blog?.title);
return slug ? `/blogs/${encodeURIComponent(slug)}` : `/blogs/${blog.id}`;
},
blogExcerpt(blog) {
const source = blog?.description || 'Öffentliche Einträge, Gedanken und Projektstände aus der Community.';
return source.length > 150 ? `${source.slice(0, 147)}...` : source;
},
},
}
</script>
<style scoped>
.blog-list {
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.blog-list__hero {
display: flex;
align-items: end;
justify-content: space-between;
gap: 20px;
padding: 24px 26px;
margin-bottom: 18px;
}
.blog-list__kicker {
display: inline-block;
margin-bottom: 10px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(120, 195, 138, 0.14);
color: #42634e;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.blog-list__hero p {
margin: 0;
color: var(--color-text-secondary);
}
.blog-list__state {
padding: 26px;
text-align: center;
color: var(--color-text-secondary);
}
.blog-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 18px;
}
.blog-card {
padding: 22px;
}
.blog-card__meta {
margin-bottom: 10px;
color: var(--color-text-muted);
font-size: 0.82rem;
font-weight: 600;
}
.blog-card h2 {
margin-bottom: 10px;
font-size: 1.3rem;
}
.blog-card p {
margin-bottom: 16px;
color: var(--color-text-secondary);
}
.blog-card__link {
color: var(--color-primary);
font-weight: 700;
}
@media (max-width: 960px) {
.blog-list__hero {
flex-direction: column;
align-items: flex-start;
}
}
</style>