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

365 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="forum-view">
<section class="forum-hero surface-card">
<div>
<span class="forum-kicker">Community-Forum</span>
<h2>{{ $t('socialnetwork.forum.title') }} {{ forumName }}</h2>
<p>Themen, Diskussionen und neue Beiträge an einem strukturierten Ort.</p>
</div>
<div class="creationtoggler">
<button @click="toggleCreation">
{{ $t(!inCreation
? 'socialnetwork.forum.showNewTopic'
: 'socialnetwork.forum.hideNewTopic') }}
</button>
</div>
</section>
<section v-if="inCreation" class="forum-creation surface-card">
<div class="forum-creation__header">
<div>
<h3>Neues Thema verfassen</h3>
<p>Erst Titel setzen, dann den Beitrag schreiben und anschließend direkt veröffentlichen.</p>
</div>
<button type="button" class="button-secondary" @click="cancelCreation">Abbrechen</button>
</div>
<label class="newtitle">
<span>{{ $t('socialnetwork.forum.topic') }}</span>
<input ref="titleInput" type="text" v-model="newTitle" />
</label>
<div class="editor-container">
<EditorContent v-if="editor" :editor="editor" class="editor" />
</div>
<div class="forum-creation__actions">
<button :disabled="!canSaveTopic" @click="saveNewTopic">
{{ $t('socialnetwork.forum.createNewTopic') }}
</button>
<span class="forum-creation__hint">Titel und Inhalt müssen beide ausgefüllt sein.</span>
</div>
</section>
<section v-else-if="titles.length > 0" class="forum-topics surface-card">
<ul class="topic-list">
<li v-for="topic in titles" :key="topic.id" class="topic-card">
<button type="button" class="topic-card__main" @click="openTopic(topic.id)">
<strong>{{ topic.title }}</strong>
<span class="topic-card__meta">
{{ topic.user?.username || topic.owner?.username || 'Community' }}
</span>
</button>
</li>
</ul>
<div class="pagination">
<button @click="goToPage(page-1)" :disabled="page<=1"></button>
<span>{{ page }} / {{ totalPages }}</span>
<button @click="goToPage(page+1)" :disabled="page>=totalPages"></button>
</div>
</section>
<div v-else class="forum-empty surface-card">
<p>{{ $t('socialnetwork.forum.noTitles') }}</p>
<button type="button" @click="toggleCreation">{{ $t('socialnetwork.forum.createNewTopic') }}</button>
</div>
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import apiClient from '../../utils/axios'
import { showApiError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'ForumView',
components: { EditorContent },
data() {
return {
forumName: '',
page: 1,
numberOfItems: 0,
titles: [],
inCreation: false,
newTitle: '',
editor: null,
}
},
computed: {
// Neu: ForumID immer direkt aus der Route ziehen
forumId() {
return this.$route.params.id
},
totalPages() {
return Math.ceil(this.numberOfItems / 25)
},
canSaveTopic() {
const content = this.editor ? this.editor.getText().trim() : '';
return this.newTitle.trim().length >= 3 && content.length > 0;
}
},
watch: {
// Sobald sich die RouteID ändert, zurück auf Seite 1 und neu laden
forumId: {
handler() {
this.page = 1
this.loadForum()
},
immediate: true
},
// Wenn sich die Seite ändert, ebenfalls neu laden
page(newPage, oldPage) {
if (newPage !== oldPage) {
this.loadForum()
}
}
},
async mounted() {
// Editor initialisieren
this.editor = new Editor({
extensions: [StarterKit],
content: '',
editable: true,
editorProps: { attributes: { class: 'pm-root' } },
})
},
beforeUnmount() {
if (this.editor) this.editor.destroy()
},
methods: {
focusTitleInput() {
this.$nextTick(() => this.$refs.titleInput?.focus?.());
},
toggleCreation() {
this.inCreation = !this.inCreation;
if (this.inCreation && this.editor) {
this.editor.commands.setContent('');
this.newTitle = '';
this.focusTitleInput();
}
},
cancelCreation() {
this.inCreation = false;
this.newTitle = '';
if (this.editor) {
this.editor.commands.setContent('');
}
},
async loadForum() {
try {
const { data } = await apiClient.get(
`/api/forum/${this.forumId}/${this.page}`
)
this.forumName = data.name
this.titles = data.titles
this.numberOfItems = data.totalTopics
} catch (err) {
console.error('Fehler beim Laden des Forums', err)
}
},
async saveNewTopic() {
const content = this.editor ? this.editor.getHTML() : ''
if (!this.canSaveTopic) return
try {
const { data } = await apiClient.post(
'/api/forum/topic',
{
forumId: this.forumId,
title: this.newTitle,
content
}
)
// Neu: Server kann aktuelle Liste zurückliefern
this.forumName = data.name
this.titles = data.titles
this.numberOfItems = data.totalTopics
this.page = data.page
this.inCreation = false
this.newTitle = ''
this.editor?.commands.setContent('')
showSuccess(this, 'Thema erfolgreich erstellt.')
} catch (err) {
console.error('Fehler beim Erstellen des Themas', err)
showApiError(this, err, 'Fehler beim Erstellen des Themas')
}
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.page = page
}
},
openProfile(id) {
this.$root.$refs.userProfileDialog.userId = id
this.$root.$refs.userProfileDialog.open()
},
openTopic(topicId) {
this.$router.push(`/socialnetwork/forumtopic/${topicId}`)
},
}
}
</script>
<style lang="scss" scoped>
.forum-view {
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.forum-hero {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 18px;
padding: 24px 26px;
margin-bottom: 16px;
}
.forum-kicker {
display: inline-block;
margin-bottom: 10px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.14);
color: #8a5411;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.forum-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.creationtoggler {
margin-bottom: 0;
}
.forum-creation,
.forum-topics,
.forum-empty {
padding: 22px;
}
.forum-creation__header {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.forum-creation__header h3 {
margin: 0 0 6px;
}
.forum-creation__header p,
.forum-creation__hint {
margin: 0;
color: var(--color-text-secondary);
}
.forum-creation__actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.newtitle {
display: flex;
flex-direction: column;
gap: 0.6em;
margin-bottom: 1rem;
}
.editor-container {
margin: 1em 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0;
min-height: 260px;
background-color: white;
}
.editor {
min-height: 260px;
outline: none;
cursor: text;
}
.editor :deep(.ProseMirror) {
min-height: 260px;
outline: none;
padding: 10px;
box-sizing: border-box;
width: 100%;
}
.editor :deep(.ProseMirror p) { margin: 0 0 .6rem; }
.editor :deep(.ProseMirror p:first-child) { margin-top: 0; }
.editor :deep(.ProseMirror-focused) { outline: 2px solid rgba(100,150,255,.35); }
.topic-list {
list-style: none;
padding: 0;
margin: 0;
}
.topic-card + .topic-card {
margin-top: 12px;
}
.topic-card__main {
width: 100%;
justify-content: space-between;
background: rgba(255, 255, 255, 0.72);
border: 1px solid var(--color-border);
box-shadow: none;
padding: 14px 16px;
border-radius: var(--radius-lg);
color: var(--color-text-primary);
}
.topic-card__main strong {
text-align: left;
}
.topic-card__meta {
color: var(--color-text-muted);
font-size: 0.82rem;
white-space: nowrap;
}
.pagination {
display: flex;
justify-content: center;
gap: 0.5em;
margin: 1em 0;
}
.pagination span {
padding: 0.5em;
}
.forum-empty {
color: var(--color-text-secondary);
text-align: center;
}
.forum-empty p {
margin-bottom: 12px;
}
@media (max-width: 960px) {
.forum-hero {
flex-direction: column;
align-items: flex-start;
}
.forum-creation__header {
flex-direction: column;
}
.topic-card__main {
align-items: flex-start;
}
}
</style>