dialog: Menü-Eintrag DIALOG + Übersicht aller Dialoge; Breite an andere Seiten angeglichen
- Menü: FORUM (extern) → DIALOG → /dialog/ - /dialog/ ohne ?thread zeigt Übersicht aller begonnenen Dialoge (GET /api/threads, Titel aus Inhaltsdateien, sortiert nach Aktivität) - .dialog-page füllt jetzt die normale Inhaltsspalte (kein eigenes max-width/ Padding mehr) → gleiche Breite wie andere Seiten - Threads entstehen erst mit der ersten Wortmeldung Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -440,7 +440,17 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); }
|
|||||||
.dialog-link:hover { background: var(--accent); color: #fff; }
|
.dialog-link:hover { background: var(--accent); color: #fff; }
|
||||||
|
|
||||||
/* Eigene Dialog-Seite (/dialog/?thread=…) */
|
/* 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 { margin: 0 0 var(--spacing-sm); }
|
||||||
.dialog-back a { color: var(--color-text-muted); text-decoration: none; }
|
.dialog-back a { color: var(--color-text-muted); text-decoration: none; }
|
||||||
.dialog-back a:hover { color: var(--accent); }
|
.dialog-back a:hover { color: var(--accent); }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import publish from './routes/publish.js';
|
|||||||
import upload from './routes/upload.js';
|
import upload from './routes/upload.js';
|
||||||
import profile from './routes/profile.js';
|
import profile from './routes/profile.js';
|
||||||
import users from './routes/users.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';
|
import { requireAuth } from './auth.js';
|
||||||
|
|
||||||
const SITE_DIR = process.env.SITE_DIR || '/site';
|
const SITE_DIR = process.env.SITE_DIR || '/site';
|
||||||
@@ -19,8 +19,9 @@ const app = new Hono();
|
|||||||
|
|
||||||
// --- API ---
|
// --- API ---
|
||||||
app.get('/api/health', (c) => c.json({ ok: true, hugo: '0.161.1+extended' }));
|
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/comments', listComments);
|
||||||
|
app.get('/api/threads', listThreads);
|
||||||
app.post('/api/auth/login', login);
|
app.post('/api/auth/login', login);
|
||||||
// Alles weitere unter /api/* braucht ein gültiges Supabase-Token.
|
// Alles weitere unter /api/* braucht ein gültiges Supabase-Token.
|
||||||
app.use('/api/*', requireAuth);
|
app.use('/api/*', requireAuth);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { supabase } from '../supabase.js';
|
import { supabase } from '../supabase.js';
|
||||||
|
import { listEntries } from '../files.js';
|
||||||
|
|
||||||
// Dialog: flache Wortmeldungen pro Thread (= Beitrags-Pfad), optionaler Bezug.
|
// Dialog: flache Wortmeldungen pro Thread (= Beitrags-Pfad), optionaler Bezug.
|
||||||
const SITE_DIR = process.env.SITE_DIR || '/site';
|
const SITE_DIR = process.env.SITE_DIR || '/site';
|
||||||
@@ -26,6 +27,25 @@ export async function listComments(c) {
|
|||||||
return c.json(out);
|
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.
|
// EINGELOGGT: Wortmeldung schreiben.
|
||||||
export async function createComment(c) {
|
export async function createComment(c) {
|
||||||
const user = c.get('user');
|
const user = c.get('user');
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ menus:
|
|||||||
- name: MANIFEST
|
- name: MANIFEST
|
||||||
pageRef: /manifest
|
pageRef: /manifest
|
||||||
weight: 30
|
weight: 30
|
||||||
- name: FORUM
|
- name: DIALOG
|
||||||
url: https://openstudio.kgva.ch
|
pageRef: /dialog
|
||||||
weight: 40
|
weight: 40
|
||||||
- name: CODE
|
- name: CODE
|
||||||
url: https://gitea.kgva.ch
|
url: https://gitea.kgva.ch
|
||||||
|
|||||||
+24
-1
@@ -4,7 +4,30 @@
|
|||||||
const root = document.getElementById('ob-dialog');
|
const root = document.getElementById('ob-dialog');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
const thread = root.dataset.thread || new URLSearchParams(location.search).get('thread') || '';
|
const thread = root.dataset.thread || new URLSearchParams(location.search).get('thread') || '';
|
||||||
if (!thread) { root.innerHTML = '<p class="dialog-empty">Kein Thema gewählt.</p>'; 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';
|
const TKEY = 'ob_dialog_token', NKEY = 'ob_dialog_name';
|
||||||
let token = localStorage.getItem(TKEY);
|
let token = localStorage.getItem(TKEY);
|
||||||
let myName = localStorage.getItem(NKEY);
|
let myName = localStorage.getItem(NKEY);
|
||||||
|
|||||||
Reference in New Issue
Block a user