xgit simple git

nexus

nexus

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

admin/categories.php

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'; ?>