xgit simple git

nexus

nexus

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

forum/topic.php

1 <?php
2 require_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]);
13 if (!$topic) render_404();
14 
15 // Fetch full category for permission checks
16 $topicCat = DB::row('SELECT * FROM categories WHERE id=?', [$topic['category_id']]);
17 if (!can_read_category($topicCat ?? [])) render_403();
18 
19 DB::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');
26 if ($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 = [];
67 if ($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'];
76 include __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) ── */
125 echo 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'; ?>