adfs
All checks were successful
Deploy / deploy (push) Successful in 47s

This commit is contained in:
2026-07-18 01:04:18 +02:00
parent e39a483aff
commit 57e197755e
4 changed files with 246 additions and 54 deletions

View File

@@ -9,6 +9,7 @@ $search = trim((string)($_GET['q'] ?? ''));
$communityCfg = require __DIR__ . '/../../../config/community.php'; $communityCfg = require __DIR__ . '/../../../config/community.php';
$community = $pdo ? new \App\Community($pdo, $communityCfg) : null; $community = $pdo ? new \App\Community($pdo, $communityCfg) : null;
$stats = $community ? $community->getStats() : ['threads' => 0, 'posts' => 0, 'members' => 0];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'thread_create') { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'thread_create') {
if (!$userId) { if (!$userId) {
@@ -29,50 +30,100 @@ $threads = $community ? ($search !== '' ? $community->searchThreads($search, 50)
?> ?>
<main class="section"> <main class="section">
<div class="container"> <div class="container">
<p class="eyebrow">Community</p> <div class="forum-hero">
<h1>Forum</h1> <div>
<div class="forum-breadcrumbs">
<a href="/">Home</a>
<span>/</span>
<span>Community</span>
</div>
<h1>Community</h1>
<p class="forum-hero__copy">Fragen stellen, Erfahrungen teilen, Antworten finden und gemeinsam weiterdenken.</p>
</div>
<div class="forum-hero__actions">
<?php if ($userId): ?>
<button class="btn" type="button" data-modal-open="modalThread">Neues Thema</button>
<?php else: ?>
<a class="btn" href="/login">Einloggen und schreiben</a>
<?php endif; ?>
</div>
</div>
<?php if ($error): ?> <?php if ($error): ?>
<div class="toast-bar" style="border-color:#f87171; color:#991b1b;"><?= htmlspecialchars($error, ENT_QUOTES) ?></div> <div class="toast-bar" style="border-color:#f87171; color:#991b1b;"><?= htmlspecialchars($error, ENT_QUOTES) ?></div>
<?php endif; ?> <?php endif; ?>
<div class="flex between center-y" style="margin:14px 0; gap:12px; flex-wrap:wrap;"> <div class="forum-stats">
<form method="get" class="flex gap-8" style="flex-wrap:wrap; align-items:flex-end;"> <div class="forum-stat">
<div class="stack gap-4"> <span class="forum-stat__label">Themen</span>
<label class="label" for="searchQ">Suche nach Stichwort</label> <strong><?= number_format($stats['threads']) ?></strong>
<input id="searchQ" name="q" class="input" value="<?= htmlspecialchars($search, ENT_QUOTES) ?>" placeholder="Schlagwort eingeben"> </div>
</div> <div class="forum-stat">
<button class="btn" type="submit">Suchen</button> <span class="forum-stat__label">Beiträge</span>
</form> <strong><?= number_format($stats['posts']) ?></strong>
<?php if ($userId): ?> </div>
<button class="btn" type="button" data-modal-open="modalThread">Neue Frage</button> <div class="forum-stat">
<?php else: ?> <span class="forum-stat__label">Mitglieder</span>
<a class="btn ghost" href="/login">Login für neue Frage</a> <strong><?= number_format($stats['members']) ?></strong>
<?php endif; ?> </div>
</div> </div>
<div class="stack gap-8" style="margin-top:10px;"> <div class="forum-board">
<?php foreach ($threads as $t): ?> <div class="forum-board__head">
<?php <div>
$pts = ($community && $pdo) ? $community->computePoints((int)$t['uid']) : 0.0; <h2>Forum</h2>
$lvl = $community ? $community->membershipLevel($pts) : ['label'=>'','icon'=>'']; <p class="muted">Hier findest du die neuesten Themen, Antworten und laufenden Gespräche.</p>
?> </div>
<article class="card" style="padding:14px;"> <form method="get" class="forum-search">
<div class="event__body"> <label class="label" for="searchQ">Suche</label>
<div class="event__meta" style="flex-wrap:wrap; gap:8px;"> <div class="forum-search__row">
<span><?= htmlspecialchars($t['created_at'], ENT_QUOTES) ?></span> <input id="searchQ" name="q" class="input" value="<?= htmlspecialchars($search, ENT_QUOTES) ?>" placeholder="Schlagwort eingeben">
<span><?= htmlspecialchars($t['display_name'] ?: 'Mitglied', ENT_QUOTES) ?></span> <button class="btn" type="submit">Suchen</button>
<span><?= htmlspecialchars($lvl['icon'] ?? '', ENT_QUOTES) ?> <?= htmlspecialchars($lvl['label'] ?? '', ENT_QUOTES) ?> (<?= number_format($pts,1) ?> Punkte)</span>
<span>Beiträge: <?= (int)$t['user_posts'] + (int)$t['answers'] ?></span>
<span>Antworten: <?= (int)$t['answers'] ?></span>
</div>
<h3 style="margin:6px 0;"><a href="/community_thread?id=<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['title'], ENT_QUOTES) ?></a></h3>
<p class="muted small"><?= nl2br(htmlspecialchars(substr($t['body'], 0, 200), ENT_QUOTES)) ?><?= strlen($t['body']) > 200 ? '…' : '' ?></p>
</div> </div>
</article> </form>
<?php endforeach; ?> </div>
<?php if (!$threads): ?>
<p class="muted">Keine Treffer.</p> <div class="forum-list">
<?php endif; ?> <div class="forum-list__header">
<span>Thema</span>
<span>Antworten</span>
<span>Letzte Aktivität</span>
</div>
<?php foreach ($threads as $t): ?>
<?php
$pts = ($community && $pdo) ? $community->computePoints((int)$t['uid']) : 0.0;
$lvl = $community ? $community->membershipLevel($pts) : ['label'=>'','icon'=>''];
$preview = mb_substr((string)$t['body'], 0, 220);
?>
<article class="forum-row">
<div class="forum-row__topic">
<div class="forum-row__icon" aria-hidden="true">💬</div>
<div>
<h3><a href="/community_thread?id=<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['title'], ENT_QUOTES) ?></a></h3>
<div class="forum-row__meta">
<span><?= htmlspecialchars($t['display_name'] ?: 'Mitglied', ENT_QUOTES) ?></span>
<span><?= htmlspecialchars($lvl['icon'] ?? '', ENT_QUOTES) ?> <?= htmlspecialchars($lvl['label'] ?? '', ENT_QUOTES) ?></span>
<span><?= number_format($pts, 1) ?> Punkte</span>
</div>
<p><?= nl2br(htmlspecialchars($preview, ENT_QUOTES)) ?><?= mb_strlen((string)$t['body']) > 220 ? '…' : '' ?></p>
</div>
</div>
<div class="forum-row__count">
<strong><?= (int)$t['answers'] ?></strong>
<span>Antworten</span>
</div>
<div class="forum-row__activity">
<strong><?= htmlspecialchars($t['last_activity_by'] ?: 'Mitglied', ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars($t['last_activity_at'], ENT_QUOTES) ?></span>
</div>
</article>
<?php endforeach; ?>
<?php if (!$threads): ?>
<div class="forum-empty">Keine Themen gefunden.</div>
<?php endif; ?>
</div>
</div> </div>
</div> </div>
</main> </main>

View File

@@ -32,33 +32,77 @@ if (!$thread) {
echo "<p>Thread nicht gefunden.</p>"; echo "<p>Thread nicht gefunden.</p>";
return; return;
} }
$threadAuthor = (string)($thread['display_name'] ?: 'Mitglied');
$threadPoints = ($community && $pdo) ? $community->computePoints((int)$thread['user_id']) : 0.0;
$threadLevel = $community ? $community->membershipLevel($threadPoints) : ['label' => '', 'icon' => ''];
?> ?>
<main class="section"> <main class="section">
<div class="container"> <div class="container">
<p class="eyebrow">Community</p> <div class="forum-breadcrumbs">
<h1><?= htmlspecialchars($thread['title'], ENT_QUOTES) ?></h1> <a href="/">Home</a>
<p class="muted">Von <?= htmlspecialchars($thread['display_name'] ?: 'Mitglied', ENT_QUOTES) ?> · <?= htmlspecialchars($thread['created_at'], ENT_QUOTES) ?></p> <span>/</span>
<article class="card" style="margin-top:12px;"> <a href="/community">Community</a>
<div class="event__body"> <span>/</span>
<p><?= nl2br(htmlspecialchars($thread['body'], ENT_QUOTES)) ?></p> <span><?= htmlspecialchars($thread['title'], ENT_QUOTES) ?></span>
</div>
<div class="forum-thread-head">
<div>
<h1><?= htmlspecialchars($thread['title'], ENT_QUOTES) ?></h1>
<p class="forum-thread-head__meta">Erstellt von <?= htmlspecialchars($threadAuthor, ENT_QUOTES) ?> am <?= htmlspecialchars($thread['created_at'], ENT_QUOTES) ?></p>
</div>
<div class="forum-thread-head__meta-box">
<strong><?= count($posts) ?></strong>
<span>Antworten</span>
</div>
</div>
<article class="forum-post forum-post--lead">
<aside class="forum-post__author">
<div class="forum-post__avatar" aria-hidden="true"><?= strtoupper(mb_substr($threadAuthor, 0, 1)) ?></div>
<strong><?= htmlspecialchars($threadAuthor, ENT_QUOTES) ?></strong>
<span><?= htmlspecialchars($threadLevel['icon'] ?? '', ENT_QUOTES) ?> <?= htmlspecialchars($threadLevel['label'] ?? '', ENT_QUOTES) ?></span>
<span><?= number_format($threadPoints, 1) ?> Punkte</span>
</aside>
<div class="forum-post__body">
<div class="forum-post__head">
<span><?= htmlspecialchars($thread['created_at'], ENT_QUOTES) ?></span>
<span>#1</span>
</div>
<div class="forum-post__content">
<?= nl2br(htmlspecialchars($thread['body'], ENT_QUOTES)) ?>
</div>
</div> </div>
</article> </article>
<h3 style="margin-top:16px;">Antworten (<?= count($posts) ?>)</h3> <div class="forum-replies-head">
<div class="stack gap-12" style="margin-top:10px;"> <h2>Antworten</h2>
<span><?= count($posts) ?> Beitrag/Beiträge</span>
</div>
<div class="forum-replies">
<?php foreach ($posts as $p): ?> <?php foreach ($posts as $p): ?>
<article class="card"> <?php
<div class="event__body"> $postAuthor = (string)($p['display_name'] ?: 'Mitglied');
<div class="event__meta"> ?>
<article class="forum-post">
<aside class="forum-post__author">
<div class="forum-post__avatar" aria-hidden="true"><?= strtoupper(mb_substr($postAuthor, 0, 1)) ?></div>
<strong><?= htmlspecialchars($postAuthor, ENT_QUOTES) ?></strong>
</aside>
<div class="forum-post__body">
<div class="forum-post__head">
<span><?= htmlspecialchars($p['created_at'], ENT_QUOTES) ?></span> <span><?= htmlspecialchars($p['created_at'], ENT_QUOTES) ?></span>
<span><?= htmlspecialchars($p['display_name'] ?: 'Mitglied', ENT_QUOTES) ?></span>
</div> </div>
<p><?= nl2br(htmlspecialchars($p['body'], ENT_QUOTES)) ?></p> <div class="forum-post__content">
<?= nl2br(htmlspecialchars($p['body'], ENT_QUOTES)) ?>
</div>
</div> </div>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (!$posts): ?> <?php if (!$posts): ?>
<p class="muted">Noch keine Antworten.</p> <div class="forum-empty">Noch keine Antworten.</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -67,7 +111,7 @@ if (!$thread) {
<?php endif; ?> <?php endif; ?>
<?php if ($userId): ?> <?php if ($userId): ?>
<form method="post" class="stack gap-12 card" style="margin-top:14px; padding:16px;"> <form method="post" class="forum-reply-form">
<input type="hidden" name="action" value="reply"> <input type="hidden" name="action" value="reply">
<div class="stack gap-6"> <div class="stack gap-6">
<label class="label" for="replyBody">Antwort</label> <label class="label" for="replyBody">Antwort</label>
@@ -76,7 +120,7 @@ if (!$thread) {
<button class="btn" type="submit">Antwort senden</button> <button class="btn" type="submit">Antwort senden</button>
</form> </form>
<?php else: ?> <?php else: ?>
<p class="muted" style="margin-top:12px;">Bitte <a href="/login">einloggen</a>, um zu antworten.</p> <div class="forum-login-note">Bitte <a href="/login">einloggen</a>, um zu antworten.</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</main> </main>

View File

@@ -148,6 +148,55 @@ body {
.page-copy p { margin: 0; font-size: 18px; line-height: 1.7; color: var(--color-text); } .page-copy p { margin: 0; font-size: 18px; line-height: 1.7; color: var(--color-text); }
.section__intro p { margin: 0; font-size: 18px; line-height: 1.7; color: var(--color-text); text-align: left; } .section__intro p { margin: 0; font-size: 18px; line-height: 1.7; color: var(--color-text); text-align: left; }
.section__intro p + p { margin-top: 18px; } .section__intro p + p { margin-top: 18px; }
.forum-breadcrumbs { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; color: var(--color-muted); font-size: 14px; margin-bottom: 16px; }
.forum-breadcrumbs a:hover { color: var(--color-primary); }
.forum-hero { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; padding: 26px 28px; background: linear-gradient(135deg, #fffdf8, #f8f2e7); border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); }
.forum-hero h1 { margin: 6px 0 8px; }
.forum-hero__copy { margin: 0; max-width: 70ch; color: var(--color-muted); font-size: 16px; }
.forum-hero__actions { display: flex; align-items: center; gap: 12px; }
.forum-stats { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 14px; margin: 18px 0 24px; }
.forum-stat { padding: 18px 20px; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-card); }
.forum-stat strong { display: block; font-size: 28px; line-height: 1.1; }
.forum-stat__label { display: block; margin-bottom: 8px; color: var(--color-muted); font-size: 13px; text-transform: uppercase; letter-spacing: .4px; }
.forum-board { background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; }
.forum-board__head { display: flex; justify-content: space-between; gap: 20px; align-items: end; padding: 22px 24px; border-bottom: 1px solid var(--color-border); background: #fffdfa; }
.forum-board__head h2 { margin: 0 0 6px; }
.forum-search { display: grid; gap: 8px; min-width: min(100%, 360px); }
.forum-search__row { display: flex; gap: 10px; }
.forum-list__header { display: grid; grid-template-columns: minmax(0, 1fr) 120px 220px; gap: 16px; padding: 14px 24px; background: #f8f4ec; color: var(--color-muted); font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .35px; }
.forum-row { display: grid; grid-template-columns: minmax(0, 1fr) 120px 220px; gap: 16px; padding: 20px 24px; border-top: 1px solid var(--color-border); align-items: start; }
.forum-row__topic { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 14px; }
.forum-row__icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: var(--color-accent-soft); font-size: 18px; }
.forum-row__topic h3 { margin: 0 0 8px; font-size: 20px; line-height: 1.2; }
.forum-row__topic h3 a:hover { color: var(--color-primary); }
.forum-row__meta { display: flex; flex-wrap: wrap; gap: 8px 12px; color: var(--color-muted); font-size: 13px; margin-bottom: 8px; }
.forum-row__topic p { margin: 0; color: var(--color-text); line-height: 1.55; }
.forum-row__count, .forum-row__activity { display: grid; gap: 4px; align-content: start; }
.forum-row__count strong, .forum-row__activity strong { font-size: 18px; line-height: 1.1; }
.forum-row__count span, .forum-row__activity span { color: var(--color-muted); font-size: 13px; }
.forum-empty { padding: 28px 24px; color: var(--color-muted); }
.forum-thread-head { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; margin-bottom: 18px; }
.forum-thread-head h1 { margin: 0 0 8px; }
.forum-thread-head__meta { margin: 0; color: var(--color-muted); }
.forum-thread-head__meta-box { min-width: 120px; display: grid; gap: 4px; padding: 16px 18px; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-md); text-align: center; box-shadow: var(--shadow-card); }
.forum-thread-head__meta-box strong { font-size: 28px; line-height: 1; }
.forum-thread-head__meta-box span { color: var(--color-muted); font-size: 13px; text-transform: uppercase; letter-spacing: .35px; }
.forum-post { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 0; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; }
.forum-post + .forum-post { margin-top: 14px; }
.forum-post--lead { margin-bottom: 24px; }
.forum-post__author { display: grid; gap: 8px; align-content: start; padding: 22px 18px; background: #f8f4ec; border-right: 1px solid var(--color-border); }
.forum-post__avatar { width: 52px; height: 52px; border-radius: 16px; display: flex; align-items: center; justify-content: center; background: var(--color-primary); color: #fff; font-size: 22px; font-weight: 700; }
.forum-post__author strong { font-size: 17px; }
.forum-post__author span { color: var(--color-muted); font-size: 14px; }
.forum-post__body { padding: 20px 24px; }
.forum-post__head { display: flex; justify-content: space-between; gap: 12px; color: var(--color-muted); font-size: 14px; margin-bottom: 14px; }
.forum-post__content { font-size: 17px; line-height: 1.7; color: var(--color-text); }
.forum-replies-head { display: flex; justify-content: space-between; gap: 16px; align-items: center; margin: 0 0 14px; }
.forum-replies-head h2 { margin: 0; }
.forum-replies-head span { color: var(--color-muted); }
.forum-replies { display: grid; gap: 0; }
.forum-reply-form { display: grid; gap: 14px; margin-top: 18px; padding: 22px 24px; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); }
.forum-login-note { margin-top: 18px; color: var(--color-muted); }
.flex { display:flex; } .flex { display:flex; }
.between { justify-content: space-between; } .between { justify-content: space-between; }
@@ -155,10 +204,28 @@ body {
.gap-12 { gap: 12px; } .gap-12 { gap: 12px; }
.stack { display:flex; flex-direction: column; } .stack { display:flex; flex-direction: column; }
@media (max-width: 980px){
.forum-list__header,
.forum-row { grid-template-columns: minmax(0, 1fr); }
.forum-row__count,
.forum-row__activity { grid-auto-flow: column; justify-content: start; gap: 10px; align-items: baseline; }
.forum-post { grid-template-columns: 1fr; }
.forum-post__author { border-right: 0; border-bottom: 1px solid var(--color-border); }
}
@media (max-width: 720px){ @media (max-width: 720px){
.nav-row { padding: 12px 0; } .nav-row { padding: 12px 0; }
.hero { padding: 40px 0; } .hero { padding: 40px 0; }
.section { padding: 48px 0; } .section { padding: 48px 0; }
.forum-hero,
.forum-board__head,
.forum-thread-head { grid-template-columns: 1fr; display: grid; }
.forum-stats { grid-template-columns: 1fr; }
.forum-search__row { flex-direction: column; }
.forum-row,
.forum-list__header { padding-left: 16px; padding-right: 16px; }
.forum-post__body,
.forum-reply-form { padding: 18px 16px; }
} }
/* Auth & Dashboard */ /* Auth & Dashboard */

View File

@@ -9,6 +9,19 @@ final class Community
{ {
} }
public function getStats(): array
{
$threads = (int)$this->pdo->query('SELECT COUNT(*) FROM forum_threads')->fetchColumn();
$posts = (int)$this->pdo->query('SELECT COUNT(*) FROM forum_posts')->fetchColumn();
$members = (int)$this->pdo->query('SELECT COUNT(*) FROM users WHERE status = "active"')->fetchColumn();
return [
'threads' => $threads,
'posts' => $posts,
'members' => $members,
];
}
public function createThread(int $userId, string $title, string $body): void public function createThread(int $userId, string $title, string $body): void
{ {
$stmt = $this->pdo->prepare('INSERT INTO forum_threads (user_id, title, body) VALUES (:uid, :title, :body)'); $stmt = $this->pdo->prepare('INSERT INTO forum_threads (user_id, title, body) VALUES (:uid, :title, :body)');
@@ -45,10 +58,27 @@ final class Community
} }
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : ''; $where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
$sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, $sql = "SELECT ft.id, ft.title, ft.body, ft.created_at, ft.updated_at,
u.id as uid, u.created_at as user_created, u.id as uid, u.created_at as user_created,
p.display_name, p.display_name,
(SELECT COUNT(*) FROM forum_posts fp WHERE fp.thread_id = ft.id) AS answers, (SELECT COUNT(*) FROM forum_posts fp WHERE fp.thread_id = ft.id) AS answers,
COALESCE(
(SELECT MAX(fp.created_at) FROM forum_posts fp WHERE fp.thread_id = ft.id),
ft.created_at
) AS last_activity_at,
COALESCE(
(
SELECT p2.display_name
FROM forum_posts fp3
JOIN users u2 ON u2.id = fp3.user_id
LEFT JOIN user_profiles p2 ON p2.user_id = u2.id
WHERE fp3.thread_id = ft.id
ORDER BY fp3.created_at DESC
LIMIT 1
),
p.display_name,
'Mitglied'
) AS last_activity_by,
(SELECT COUNT(*) FROM forum_posts fp2 WHERE fp2.user_id = u.id) + (SELECT COUNT(*) FROM forum_posts fp2 WHERE fp2.user_id = u.id) +
(SELECT COUNT(*) FROM forum_threads ft2 WHERE ft2.user_id = u.id) AS user_posts (SELECT COUNT(*) FROM forum_threads ft2 WHERE ft2.user_id = u.id) AS user_posts
FROM forum_threads ft FROM forum_threads ft