1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_admin();
4$PAGE_TITLE = 'Categories';
5$ADMIN_PAGE = 'categories';
6$flash = null;
7
8
9function roleSelect(string $name, string $current, string $label): string {
10 $opts = [
11 'guest' => '๐ Everyone (guests)',
12 'member' => '๐ค Members only',
13 'moderator' => '๐ก๏ธ Moderators+',
14 'admin' => '๐ Admins only',
15 ];
16 $html = '<div class="fg" style="margin-bottom:8px">'
17 . '<label style="font-size:12px;color:var(--muted)">' . htmlspecialchars($label) . '</label>'
18 . '<select name="' . htmlspecialchars($name) . '" class="fi" style="padding:6px 8px;font-size:13px">';
19 foreach ($opts as $val => $lbl) {
20 $html .= '<option value="' . $val . '"' . ($current === $val ? ' selected' : '') . '>' . $lbl . '</option>';
21 }
22 return $html . '</select></div>';
23}
24
25if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_ok()) {
26 $action = post('action');
27 $validRoles = ['guest', 'member', 'moderator', 'admin'];
28
29 if ($action === 'create') {
30 $name = post('name');
31 if ($name) {
32 $readRole = in_array(post('read_role'), $validRoles) ? post('read_role') : 'guest';
33 $postRole = in_array(post('post_role'), $validRoles) ? post('post_role') : 'member';
34 $replyRole = in_array(post('reply_role'), $validRoles) ? post('reply_role') : 'member';
35 $slug = unique_slug($name, 'categories');
36 DB::insert(
37 'INSERT INTO categories (name,slug,description,color,icon,position,parent_id,read_role,post_role,reply_role)
38 VALUES (?,?,?,?,?,?,?,?,?,?)',
39 [$name, $slug, post('desc'), post('color', '#3b82f6'), post('icon', '๐ฌ'),
40 (int)post('pos', 99), post('parent_id') ?: null, $readRole, $postRole, $replyRole]
41 );
42 $flash = ['ok', 'Category created!'];
43 }
44 } elseif ($action === 'update') {
45 $readRole = in_array(post('read_role'), $validRoles) ? post('read_role') : 'guest';
46 $postRole = in_array(post('post_role'), $validRoles) ? post('post_role') : 'member';
47 $replyRole = in_array(post('reply_role'), $validRoles) ? post('reply_role') : 'member';
48 DB::run(
49 'UPDATE categories SET name=?,description=?,color=?,icon=?,position=?,read_role=?,post_role=?,reply_role=? WHERE id=?',
50 [post('name'), post('desc'), post('color', '#3b82f6'), post('icon', '๐ฌ'),
51 (int)post('pos'), $readRole, $postRole, $replyRole, (int)post('id')]
52 );
53 $flash = ['ok', 'Saved!'];
54 } elseif ($action === 'delete') {
55 $cid = (int)post('id');
56 $tc = (int)DB::val('SELECT COUNT(*) FROM topics WHERE category_id=?', [$cid]);
57 if ($tc > 0) {
58 $flash = ['err', 'Cannot delete a category that has topics.'];
59 } else {
60 DB::run('DELETE FROM categories WHERE id=?', [$cid]);
61 $flash = ['ok', 'Deleted.'];
62 }
63 }
64}
65
66$cats = DB::rows('SELECT * FROM categories ORDER BY position, id');
67include __DIR__ . '/../views/partials/admin_layout.php';
68?>
69
70<?php if ($flash): ?>
71 <div class="alert <?= $flash[0] ?>"><?= e($flash[1]) ?></div>
72<?php endif; ?>
73
74<div class="admin-two-col">
75
76 <!-- โโ Category list โโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
77 <div class="acard">
78 <div class="acard-head"><h2>All Categories</h2></div>
79 <table class="atable">
80 <thead>
81 <tr><th>Name</th><th>Permissions</th><th>Topics</th><th>Posts</th><th></th></tr>
82 </thead>
83 <tbody>
84 <?php foreach ($cats as $c): ?>
85 <tr id="cr-<?= $c['id'] ?>">
86 <td>
87 <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:<?= e($c['color']) ?>;margin-right:6px;vertical-align:middle"></span>
88 <?= e($c['icon']) ?> <?= e($c['name']) ?>
89 </td>
90 <td style="font-size:11px;color:var(--muted)">
91 Read: <strong><?= e($c['read_role'] ?? 'guest') ?></strong> ยท
92 Post: <strong><?= e($c['post_role'] ?? 'member') ?></strong> ยท
93 Reply: <strong><?= e($c['reply_role'] ?? 'member') ?></strong>
94 </td>
95 <td><?= $c['topic_count'] ?></td>
96 <td><?= $c['post_count'] ?></td>
97 <td style="white-space:nowrap">
98 <button class="btn-ghost btn-sm" onclick="toggleEdit(<?= $c['id'] ?>)">Edit</button>
99 <form method="POST" style="display:inline">
100 <?= csrf_input() ?>
101 <input type="hidden" name="action" value="delete">
102 <input type="hidden" name="id" value="<?= $c['id'] ?>">
103 <button class="btn-danger btn-sm" onclick="return confirm('Delete this category?')">Delete</button>
104 </form>
105 </td>
106 </tr>
107
108 <!-- Inline edit row -->
109 <tr id="ce-<?= $c['id'] ?>" style="display:none;background:#f8fafc">
110 <td colspan="5" style="padding:16px 18px">
111 <form method="POST">
112 <?= csrf_input() ?>
113 <input type="hidden" name="action" value="update">
114 <input type="hidden" name="id" value="<?= $c['id'] ?>">
115
116 <div style="display:grid;grid-template-columns:1fr 60px 80px 70px;gap:10px;margin-bottom:10px">
117 <div class="fg" style="margin:0">
118 <label style="font-size:12px;color:var(--muted)">Name</label>
119 <input type="text" name="name" value="<?= e($c['name']) ?>" class="fi" required>
120 </div>
121 <div class="fg" style="margin:0">
122 <label style="font-size:12px;color:var(--muted)">Icon</label>
123 <input type="text" name="icon" value="<?= e($c['icon']) ?>" class="fi" style="font-size:1.3rem;text-align:center">
124 </div>
125 <div class="fg" style="margin:0">
126 <label style="font-size:12px;color:var(--muted)">Color</label>
127 <input type="color" name="color" value="<?= e($c['color']) ?>" class="fi" style="height:40px;padding:2px">
128 </div>
129 <div class="fg" style="margin:0">
130 <label style="font-size:12px;color:var(--muted)">Order</label>
131 <input type="number" name="pos" value="<?= $c['position'] ?>" class="fi" min="0">
132 </div>
133 </div>
134
135 <div style="margin-bottom:12px">
136 <label style="font-size:12px;color:var(--muted)">Description</label>
137 <input type="text" name="desc" value="<?= e($c['description']) ?>" class="fi" placeholder="Short description">
138 </div>
139
140 <div style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px">
141 <div style="font-size:12px;font-weight:600;color:#64748b;margin-bottom:10px">๐ Role Permissions</div>
142 <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
143 <?= roleSelect('read_role', $c['read_role'] ?? 'guest', 'Who can read') ?>
144 <?= roleSelect('post_role', $c['post_role'] ?? 'member', 'Who can post topics') ?>
145 <?= roleSelect('reply_role', $c['reply_role'] ?? 'member', 'Who can reply') ?>
146 </div>
147 </div>
148
149 <div style="display:flex;gap:8px;justify-content:flex-end">
150 <button type="button" class="btn-ghost btn-sm" onclick="toggleEdit(<?= $c['id'] ?>)">Cancel</button>
151 <button type="submit" class="btn-primary btn-sm">Save Changes</button>
152 </div>
153 </form>
154 </td>
155 </tr>
156 <?php endforeach; ?>
157
158 <?php if (empty($cats)): ?>
159 <tr><td colspan="5" style="text-align:center;padding:24px;color:var(--muted)">No categories yet.</td></tr>
160 <?php endif; ?>
161 </tbody>
162 </table>
163 </div>
164
165 <!-- โโ Create category โโโโโโโโโโโโโโโโโโโโโโโโโโ -->
166 <div class="acard">
167 <div class="acard-head"><h2>Create Category</h2></div>
168 <div class="acard-body">
169 <form method="POST">
170 <?= csrf_input() ?>
171 <input type="hidden" name="action" value="create">
172
173 <div class="fg">
174 <label>Name <span class="req">*</span></label>
175 <input type="text" name="name" class="fi" required placeholder="Category name">
176 </div>
177
178 <div class="fg">
179 <label>Description</label>
180 <input type="text" name="desc" class="fi" placeholder="Short description (optional)">
181 </div>
182
183 <div style="display:grid;grid-template-columns:1fr 70px 90px 80px;gap:10px;margin-bottom:16px">
184 <div class="fg" style="margin:0">
185 <label>Icon</label>
186 <input type="text" name="icon" class="fi" value="๐ฌ" style="font-size:1.3rem;text-align:center">
187 </div>
188 <div class="fg" style="margin:0">
189 <label>Color</label>
190 <input type="color" name="color" class="fi" value="#3b82f6" style="height:40px;padding:2px">
191 </div>
192 <div class="fg" style="margin:0">
193 <label>Position</label>
194 <input type="number" name="pos" class="fi" value="99" min="0">
195 </div>
196 </div>
197
198 <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:16px">
199 <div style="font-size:13px;font-weight:600;margin-bottom:10px">๐ Role Permissions</div>
200 <div style="display:grid;grid-template-columns:1fr;gap:8px">
201 <?= roleSelect('read_role', 'guest', 'Who can read this category') ?>
202 <?= roleSelect('post_role', 'member', 'Who can create topics') ?>
203 <?= roleSelect('reply_role', 'member', 'Who can reply to topics') ?>
204 </div>
205 </div>
206
207 <div class="fg">
208 <label>Parent Category <small>(optional)</small></label>
209 <select name="parent_id" class="fi">
210 <option value="">None (top-level)</option>
211 <?php foreach (array_filter($cats, fn($c) => !$c['parent_id']) as $c): ?>
212 <option value="<?= $c['id'] ?>"><?= e($c['icon']) ?> <?= e($c['name']) ?></option>
213 <?php endforeach; ?>
214 </select>
215 </div>
216
217 <button type="submit" class="btn-primary" style="width:100%">Create Category</button>
218 </form>
219 </div>
220 </div>
221
222</div>
223
224<script>
225function toggleEdit(id) {
226 var row = document.getElementById('cr-' + id);
227 var edit = document.getElementById('ce-' + id);
228 var show = edit.style.display === 'none';
229 edit.style.display = show ? '' : 'none';
230 row.style.opacity = show ? '0.4' : '1';
231}
232</script>
233
234<?php include __DIR__ . '/../views/partials/admin_layout_end.php'; ?>