1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_admin();
4$PAGE_TITLE = 'Addons';
5$ADMIN_PAGE = 'addons';
6$flash = null;
7
8if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_ok()) {
9 $action = post('action');
10 $slug = preg_replace('/[^a-zA-Z0-9_\-]/', '', post('slug'));
11
12 if ($action === 'activate' && $slug) {
13 if (AddonManager::activate($slug)) {
14 $flash = ['ok', "Addon «{$slug}» activated."];
15 } else {
16 $flash = ['err', "Failed to activate «{$slug}». Check error logs."];
17 }
18 } elseif ($action === 'deactivate' && $slug) {
19 AddonManager::deactivate($slug);
20 $flash = ['ok', "Addon «{$slug}» deactivated."];
21 }
22}
23
24$addons = AddonManager::all();
25include __DIR__ . '/../views/partials/admin_layout.php';
26?>
27
28<?php if ($flash): ?>
29 <div class="alert <?= $flash[0] ?>"><?= e($flash[1]) ?></div>
30<?php endif; ?>
31
32<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
33 <div>
34 <h1 style="font-size:1.3rem;font-weight:700;margin:0">🧩 Addons</h1>
35 <p style="color:var(--muted);font-size:14px;margin:4px 0 0">Extend your forum with addons. Place addon folders in <code>addons/</code>.</p>
36 </div>
37 <a href="#developer-docs" class="btn-ghost btn-sm">📖 Developer Docs</a>
38</div>
39
40<?php if (empty($addons)): ?>
41 <div class="acard">
42 <div class="acard-body" style="text-align:center;padding:40px">
43 <div style="font-size:3rem;margin-bottom:12px">🧩</div>
44 <h2 style="font-size:1.1rem;margin-bottom:8px">No addons installed</h2>
45 <p style="color:var(--muted);font-size:14px;margin-bottom:16px">
46 Place addon folders in <code><?= e(ROOT) ?>/addons/</code> and they will appear here.
47 </p>
48 <a href="#developer-docs" class="btn-primary">Create Your First Addon →</a>
49 </div>
50 </div>
51<?php else: ?>
52 <div style="display:flex;flex-direction:column;gap:14px;margin-bottom:28px">
53 <?php foreach ($addons as $slug => $addon): ?>
54 <div class="acard addon-card <?= $addon['active'] ? 'addon-active' : '' ?>">
55 <div class="addon-card-inner">
56 <div class="addon-info">
57 <div class="addon-header">
58 <span class="addon-name"><?= e($addon['name']) ?></span>
59 <span class="addon-version">v<?= e($addon['version']) ?></span>
60 <?php if ($addon['active']): ?>
61 <span class="addon-status-badge active">● Active</span>
62 <?php else: ?>
63 <span class="addon-status-badge inactive">○ Inactive</span>
64 <?php endif; ?>
65 </div>
66 <p class="addon-desc"><?= e($addon['description']) ?></p>
67 <div class="addon-meta">
68 <span>By <strong><?= e($addon['author']) ?></strong></span>
69 <?php if ($addon['url']): ?>
70 <a href="<?= e($addon['url']) ?>" target="_blank" rel="noopener" style="font-size:12px">🔗 Website</a>
71 <?php endif; ?>
72 <span class="addon-slug-tag"><code><?= e($slug) ?></code></span>
73 <?php if (!empty($addon['hooks'])): ?>
74 <span style="font-size:12px;color:var(--muted)">
75 Hooks: <?= implode(', ', array_map('htmlspecialchars', $addon['hooks'])) ?>
76 </span>
77 <?php endif; ?>
78 </div>
79 </div>
80 <div class="addon-actions">
81 <form method="POST">
82 <?= csrf_input() ?>
83 <input type="hidden" name="slug" value="<?= e($slug) ?>">
84 <?php if ($addon['active']): ?>
85 <input type="hidden" name="action" value="deactivate">
86 <button class="btn-warn btn-sm" onclick="return confirm('Deactivate this addon?')">
87 ⏸ Deactivate
88 </button>
89 <?php else: ?>
90 <input type="hidden" name="action" value="activate">
91 <button class="btn-ok btn-sm">▶ Activate</button>
92 <?php endif; ?>
93 </form>
94 </div>
95 </div>
96 </div>
97 <?php endforeach; ?>
98 </div>
99<?php endif; ?>
100
101<!-- ── Developer Documentation ─────────────────────── -->
102<div class="acard" id="developer-docs">
103 <div class="acard-head">
104 <h2>📖 Developer Documentation — How to Create an Addon</h2>
105 </div>
106 <div class="acard-body addon-docs">
107
108 <h3>Overview</h3>
109 <p>Addons extend Nexus Forum without modifying core files. Each addon is a folder inside <code>addons/</code> containing at minimum a <code>nexus-addon.json</code> manifest and a <code>main.php</code> entry point.</p>
110
111 <h3>Directory Structure</h3>
112 <pre><code>addons/
113└── your-addon-slug/
114 ├── nexus-addon.json ← Required: manifest
115 ├── main.php ← Required: entry point, registers hooks
116 ├── install.php ← Optional: runs on first activation
117 ├── uninstall.php ← Optional: runs on deactivation
118 └── assets/ ← Optional: CSS, JS, images</code></pre>
119
120 <h3>1. The Manifest (<code>nexus-addon.json</code>)</h3>
121 <pre><code>{
122 "name": "My Addon",
123 "description": "A short description of what this addon does.",
124 "version": "1.0.0",
125 "author": "Your Name",
126 "url": "https://yoursite.com/addon",
127 "hooks": [
128 "after_post_saved",
129 "render_post_footer"
130 ],
131 "requires": {
132 "nexus": ">=14"
133 }
134}</code></pre>
135
136 <h3>2. The Entry Point (<code>main.php</code>)</h3>
137 <p>Use <code>addon_on($hook, $callback)</code> to register listeners. Hooks fire in priority order (default 10). Lower number = runs first.</p>
138 <pre><code><?php
139
140
141
142addon_on('render_post_footer', function(array $post): string {
143 return '<div class="custom-footer">Addon says hi on post #' . $post['id'] . '</div>';
144});
145
146
147addon_on('after_topic_created', function(array $data): void {
148
149
150 error_log('New topic: ' . $data['title']);
151});
152
153
154addon_on('render_post_content', function(string $html): string {
155
156 return str_replace(':-)', '😊', $html);
157});</code></pre>
158
159 <h3>3. Available Hooks</h3>
160 <table class="atable" style="margin-top:8px">
161 <thead><tr><th>Hook</th><th>Receives</th><th>Returns</th><th>When</th></tr></thead>
162 <tbody>
163 <tr><td><code>after_topic_created</code></td><td><code>array</code> topic data</td><td><em>void</em></td><td>After a new topic is saved</td></tr>
164 <tr><td><code>after_reply_saved</code></td><td><code>array</code> post data</td><td><em>void</em></td><td>After a reply is posted</td></tr>
165 <tr><td><code>after_user_registered</code></td><td><code>array</code> user data</td><td><em>void</em></td><td>After a new user registers</td></tr>
166 <tr><td><code>render_post_content</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Before post content is output (filter)</td></tr>
167 <tr><td><code>render_post_footer</code></td><td><code>array</code> post row</td><td><code>string</code> HTML</td><td>Below each post body</td></tr>
168 <tr><td><code>render_topic_header</code></td><td><code>array</code> topic row</td><td><code>string</code> HTML</td><td>Above topic content</td></tr>
169 <tr><td><code>admin_nav_items</code></td><td><code>array</code> nav items</td><td><code>array</code></td><td>Admin sidebar links</td></tr>
170 <tr><td><code>user_karma_changed</code></td><td><code>array</code> {user_id, old, new}</td><td><em>void</em></td><td>When karma is adjusted</td></tr>
171 <tr><td><code>before_page_head</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Inside <head> on every page</td></tr>
172 </tbody>
173 </table>
174
175 <h3>4. Install / Uninstall Scripts</h3>
176 <pre><code><?php
177
178
179DB::connect()->exec("
180 CREATE TABLE IF NOT EXISTS my_addon_logs (
181 id INTEGER PRIMARY KEY AUTOINCREMENT,
182 message TEXT,
183 created_at TEXT DEFAULT CURRENT_TIMESTAMP
184 )
185");
186cfg_set('my_addon_enabled', '1');</code></pre>
187
188 <pre><code><?php
189
190
191
192cfg_set('my_addon_enabled', '0');</code></pre>
193
194 <h3>5. Accessing Nexus APIs</h3>
195 <p>All Nexus helpers are available inside addon code:</p>
196 <pre><code>
197$rows = DB::rows("SELECT * FROM topics WHERE category_id=?", [1]);
198DB::insert("INSERT INTO my_addon_logs (message) VALUES (?)", ['hello']);
199
200
201global $USER;
202
203
204$siteName = cfg('site_name', 'My Forum');
205cfg_set('my_addon_setting', 'value');
206
207
208add_notification($userId, 'my_addon', ['key' => 'value']);
209
210
211add_karma($userId, 5);
212
213
214$url = u('forum/topic.php?slug=hello');
215
216 <h3>6. Example Addon</h3>
217 <p>A complete working example is included at <code>addons/example-hello-world/</code>.</p>
218 </div>
219</div>
220
221<style>
222.addon-card { transition:box-shadow .18s; }
223.addon-card.addon-active { border-color:var(--green); }
224.addon-card-inner { display:flex; align-items:flex-start; gap:16px; justify-content:space-between; flex-wrap:wrap; }
225.addon-info { flex:1; min-width:0; }
226.addon-header { display:flex; align-items:center; gap:10px; margin-bottom:6px; flex-wrap:wrap; }
227.addon-name { font-size:15px; font-weight:700; }
228.addon-version { font-size:11px; background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:1px 7px; color:var(--muted); }
229.addon-status-badge { font-size:11px; font-weight:700; padding:2px 8px; border-radius:8px; }
230.addon-status-badge.active { background:
231.addon-status-badge.inactive { background:
232.addon-desc { font-size:14px; color:var(--muted); margin:0 0 8px; }
233.addon-meta { display:flex; align-items:center; gap:14px; font-size:13px; color:var(--muted); flex-wrap:wrap; }
234.addon-slug-tag code { background:var(--bg); padding:1px 6px; border-radius:4px; font-size:12px; border:1px solid var(--border); }
235.addon-actions { flex-shrink:0; }
236.addon-docs h3 { font-size:14px; font-weight:700; margin:20px 0 8px; color:var(--text); }
237.addon-docs p { font-size:14px; color:var(--muted); margin-bottom:10px; line-height:1.7; }
238.addon-docs pre { background:
239.addon-docs code { font-family:var(--mono); font-size:13px; }
240.addon-docs p code { background:var(--bg); border:1px solid var(--border); padding:1px 6px; border-radius:4px; }
241</style>
242
243<?php include __DIR__ . '/../views/partials/admin_layout_end.php'; ?>