update
This commit is contained in:
212
frontend/src/components/Kommentar.vue
Normal file
212
frontend/src/components/Kommentar.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup>
|
||||
// Ein Kommentar in der Diskussion: der Text mit Name, Datum und
|
||||
// „Kommentieren“. Antworten rücken darunter ein — die Komponente ruft sich dafür selbst auf, so tief
|
||||
// es geht. Ab der vierten Ebene nicht weiter, sonst bleibt auf dem Handy
|
||||
// kein Platz für den Text.
|
||||
import { ref } from 'vue'
|
||||
import { rendern } from '../markdown'
|
||||
import MarkdownFeld from './MarkdownFeld.vue'
|
||||
|
||||
const props = defineProps({
|
||||
kommentar: { type: Object, required: true },
|
||||
watchedId: { type: Number, required: true },
|
||||
angemeldet: Boolean,
|
||||
host: Boolean,
|
||||
tiefe: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const antwortet = ref(false)
|
||||
const entwurf = ref('')
|
||||
|
||||
async function antworten() {
|
||||
const text = entwurf.value.trim()
|
||||
if (!text) return
|
||||
const resp = await fetch(`/api/watched/${props.watchedId}/notes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, parent_id: props.kommentar.id }),
|
||||
})
|
||||
if (resp.ok) {
|
||||
entwurf.value = ''
|
||||
antwortet.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen() {
|
||||
await fetch(`/api/watched-notes/${props.kommentar.id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
function datum(iso) {
|
||||
return new Date(iso).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="zeile">
|
||||
<div class="inhalt">
|
||||
<div class="markdown" v-html="rendern(kommentar.text)"></div>
|
||||
<p class="meta">
|
||||
<span :class="{ eigen: kommentar.mine }">{{ kommentar.name || 'Anonym' }}</span>
|
||||
<span class="punkt">·</span>{{ datum(kommentar.created_at) }}
|
||||
<template v-if="angemeldet">
|
||||
<span class="punkt">·</span>
|
||||
<button type="button" class="link" :class="{ offen: antwortet }" @click="antwortet = !antwortet">
|
||||
Kommentieren
|
||||
</button>
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<MarkdownFeld
|
||||
v-if="antwortet"
|
||||
v-model="entwurf"
|
||||
kompakt
|
||||
class="neu"
|
||||
:platzhalter="`Antwort an ${kommentar.name || 'Anonym'} …`"
|
||||
knopf="Antworten"
|
||||
@senden="antworten"
|
||||
@abbrechen="antwortet = false"
|
||||
/>
|
||||
|
||||
<ul v-if="kommentar.replies.length" class="antworten" :class="{ flach: tiefe >= 3 }">
|
||||
<Kommentar
|
||||
v-for="k in kommentar.replies"
|
||||
:key="k.id"
|
||||
:kommentar="k"
|
||||
:watched-id="watchedId"
|
||||
:angemeldet="angemeldet"
|
||||
:host="host"
|
||||
:tiefe="tiefe + 1"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Erst beim Überfahren sichtbar, sonst ist jede Zeile voller Knöpfe -->
|
||||
<div class="aktionen">
|
||||
<button
|
||||
v-if="kommentar.mine || host"
|
||||
type="button"
|
||||
class="weg"
|
||||
:title="kommentar.replies.length ? 'Samt allen Antworten darunter' : 'Kommentar entfernen'"
|
||||
@click="loeschen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.zeile {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.55rem 0.5rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.zeile:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.inhalt {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.punkt {
|
||||
margin: 0 0.35rem;
|
||||
}
|
||||
|
||||
.eigen {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 0;
|
||||
color: var(--muted);
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.link.offen {
|
||||
color: var(--text);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.neu {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
/* Antworten hängen eingerückt unter dem Kommentar */
|
||||
.antworten {
|
||||
margin-top: 0.4rem;
|
||||
padding-left: 0.4rem;
|
||||
list-style: none;
|
||||
border-left: 2px solid var(--line);
|
||||
}
|
||||
|
||||
.antworten .zeile {
|
||||
padding-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Ab der vierten Ebene nicht weiter einrücken */
|
||||
.antworten.flach {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.aktionen {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s;
|
||||
}
|
||||
|
||||
.zeile:hover > .aktionen,
|
||||
.zeile:focus-within > .aktionen {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.weg {
|
||||
padding: 0 0.25rem;
|
||||
color: var(--muted);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.weg:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.aktionen {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px), (pointer: coarse) {
|
||||
.link,
|
||||
.weg {
|
||||
padding: 0.3rem 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user