1 <?php 2 require_once __DIR__ . '/../includes/bootstrap.php'; 3 must_admin(); 4 $PAGE_TITLE = 'Addons'; 5 $ADMIN_PAGE = 'addons'; 6 $flash = null; 7 8 if ($_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(); 25 include __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 // main.php — loaded once per request if addon is active 140 141 // Example: add a footer line after every post 142 addon_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 // Example: run code when a new topic is created 147 addon_on('after_topic_created', function(array $data): void { 148 // $data has: topic_id, title, slug, category_id, user_id 149 // Do something: call external API, log it, etc. 150 error_log('New topic: ' . $data['title']); 151 }); 152 153 // Example: filter post content before display 154 addon_on('render_post_content', function(string $html): string { 155 // Transform HTML before output 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 // install.php — runs once when admin activates the addon 178 // Use this to create tables, insert settings, etc. 179 DB::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 "); 186 cfg_set('my_addon_enabled', '1');</code></pre> 187 188 <pre><code><?php 189 // uninstall.php — runs when admin deactivates the addon 190 // Clean up tables and settings (optional — you may want to keep data) 191 // DB::run("DROP TABLE IF EXISTS my_addon_logs"); 192 cfg_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>// Database 197 $rows = DB::rows("SELECT * FROM topics WHERE category_id=?", [1]); 198 DB::insert("INSERT INTO my_addon_logs (message) VALUES (?)", ['hello']); 199 200 // Users 201 global $USER; // currently logged-in user (or null) 202 203 // Settings 204 $siteName = cfg('site_name', 'My Forum'); 205 cfg_set('my_addon_setting', 'value'); 206 207 // Notifications 208 add_notification($userId, 'my_addon', ['key' => 'value']); 209 210 // Karma 211 add_karma($userId, 5); // award 5 karma points 212 213 // URL helpers 214 $url = u('forum/topic.php?slug=hello'); // respects BASE path</code></pre> 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:#f0fdf4; color:#166534; border:1px solid #bbf7d0; } 231 .addon-status-badge.inactive { background:#f8fafc; color:#94a3b8; border:1px solid #e2e8f0; } 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:#1e293b; color:#e2e8f0; padding:16px; border-radius:var(--r); overflow-x:auto; margin:8px 0 16px; font-size:13px; line-height:1.6; } 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'; ?>