@sonicjs-cms/core 2.0.0-alpha.8 → 2.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { getCacheService, CACHE_CONFIGS } from './chunk-3MNMOLSA.js';
2
2
  import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-WESS2U3K.js';
3
+ import { PluginService } from './chunk-7N3HK7ZK.js';
3
4
  import { QueryFilterBuilder, sanitizeInput, escapeHtml } from './chunk-JIINOD2W.js';
4
5
  import { __esm, __export, __toCommonJS } from './chunk-V4OQ3NZ2.js';
5
6
  import { Hono } from 'hono';
@@ -11987,6 +11988,1626 @@ function formatFileSize(bytes) {
11987
11988
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
11988
11989
  }
11989
11990
 
11991
+ // src/templates/pages/admin-plugins-list.template.ts
11992
+ init_admin_layout_catalyst_template();
11993
+ function renderPluginsListPage(data) {
11994
+ const pageContent = `
11995
+ <div>
11996
+ <!-- Header -->
11997
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
11998
+ <div>
11999
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Plugins</h1>
12000
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage and extend functionality with plugins</p>
12001
+ </div>
12002
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
12003
+ <div class="relative inline-block text-left">
12004
+ <button onclick="toggleDropdown()" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
12005
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
12006
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
12007
+ </svg>
12008
+ Install Plugin
12009
+ <svg class="-mr-1 ml-2 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
12010
+ <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
12011
+ </svg>
12012
+ </button>
12013
+ <div id="plugin-dropdown" class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 focus:outline-none">
12014
+ <div class="py-1">
12015
+ <button onclick="installPlugin('faq-plugin')" class="block w-full text-left px-4 py-2 text-sm text-zinc-950 dark:text-white hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors first:rounded-t-xl">
12016
+ <div class="flex items-center">
12017
+ <span class="text-lg mr-2">\u2753</span>
12018
+ <div>
12019
+ <div class="font-medium">FAQ System</div>
12020
+ <div class="text-xs text-zinc-500 dark:text-zinc-400">Community FAQ management plugin</div>
12021
+ </div>
12022
+ </div>
12023
+ </button>
12024
+ <div class="border-t border-zinc-950/5 dark:border-white/10 my-1"></div>
12025
+ <button onclick="showNotification('Plugin marketplace coming soon!', 'info')" class="block w-full text-left px-4 py-2 text-sm text-zinc-950 dark:text-white hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors last:rounded-b-xl">
12026
+ <div class="flex items-center">
12027
+ <svg class="w-5 h-5 mr-2 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12028
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
12029
+ </svg>
12030
+ <div>
12031
+ <div class="font-medium">Browse Marketplace</div>
12032
+ <div class="text-xs text-zinc-500 dark:text-zinc-400">Discover more plugins</div>
12033
+ </div>
12034
+ </div>
12035
+ </button>
12036
+ </div>
12037
+ </div>
12038
+ </div>
12039
+ </div>
12040
+ </div>
12041
+
12042
+ <!-- Stats -->
12043
+ <div class="mb-6">
12044
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
12045
+ <dl class="mt-5 grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-4 md:divide-x md:divide-y-0">
12046
+ <div class="px-4 py-5 sm:p-6">
12047
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
12048
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12049
+ <div class="flex items-baseline text-2xl font-semibold text-cyan-400">
12050
+ ${data.stats?.total || 0}
12051
+ </div>
12052
+ <div class="inline-flex items-baseline rounded-full bg-lime-400/10 text-lime-600 dark:text-lime-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12053
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12054
+ <path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />
12055
+ </svg>
12056
+ <span class="sr-only">Increased by</span>
12057
+ 8.5%
12058
+ </div>
12059
+ </dd>
12060
+ </div>
12061
+ <div class="px-4 py-5 sm:p-6">
12062
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Active Plugins</dt>
12063
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12064
+ <div class="flex items-baseline text-2xl font-semibold text-lime-400">
12065
+ ${data.stats?.active || 0}
12066
+ </div>
12067
+ <div class="inline-flex items-baseline rounded-full bg-lime-400/10 text-lime-600 dark:text-lime-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12068
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12069
+ <path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />
12070
+ </svg>
12071
+ <span class="sr-only">Increased by</span>
12072
+ 12.3%
12073
+ </div>
12074
+ </dd>
12075
+ </div>
12076
+ <div class="px-4 py-5 sm:p-6">
12077
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Inactive Plugins</dt>
12078
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12079
+ <div class="flex items-baseline text-2xl font-semibold text-purple-400">
12080
+ ${data.stats?.inactive || 0}
12081
+ </div>
12082
+ <div class="inline-flex items-baseline rounded-full bg-pink-400/10 text-pink-600 dark:text-pink-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12083
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12084
+ <path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />
12085
+ </svg>
12086
+ <span class="sr-only">Decreased by</span>
12087
+ 3.2%
12088
+ </div>
12089
+ </dd>
12090
+ </div>
12091
+ <div class="px-4 py-5 sm:p-6">
12092
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Plugin Errors</dt>
12093
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12094
+ <div class="flex items-baseline text-2xl font-semibold text-pink-400">
12095
+ ${data.stats?.errors || 0}
12096
+ </div>
12097
+ <div class="inline-flex items-baseline rounded-full bg-pink-400/10 text-pink-600 dark:text-pink-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12098
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12099
+ <path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />
12100
+ </svg>
12101
+ <span class="sr-only">Decreased by</span>
12102
+ 1.5%
12103
+ </div>
12104
+ </dd>
12105
+ </div>
12106
+ </dl>
12107
+ </div>
12108
+
12109
+ <!-- Filters -->
12110
+ <div class="relative rounded-xl overflow-hidden mb-6">
12111
+ <!-- Gradient Background -->
12112
+ <div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10 dark:from-cyan-400/20 dark:via-blue-400/20 dark:to-purple-400/20"></div>
12113
+
12114
+ <div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
12115
+ <div class="px-6 py-5">
12116
+ <div class="flex items-center justify-between">
12117
+ <div class="flex items-center space-x-4 flex-1">
12118
+ <div>
12119
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
12120
+ <div class="mt-2 grid grid-cols-1">
12121
+ <select class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
12122
+ <option value="">All Categories</option>
12123
+ <option value="content">Content Management</option>
12124
+ <option value="media">Media</option>
12125
+ <option value="seo">SEO & Analytics</option>
12126
+ <option value="security">Security</option>
12127
+ <option value="utilities">Utilities</option>
12128
+ </select>
12129
+ <svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
12130
+ <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
12131
+ </svg>
12132
+ </div>
12133
+ </div>
12134
+ <div>
12135
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
12136
+ <div class="mt-2 grid grid-cols-1">
12137
+ <select class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
12138
+ <option value="">All Status</option>
12139
+ <option value="active">Active</option>
12140
+ <option value="inactive">Inactive</option>
12141
+ <option value="error">Error</option>
12142
+ </select>
12143
+ <svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
12144
+ <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
12145
+ </svg>
12146
+ </div>
12147
+ </div>
12148
+ <div class="flex-1 max-w-md">
12149
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
12150
+ <div class="relative group">
12151
+ <div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
12152
+ <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
12153
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
12154
+ </svg>
12155
+ </div>
12156
+ <input
12157
+ type="text"
12158
+ placeholder="Search plugins..."
12159
+ class="w-full rounded-full bg-transparent px-11 py-2 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 border-2 border-cyan-200/50 dark:border-cyan-700/50 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
12160
+ />
12161
+ </div>
12162
+ </div>
12163
+ </div>
12164
+ <div class="flex items-center gap-x-3 ml-4">
12165
+ <button
12166
+ onclick="location.reload()"
12167
+ class="inline-flex items-center gap-x-1.5 px-3 py-1.5 bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm text-zinc-950 dark:text-white text-sm font-medium rounded-full ring-1 ring-inset ring-cyan-200/50 dark:ring-cyan-700/50 hover:bg-gradient-to-r hover:from-cyan-50 hover:to-blue-50 dark:hover:from-cyan-900/30 dark:hover:to-blue-900/30 hover:ring-cyan-300 dark:hover:ring-cyan-600 transition-all duration-200"
12168
+ >
12169
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12170
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
12171
+ </svg>
12172
+ Refresh
12173
+ </button>
12174
+ </div>
12175
+ </div>
12176
+ </div>
12177
+ </div>
12178
+ </div>
12179
+
12180
+ <!-- Plugins Grid -->
12181
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
12182
+ ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
12183
+ </div>
12184
+
12185
+ <script>
12186
+ async function togglePlugin(pluginId, action) {
12187
+ const button = event.target;
12188
+ const originalText = button.textContent;
12189
+ button.disabled = true;
12190
+ button.textContent = action === 'activate' ? 'Activating...' : 'Deactivating...';
12191
+
12192
+ try {
12193
+ const response = await fetch(\`/admin/plugins/\${pluginId}/\${action}\`, {
12194
+ method: 'POST',
12195
+ headers: {
12196
+ 'Content-Type': 'application/json'
12197
+ }
12198
+ });
12199
+
12200
+ const result = await response.json();
12201
+
12202
+ if (result.success) {
12203
+ // Update UI
12204
+ const card = button.closest('.plugin-card');
12205
+ const statusBadge = card.querySelector('.status-badge');
12206
+
12207
+ if (action === 'activate') {
12208
+ // Update status badge
12209
+ statusBadge.className = 'status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-lime-700/10 dark:ring-lime-400/20';
12210
+ statusBadge.innerHTML = '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>Active';
12211
+ // Update card border to green
12212
+ card.className = 'plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-[3px] ring-lime-500 dark:ring-lime-400 p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all';
12213
+ // Update button
12214
+ button.textContent = 'Deactivate';
12215
+ button.onclick = () => togglePlugin(pluginId, 'deactivate');
12216
+ button.className = 'bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors';
12217
+ } else {
12218
+ // Update status badge
12219
+ statusBadge.className = 'status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-700/10 dark:ring-zinc-400/20';
12220
+ statusBadge.innerHTML = '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>Inactive';
12221
+ // Update card border to pink
12222
+ card.className = 'plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-[3px] ring-pink-500 dark:ring-pink-400 p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all';
12223
+ // Update button
12224
+ button.textContent = 'Activate';
12225
+ button.onclick = () => togglePlugin(pluginId, 'activate');
12226
+ button.className = 'bg-lime-600 dark:bg-lime-700 hover:bg-lime-700 dark:hover:bg-lime-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors';
12227
+ }
12228
+
12229
+ showNotification(\`Plugin \${action}d successfully\`, 'success');
12230
+ } else {
12231
+ throw new Error(result.error || \`Failed to \${action} plugin\`);
12232
+ }
12233
+ } catch (error) {
12234
+ showNotification(error.message, 'error');
12235
+ button.textContent = originalText;
12236
+ } finally {
12237
+ button.disabled = false;
12238
+ }
12239
+ }
12240
+
12241
+ async function installPlugin(pluginName) {
12242
+ const button = event.target;
12243
+ button.disabled = true;
12244
+ button.textContent = 'Installing...';
12245
+
12246
+ try {
12247
+ const response = await fetch('/admin/plugins/install', {
12248
+ method: 'POST',
12249
+ headers: {
12250
+ 'Content-Type': 'application/json'
12251
+ },
12252
+ body: JSON.stringify({ name: pluginName })
12253
+ });
12254
+
12255
+ const result = await response.json();
12256
+
12257
+ if (result.success) {
12258
+ showNotification('Plugin installed successfully!', 'success');
12259
+ setTimeout(() => location.reload(), 1500);
12260
+ } else {
12261
+ throw new Error(result.error || 'Failed to install plugin');
12262
+ }
12263
+ } catch (error) {
12264
+ showNotification(error.message, 'error');
12265
+ button.disabled = false;
12266
+ button.textContent = 'Install';
12267
+ }
12268
+ }
12269
+
12270
+ let pluginToUninstall = null;
12271
+
12272
+ async function uninstallPlugin(pluginId) {
12273
+ pluginToUninstall = pluginId;
12274
+ showConfirmDialog('uninstall-plugin-confirm');
12275
+ }
12276
+
12277
+ async function performUninstallPlugin() {
12278
+ if (!pluginToUninstall) return;
12279
+
12280
+ const button = event.target;
12281
+ if (button) button.disabled = true;
12282
+
12283
+ try {
12284
+ const response = await fetch(\`/admin/plugins/\${pluginToUninstall}/uninstall\`, {
12285
+ method: 'POST',
12286
+ headers: {
12287
+ 'Content-Type': 'application/json'
12288
+ }
12289
+ });
12290
+
12291
+ const result = await response.json();
12292
+
12293
+ if (result.success) {
12294
+ showNotification('Plugin uninstalled successfully!', 'success');
12295
+ setTimeout(() => location.reload(), 1500);
12296
+ } else {
12297
+ throw new Error(result.error || 'Failed to uninstall plugin');
12298
+ }
12299
+ } catch (error) {
12300
+ showNotification(error.message, 'error');
12301
+ if (button) button.disabled = false;
12302
+ } finally {
12303
+ pluginToUninstall = null;
12304
+ }
12305
+ }
12306
+
12307
+ function openPluginSettings(pluginId) {
12308
+ window.location.href = \`/admin/plugins/\${pluginId}\`;
12309
+ }
12310
+
12311
+ function showPluginDetails(pluginId) {
12312
+ // TODO: Implement plugin details modal
12313
+ showNotification('Plugin details coming soon!', 'info');
12314
+ }
12315
+
12316
+ function showNotification(message, type) {
12317
+ const notification = document.createElement('div');
12318
+ const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600';
12319
+ notification.className = \`fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 \${bgColor}\`;
12320
+ notification.textContent = message;
12321
+ document.body.appendChild(notification);
12322
+
12323
+ setTimeout(() => {
12324
+ notification.remove();
12325
+ }, 3000);
12326
+ }
12327
+
12328
+ function toggleDropdown() {
12329
+ const dropdown = document.getElementById('plugin-dropdown');
12330
+ dropdown.classList.toggle('hidden');
12331
+ }
12332
+
12333
+ // Close dropdown when clicking outside
12334
+ document.addEventListener('click', (event) => {
12335
+ const dropdown = document.getElementById('plugin-dropdown');
12336
+ const button = event.target.closest('button[onclick="toggleDropdown()"]');
12337
+
12338
+ if (!button && !dropdown.contains(event.target)) {
12339
+ dropdown.classList.add('hidden');
12340
+ }
12341
+ });
12342
+ </script>
12343
+
12344
+ <!-- Confirmation Dialogs -->
12345
+ ${renderConfirmationDialog({
12346
+ id: "uninstall-plugin-confirm",
12347
+ title: "Uninstall Plugin",
12348
+ message: "Are you sure you want to uninstall this plugin? This action cannot be undone.",
12349
+ confirmText: "Uninstall",
12350
+ cancelText: "Cancel",
12351
+ iconColor: "red",
12352
+ confirmClass: "bg-red-500 hover:bg-red-400",
12353
+ onConfirm: "performUninstallPlugin()"
12354
+ })}
12355
+
12356
+ ${getConfirmationDialogScript()}
12357
+ `;
12358
+ const layoutData = {
12359
+ title: "Plugins",
12360
+ currentPath: "/admin/plugins",
12361
+ user: data.user,
12362
+ version: data.version,
12363
+ content: pageContent
12364
+ };
12365
+ return renderAdminLayoutCatalyst(layoutData);
12366
+ }
12367
+ function renderPluginCard(plugin) {
12368
+ const statusColors = {
12369
+ active: "bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-lime-700/10 dark:ring-lime-400/20",
12370
+ inactive: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-700/10 dark:ring-zinc-400/20",
12371
+ error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-700/10 dark:ring-red-400/20"
12372
+ };
12373
+ const statusIcons = {
12374
+ active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
12375
+ inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
12376
+ error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>'
12377
+ };
12378
+ const borderColors = {
12379
+ active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
12380
+ inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
12381
+ error: "ring-[3px] ring-red-500 dark:ring-red-400"
12382
+ };
12383
+ const criticalCorePlugins = ["core-auth", "core-media"];
12384
+ const canToggle = !criticalCorePlugins.includes(plugin.id);
12385
+ const actionButton = plugin.status === "active" ? `<button onclick="togglePlugin('${plugin.id}', 'deactivate')" class="bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Deactivate</button>` : `<button onclick="togglePlugin('${plugin.id}', 'activate')" class="bg-lime-600 dark:bg-lime-700 hover:bg-lime-700 dark:hover:bg-lime-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Activate</button>`;
12386
+ return `
12387
+ <div class="plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ${borderColors[plugin.status]} p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all">
12388
+ <div class="flex items-start justify-between mb-4">
12389
+ <div class="flex items-center gap-3">
12390
+ <div class="w-12 h-12 rounded-lg flex items-center justify-center ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 bg-zinc-50 dark:bg-zinc-800">
12391
+ ${plugin.icon || getDefaultPluginIcon(plugin.category)}
12392
+ </div>
12393
+ <div>
12394
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">${plugin.displayName}</h3>
12395
+ <p class="text-sm text-zinc-500 dark:text-zinc-400">v${plugin.version} by ${plugin.author}</p>
12396
+ </div>
12397
+ </div>
12398
+ <div class="flex flex-col items-end gap-2">
12399
+ <span class="status-badge inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium ring-1 ring-inset ${statusColors[plugin.status]}">
12400
+ ${statusIcons[plugin.status]}${plugin.status.charAt(0).toUpperCase() + plugin.status.slice(1)}
12401
+ </span>
12402
+ ${plugin.isCore ? '<span class="inline-flex items-center rounded-md px-2.5 py-1 text-sm font-medium bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-700/10 dark:ring-cyan-400/20">Core</span>' : ""}
12403
+ </div>
12404
+ </div>
12405
+
12406
+ <p class="text-zinc-600 dark:text-zinc-300 text-sm mb-4 line-clamp-3">${plugin.description}</p>
12407
+
12408
+ <div class="flex items-center gap-4 mb-4 text-xs text-zinc-500 dark:text-zinc-400">
12409
+ <span class="flex items-center gap-1">
12410
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12411
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
12412
+ </svg>
12413
+ ${plugin.category}
12414
+ </span>
12415
+
12416
+ ${plugin.downloadCount ? `
12417
+ <span class="flex items-center gap-1">
12418
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12419
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
12420
+ </svg>
12421
+ ${plugin.downloadCount.toLocaleString()}
12422
+ </span>
12423
+ ` : ""}
12424
+
12425
+ ${plugin.rating ? `
12426
+ <span class="flex items-center gap-1">
12427
+ <svg class="w-4 h-4 text-yellow-500 dark:text-yellow-400 fill-current" viewBox="0 0 24 24">
12428
+ <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
12429
+ </svg>
12430
+ ${plugin.rating}
12431
+ </span>
12432
+ ` : ""}
12433
+
12434
+ <span>${plugin.lastUpdated}</span>
12435
+ </div>
12436
+
12437
+ ${plugin.dependencies && plugin.dependencies.length > 0 ? `
12438
+ <div class="mb-4">
12439
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">Dependencies:</p>
12440
+ <div class="flex flex-wrap gap-1">
12441
+ ${plugin.dependencies.map((dep) => `<span class="inline-block bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 text-xs px-2 py-1 rounded">${dep}</span>`).join("")}
12442
+ </div>
12443
+ </div>
12444
+ ` : ""}
12445
+
12446
+ <div class="flex items-center justify-between">
12447
+ <div class="flex gap-2">
12448
+ ${canToggle ? actionButton : ""}
12449
+ <button onclick="openPluginSettings('${plugin.id}')" class="bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">
12450
+ Settings
12451
+ </button>
12452
+ </div>
12453
+
12454
+ <div class="flex items-center gap-2">
12455
+ <button onclick="showPluginDetails('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Plugin Details">
12456
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12457
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
12458
+ </svg>
12459
+ </button>
12460
+
12461
+ ${!plugin.isCore ? `
12462
+ <button onclick="uninstallPlugin('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Uninstall Plugin">
12463
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12464
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
12465
+ </svg>
12466
+ </button>
12467
+ ` : ""}
12468
+ </div>
12469
+ </div>
12470
+ </div>
12471
+ `;
12472
+ }
12473
+ function getDefaultPluginIcon(category) {
12474
+ const iconColor = "text-zinc-600 dark:text-zinc-400";
12475
+ const icons = {
12476
+ "content": `
12477
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12478
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
12479
+ </svg>
12480
+ `,
12481
+ "media": `
12482
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12483
+ <path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
12484
+ </svg>
12485
+ `,
12486
+ "seo": `
12487
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12488
+ <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
12489
+ </svg>
12490
+ `,
12491
+ "analytics": `
12492
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12493
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
12494
+ </svg>
12495
+ `,
12496
+ "ecommerce": `
12497
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12498
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
12499
+ </svg>
12500
+ `,
12501
+ "email": `
12502
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12503
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
12504
+ </svg>
12505
+ `,
12506
+ "workflow": `
12507
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12508
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
12509
+ </svg>
12510
+ `,
12511
+ "security": `
12512
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12513
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
12514
+ </svg>
12515
+ `,
12516
+ "social": `
12517
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12518
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
12519
+ </svg>
12520
+ `,
12521
+ "utility": `
12522
+ <svg class="w-6 h-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
12523
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
12524
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
12525
+ </svg>
12526
+ `
12527
+ };
12528
+ const iconKey = category.toLowerCase();
12529
+ return icons[iconKey] || icons["utility"] || "";
12530
+ }
12531
+
12532
+ // src/templates/components/auth-settings-form.template.ts
12533
+ function renderAuthSettingsForm(settings) {
12534
+ const fields = settings.requiredFields;
12535
+ const validation = settings.validation;
12536
+ const registration = settings.registration;
12537
+ return `
12538
+ <div class="space-y-8">
12539
+ <!-- Required Fields Section -->
12540
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
12541
+ <h3 class="text-lg font-semibold text-white mb-4">Registration Fields</h3>
12542
+ <p class="text-sm text-gray-400 mb-6">Configure which fields are required during user registration and their minimum lengths.</p>
12543
+
12544
+ <div class="space-y-6">
12545
+ ${Object.entries(fields).map(([fieldName, config]) => `
12546
+ <div class="border-b border-white/10 pb-6 last:border-b-0 last:pb-0">
12547
+ <div class="flex items-start justify-between mb-4">
12548
+ <div>
12549
+ <h4 class="text-sm font-medium text-white">${config.label}</h4>
12550
+ <p class="text-xs text-gray-400 mt-1">Field type: ${config.type}</p>
12551
+ </div>
12552
+ <label class="relative inline-flex items-center cursor-pointer">
12553
+ <input
12554
+ type="checkbox"
12555
+ name="requiredFields_${fieldName}_required"
12556
+ ${config.required ? "checked" : ""}
12557
+ class="sr-only peer"
12558
+ >
12559
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12560
+ <span class="ml-3 text-sm font-medium text-gray-300">Required</span>
12561
+ </label>
12562
+ </div>
12563
+
12564
+ <div class="grid grid-cols-2 gap-4">
12565
+ <div>
12566
+ <label class="block text-xs font-medium text-gray-400 mb-2">Minimum Length</label>
12567
+ <input
12568
+ type="number"
12569
+ name="requiredFields_${fieldName}_minLength"
12570
+ value="${config.minLength}"
12571
+ min="0"
12572
+ max="100"
12573
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
12574
+ >
12575
+ </div>
12576
+ <div>
12577
+ <label class="block text-xs font-medium text-gray-400 mb-2">Field Label</label>
12578
+ <input
12579
+ type="text"
12580
+ name="requiredFields_${fieldName}_label"
12581
+ value="${config.label}"
12582
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
12583
+ >
12584
+ </div>
12585
+ </div>
12586
+ </div>
12587
+ `).join("")}
12588
+ </div>
12589
+ </div>
12590
+
12591
+ <!-- Password Requirements Section -->
12592
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
12593
+ <h3 class="text-lg font-semibold text-white mb-4">Password Requirements</h3>
12594
+ <p class="text-sm text-gray-400 mb-6">Additional password complexity requirements.</p>
12595
+
12596
+ <div class="space-y-4">
12597
+ <div class="flex items-center justify-between">
12598
+ <div>
12599
+ <label class="text-sm font-medium text-gray-300">Require Uppercase Letter</label>
12600
+ <p class="text-xs text-gray-400">Password must contain at least one uppercase letter (A-Z)</p>
12601
+ </div>
12602
+ <label class="relative inline-flex items-center cursor-pointer">
12603
+ <input
12604
+ type="checkbox"
12605
+ name="validation_passwordRequirements_requireUppercase"
12606
+ ${validation.passwordRequirements.requireUppercase ? "checked" : ""}
12607
+ class="sr-only peer"
12608
+ >
12609
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12610
+ </label>
12611
+ </div>
12612
+
12613
+ <div class="flex items-center justify-between">
12614
+ <div>
12615
+ <label class="text-sm font-medium text-gray-300">Require Lowercase Letter</label>
12616
+ <p class="text-xs text-gray-400">Password must contain at least one lowercase letter (a-z)</p>
12617
+ </div>
12618
+ <label class="relative inline-flex items-center cursor-pointer">
12619
+ <input
12620
+ type="checkbox"
12621
+ name="validation_passwordRequirements_requireLowercase"
12622
+ ${validation.passwordRequirements.requireLowercase ? "checked" : ""}
12623
+ class="sr-only peer"
12624
+ >
12625
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12626
+ </label>
12627
+ </div>
12628
+
12629
+ <div class="flex items-center justify-between">
12630
+ <div>
12631
+ <label class="text-sm font-medium text-gray-300">Require Numbers</label>
12632
+ <p class="text-xs text-gray-400">Password must contain at least one number (0-9)</p>
12633
+ </div>
12634
+ <label class="relative inline-flex items-center cursor-pointer">
12635
+ <input
12636
+ type="checkbox"
12637
+ name="validation_passwordRequirements_requireNumbers"
12638
+ ${validation.passwordRequirements.requireNumbers ? "checked" : ""}
12639
+ class="sr-only peer"
12640
+ >
12641
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12642
+ </label>
12643
+ </div>
12644
+
12645
+ <div class="flex items-center justify-between">
12646
+ <div>
12647
+ <label class="text-sm font-medium text-gray-300">Require Special Characters</label>
12648
+ <p class="text-xs text-gray-400">Password must contain at least one special character (!@#$%^&*)</p>
12649
+ </div>
12650
+ <label class="relative inline-flex items-center cursor-pointer">
12651
+ <input
12652
+ type="checkbox"
12653
+ name="validation_passwordRequirements_requireSpecialChars"
12654
+ ${validation.passwordRequirements.requireSpecialChars ? "checked" : ""}
12655
+ class="sr-only peer"
12656
+ >
12657
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12658
+ </label>
12659
+ </div>
12660
+ </div>
12661
+ </div>
12662
+
12663
+ <!-- Registration Settings Section -->
12664
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
12665
+ <h3 class="text-lg font-semibold text-white mb-4">Registration Settings</h3>
12666
+ <p class="text-sm text-gray-400 mb-6">General registration behavior.</p>
12667
+
12668
+ <div class="space-y-4">
12669
+ <div class="flex items-center justify-between">
12670
+ <div>
12671
+ <label class="text-sm font-medium text-gray-300">Allow User Registration</label>
12672
+ <p class="text-xs text-gray-400">Enable or disable public user registration</p>
12673
+ </div>
12674
+ <label class="relative inline-flex items-center cursor-pointer">
12675
+ <input
12676
+ type="checkbox"
12677
+ name="registration_enabled"
12678
+ ${registration.enabled ? "checked" : ""}
12679
+ class="sr-only peer"
12680
+ >
12681
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12682
+ </label>
12683
+ </div>
12684
+
12685
+ <div class="flex items-center justify-between">
12686
+ <div>
12687
+ <label class="text-sm font-medium text-gray-300">Require Email Verification</label>
12688
+ <p class="text-xs text-gray-400">Users must verify their email before accessing the system</p>
12689
+ </div>
12690
+ <label class="relative inline-flex items-center cursor-pointer">
12691
+ <input
12692
+ type="checkbox"
12693
+ name="registration_requireEmailVerification"
12694
+ ${registration.requireEmailVerification ? "checked" : ""}
12695
+ class="sr-only peer"
12696
+ >
12697
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12698
+ </label>
12699
+ </div>
12700
+
12701
+ <div>
12702
+ <label class="block text-sm font-medium text-gray-300 mb-2">Default User Role</label>
12703
+ <select
12704
+ name="registration_defaultRole"
12705
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white focus:border-blue-400 focus:outline-none transition-colors w-full"
12706
+ >
12707
+ <option value="viewer" ${registration.defaultRole === "viewer" ? "selected" : ""}>Viewer</option>
12708
+ <option value="editor" ${registration.defaultRole === "editor" ? "selected" : ""}>Editor</option>
12709
+ <option value="admin" ${registration.defaultRole === "admin" ? "selected" : ""}>Admin</option>
12710
+ </select>
12711
+ <p class="text-xs text-gray-400 mt-1">Role assigned to new users upon registration</p>
12712
+ </div>
12713
+ </div>
12714
+ </div>
12715
+
12716
+ <!-- Validation Settings Section -->
12717
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
12718
+ <h3 class="text-lg font-semibold text-white mb-4">Validation Settings</h3>
12719
+ <p class="text-sm text-gray-400 mb-6">Additional validation rules.</p>
12720
+
12721
+ <div class="space-y-4">
12722
+ <div class="flex items-center justify-between">
12723
+ <div>
12724
+ <label class="text-sm font-medium text-gray-300">Enforce Email Format</label>
12725
+ <p class="text-xs text-gray-400">Validate that email addresses are in correct format</p>
12726
+ </div>
12727
+ <label class="relative inline-flex items-center cursor-pointer">
12728
+ <input
12729
+ type="checkbox"
12730
+ name="validation_emailFormat"
12731
+ ${validation.emailFormat ? "checked" : ""}
12732
+ class="sr-only peer"
12733
+ >
12734
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12735
+ </label>
12736
+ </div>
12737
+
12738
+ <div class="flex items-center justify-between">
12739
+ <div>
12740
+ <label class="text-sm font-medium text-gray-300">Prevent Duplicate Usernames</label>
12741
+ <p class="text-xs text-gray-400">Ensure usernames are unique across all users</p>
12742
+ </div>
12743
+ <label class="relative inline-flex items-center cursor-pointer">
12744
+ <input
12745
+ type="checkbox"
12746
+ name="validation_allowDuplicateUsernames"
12747
+ ${!validation.allowDuplicateUsernames ? "checked" : ""}
12748
+ class="sr-only peer"
12749
+ >
12750
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
12751
+ </label>
12752
+ </div>
12753
+ </div>
12754
+ </div>
12755
+ </div>
12756
+ `;
12757
+ }
12758
+
12759
+ // src/templates/pages/admin-plugin-settings.template.ts
12760
+ function renderPluginSettingsPage(data) {
12761
+ const { plugin, activity = [], user } = data;
12762
+ const pageContent = `
12763
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
12764
+ <!-- Header with breadcrumb -->
12765
+ <div class="flex items-center mb-6">
12766
+ <nav class="flex" aria-label="Breadcrumb">
12767
+ <ol class="flex items-center space-x-2">
12768
+ <li>
12769
+ <a href="/admin/plugins" class="text-gray-400 hover:text-white transition-colors">
12770
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12771
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
12772
+ </svg>
12773
+ Plugins
12774
+ </a>
12775
+ </li>
12776
+ <li>
12777
+ <svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
12778
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
12779
+ </svg>
12780
+ </li>
12781
+ <li>
12782
+ <span class="text-gray-300">${plugin.displayName}</span>
12783
+ </li>
12784
+ </ol>
12785
+ </nav>
12786
+ </div>
12787
+
12788
+ <!-- Plugin Header -->
12789
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
12790
+ <div class="flex items-start justify-between">
12791
+ <div class="flex items-center gap-4">
12792
+ <div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center text-white text-2xl font-bold">
12793
+ ${plugin.icon || plugin.displayName.charAt(0).toUpperCase()}
12794
+ </div>
12795
+ <div>
12796
+ <h1 class="text-2xl font-semibold text-white mb-1">${plugin.displayName}</h1>
12797
+ <p class="text-gray-300 mb-2">${plugin.description}</p>
12798
+ <div class="flex items-center gap-4 text-sm text-gray-400">
12799
+ <span>v${plugin.version}</span>
12800
+ <span>by ${plugin.author}</span>
12801
+ <span>${plugin.category}</span>
12802
+ ${plugin.downloadCount ? `<span>${plugin.downloadCount.toLocaleString()} downloads</span>` : ""}
12803
+ ${plugin.rating ? `<span>\u2605 ${plugin.rating}</span>` : ""}
12804
+ </div>
12805
+ </div>
12806
+ </div>
12807
+
12808
+ <div class="flex items-center gap-3">
12809
+ ${renderStatusBadge(plugin.status)}
12810
+ ${renderToggleButton(plugin)}
12811
+ </div>
12812
+ </div>
12813
+ </div>
12814
+
12815
+ <!-- Tabs -->
12816
+ <div class="mb-6">
12817
+ <nav class="flex space-x-8" aria-label="Tabs">
12818
+ <button onclick="showTab('settings')" id="settings-tab" class="tab-button active border-b-2 border-blue-400 py-2 px-1 text-sm font-medium text-blue-400">
12819
+ Settings
12820
+ </button>
12821
+ <button onclick="showTab('activity')" id="activity-tab" class="tab-button border-b-2 border-transparent py-2 px-1 text-sm font-medium text-gray-400 hover:text-gray-300">
12822
+ Activity Log
12823
+ </button>
12824
+ <button onclick="showTab('info')" id="info-tab" class="tab-button border-b-2 border-transparent py-2 px-1 text-sm font-medium text-gray-400 hover:text-gray-300">
12825
+ Information
12826
+ </button>
12827
+ </nav>
12828
+ </div>
12829
+
12830
+ <!-- Tab Content -->
12831
+ <div id="tab-content">
12832
+ <!-- Settings Tab -->
12833
+ <div id="settings-content" class="tab-content">
12834
+ ${renderSettingsTab(plugin)}
12835
+ </div>
12836
+
12837
+ <!-- Activity Tab -->
12838
+ <div id="activity-content" class="tab-content hidden">
12839
+ ${renderActivityTab(activity)}
12840
+ </div>
12841
+
12842
+ <!-- Information Tab -->
12843
+ <div id="info-content" class="tab-content hidden">
12844
+ ${renderInformationTab(plugin)}
12845
+ </div>
12846
+ </div>
12847
+ </div>
12848
+
12849
+ <script>
12850
+ function showTab(tabName) {
12851
+ // Hide all tab contents
12852
+ document.querySelectorAll('.tab-content').forEach(content => {
12853
+ content.classList.add('hidden');
12854
+ });
12855
+
12856
+ // Remove active class from all tabs
12857
+ document.querySelectorAll('.tab-button').forEach(tab => {
12858
+ tab.classList.remove('active', 'border-blue-400', 'text-blue-400');
12859
+ tab.classList.add('border-transparent', 'text-gray-400');
12860
+ });
12861
+
12862
+ // Show selected tab content
12863
+ document.getElementById(tabName + '-content').classList.remove('hidden');
12864
+
12865
+ // Add active class to selected tab
12866
+ const activeTab = document.getElementById(tabName + '-tab');
12867
+ activeTab.classList.add('active', 'border-blue-400', 'text-blue-400');
12868
+ activeTab.classList.remove('border-transparent', 'text-gray-400');
12869
+ }
12870
+
12871
+ async function togglePlugin(pluginId, action) {
12872
+ const button = event.target;
12873
+ const originalText = button.textContent;
12874
+ button.disabled = true;
12875
+ button.textContent = action === 'activate' ? 'Activating...' : 'Deactivating...';
12876
+
12877
+ try {
12878
+ const response = await fetch(\`/admin/plugins/\${pluginId}/\${action}\`, {
12879
+ method: 'POST',
12880
+ headers: {
12881
+ 'Content-Type': 'application/json'
12882
+ }
12883
+ });
12884
+
12885
+ const result = await response.json();
12886
+
12887
+ if (result.success) {
12888
+ showNotification(\`Plugin \${action}d successfully\`, 'success');
12889
+ setTimeout(() => location.reload(), 1000);
12890
+ } else {
12891
+ throw new Error(result.error || \`Failed to \${action} plugin\`);
12892
+ }
12893
+ } catch (error) {
12894
+ showNotification(error.message, 'error');
12895
+ button.textContent = originalText;
12896
+ button.disabled = false;
12897
+ }
12898
+ }
12899
+
12900
+ async function saveSettings() {
12901
+ const form = document.getElementById('settings-form');
12902
+ const formData = new FormData(form);
12903
+ const isAuthPlugin = '${plugin.id}' === 'core-auth';
12904
+ let settings = {};
12905
+
12906
+ if (isAuthPlugin) {
12907
+ // Handle nested auth settings structure
12908
+ settings = {
12909
+ requiredFields: {},
12910
+ validation: {
12911
+ passwordRequirements: {}
12912
+ },
12913
+ registration: {}
12914
+ };
12915
+
12916
+ for (let [key, value] of formData.entries()) {
12917
+ const input = form.querySelector(\`[name="\${key}"]\`);
12918
+ const fieldValue = input.type === 'checkbox' ? input.checked :
12919
+ input.type === 'number' ? parseInt(value) || 0 : value;
12920
+
12921
+ // Parse nested field names like "requiredFields_email_required"
12922
+ if (key.startsWith('requiredFields_')) {
12923
+ const parts = key.replace('requiredFields_', '').split('_');
12924
+ const fieldName = parts[0];
12925
+ const propName = parts[1];
12926
+
12927
+ if (!settings.requiredFields[fieldName]) {
12928
+ settings.requiredFields[fieldName] = { type: 'text', label: '' };
12929
+ }
12930
+ settings.requiredFields[fieldName][propName] = fieldValue;
12931
+ } else if (key.startsWith('validation_passwordRequirements_')) {
12932
+ const propName = key.replace('validation_passwordRequirements_', '');
12933
+ settings.validation.passwordRequirements[propName] = fieldValue;
12934
+ } else if (key.startsWith('validation_')) {
12935
+ const propName = key.replace('validation_', '');
12936
+ // Invert the allowDuplicateUsernames logic
12937
+ if (propName === 'allowDuplicateUsernames') {
12938
+ settings.validation[propName] = !fieldValue;
12939
+ } else {
12940
+ settings.validation[propName] = fieldValue;
12941
+ }
12942
+ } else if (key.startsWith('registration_')) {
12943
+ const propName = key.replace('registration_', '');
12944
+ settings.registration[propName] = fieldValue;
12945
+ }
12946
+ }
12947
+ } else {
12948
+ // Handle regular plugin settings
12949
+ for (let [key, value] of formData.entries()) {
12950
+ if (key.startsWith('setting_')) {
12951
+ const settingKey = key.replace('setting_', '');
12952
+
12953
+ const input = form.querySelector(\`[name="\${key}"]\`);
12954
+ if (input.type === 'checkbox') {
12955
+ settings[settingKey] = input.checked;
12956
+ } else if (input.type === 'number') {
12957
+ settings[settingKey] = parseInt(value) || 0;
12958
+ } else {
12959
+ settings[settingKey] = value;
12960
+ }
12961
+ }
12962
+ }
12963
+ }
12964
+
12965
+ const saveButton = document.getElementById('save-button');
12966
+ saveButton.disabled = true;
12967
+ saveButton.textContent = 'Saving...';
12968
+
12969
+ try {
12970
+ const response = await fetch(\`/admin/plugins/${plugin.id}/settings\`, {
12971
+ method: 'POST',
12972
+ headers: {
12973
+ 'Content-Type': 'application/json'
12974
+ },
12975
+ body: JSON.stringify(settings)
12976
+ });
12977
+
12978
+ const result = await response.json();
12979
+
12980
+ if (result.success) {
12981
+ showNotification('Settings saved successfully', 'success');
12982
+ // Reload page after 1 second to show updated settings
12983
+ setTimeout(() => location.reload(), 1000);
12984
+ } else {
12985
+ throw new Error(result.error || 'Failed to save settings');
12986
+ }
12987
+ } catch (error) {
12988
+ showNotification(error.message, 'error');
12989
+ } finally {
12990
+ saveButton.disabled = false;
12991
+ saveButton.textContent = 'Save Settings';
12992
+ }
12993
+ }
12994
+
12995
+ function showNotification(message, type) {
12996
+ const notification = document.createElement('div');
12997
+ const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600';
12998
+ notification.className = \`fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 \${bgColor}\`;
12999
+ notification.textContent = message;
13000
+ document.body.appendChild(notification);
13001
+
13002
+ setTimeout(() => {
13003
+ notification.remove();
13004
+ }, 3000);
13005
+ }
13006
+ </script>
13007
+ `;
13008
+ const layoutData = {
13009
+ title: `${plugin.displayName} Settings`,
13010
+ pageTitle: `${plugin.displayName} Settings`,
13011
+ currentPath: `/admin/plugins/${plugin.id}`,
13012
+ user,
13013
+ content: pageContent
13014
+ };
13015
+ return renderAdminLayout(layoutData);
13016
+ }
13017
+ function renderStatusBadge(status) {
13018
+ const statusColors = {
13019
+ active: "bg-green-900/50 text-green-300 border-green-600/30",
13020
+ inactive: "bg-gray-800/50 text-gray-400 border-gray-600/30",
13021
+ error: "bg-red-900/50 text-red-300 border-red-600/30"
13022
+ };
13023
+ const statusIcons = {
13024
+ active: '<div class="w-2 h-2 bg-green-400 rounded-full mr-2"></div>',
13025
+ inactive: '<div class="w-2 h-2 bg-gray-500 rounded-full mr-2"></div>',
13026
+ error: '<div class="w-2 h-2 bg-red-400 rounded-full mr-2"></div>'
13027
+ };
13028
+ return `
13029
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${statusColors[status] || statusColors.inactive} border">
13030
+ ${statusIcons[status] || statusIcons.inactive}${status.charAt(0).toUpperCase() + status.slice(1)}
13031
+ </span>
13032
+ `;
13033
+ }
13034
+ function renderToggleButton(plugin) {
13035
+ if (plugin.isCore) {
13036
+ return '<span class="text-sm text-gray-400">Core Plugin</span>';
13037
+ }
13038
+ return plugin.status === "active" ? `<button onclick="togglePlugin('${plugin.id}', 'deactivate')" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">Deactivate</button>` : `<button onclick="togglePlugin('${plugin.id}', 'activate')" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors">Activate</button>`;
13039
+ }
13040
+ function renderSettingsTab(plugin) {
13041
+ const settings = plugin.settings || {};
13042
+ const isSeedDataPlugin = plugin.id === "seed-data" || plugin.name === "seed-data";
13043
+ const isAuthPlugin = plugin.id === "core-auth" || plugin.name === "core-auth";
13044
+ return `
13045
+ ${isSeedDataPlugin ? `
13046
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
13047
+ <div class="flex items-center justify-between">
13048
+ <div>
13049
+ <h2 class="text-xl font-semibold text-white mb-2">Seed Data Generator</h2>
13050
+ <p class="text-gray-400">Generate realistic example data for testing and development.</p>
13051
+ </div>
13052
+ <a
13053
+ href="/admin/seed-data"
13054
+ target="_blank"
13055
+ class="inline-flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
13056
+ >
13057
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13058
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
13059
+ </svg>
13060
+ Open Seed Data Tool
13061
+ </a>
13062
+ </div>
13063
+ </div>
13064
+ ` : ""}
13065
+
13066
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
13067
+ ${isAuthPlugin ? `
13068
+ <h2 class="text-xl font-semibold text-white mb-4">Authentication Settings</h2>
13069
+ <p class="text-gray-400 mb-6">Configure user registration fields and validation rules.</p>
13070
+ ` : `
13071
+ <h2 class="text-xl font-semibold text-white mb-4">Plugin Settings</h2>
13072
+ `}
13073
+
13074
+ <form id="settings-form" class="space-y-6">
13075
+ ${isAuthPlugin && Object.keys(settings).length > 0 ? renderAuthSettingsForm(settings) : Object.keys(settings).length > 0 ? renderSettingsFields(settings) : renderNoSettings(plugin)}
13076
+
13077
+ ${Object.keys(settings).length > 0 ? `
13078
+ <div class="flex items-center justify-end pt-6 border-t border-white/10">
13079
+ <button
13080
+ type="button"
13081
+ id="save-button"
13082
+ onclick="saveSettings()"
13083
+ class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition-colors"
13084
+ >
13085
+ Save Settings
13086
+ </button>
13087
+ </div>
13088
+ ` : ""}
13089
+ </form>
13090
+ </div>
13091
+ `;
13092
+ }
13093
+ function renderSettingsFields(settings) {
13094
+ return Object.entries(settings).map(([key, value]) => {
13095
+ const fieldId = `setting_${key}`;
13096
+ const displayName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
13097
+ if (typeof value === "boolean") {
13098
+ return `
13099
+ <div class="flex items-center justify-between">
13100
+ <div>
13101
+ <label for="${fieldId}" class="text-sm font-medium text-gray-300">${displayName}</label>
13102
+ <p class="text-xs text-gray-400">Enable or disable this feature</p>
13103
+ </div>
13104
+ <label class="relative inline-flex items-center cursor-pointer">
13105
+ <input type="checkbox" name="${fieldId}" id="${fieldId}" ${value ? "checked" : ""} class="sr-only peer">
13106
+ <div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
13107
+ </label>
13108
+ </div>
13109
+ `;
13110
+ } else if (typeof value === "number") {
13111
+ return `
13112
+ <div>
13113
+ <label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
13114
+ <input
13115
+ type="number"
13116
+ name="${fieldId}"
13117
+ id="${fieldId}"
13118
+ value="${value}"
13119
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13120
+ >
13121
+ </div>
13122
+ `;
13123
+ } else {
13124
+ return `
13125
+ <div>
13126
+ <label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
13127
+ <input
13128
+ type="text"
13129
+ name="${fieldId}"
13130
+ id="${fieldId}"
13131
+ value="${value}"
13132
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13133
+ >
13134
+ </div>
13135
+ `;
13136
+ }
13137
+ }).join("");
13138
+ }
13139
+ function renderNoSettings(plugin) {
13140
+ if (plugin.id === "seed-data" || plugin.name === "seed-data") {
13141
+ return `
13142
+ <div class="text-center py-8">
13143
+ <svg class="w-16 h-16 text-green-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13144
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
13145
+ </svg>
13146
+ <h3 class="text-lg font-medium text-gray-300 mb-2">Seed Data Generator</h3>
13147
+ <p class="text-gray-400 mb-6">Generate realistic example data for testing and development.</p>
13148
+ <a
13149
+ href="/admin/seed-data"
13150
+ class="inline-flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
13151
+ >
13152
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13153
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
13154
+ </svg>
13155
+ Generate Seed Data
13156
+ </a>
13157
+ </div>
13158
+ `;
13159
+ }
13160
+ return `
13161
+ <div class="text-center py-8">
13162
+ <svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13163
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
13164
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
13165
+ </svg>
13166
+ <h3 class="text-lg font-medium text-gray-300 mb-2">No Settings Available</h3>
13167
+ <p class="text-gray-400">This plugin doesn't have any configurable settings.</p>
13168
+ </div>
13169
+ `;
13170
+ }
13171
+ function renderActivityTab(activity) {
13172
+ return `
13173
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
13174
+ <h2 class="text-xl font-semibold text-white mb-4">Activity Log</h2>
13175
+
13176
+ ${activity.length > 0 ? `
13177
+ <div class="space-y-4">
13178
+ ${activity.map((item) => `
13179
+ <div class="flex items-start gap-3 p-3 rounded-lg bg-white/5">
13180
+ <div class="w-2 h-2 bg-blue-400 rounded-full mt-2"></div>
13181
+ <div class="flex-1">
13182
+ <div class="flex items-center justify-between">
13183
+ <span class="text-sm font-medium text-white">${item.action}</span>
13184
+ <span class="text-xs text-gray-400">${formatTimestamp(item.timestamp)}</span>
13185
+ </div>
13186
+ <p class="text-sm text-gray-300 mt-1">${item.message}</p>
13187
+ ${item.user ? `<p class="text-xs text-gray-400 mt-1">by ${item.user}</p>` : ""}
13188
+ </div>
13189
+ </div>
13190
+ `).join("")}
13191
+ </div>
13192
+ ` : `
13193
+ <div class="text-center py-8">
13194
+ <svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13195
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
13196
+ </svg>
13197
+ <h3 class="text-lg font-medium text-gray-300 mb-2">No Activity</h3>
13198
+ <p class="text-gray-400">No recent activity for this plugin.</p>
13199
+ </div>
13200
+ `}
13201
+ </div>
13202
+ `;
13203
+ }
13204
+ function renderInformationTab(plugin) {
13205
+ return `
13206
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
13207
+ <!-- Plugin Details -->
13208
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
13209
+ <h2 class="text-xl font-semibold text-white mb-4">Plugin Details</h2>
13210
+ <div class="space-y-3">
13211
+ <div class="flex justify-between">
13212
+ <span class="text-gray-400">Name:</span>
13213
+ <span class="text-white">${plugin.displayName}</span>
13214
+ </div>
13215
+ <div class="flex justify-between">
13216
+ <span class="text-gray-400">Version:</span>
13217
+ <span class="text-white">${plugin.version}</span>
13218
+ </div>
13219
+ <div class="flex justify-between">
13220
+ <span class="text-gray-400">Author:</span>
13221
+ <span class="text-white">${plugin.author}</span>
13222
+ </div>
13223
+ <div class="flex justify-between">
13224
+ <span class="text-gray-400">Category:</span>
13225
+ <span class="text-white">${plugin.category}</span>
13226
+ </div>
13227
+ <div class="flex justify-between">
13228
+ <span class="text-gray-400">Status:</span>
13229
+ <span class="text-white">${plugin.status}</span>
13230
+ </div>
13231
+ <div class="flex justify-between">
13232
+ <span class="text-gray-400">Last Updated:</span>
13233
+ <span class="text-white">${plugin.lastUpdated}</span>
13234
+ </div>
13235
+ </div>
13236
+ </div>
13237
+
13238
+ <!-- Dependencies & Permissions -->
13239
+ <div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6">
13240
+ <h2 class="text-xl font-semibold text-white mb-4">Dependencies & Permissions</h2>
13241
+
13242
+ ${plugin.dependencies && plugin.dependencies.length > 0 ? `
13243
+ <div class="mb-6">
13244
+ <h3 class="text-sm font-medium text-gray-300 mb-2">Dependencies:</h3>
13245
+ <div class="space-y-1">
13246
+ ${plugin.dependencies.map((dep) => `
13247
+ <div class="inline-block bg-white/10 text-gray-300 text-sm px-2 py-1 rounded mr-2">${dep}</div>
13248
+ `).join("")}
13249
+ </div>
13250
+ </div>
13251
+ ` : ""}
13252
+
13253
+ ${plugin.permissions && plugin.permissions.length > 0 ? `
13254
+ <div>
13255
+ <h3 class="text-sm font-medium text-gray-300 mb-2">Permissions:</h3>
13256
+ <div class="space-y-1">
13257
+ ${plugin.permissions.map((perm) => `
13258
+ <div class="inline-block bg-white/10 text-gray-300 text-sm px-2 py-1 rounded mr-2">${perm}</div>
13259
+ `).join("")}
13260
+ </div>
13261
+ </div>
13262
+ ` : ""}
13263
+
13264
+ ${(!plugin.dependencies || plugin.dependencies.length === 0) && (!plugin.permissions || plugin.permissions.length === 0) ? `
13265
+ <p class="text-gray-400">No dependencies or special permissions required.</p>
13266
+ ` : ""}
13267
+ </div>
13268
+ </div>
13269
+ `;
13270
+ }
13271
+ function formatTimestamp(timestamp) {
13272
+ const date = new Date(timestamp * 1e3);
13273
+ return date.toLocaleString();
13274
+ }
13275
+
13276
+ // src/routes/admin-plugins.ts
13277
+ var adminPluginRoutes = new Hono();
13278
+ adminPluginRoutes.get("/", async (c) => {
13279
+ try {
13280
+ const user = c.get("user");
13281
+ const db = c.env.DB;
13282
+ if (user?.role !== "admin") {
13283
+ return c.text("Access denied", 403);
13284
+ }
13285
+ const pluginService = new PluginService(db);
13286
+ let plugins = [];
13287
+ let stats = { total: 0, active: 0, inactive: 0, errors: 0 };
13288
+ try {
13289
+ plugins = await pluginService.getAllPlugins();
13290
+ stats = await pluginService.getPluginStats();
13291
+ } catch (error) {
13292
+ console.error("Error loading plugins:", error);
13293
+ }
13294
+ const templatePlugins = plugins.map((p) => ({
13295
+ id: p.id,
13296
+ name: p.name,
13297
+ displayName: p.display_name,
13298
+ description: p.description,
13299
+ version: p.version,
13300
+ author: p.author,
13301
+ status: p.status,
13302
+ category: p.category,
13303
+ icon: p.icon,
13304
+ downloadCount: p.download_count,
13305
+ rating: p.rating,
13306
+ lastUpdated: formatLastUpdated(p.last_updated),
13307
+ dependencies: p.dependencies,
13308
+ permissions: p.permissions,
13309
+ isCore: p.is_core
13310
+ }));
13311
+ const pageData = {
13312
+ plugins: templatePlugins,
13313
+ stats,
13314
+ user: {
13315
+ name: user?.email || "User",
13316
+ email: user?.email || "",
13317
+ role: user?.role || "user"
13318
+ },
13319
+ version: c.get("appVersion")
13320
+ };
13321
+ return c.html(renderPluginsListPage(pageData));
13322
+ } catch (error) {
13323
+ console.error("Error loading plugins page:", error);
13324
+ return c.text("Internal server error", 500);
13325
+ }
13326
+ });
13327
+ adminPluginRoutes.get("/:id", async (c) => {
13328
+ try {
13329
+ const user = c.get("user");
13330
+ const db = c.env.DB;
13331
+ const pluginId = c.req.param("id");
13332
+ if (user?.role !== "admin") {
13333
+ return c.redirect("/admin/plugins");
13334
+ }
13335
+ const pluginService = new PluginService(db);
13336
+ const plugin = await pluginService.getPlugin(pluginId);
13337
+ if (!plugin) {
13338
+ return c.text("Plugin not found", 404);
13339
+ }
13340
+ const activity = await pluginService.getPluginActivity(pluginId, 20);
13341
+ const templatePlugin = {
13342
+ id: plugin.id,
13343
+ name: plugin.name,
13344
+ displayName: plugin.display_name,
13345
+ description: plugin.description,
13346
+ version: plugin.version,
13347
+ author: plugin.author,
13348
+ status: plugin.status,
13349
+ category: plugin.category,
13350
+ icon: plugin.icon,
13351
+ downloadCount: plugin.download_count,
13352
+ rating: plugin.rating,
13353
+ lastUpdated: formatLastUpdated(plugin.last_updated),
13354
+ dependencies: plugin.dependencies,
13355
+ permissions: plugin.permissions,
13356
+ isCore: plugin.is_core,
13357
+ settings: plugin.settings
13358
+ };
13359
+ const templateActivity = (activity || []).map((item) => ({
13360
+ id: item.id,
13361
+ action: item.action,
13362
+ message: item.message,
13363
+ timestamp: item.timestamp,
13364
+ user: item.user_email
13365
+ }));
13366
+ const pageData = {
13367
+ plugin: templatePlugin,
13368
+ activity: templateActivity,
13369
+ user: {
13370
+ name: user?.email || "User",
13371
+ email: user?.email || "",
13372
+ role: user?.role || "user"
13373
+ }
13374
+ };
13375
+ return c.html(renderPluginSettingsPage(pageData));
13376
+ } catch (error) {
13377
+ console.error("Error getting plugin settings page:", error);
13378
+ return c.text("Internal server error", 500);
13379
+ }
13380
+ });
13381
+ adminPluginRoutes.post("/:id/activate", async (c) => {
13382
+ try {
13383
+ const user = c.get("user");
13384
+ const db = c.env.DB;
13385
+ const pluginId = c.req.param("id");
13386
+ if (user?.role !== "admin") {
13387
+ return c.json({ error: "Access denied" }, 403);
13388
+ }
13389
+ const pluginService = new PluginService(db);
13390
+ await pluginService.activatePlugin(pluginId);
13391
+ return c.json({ success: true });
13392
+ } catch (error) {
13393
+ console.error("Error activating plugin:", error);
13394
+ const message = error instanceof Error ? error.message : "Failed to activate plugin";
13395
+ return c.json({ error: message }, 400);
13396
+ }
13397
+ });
13398
+ adminPluginRoutes.post("/:id/deactivate", async (c) => {
13399
+ try {
13400
+ const user = c.get("user");
13401
+ const db = c.env.DB;
13402
+ const pluginId = c.req.param("id");
13403
+ if (user?.role !== "admin") {
13404
+ return c.json({ error: "Access denied" }, 403);
13405
+ }
13406
+ const pluginService = new PluginService(db);
13407
+ await pluginService.deactivatePlugin(pluginId);
13408
+ return c.json({ success: true });
13409
+ } catch (error) {
13410
+ console.error("Error deactivating plugin:", error);
13411
+ const message = error instanceof Error ? error.message : "Failed to deactivate plugin";
13412
+ return c.json({ error: message }, 400);
13413
+ }
13414
+ });
13415
+ adminPluginRoutes.post("/install", async (c) => {
13416
+ try {
13417
+ const user = c.get("user");
13418
+ const db = c.env.DB;
13419
+ if (user?.role !== "admin") {
13420
+ return c.json({ error: "Access denied" }, 403);
13421
+ }
13422
+ const body = await c.req.json();
13423
+ const pluginService = new PluginService(db);
13424
+ if (body.name === "faq-plugin") {
13425
+ const faqPlugin = await pluginService.installPlugin({
13426
+ id: "third-party-faq",
13427
+ name: "faq-plugin",
13428
+ display_name: "FAQ System",
13429
+ description: "Frequently Asked Questions management system with categories, search, and custom styling",
13430
+ version: "2.0.0",
13431
+ author: "Community Developer",
13432
+ category: "content",
13433
+ icon: "\u2753",
13434
+ permissions: ["manage:faqs"],
13435
+ dependencies: [],
13436
+ settings: {
13437
+ enableSearch: true,
13438
+ enableCategories: true,
13439
+ questionsPerPage: 10
13440
+ }
13441
+ });
13442
+ return c.json({ success: true, plugin: faqPlugin });
13443
+ }
13444
+ if (body.name === "demo-login-plugin") {
13445
+ const demoPlugin = await pluginService.installPlugin({
13446
+ id: "demo-login-prefill",
13447
+ name: "demo-login-plugin",
13448
+ display_name: "Demo Login Prefill",
13449
+ description: "Prefills login form with demo credentials (admin@sonicjs.com/admin123) for easy site demonstration",
13450
+ version: "1.0.0-beta.1",
13451
+ author: "SonicJS",
13452
+ category: "demo",
13453
+ icon: "\u{1F3AF}",
13454
+ permissions: [],
13455
+ dependencies: [],
13456
+ settings: {
13457
+ enableNotice: true,
13458
+ demoEmail: "admin@sonicjs.com",
13459
+ demoPassword: "admin123"
13460
+ }
13461
+ });
13462
+ return c.json({ success: true, plugin: demoPlugin });
13463
+ }
13464
+ if (body.name === "core-auth") {
13465
+ const authPlugin = await pluginService.installPlugin({
13466
+ id: "core-auth",
13467
+ name: "core-auth",
13468
+ display_name: "Authentication System",
13469
+ description: "Core authentication and user management system",
13470
+ version: "1.0.0-beta.1",
13471
+ author: "SonicJS Team",
13472
+ category: "security",
13473
+ icon: "\u{1F510}",
13474
+ permissions: ["manage:users", "manage:roles", "manage:permissions"],
13475
+ dependencies: [],
13476
+ is_core: true,
13477
+ settings: {}
13478
+ });
13479
+ return c.json({ success: true, plugin: authPlugin });
13480
+ }
13481
+ if (body.name === "core-media") {
13482
+ const mediaPlugin = await pluginService.installPlugin({
13483
+ id: "core-media",
13484
+ name: "core-media",
13485
+ display_name: "Media Manager",
13486
+ description: "Core media upload and management system",
13487
+ version: "1.0.0-beta.1",
13488
+ author: "SonicJS Team",
13489
+ category: "media",
13490
+ icon: "\u{1F4F8}",
13491
+ permissions: ["manage:media", "upload:files"],
13492
+ dependencies: [],
13493
+ is_core: true,
13494
+ settings: {}
13495
+ });
13496
+ return c.json({ success: true, plugin: mediaPlugin });
13497
+ }
13498
+ if (body.name === "core-workflow") {
13499
+ const workflowPlugin = await pluginService.installPlugin({
13500
+ id: "core-workflow",
13501
+ name: "core-workflow",
13502
+ display_name: "Workflow Engine",
13503
+ description: "Content workflow and approval system",
13504
+ version: "1.0.0-beta.1",
13505
+ author: "SonicJS Team",
13506
+ category: "content",
13507
+ icon: "\u{1F504}",
13508
+ permissions: ["manage:workflows", "approve:content"],
13509
+ dependencies: [],
13510
+ is_core: true,
13511
+ settings: {}
13512
+ });
13513
+ return c.json({ success: true, plugin: workflowPlugin });
13514
+ }
13515
+ if (body.name === "database-tools") {
13516
+ const databaseToolsPlugin = await pluginService.installPlugin({
13517
+ id: "database-tools",
13518
+ name: "database-tools",
13519
+ display_name: "Database Tools",
13520
+ description: "Database management tools including truncate, backup, and validation",
13521
+ version: "1.0.0-beta.1",
13522
+ author: "SonicJS Team",
13523
+ category: "system",
13524
+ icon: "\u{1F5C4}\uFE0F",
13525
+ permissions: ["manage:database", "admin"],
13526
+ dependencies: [],
13527
+ is_core: false,
13528
+ settings: {
13529
+ enableTruncate: true,
13530
+ enableBackup: true,
13531
+ enableValidation: true,
13532
+ requireConfirmation: true
13533
+ }
13534
+ });
13535
+ return c.json({ success: true, plugin: databaseToolsPlugin });
13536
+ }
13537
+ if (body.name === "seed-data") {
13538
+ const seedDataPlugin = await pluginService.installPlugin({
13539
+ id: "seed-data",
13540
+ name: "seed-data",
13541
+ display_name: "Seed Data",
13542
+ description: "Generate realistic example users and content for testing and development",
13543
+ version: "1.0.0-beta.1",
13544
+ author: "SonicJS Team",
13545
+ category: "development",
13546
+ icon: "\u{1F331}",
13547
+ permissions: ["admin"],
13548
+ dependencies: [],
13549
+ is_core: false,
13550
+ settings: {
13551
+ userCount: 20,
13552
+ contentCount: 200,
13553
+ defaultPassword: "password123"
13554
+ }
13555
+ });
13556
+ return c.json({ success: true, plugin: seedDataPlugin });
13557
+ }
13558
+ return c.json({ error: "Plugin not found in registry" }, 404);
13559
+ } catch (error) {
13560
+ console.error("Error installing plugin:", error);
13561
+ const message = error instanceof Error ? error.message : "Failed to install plugin";
13562
+ return c.json({ error: message }, 400);
13563
+ }
13564
+ });
13565
+ adminPluginRoutes.post("/:id/uninstall", async (c) => {
13566
+ try {
13567
+ const user = c.get("user");
13568
+ const db = c.env.DB;
13569
+ const pluginId = c.req.param("id");
13570
+ if (user?.role !== "admin") {
13571
+ return c.json({ error: "Access denied" }, 403);
13572
+ }
13573
+ const pluginService = new PluginService(db);
13574
+ await pluginService.uninstallPlugin(pluginId);
13575
+ return c.json({ success: true });
13576
+ } catch (error) {
13577
+ console.error("Error uninstalling plugin:", error);
13578
+ const message = error instanceof Error ? error.message : "Failed to uninstall plugin";
13579
+ return c.json({ error: message }, 400);
13580
+ }
13581
+ });
13582
+ adminPluginRoutes.post("/:id/settings", async (c) => {
13583
+ try {
13584
+ const user = c.get("user");
13585
+ const db = c.env.DB;
13586
+ const pluginId = c.req.param("id");
13587
+ if (user?.role !== "admin") {
13588
+ return c.json({ error: "Access denied" }, 403);
13589
+ }
13590
+ const settings = await c.req.json();
13591
+ const pluginService = new PluginService(db);
13592
+ await pluginService.updatePluginSettings(pluginId, settings);
13593
+ return c.json({ success: true });
13594
+ } catch (error) {
13595
+ console.error("Error updating plugin settings:", error);
13596
+ const message = error instanceof Error ? error.message : "Failed to update settings";
13597
+ return c.json({ error: message }, 400);
13598
+ }
13599
+ });
13600
+ function formatLastUpdated(timestamp) {
13601
+ const now = Date.now() / 1e3;
13602
+ const diff = now - timestamp;
13603
+ if (diff < 60) return "just now";
13604
+ if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
13605
+ if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
13606
+ if (diff < 604800) return `${Math.floor(diff / 86400)} days ago`;
13607
+ if (diff < 2592e3) return `${Math.floor(diff / 604800)} weeks ago`;
13608
+ return `${Math.floor(diff / 2592e3)} months ago`;
13609
+ }
13610
+
11990
13611
  // src/routes/index.ts
11991
13612
  var ROUTES_INFO = {
11992
13613
  message: "Routes migration in progress",
@@ -11999,12 +13620,13 @@ var ROUTES_INFO = {
11999
13620
  "authRoutes",
12000
13621
  "adminContentRoutes",
12001
13622
  "adminUsersRoutes",
12002
- "adminMediaRoutes"
13623
+ "adminMediaRoutes",
13624
+ "adminPluginRoutes"
12003
13625
  ],
12004
13626
  status: "Routes are being added incrementally",
12005
13627
  reference: "https://github.com/sonicjs/sonicjs"
12006
13628
  };
12007
13629
 
12008
- export { ROUTES_INFO, adminMediaRoutes, admin_api_default, admin_content_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, userRoutes };
12009
- //# sourceMappingURL=chunk-RWLPYK7R.js.map
12010
- //# sourceMappingURL=chunk-RWLPYK7R.js.map
13630
+ export { ROUTES_INFO, adminMediaRoutes, adminPluginRoutes, admin_api_default, admin_content_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, userRoutes };
13631
+ //# sourceMappingURL=chunk-N7EIZ74L.js.map
13632
+ //# sourceMappingURL=chunk-N7EIZ74L.js.map