213 lines
4.4 KiB
Vue
213 lines
4.4 KiB
Vue
<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>
|