diff --git a/assets/css/custom.css b/assets/css/custom.css index 30518da..83bcd28 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -440,7 +440,17 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); } .dialog-link:hover { background: var(--accent); color: #fff; } /* Eigene Dialog-Seite (/dialog/?thread=…) */ -.dialog-page { max-width: var(--container-width); margin: 0 auto; padding: var(--spacing-lg) var(--spacing-md); } +/* Füllt die normale Inhaltsspalte (kein eigenes max-width/Seiten-Padding → gleiche Breite wie andere Seiten) */ +.dialog-page { padding: var(--spacing-lg) 0; } +.dialog-overview { display: flex; flex-direction: column; gap: 0.6em; } +.dialog-overview-item { + display: flex; justify-content: space-between; align-items: baseline; gap: 1em; + border: 1px solid var(--color-border); border-radius: 12px; + padding: 0.8em 1em; background: var(--color-bg-secondary); +} +.dialog-overview-item:hover { border-color: var(--accent); color: inherit; } +.dialog-ov-title { font-family: var(--font-family-serif); font-weight: 600; } +.dialog-ov-meta { font-size: var(--font-size-small); color: var(--color-text-muted); flex: none; } .dialog-back { margin: 0 0 var(--spacing-sm); } .dialog-back a { color: var(--color-text-muted); text-decoration: none; } .dialog-back a:hover { color: var(--accent); } diff --git a/cms/api/src/index.js b/cms/api/src/index.js index 2ed98e4..47290ba 100644 --- a/cms/api/src/index.js +++ b/cms/api/src/index.js @@ -8,7 +8,7 @@ import publish from './routes/publish.js'; import upload from './routes/upload.js'; import profile from './routes/profile.js'; import users from './routes/users.js'; -import { listComments, createComment, deleteComment, login } from './routes/comments.js'; +import { listComments, listThreads, createComment, deleteComment, login } from './routes/comments.js'; import { requireAuth } from './auth.js'; const SITE_DIR = process.env.SITE_DIR || '/site'; @@ -19,8 +19,9 @@ const app = new Hono(); // --- API --- app.get('/api/health', (c) => c.json({ ok: true, hugo: '0.161.1+extended' })); -// Öffentlich (ohne Login): Dialog lesen + Login fürs Dialog-Widget. +// Öffentlich (ohne Login): Dialog lesen, Übersicht, Login fürs Dialog-Widget. app.get('/api/comments', listComments); +app.get('/api/threads', listThreads); app.post('/api/auth/login', login); // Alles weitere unter /api/* braucht ein gültiges Supabase-Token. app.use('/api/*', requireAuth); diff --git a/cms/api/src/routes/comments.js b/cms/api/src/routes/comments.js index c0f4af7..aef47f2 100644 --- a/cms/api/src/routes/comments.js +++ b/cms/api/src/routes/comments.js @@ -1,6 +1,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { supabase } from '../supabase.js'; +import { listEntries } from '../files.js'; // Dialog: flache Wortmeldungen pro Thread (= Beitrags-Pfad), optionaler Bezug. const SITE_DIR = process.env.SITE_DIR || '/site'; @@ -26,6 +27,25 @@ export async function listComments(c) { return c.json(out); } +// ÖFFENTLICH: Übersicht aller begonnenen Dialoge (Threads mit Wortmeldungen). +export async function listThreads(c) { + const { data, error } = await supabase.from('comments').select('thread,created_at,deleted'); + if (error) return c.json({ error: error.message }, 500); + const map = {}; + for (const r of data || []) { + if (r.deleted) continue; + const t = map[r.thread] || (map[r.thread] = { thread: r.thread, count: 0, last: r.created_at }); + t.count += 1; + if (r.created_at > t.last) t.last = r.created_at; + } + let titles = {}; + try { (await listEntries()).forEach((e) => { titles[e.url] = e.title; }); } catch { /* egal */ } + const out = Object.values(map) + .map((t) => ({ ...t, title: titles[t.thread] || t.thread })) + .sort((a, b) => (b.last || '').localeCompare(a.last || '')); + return c.json(out); +} + // EINGELOGGT: Wortmeldung schreiben. export async function createComment(c) { const user = c.get('user'); diff --git a/hugo.yaml b/hugo.yaml index ea72ef5..e72e7f1 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -72,8 +72,8 @@ menus: - name: MANIFEST pageRef: /manifest weight: 30 - - name: FORUM - url: https://openstudio.kgva.ch + - name: DIALOG + pageRef: /dialog weight: 40 - name: CODE url: https://gitea.kgva.ch diff --git a/static/dialog.js b/static/dialog.js index b6db82d..495862b 100644 --- a/static/dialog.js +++ b/static/dialog.js @@ -4,7 +4,30 @@ const root = document.getElementById('ob-dialog'); if (!root) return; const thread = root.dataset.thread || new URLSearchParams(location.search).get('thread') || ''; - if (!thread) { root.innerHTML = '
Kein Thema gewählt.
'; return; } + if (!thread) { renderOverview(); return; } + + // Übersicht aller begonnenen Dialoge (wenn kein Thema gewählt). + function renderOverview() { + root.innerHTML = ''; + const h = document.createElement('h2'); h.className = 'dialog-title'; h.textContent = 'Dialoge'; + const list = document.createElement('div'); list.className = 'dialog-overview'; + root.append(h, list); + fetch('/api/threads').then((r) => (r.ok ? r.json() : [])).then((rows) => { + if (!rows.length) { + const e = document.createElement('p'); e.className = 'dialog-empty'; + e.textContent = 'Noch keine Dialoge begonnen.'; + list.appendChild(e); return; + } + rows.forEach((t) => { + const a = document.createElement('a'); a.className = 'dialog-overview-item'; + a.href = '/dialog/?thread=' + encodeURIComponent(t.thread); + const ti = document.createElement('span'); ti.className = 'dialog-ov-title'; ti.textContent = t.title; + const mt = document.createElement('span'); mt.className = 'dialog-ov-meta'; + mt.textContent = t.count + (t.count === 1 ? ' Wortmeldung' : ' Wortmeldungen'); + a.append(ti, mt); list.appendChild(a); + }); + }).catch(() => {}); + } const TKEY = 'ob_dialog_token', NKEY = 'ob_dialog_name'; let token = localStorage.getItem(TKEY); let myName = localStorage.getItem(NKEY);