xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

forum/topic.php


1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3 
4$slug  = get('slug');
5$topic = DB::row("
6    SELECT t.*, u.username, u.avatar,
7           c.name AS cat_name, c.slug AS cat_slug, c.color AS cat_color
8    FROM topics t
9    JOIN users u ON u.id = t.user_id
10    JOIN categories c ON c.id = t.category_id
11    WHERE t.slug = ?
12", [$slug]);
13if (!$topic) render_404();
14 
15// Fetch full category for permission checks
16$topicCat = DB::row('SELECT * FROM categories WHERE id=?', [$topic['category_id']]);
17if (!can_read_category($topicCat ?? [])) render_403();
18 
19DB::run('UPDATE topics SET views = views + 1 WHERE id = ?', [$topic['id']]);
20 
21$page = max(1, (int)get('page','1'));
22$pp   = max(1, (int)cfg('posts_per_page','20'));
23 
24// ?goto=POST_ID β€” jump directly to a specific post, calculating its page
25$gotoId = (int)get('goto');
26if ($gotoId) {
27    // Find what position this post is in the thread (0-indexed)
28    $postPos = (int)DB::val(
29        'SELECT COUNT(*) FROM posts WHERE topic_id=? AND deleted=0 AND id<=?',
30        [$topic['id'], $gotoId]
31    );
32    if ($postPos > 0) {
33        $targetPage = (int)ceil($postPos / $pp);
34        if ($targetPage !== $page) {
35            // Redirect to the correct page with the anchor
36            $redirectUrl = u('forum/topic.php?slug='.urlencode($slug).'&page='.$targetPage).'#post-'.$gotoId;
37            header('Location: ' . $redirectUrl);
38            exit;
39        }
40        // Already on the right page β€” anchor will handle scrolling
41        $page = $targetPage;
42    }
43}
44 
45$off  = ($page - 1) * $pp;
46 
47$posts = DB::rows("
48    SELECT p.*, u.username, u.avatar, u.role, u.post_count, u.karma, u.location,
49           (SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) AS likes
50    FROM posts p
51    JOIN users u ON u.id = p.user_id
52    WHERE p.topic_id = ? AND p.deleted = 0
53    ORDER BY p.post_num
54    LIMIT ? OFFSET ?
55", [$topic['id'], $pp, $off]);
56 
57$total = (int)DB::val('SELECT COUNT(*) FROM posts WHERE topic_id=? AND deleted=0', [$topic['id']]);
58$pages = max(1, (int)ceil($total / $pp));
59 
60$tags = DB::rows("
61    SELECT tg.* FROM tags tg
62    JOIN topic_tags tt ON tg.id = tt.tag_id
63    WHERE tt.topic_id = ?
64", [$topic['id']]);
65 
66$liked = [];
67if ($USER) {
68    foreach (DB::rows('SELECT post_id FROM likes WHERE user_id=?', [$USER['id']]) as $l)
69        $liked[$l['post_id']] = true;
70}
71 
72// Post captcha for reply box
73$post_cap = post_captcha_enabled() ? post_captcha_generate() : null;
74 
75$PAGE_TITLE = $topic['title'];
76include __DIR__ . '/../views/partials/layout.php';
77?>
78 
79<nav class="bc">
80  <a href="<?= u('/') ?>">Home</a> β€Ί
81  <a href="<?= u('forum/category.php?slug='.urlencode($topic['cat_slug'])) ?>"><?= e($topic['cat_name']) ?></a> β€Ί
82  <span><?= e(mb_substr($topic['title'],0,60)) ?></span>
83</nav>
84 
85<!-- Topic header -->
86<div class="topic-hdr">
87  <div class="topic-hdr-main">
88    <div class="topic-badges">
89      <?php if ($topic['pinned']): ?><span class="tbadge pin">πŸ“Œ Pinned</span><?php endif; ?>
90      <?php if ($topic['closed']): ?><span class="tbadge closed">πŸ”’ Closed</span><?php endif; ?>
91    </div>
92    <h1 class="topic-title"><?= e($topic['title']) ?></h1>
93    <div class="topic-meta">
94      <a href="<?= u('forum/category.php?slug='.urlencode($topic['cat_slug'])) ?>"
95         class="cat-tag" style="--cc:<?= e($topic['cat_color']) ?>"><?= e($topic['cat_name']) ?></a>
96      <?php foreach ($tags as $tg): ?>
97        <span class="tag"><?= e($tg['name']) ?></span>
98      <?php endforeach; ?>
99      <span>Β·</span>
100      <span><?= max(0, $total - 1) ?> <?= $total === 2 ? 'reply' : 'replies' ?></span>
101      <span>Β·</span>
102      <span><?= number_format($topic['views']) ?> views</span>
103    </div>
104  </div>
105  <?php if ($USER && is_admin()): ?>
106    <div class="topic-mod">
107      <?php foreach ([
108        [$topic['pinned'],   'unpin',    'pin',       'πŸ“Œ', $topic['pinned']   ? 'Unpin'   : 'Pin'],
109        [$topic['closed'],   'open',     'close',     'πŸ”’', $topic['closed']   ? 'Open'    : 'Close'],
110        [$topic['archived'], 'unarchive','archive',   'πŸ“¦', $topic['archived'] ? 'Unarchive': 'Archive'],
111      ] as [$state, $onAct, $offAct, $icon, $label]): ?>
112        <form method="POST" action="<?= u('api/topic_action.php') ?>" style="display:inline">
113          <?= csrf_input() ?>
114          <input type="hidden" name="id"     value="<?= $topic['id'] ?>">
115          <input type="hidden" name="action" value="<?= $state ? $onAct : $offAct ?>">
116          <button class="btn-sm btn-ghost"><?= $icon ?> <?= $label ?></button>
117        </form>
118      <?php endforeach; ?>
119    </div>
120  <?php endif; ?>
121</div>
122 
123<?php
124/* ── render_topic_header addon hook (collector) ── */
125echo addon_collect('render_topic_header', $topic);
126?>
127 
128<!-- Posts list -->
129<?php $postLayoutH = cfg('post_layout_horizontal','0') === '1'; ?>
130<div id="postsList" class="<?= $postLayoutH ? 'posts-horizontal' : '' ?>">
131<?php foreach ($posts as $p): ?>
132  <div class="post <?= $p['post_num'] == 1 ? 'post-op' : '' ?><?= $postLayoutH ? ' post-h' : '' ?>" id="post-<?= $p['id'] ?>">
133 
134    <!-- Author sidebar -->
135    <div class="post-side">
136      <?php if ($p['avatar']): ?>
137        <img src="<?= e($p['avatar']) ?>" class="av-lg" alt="">
138      <?php else: ?>
139        <span class="av-lg av-init"><?= strtoupper($p['username'][0]) ?></span>
140      <?php endif; ?>
141      <a href="<?= u('users/profile.php?u='.urlencode($p['username'])) ?>" class="post-name">
142        @<?= e($p['username']) ?>
143      </a>
144      <?php if ($p['role'] === 'admin'):     ?><span class="role-flair admin">Admin</span><?php endif; ?>
145      <?php if ($p['role'] === 'moderator'): ?><span class="role-flair mod">Mod</span><?php endif; ?>
146      <span class="post-pcnt"><?= $p['post_count'] ?> posts</span>
147      <?php if (!empty($p['location'])): ?>
148        <span class="post-location" title="Location">
149          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="9" height="9"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
150          <?= e(mb_substr($p['location'],0,20)) ?>
151        </span>
152      <?php endif; ?>
153      <div class="post-karma-badge">
154        <span class="pkb-icon">
155          <svg viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="10" height="10"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>
156        </span>
157        <span class="pkb-val"><?= number_format((int)$p['karma']) ?></span>
158      </div>
159    </div>
160 
161    <!-- Post body -->
162    <div class="post-body">
163      <div class="post-meta-bar">
164        <a class="pnum" href="#post-<?= $p['id'] ?>" title="Permalink to this post">#<?= $p['post_num'] ?></a>
165        <button class="post-link-btn" title="Copy link to this post"
166                onclick="copyPostLink('<?= $p['id'] ?>',this)"
167                data-url="<?= e(rtrim(u('forum/topic.php?slug='.urlencode($slug)), '/')) ?>&amp;goto=<?= $p['id'] ?>">
168          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
169            <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
170            <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
171          </svg>
172        </button>
173        <time class="ago" data-ts="<?= e($p['created_at']) ?>"></time>
174        <?php if ($p['edited']): ?>
175          <em class="edit-lbl" title="<?= e($p['edit_reason'] ?? '') ?>">(edited)</em>
176        <?php endif; ?>
177        <div class="post-acts">
178          <?php if ($USER): ?>
179            <button class="pa-btn like-btn <?= isset($liked[$p['id']]) ? 'liked' : '' ?>"
180                    onclick="doLike(<?= $p['id'] ?>,this)">
181              β™₯ <span class="lc"><?= $p['likes'] ?></span>
182            </button>
183            <?php if (!$topic['closed']): ?>
184              <button class="pa-btn" onclick="doQuote(<?= $p['id'] ?>,'<?= e($p['username']) ?>')">↩ Quote</button>
185            <?php endif; ?>
186            <?php if ($USER['id'] == $p['user_id'] || is_admin()): ?>
187              <button class="pa-btn" onclick="doEdit(<?= $p['id'] ?>)">✏ Edit</button>
188              <button class="pa-btn del" onclick="doDelete(<?= $p['id'] ?>)">πŸ—‘ Delete</button>
189            <?php endif; ?>
190          <?php endif; ?>
191        </div>
192      </div>
193 
194      <?php if ($p['reply_to']): ?>
195        <div class="reply-ref">
196          ↩ In reply to <a href="#post-<?= $p['reply_to'] ?>">#<?= $p['reply_to'] ?></a>
197        </div>
198      <?php endif; ?>
199 
200      <!-- Rendered content β€” server-side markdown + embeds -->
201      <div class="post-content rendered-post" id="pc-<?= $p['id'] ?>"
202           data-raw="<?= e(base64_encode($p['content'])) ?>">
203        <?= addon_hook('render_post_content', render_post($p['content'])) ?>
204      </div>
205 
206      <?php /* ── render_post_footer addon hook (collector) ── */
207      echo addon_collect('render_post_footer', $p);
208      ?>
209 
210      <!-- Edit box β€” hidden by default, toggled by doEdit() -->
211      <div class="edit-box" id="eb-<?= $p['id'] ?>">
212        <div class="ed-toolbar">
213          <button type="button" onclick="fmt('bold','et-<?= $p['id'] ?>')"><b>B</b></button>
214          <button type="button" onclick="fmt('italic','et-<?= $p['id'] ?>')"><i>I</i></button>
215          <button type="button" onclick="fmt('quote','et-<?= $p['id'] ?>')" title="Quote"><svg viewBox="0 0 24 24" fill="currentColor" width="13" height="13" style="vertical-align:middle"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg></button>
216          <button type="button" onclick="pickImg('pi-<?= $p['id'] ?>','et-<?= $p['id'] ?>')">πŸ–Ό</button>
217          <input type="file" id="pi-<?= $p['id'] ?>" accept="image/*" style="display:none"
218                 onchange="uploadImg(this,'et-<?= $p['id'] ?>')">
219        </div>
220        <textarea id="et-<?= $p['id'] ?>" class="edit-ta fi" rows="7"
221                  placeholder="Edit your post…"></textarea>
222        <input type="text" id="er-<?= $p['id'] ?>" class="fi edit-reason-inp"
223               placeholder="Edit reason (optional)" style="margin-top:6px">
224        <div class="edit-btns">
225          <button class="btn-ghost btn-sm" onclick="cancelEdit(<?= $p['id'] ?>)">Cancel</button>
226          <button class="btn-primary btn-sm" onclick="saveEdit(<?= $p['id'] ?>)">Save Changes</button>
227        </div>
228      </div>
229 
230    </div>
231  </div>
232<?php endforeach; ?>
233</div>
234 
235<!-- Pagination -->
236<?php if ($pages > 1): ?>
237  <nav class="pager">
238    <?php if ($page > 1): ?>
239      <a href="?slug=<?= urlencode($slug) ?>&page=<?= $page-1 ?>" class="pg-btn">← Prev</a>
240    <?php endif; ?>
241    <?php for ($i = max(1,$page-2); $i <= min($pages,$page+2); $i++): ?>
242      <a href="?slug=<?= urlencode($slug) ?>&page=<?= $i ?>"
243         class="pg-btn <?= $i===$page?'active':'' ?>"><?= $i ?></a>
244    <?php endfor; ?>
245    <?php if ($page < $pages): ?>
246      <a href="?slug=<?= urlencode($slug) ?>&page=<?= $page+1 ?>" class="pg-btn">Next β†’</a>
247    <?php endif; ?>
248  </nav>
249<?php endif; ?>
250 
251<!-- Reply box -->
252<?php if ($USER && !$topic['closed']): ?>
253  <div class="reply-box" id="replyBox">
254    <div class="reply-hdr">
255      <?php if ($USER['avatar']): ?>
256        <img src="<?= e($USER['avatar']) ?>" class="av-md" alt="">
257      <?php else: ?>
258        <span class="av-md av-init"><?= strtoupper($USER['username'][0]) ?></span>
259      <?php endif; ?>
260      <span>Reply as <strong>@<?= e($USER['username']) ?></strong></span>
261    </div>
262 
263    <div class="ed-toolbar">
264      <button type="button" onclick="fmt('bold')"><b>B</b></button>
265      <button type="button" onclick="fmt('italic')"><i>I</i></button>
266      <button type="button" onclick="fmt('link')">πŸ”—</button>
267      <button type="button" onclick="fmt('quote')" title="Quote"><svg viewBox="0 0 24 24" fill="currentColor" width="13" height="13" style="vertical-align:middle"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg></button>
268      <button type="button" onclick="fmt('codeblock')">πŸ“„</button>
269      <button type="button" onclick="fmt('heading')">H</button>
270      <button type="button" onclick="fmt('ul')">≑</button>
271      <div class="ed-sep"></div>
272      <button type="button" onclick="pickImg('mainImg','replyTa')">πŸ–Ό</button>
273      <input type="file" id="mainImg" accept="image/*" style="display:none"
274             onchange="uploadImg(this,'replyTa')">
275      <div class="ed-sep"></div>
276      <button type="button" id="prevBtn" onclick="togglePreview()">πŸ‘ Preview</button>
277    </div>
278 
279    <div class="ed-panes">
280      <textarea id="replyTa" class="reply-ta"
281                placeholder="Write your reply… Markdown supported.&#10;Paste or drag images to embed.&#10;Type @username to mention someone."
282                rows="8"></textarea>
283      <div id="replyPreview" class="reply-preview hidden"></div>
284    </div>
285 
286    <div class="reply-footer">
287      <span class="hint">Markdown Β· Images Β· @mentions Β· Auto-embeds YouTube &amp; more</span>
288      <div class="reply-footer-right">
289        <span id="charCnt" class="cnt">0</span>
290        <button class="btn-primary" id="replyBtn"
291                onclick="sendReply('<?= e($topic['slug']) ?>')">Post Reply</button>
292      </div>
293    </div>
294 
295    <?php if ($post_cap): ?>
296    <div class="post-captcha-row" id="postCaptchaRow">
297      <span class="post-captcha-label">
298        πŸ”’ Security β€” What is <strong class="captcha-q"><?= e($post_cap['q']) ?></strong>
299      </span>
300      <input type="number" id="postCaptchaInput" class="fi post-captcha-inp"
301             placeholder="?" autocomplete="off" min="-99" max="99">
302      <span class="hint">Solve to post</span>
303    </div>
304    <?php endif; ?>
305 
306    <div class="rate-limit-info" id="rateLimitInfo" style="display:none">
307      <div class="rate-limit-bar">
308        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
309        <span id="rateLimitMsg">Please wait before posting again:</span>
310        <div class="rate-countdown" id="rateCountdown"></div>
311      </div>
312    </div>
313  </div>
314 
315<?php elseif (!$USER): ?>
316  <div class="reply-cta">
317    <p>Join the discussion!</p>
318    <a href="<?= u('auth/login.php') ?>" class="btn-ghost">Log In</a>
319    <a href="<?= u('auth/register.php') ?>" class="btn-primary">Sign Up to Reply</a>
320  </div>
321 
322<?php else: ?>
323  <div class="closed-notice">πŸ”’ This topic is closed to new replies.</div>
324<?php endif; ?>
325 
326<?php include __DIR__ . '/../views/partials/layout_end.php'; ?>