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
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
25$gotoId = (int)get('goto');
26if ($gotoId) {
27
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
36 $redirectUrl = u('forum/topic.php?slug='.urlencode($slug).'&page='.$targetPage).'#post-'.$gotoId;
37 header('Location: ' . $redirectUrl);
38 exit;
39 }
40
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
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
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">
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)), '/')) ?>&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'] ?>">
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
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. Paste or drag images to embed. 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 & 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'; ?>