dialog: Position/Rolle + Breadcrumb-Nav + nüchterne Wortmeldungen, Footer voll-breit
- comments: author_role (Position bei OPENBUREAU) aus authors.json gespeichert/ausgeliefert - schema: comments.author_role hinzugefügt - dialog.js: Breadcrumb (Dialoge › Forum), volles Datum/Uhrzeit, Box→Trennlinien-Layout - css: Footer voll-breit (Flex statt Grid), Balken zwischen Header/main/Footer entfernt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+35
-34
@@ -98,6 +98,7 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0; /* Theme-main.css setzt body{gap:spacing-lg} → erzeugte Balken zwischen Header/main/Footer */
|
||||||
}
|
}
|
||||||
body > header.site-header,
|
body > header.site-header,
|
||||||
body > footer { flex: none; }
|
body > footer { flex: none; }
|
||||||
@@ -114,7 +115,7 @@ body > main::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
|||||||
body:not(.is-home) > main {
|
body:not(.is-home) > main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: safe center;
|
justify-content: flex-start; /* Inhalt startet direkt unter dem Header (keine Zentrier-Bänder) */
|
||||||
}
|
}
|
||||||
/* Inhalt 72ch-zentriert in der vollbreiten Scroll-Fläche (Home = Vollbreite). */
|
/* Inhalt 72ch-zentriert in der vollbreiten Scroll-Fläche (Home = Vollbreite). */
|
||||||
body:not(.is-home) > main > * {
|
body:not(.is-home) > main > * {
|
||||||
@@ -396,8 +397,7 @@ body.is-home .journal-list::-webkit-scrollbar { width: 0; height: 0; display: no
|
|||||||
body.is-home .journal-entry { flex: 0 0 auto; }
|
body.is-home .journal-entry { flex: 0 0 auto; }
|
||||||
body.is-home .more { flex: none; padding: 0.4rem 10px; margin: 0; }
|
body.is-home .more { flex: none; padding: 0.4rem 10px; margin: 0; }
|
||||||
/* Footer kompakt (~1/3): kein großer Außenabstand, knappes Padding. */
|
/* Footer kompakt (~1/3): kein großer Außenabstand, knappes Padding. */
|
||||||
body.is-home > footer { margin-top: 0; padding: 0.55rem 0; }
|
body.is-home > footer { margin-top: 0; padding: 0.55rem 1.5rem; }
|
||||||
body.is-home > footer .footer-grid { row-gap: 0.2rem; }
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
/* Mobil: kein Full-Height-Rahmen — normale Seite scrollt, kein interner Scroll. */
|
/* Mobil: kein Full-Height-Rahmen — normale Seite scrollt, kein interner Scroll. */
|
||||||
@@ -570,7 +570,9 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); }
|
|||||||
|
|
||||||
/* Eigene Dialog-Seite (/dialog/?thread=…) */
|
/* Eigene Dialog-Seite (/dialog/?thread=…) */
|
||||||
/* Füllt die normale Inhaltsspalte (kein eigenes max-width/Seiten-Padding → gleiche Breite wie andere Seiten) */
|
/* Füllt die normale Inhaltsspalte (kein eigenes max-width/Seiten-Padding → gleiche Breite wie andere Seiten) */
|
||||||
.dialog-page { padding: var(--spacing-sm) 0 var(--spacing-xl); }
|
/* width:100% → füllt immer die ganze Inhaltsspalte (sonst schrumpft .dialog-page
|
||||||
|
als Flex-Item mit margin-inline:auto bei schmalem Inhalt, z.B. Forum-Ansicht). */
|
||||||
|
.dialog-page { width: 100%; padding: var(--spacing-sm) 0 var(--spacing-xl); }
|
||||||
.dialog-overview { display: flex; flex-direction: column; gap: 0.6em; }
|
.dialog-overview { display: flex; flex-direction: column; gap: 0.6em; }
|
||||||
.dialog-overview-item {
|
.dialog-overview-item {
|
||||||
display: flex; justify-content: space-between; align-items: baseline; gap: 1em;
|
display: flex; justify-content: space-between; align-items: baseline; gap: 1em;
|
||||||
@@ -584,23 +586,33 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); }
|
|||||||
.dialog-back:empty { margin: 0; }
|
.dialog-back:empty { margin: 0; }
|
||||||
.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); }
|
||||||
|
/* Dialog-Navigation oben: Breadcrumb (Dialoge › Forum). */
|
||||||
|
.dialog-crumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45em; font-size: var(--font-size-small); }
|
||||||
|
.dialog-crumb a { color: var(--color-text-muted); text-decoration: none; }
|
||||||
|
.dialog-crumb a:hover { color: var(--accent); }
|
||||||
|
.dialog-crumb-sep { color: var(--color-text-muted); }
|
||||||
|
|
||||||
.dialog-title {
|
.dialog-title {
|
||||||
font-family: var(--font-family-serif);
|
font-family: var(--font-family-serif);
|
||||||
margin: 0 0 var(--spacing-md);
|
margin: 0 0 var(--spacing-md);
|
||||||
}
|
}
|
||||||
.dialog-list { display: flex; flex-direction: column; gap: var(--spacing-md); margin-bottom: var(--spacing-lg); }
|
.dialog-list { display: flex; flex-direction: column; margin-bottom: var(--spacing-lg); }
|
||||||
.dialog-empty { color: var(--color-text-muted); font-style: italic; }
|
.dialog-empty { color: var(--color-text-muted); font-style: italic; }
|
||||||
.dialog-card { border: 1px solid var(--color-border); border-radius: 12px; padding: var(--spacing-md); background: var(--color-bg-secondary); }
|
/* Nüchterne Wortmeldung: keine Box — nur feine Trennlinie + Abstand. */
|
||||||
.dialog-card-head { display: flex; align-items: center; gap: 0.7em; margin-bottom: 0.6em; }
|
.dialog-post { padding: 1.15em 0; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.dialog-post:first-child { padding-top: 0.2em; }
|
||||||
|
.dialog-post:last-child { border-bottom: none; }
|
||||||
|
.dialog-post-head { display: flex; align-items: center; gap: 0.7em; margin-bottom: 0.5em; }
|
||||||
.dialog-avatar {
|
.dialog-avatar {
|
||||||
width: 40px; height: 40px; border-radius: 50%; flex: none;
|
width: 40px; height: 40px; border-radius: 50%; flex: none;
|
||||||
background: var(--color-border) center/cover no-repeat;
|
background: var(--color-border) center/cover no-repeat;
|
||||||
display: grid; place-items: center; font-weight: 600; color: var(--color-text-muted);
|
display: grid; place-items: center; font-weight: 600; color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
.dialog-meta { display: flex; flex-direction: column; line-height: 1.3; }
|
.dialog-ident { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
|
||||||
|
.dialog-nameline { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5em; }
|
||||||
.dialog-name { font-weight: 600; }
|
.dialog-name { font-weight: 600; }
|
||||||
.dialog-time { font-size: var(--font-size-small); color: var(--color-text-muted); }
|
.dialog-pos { font-size: var(--font-size-small); color: var(--color-text-muted); }
|
||||||
|
.dialog-time { font-size: var(--font-size-small); color: var(--color-text-muted); margin-top: 0.1em; }
|
||||||
.dialog-replyto { font-size: var(--font-size-small); color: var(--accent); }
|
.dialog-replyto { font-size: var(--font-size-small); color: var(--accent); }
|
||||||
.dialog-body { font-family: var(--font-family-serif); line-height: 1.6; white-space: pre-wrap; }
|
.dialog-body { font-family: var(--font-family-serif); line-height: 1.6; white-space: pre-wrap; }
|
||||||
.dialog-actions { display: flex; gap: 0.8em; margin-top: 0.6em; }
|
.dialog-actions { display: flex; gap: 0.8em; margin-top: 0.6em; }
|
||||||
@@ -1355,31 +1367,23 @@ footer {
|
|||||||
background: var(--color-dark-panel);
|
background: var(--color-dark-panel);
|
||||||
color: var(--color-dark-panel-text); /* hell & lesbar auf Schwarz */
|
color: var(--color-dark-panel-text); /* hell & lesbar auf Schwarz */
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 0.55rem 0; /* kompakt wie auf der Journal-Seite */
|
/* voll-breit; horizontal bündig zum Journal-Karten-Inhalt (1.5rem) */
|
||||||
|
padding: 0.55rem 1.5rem;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
/* inner grid aligns with content column, same trick as header */
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns:
|
|
||||||
1fr
|
|
||||||
min(var(--container-width), 100% - 3.5rem)
|
|
||||||
1fr;
|
|
||||||
}
|
}
|
||||||
footer > * { grid-column: 2; }
|
|
||||||
footer a, footer a:hover, footer a:focus { border: none; border-bottom: none; text-decoration: none; }
|
footer a, footer a:hover, footer a:focus { border: none; border-bottom: none; text-decoration: none; }
|
||||||
footer a { color: var(--color-dark-panel-text); }
|
footer a { color: var(--color-dark-panel-text); }
|
||||||
footer a:hover { color: var(--accent-soft); }
|
footer a:hover { color: var(--accent-soft); }
|
||||||
footer p { margin: 0; }
|
footer p { margin: 0; }
|
||||||
|
|
||||||
/* Zwei Zeilen: oben Inhalts-Absatz (links) | Links (rechts),
|
/* Lizenzen ganz links (linksbündig), Footer-Menü ganz rechts. */
|
||||||
unten Lizenz/Copyright (links). */
|
|
||||||
.footer-grid {
|
.footer-grid {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto;
|
justify-content: space-between;
|
||||||
align-items: start;
|
align-items: center;
|
||||||
column-gap: var(--spacing-lg);
|
column-gap: var(--spacing-lg);
|
||||||
row-gap: 0.2rem;
|
|
||||||
}
|
}
|
||||||
.footer-legal { grid-row: 1; grid-column: 1; }
|
.footer-legal { text-align: left; }
|
||||||
.footer-licenses {
|
.footer-licenses {
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -1389,14 +1393,13 @@ footer p { margin: 0; }
|
|||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--color-dark-panel-muted);
|
color: var(--color-dark-panel-muted);
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
grid-row: 1; grid-column: 2;
|
|
||||||
justify-self: end;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 0.4rem 1.3rem;
|
gap: 0.4rem 1.3rem;
|
||||||
font-family: var(--font-family-display);
|
font-family: var(--font-family-display);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -1408,14 +1411,12 @@ footer p { margin: 0; }
|
|||||||
|
|
||||||
/* Mobile: alles linksbündig stapeln */
|
/* Mobile: alles linksbündig stapeln */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.footer-grid { grid-template-columns: 1fr; }
|
.footer-grid {
|
||||||
.footer-legal,
|
flex-direction: column;
|
||||||
.footer-links {
|
align-items: flex-start;
|
||||||
grid-column: 1;
|
row-gap: 0.5rem;
|
||||||
justify-self: start;
|
|
||||||
}
|
}
|
||||||
.footer-legal { grid-row: 1; }
|
.footer-links { justify-content: flex-start; }
|
||||||
.footer-links { grid-row: 2; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------
|
/* ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { profileFor, threadLocked } from '../dialog-store.js';
|
|||||||
|
|
||||||
// Dialog: flache Wortmeldungen pro Thread (= Thread-Key), optionaler Bezug.
|
// Dialog: flache Wortmeldungen pro Thread (= Thread-Key), optionaler Bezug.
|
||||||
|
|
||||||
const COLS = 'id,thread,parent_id,author_name,author_avatar,body,created_at,deleted';
|
const COLS = 'id,thread,parent_id,author_name,author_avatar,author_role,body,created_at,deleted';
|
||||||
|
|
||||||
// ÖFFENTLICH: Wortmeldungen eines Threads lesen.
|
// ÖFFENTLICH: Wortmeldungen eines Threads lesen.
|
||||||
export async function listComments(c) {
|
export async function listComments(c) {
|
||||||
@@ -32,6 +32,7 @@ export async function createComment(c) {
|
|||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
author_name: prof?.name || email.split('@')[0],
|
author_name: prof?.name || email.split('@')[0],
|
||||||
author_avatar: prof?.avatar || null,
|
author_avatar: prof?.avatar || null,
|
||||||
|
author_role: prof?.title || null, // „Position bei OPENBUREAU" (aus data/authors.json)
|
||||||
body: body.trim(),
|
body: body.trim(),
|
||||||
};
|
};
|
||||||
const { data, error } = await supabase.from('comments').insert(row).select(COLS).single();
|
const { data, error } = await supabase.from('comments').insert(row).select(COLS).single();
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ create table if not exists public.comments (
|
|||||||
created_at timestamptz not null default now(),
|
created_at timestamptz not null default now(),
|
||||||
deleted boolean not null default false
|
deleted boolean not null default false
|
||||||
);
|
);
|
||||||
|
-- Position/Rolle bei OPENBUREAU (optional, neben dem Namen angezeigt).
|
||||||
|
alter table public.comments add column if not exists author_role text;
|
||||||
create index if not exists comments_thread_idx on public.comments (thread, created_at);
|
create index if not exists comments_thread_idx on public.comments (thread, created_at);
|
||||||
alter table public.comments enable row level security;
|
alter table public.comments enable row level security;
|
||||||
grant all on public.comments to anon, authenticated, service_role;
|
grant all on public.comments to anon, authenticated, service_role;
|
||||||
|
|||||||
+32
-13
@@ -28,6 +28,11 @@
|
|||||||
if (s < 86400) return Math.floor(s / 3600) + ' Std.';
|
if (s < 86400) return Math.floor(s / 3600) + ' Std.';
|
||||||
return d.toLocaleDateString('de-CH');
|
return d.toLocaleDateString('de-CH');
|
||||||
}
|
}
|
||||||
|
// Volles Datum + Uhrzeit (für die Wortmeldungen in der Thread-Ansicht).
|
||||||
|
function fmtFull(ts) {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.toLocaleDateString('de-CH') + ' · ' + d.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
const api = (p, opt) => fetch(p, opt).then(async (r) => ({ ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }));
|
const api = (p, opt) => fetch(p, opt).then(async (r) => ({ ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }));
|
||||||
const authHdr = () => ({ Authorization: 'Bearer ' + token });
|
const authHdr = () => ({ Authorization: 'Bearer ' + token });
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
// ── Forum-Ansicht: Threads + neuer Thread ─────────────────────────────────
|
// ── Forum-Ansicht: Threads + neuer Thread ─────────────────────────────────
|
||||||
function renderForum(slug) {
|
function renderForum(slug) {
|
||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
if (ctxEl) { ctxEl.innerHTML = ''; const b = el('a', null, '← Dialoge'); b.href = '/dialog/'; ctxEl.appendChild(b); }
|
if (ctxEl) { ctxEl.innerHTML = ''; const c = el('nav', 'dialog-crumb'); const b = el('a', null, '← Dialoge'); b.href = '/dialog/'; c.appendChild(b); ctxEl.appendChild(c); }
|
||||||
api('/api/forums/' + encodeURIComponent(slug)).then((r) => {
|
api('/api/forums/' + encodeURIComponent(slug)).then((r) => {
|
||||||
if (!r.ok) { root.appendChild(el('p', 'dialog-empty', 'Forum nicht gefunden.')); return; }
|
if (!r.ok) { root.appendChild(el('p', 'dialog-empty', 'Forum nicht gefunden.')); return; }
|
||||||
const { forum, threads } = r.body;
|
const { forum, threads } = r.body;
|
||||||
@@ -192,9 +197,19 @@
|
|||||||
const m = r.body; title.textContent = m.title || 'Dialog'; locked = m.locked;
|
const m = r.body; title.textContent = m.title || 'Dialog'; locked = m.locked;
|
||||||
if (ctxEl) {
|
if (ctxEl) {
|
||||||
ctxEl.innerHTML = '';
|
ctxEl.innerHTML = '';
|
||||||
const back = el('a', null, m.kind === 'library' ? '← zum Beitrag' : (m.forum ? '← ' + m.forum.name : '← Dialoge'));
|
if (m.kind === 'library') {
|
||||||
back.href = m.kind === 'library' ? m.url : (m.forum ? '/dialog/?forum=' + encodeURIComponent(m.forum.slug) : '/dialog/');
|
const back = el('a', null, '← zum Beitrag'); back.href = m.url; ctxEl.appendChild(back);
|
||||||
ctxEl.appendChild(back);
|
} else {
|
||||||
|
// Breadcrumb-Navigation oben: Dialoge › Forum (beide anklickbar).
|
||||||
|
const crumb = el('nav', 'dialog-crumb');
|
||||||
|
const a0 = el('a', null, 'Dialoge'); a0.href = '/dialog/'; crumb.appendChild(a0);
|
||||||
|
if (m.forum) {
|
||||||
|
crumb.appendChild(el('span', 'dialog-crumb-sep', '›'));
|
||||||
|
const a1 = el('a', null, m.forum.name); a1.href = '/dialog/?forum=' + encodeURIComponent(m.forum.slug);
|
||||||
|
crumb.appendChild(a1);
|
||||||
|
}
|
||||||
|
ctxEl.appendChild(crumb);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderModbar(); renderComposer();
|
renderModbar(); renderComposer();
|
||||||
});
|
});
|
||||||
@@ -230,23 +245,27 @@
|
|||||||
const names = {}; data.forEach((c) => { names[c.id] = c.author_name; });
|
const names = {}; data.forEach((c) => { names[c.id] = c.author_name; });
|
||||||
if (!data.length) { list.appendChild(el('p', 'dialog-empty', 'Noch keine Wortmeldungen — beginne den Dialog.')); return; }
|
if (!data.length) { list.appendChild(el('p', 'dialog-empty', 'Noch keine Wortmeldungen — beginne den Dialog.')); return; }
|
||||||
data.forEach((c) => {
|
data.forEach((c) => {
|
||||||
const card = el('article', 'dialog-card');
|
// Nüchtern: Avatar · Name (+ Position) · darunter Datum/Uhrzeit · Text.
|
||||||
const head = el('header', 'dialog-card-head');
|
const post = el('article', 'dialog-post');
|
||||||
|
const head = el('header', 'dialog-post-head');
|
||||||
const av = el('span', 'dialog-avatar');
|
const av = el('span', 'dialog-avatar');
|
||||||
if (c.author_avatar) av.style.backgroundImage = 'url(' + c.author_avatar + ')';
|
if (c.author_avatar) av.style.backgroundImage = 'url(' + c.author_avatar + ')';
|
||||||
else av.textContent = (c.author_name || '?').slice(0, 1).toUpperCase();
|
else av.textContent = (c.author_name || '?').slice(0, 1).toUpperCase();
|
||||||
const meta = el('div', 'dialog-meta');
|
const ident = el('div', 'dialog-ident');
|
||||||
meta.append(el('span', 'dialog-name', c.author_name || 'Unbekannt'), el('time', 'dialog-time', fmt(c.created_at)));
|
const nameline = el('div', 'dialog-nameline');
|
||||||
if (c.parent_id && names[c.parent_id]) meta.appendChild(el('span', 'dialog-replyto', '↳ ' + names[c.parent_id]));
|
nameline.appendChild(el('span', 'dialog-name', c.author_name || 'Unbekannt'));
|
||||||
head.append(av, meta); card.appendChild(head);
|
if (c.author_role) nameline.appendChild(el('span', 'dialog-pos', c.author_role));
|
||||||
card.appendChild(el('div', 'dialog-body', c.body));
|
if (c.parent_id && names[c.parent_id]) nameline.appendChild(el('span', 'dialog-replyto', '↳ ' + names[c.parent_id]));
|
||||||
|
ident.append(nameline, el('time', 'dialog-time', fmtFull(c.created_at)));
|
||||||
|
head.append(av, ident); post.appendChild(head);
|
||||||
|
post.appendChild(el('div', 'dialog-body', c.body));
|
||||||
if (token && !c.deleted) {
|
if (token && !c.deleted) {
|
||||||
const actions = el('div', 'dialog-actions');
|
const actions = el('div', 'dialog-actions');
|
||||||
if (!locked) { const rep = el('button', null, 'Antworten'); rep.onclick = () => { replyTo = { id: c.id, name: c.author_name }; renderComposer(); if (textarea) textarea.focus(); }; actions.appendChild(rep); }
|
if (!locked) { const rep = el('button', null, 'Antworten'); rep.onclick = () => { replyTo = { id: c.id, name: c.author_name }; renderComposer(); if (textarea) textarea.focus(); }; actions.appendChild(rep); }
|
||||||
const del = el('button', null, 'Löschen'); del.onclick = () => remove(c.id); actions.appendChild(del);
|
const del = el('button', null, 'Löschen'); del.onclick = () => remove(c.id); actions.appendChild(del);
|
||||||
card.appendChild(actions);
|
post.appendChild(actions);
|
||||||
}
|
}
|
||||||
list.appendChild(card);
|
list.appendChild(post);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user