1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 must_admin(); 4 $PAGE_TITLE = 'Categories'; 5 $ADMIN_PAGE = 'categories'; 6 $flash = null; 7 8 // Role permission helper 9 function 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 25 if ($_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'); 67 include __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> 225 function 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'; ?>