@sonicjs-cms/core 2.3.12 → 2.3.14

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.
Files changed (54) hide show
  1. package/dist/{chunk-REY542YK.js → chunk-AVPUX57O.js} +3 -3
  2. package/dist/{chunk-REY542YK.js.map → chunk-AVPUX57O.js.map} +1 -1
  3. package/dist/{chunk-RIOIKM3Y.cjs → chunk-AZLU3ROK.cjs} +4 -2
  4. package/dist/chunk-AZLU3ROK.cjs.map +1 -0
  5. package/dist/{chunk-NTXPL746.js → chunk-CAJOP354.js} +34 -2
  6. package/dist/chunk-CAJOP354.js.map +1 -0
  7. package/dist/{chunk-HTJLBF6F.cjs → chunk-D4PJFFOV.cjs} +652 -475
  8. package/dist/chunk-D4PJFFOV.cjs.map +1 -0
  9. package/dist/{chunk-P6NMVNJJ.cjs → chunk-ETS5XSAG.cjs} +34 -2
  10. package/dist/chunk-ETS5XSAG.cjs.map +1 -0
  11. package/dist/{chunk-EIE35JCC.js → chunk-H34L445M.js} +3 -3
  12. package/dist/{chunk-EIE35JCC.js.map → chunk-H34L445M.js.map} +1 -1
  13. package/dist/{chunk-74RYBO6J.js → chunk-SKPETEM5.js} +10 -5
  14. package/dist/chunk-SKPETEM5.js.map +1 -0
  15. package/dist/{chunk-IB6UBZVD.cjs → chunk-SZE3XVET.cjs} +10 -5
  16. package/dist/chunk-SZE3XVET.cjs.map +1 -0
  17. package/dist/{chunk-HDSRB23N.js → chunk-T4XRPNX2.js} +507 -330
  18. package/dist/chunk-T4XRPNX2.js.map +1 -0
  19. package/dist/{chunk-KQCYQKSV.js → chunk-V5LBQN3I.js} +4 -2
  20. package/dist/chunk-V5LBQN3I.js.map +1 -0
  21. package/dist/{chunk-OJ5WUCSH.cjs → chunk-XWPGIFS7.cjs} +4 -4
  22. package/dist/{chunk-OJ5WUCSH.cjs.map → chunk-XWPGIFS7.cjs.map} +1 -1
  23. package/dist/{chunk-K6BFUYJH.cjs → chunk-YIXSSJWD.cjs} +5 -5
  24. package/dist/{chunk-K6BFUYJH.cjs.map → chunk-YIXSSJWD.cjs.map} +1 -1
  25. package/dist/index.cjs +1080 -87
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.js +1003 -10
  28. package/dist/index.js.map +1 -1
  29. package/dist/middleware.cjs +23 -23
  30. package/dist/middleware.js +2 -2
  31. package/dist/migrations-3A53GREK.cjs +13 -0
  32. package/dist/{migrations-DQ74P6V4.cjs.map → migrations-3A53GREK.cjs.map} +1 -1
  33. package/dist/migrations-WF6VIVU2.js +4 -0
  34. package/dist/{migrations-YAFC5JVO.js.map → migrations-WF6VIVU2.js.map} +1 -1
  35. package/dist/routes.cjs +25 -25
  36. package/dist/routes.js +5 -5
  37. package/dist/services.cjs +2 -2
  38. package/dist/services.js +1 -1
  39. package/dist/templates.cjs +17 -17
  40. package/dist/templates.js +2 -2
  41. package/dist/utils.cjs +11 -11
  42. package/dist/utils.js +1 -1
  43. package/migrations/025_add_easymde_plugin.sql +25 -0
  44. package/package.json +8 -3
  45. package/dist/chunk-74RYBO6J.js.map +0 -1
  46. package/dist/chunk-HDSRB23N.js.map +0 -1
  47. package/dist/chunk-HTJLBF6F.cjs.map +0 -1
  48. package/dist/chunk-IB6UBZVD.cjs.map +0 -1
  49. package/dist/chunk-KQCYQKSV.js.map +0 -1
  50. package/dist/chunk-NTXPL746.js.map +0 -1
  51. package/dist/chunk-P6NMVNJJ.cjs.map +0 -1
  52. package/dist/chunk-RIOIKM3Y.cjs.map +0 -1
  53. package/dist/migrations-DQ74P6V4.cjs +0 -13
  54. package/dist/migrations-YAFC5JVO.js +0 -4
@@ -1,9 +1,9 @@
1
1
  import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-3YNNVSMC.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-EIE35JCC.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-H34L445M.js';
3
3
  import { PluginService } from './chunk-SGAG6FD3.js';
4
- import { MigrationService } from './chunk-NTXPL746.js';
5
- import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-KQCYQKSV.js';
6
- import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-74RYBO6J.js';
4
+ import { MigrationService } from './chunk-CAJOP354.js';
5
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-V5LBQN3I.js';
6
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-SKPETEM5.js';
7
7
  import { metricsTracker } from './chunk-FICTAGD4.js';
8
8
  import { Hono } from 'hono';
9
9
  import { cors } from 'hono/cors';
@@ -1569,7 +1569,7 @@ adminApiRoutes.post("/collections", async (c) => {
1569
1569
  }
1570
1570
  const validatedData = validation.data;
1571
1571
  const db = c.env.DB;
1572
- const ____user = c.get("user");
1572
+ const _user = c.get("user");
1573
1573
  const displayName = validatedData.displayName || validatedData.display_name || "";
1574
1574
  const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
1575
1575
  const existing = await existingStmt.bind(validatedData.name).first();
@@ -1720,7 +1720,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1720
1720
  });
1721
1721
  adminApiRoutes.get("/migrations/status", async (c) => {
1722
1722
  try {
1723
- const { MigrationService: MigrationService2 } = await import('./migrations-YAFC5JVO.js');
1723
+ const { MigrationService: MigrationService2 } = await import('./migrations-WF6VIVU2.js');
1724
1724
  const db = c.env.DB;
1725
1725
  const migrationService = new MigrationService2(db);
1726
1726
  const status = await migrationService.getMigrationStatus();
@@ -1745,7 +1745,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1745
1745
  error: "Unauthorized. Admin access required."
1746
1746
  }, 403);
1747
1747
  }
1748
- const { MigrationService: MigrationService2 } = await import('./migrations-YAFC5JVO.js');
1748
+ const { MigrationService: MigrationService2 } = await import('./migrations-WF6VIVU2.js');
1749
1749
  const db = c.env.DB;
1750
1750
  const migrationService = new MigrationService2(db);
1751
1751
  const result = await migrationService.runPendingMigrations();
@@ -1764,7 +1764,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1764
1764
  });
1765
1765
  adminApiRoutes.get("/migrations/validate", async (c) => {
1766
1766
  try {
1767
- const { MigrationService: MigrationService2 } = await import('./migrations-YAFC5JVO.js');
1767
+ const { MigrationService: MigrationService2 } = await import('./migrations-WF6VIVU2.js');
1768
1768
  const db = c.env.DB;
1769
1769
  const migrationService = new MigrationService2(db);
1770
1770
  const validation = await migrationService.validateSchema();
@@ -2117,6 +2117,27 @@ function renderRegisterPage(data) {
2117
2117
  </html>
2118
2118
  `;
2119
2119
  }
2120
+ async function isRegistrationEnabled(db) {
2121
+ try {
2122
+ const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
2123
+ if (plugin?.settings) {
2124
+ const settings = JSON.parse(plugin.settings);
2125
+ const enabled = settings?.registration?.enabled;
2126
+ return enabled !== false && enabled !== 0;
2127
+ }
2128
+ return true;
2129
+ } catch {
2130
+ return true;
2131
+ }
2132
+ }
2133
+ async function isFirstUserRegistration(db) {
2134
+ try {
2135
+ const result = await db.prepare("SELECT COUNT(*) as count FROM users").first();
2136
+ return result?.count === 0;
2137
+ } catch {
2138
+ return false;
2139
+ }
2140
+ }
2120
2141
  var baseRegistrationSchema = z.object({
2121
2142
  email: z.string().email("Valid email is required"),
2122
2143
  password: z.string().min(8, "Password must be at least 8 characters"),
@@ -2168,7 +2189,15 @@ authRoutes.get("/login", async (c) => {
2168
2189
  }
2169
2190
  return c.html(renderLoginPage(pageData, demoLoginActive));
2170
2191
  });
2171
- authRoutes.get("/register", (c) => {
2192
+ authRoutes.get("/register", async (c) => {
2193
+ const db = c.env.DB;
2194
+ const isFirstUser = await isFirstUserRegistration(db);
2195
+ if (!isFirstUser) {
2196
+ const registrationEnabled = await isRegistrationEnabled(db);
2197
+ if (!registrationEnabled) {
2198
+ return c.redirect("/auth/login?error=Registration is currently disabled");
2199
+ }
2200
+ }
2172
2201
  const error = c.req.query("error");
2173
2202
  const pageData = {
2174
2203
  error: error || void 0
@@ -2184,6 +2213,13 @@ authRoutes.post(
2184
2213
  async (c) => {
2185
2214
  try {
2186
2215
  const db = c.env.DB;
2216
+ const isFirstUser = await isFirstUserRegistration(db);
2217
+ if (!isFirstUser) {
2218
+ const registrationEnabled = await isRegistrationEnabled(db);
2219
+ if (!registrationEnabled) {
2220
+ return c.json({ error: "Registration is currently disabled" }, 403);
2221
+ }
2222
+ }
2187
2223
  let requestData;
2188
2224
  try {
2189
2225
  requestData = await c.req.json();
@@ -2376,6 +2412,17 @@ authRoutes.post("/refresh", requireAuth(), async (c) => {
2376
2412
  authRoutes.post("/register/form", async (c) => {
2377
2413
  try {
2378
2414
  const db = c.env.DB;
2415
+ const isFirstUser = await isFirstUserRegistration(db);
2416
+ if (!isFirstUser) {
2417
+ const registrationEnabled = await isRegistrationEnabled(db);
2418
+ if (!registrationEnabled) {
2419
+ return c.html(html`
2420
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
2421
+ Registration is currently disabled. Please contact an administrator.
2422
+ </div>
2423
+ `);
2424
+ }
2425
+ }
2379
2426
  const formData = await c.req.formData();
2380
2427
  const requestData = {
2381
2428
  email: formData.get("email"),
@@ -2409,6 +2456,7 @@ authRoutes.post("/register/form", async (c) => {
2409
2456
  `);
2410
2457
  }
2411
2458
  const passwordHash = await AuthManager.hashPassword(password);
2459
+ const role = isFirstUser ? "admin" : "viewer";
2412
2460
  const userId = crypto.randomUUID();
2413
2461
  const now = /* @__PURE__ */ new Date();
2414
2462
  await db.prepare(`
@@ -2421,14 +2469,13 @@ authRoutes.post("/register/form", async (c) => {
2421
2469
  firstName,
2422
2470
  lastName,
2423
2471
  passwordHash,
2424
- "admin",
2425
- // First user gets admin role
2472
+ role,
2426
2473
  1,
2427
2474
  // is_active
2428
2475
  now.getTime(),
2429
2476
  now.getTime()
2430
2477
  ).run();
2431
- const token = await AuthManager.generateToken(userId, normalizedEmail, "admin");
2478
+ const token = await AuthManager.generateToken(userId, normalizedEmail, role);
2432
2479
  setCookie(c, "auth_token", token, {
2433
2480
  httpOnly: true,
2434
2481
  secure: false,
@@ -2437,12 +2484,13 @@ authRoutes.post("/register/form", async (c) => {
2437
2484
  maxAge: 60 * 60 * 24
2438
2485
  // 24 hours
2439
2486
  });
2487
+ const redirectUrl = role === "admin" ? "/admin/dashboard" : "/admin/dashboard";
2440
2488
  return c.html(html`
2441
2489
  <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
2442
- Account created successfully! Redirecting to admin dashboard...
2490
+ Account created successfully! Redirecting...
2443
2491
  <script>
2444
2492
  setTimeout(() => {
2445
- window.location.href = '/admin/dashboard';
2493
+ window.location.href = '${redirectUrl}';
2446
2494
  }, 2000);
2447
2495
  </script>
2448
2496
  </div>
@@ -4426,6 +4474,13 @@ function getMDXEditorInitScript(config) {
4426
4474
  // Store reference to editor instance
4427
4475
  textarea.easyMDEInstance = easyMDE;
4428
4476
 
4477
+ // Sync changes back to textarea
4478
+ easyMDE.codemirror.on("change", () => {
4479
+ textarea.value = easyMDE.value();
4480
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
4481
+ textarea.dispatchEvent(new Event("change", { bubbles: true }));
4482
+ });
4483
+
4429
4484
  console.log('EasyMDE initialized for field:', textarea.id || textarea.name);
4430
4485
  } catch (error) {
4431
4486
  console.error('Error initializing EasyMDE:', error);
@@ -6455,10 +6510,9 @@ adminContentRoutes.post("/", async (c) => {
6455
6510
  const insertStmt = db.prepare(`
6456
6511
  INSERT INTO content (
6457
6512
  id, collection_id, slug, title, data, status,
6458
- scheduled_publish_at, scheduled_unpublish_at,
6459
- meta_title, meta_description, author_id, created_by, created_at, updated_at
6513
+ author_id, created_at, updated_at
6460
6514
  )
6461
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6515
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
6462
6516
  `);
6463
6517
  await insertStmt.bind(
6464
6518
  contentId,
@@ -6467,11 +6521,6 @@ adminContentRoutes.post("/", async (c) => {
6467
6521
  data.title || "Untitled",
6468
6522
  JSON.stringify(data),
6469
6523
  status,
6470
- scheduledPublishAt ? new Date(scheduledPublishAt).getTime() : null,
6471
- scheduledUnpublishAt ? new Date(scheduledUnpublishAt).getTime() : null,
6472
- data.meta_title || null,
6473
- data.meta_description || null,
6474
- user?.userId || "unknown",
6475
6524
  user?.userId || "unknown",
6476
6525
  now,
6477
6526
  now
@@ -11509,7 +11558,7 @@ adminMediaRoutes.get("/", async (c) => {
11509
11558
  const type = searchParams.get("type") || "all";
11510
11559
  const view = searchParams.get("view") || "grid";
11511
11560
  const page = parseInt(searchParams.get("page") || "1");
11512
- const ____cacheBust = searchParams.get("t");
11561
+ const _cacheBust = searchParams.get("t");
11513
11562
  const limit = 24;
11514
11563
  const offset = (page - 1) * limit;
11515
11564
  const db = c.env.DB;
@@ -12338,10 +12387,37 @@ function formatFileSize(bytes) {
12338
12387
  // src/templates/pages/admin-plugins-list.template.ts
12339
12388
  init_admin_layout_catalyst_template();
12340
12389
  function renderPluginsListPage(data) {
12390
+ const categories = [
12391
+ { value: "content", label: "Content Management" },
12392
+ { value: "media", label: "Media" },
12393
+ { value: "editor", label: "Editors" },
12394
+ { value: "seo", label: "SEO & Analytics" },
12395
+ { value: "security", label: "Security" },
12396
+ { value: "utilities", label: "Utilities" },
12397
+ { value: "system", label: "System" },
12398
+ { value: "development", label: "Development" },
12399
+ { value: "demo", label: "Demo" }
12400
+ ];
12401
+ const statuses = [
12402
+ { value: "active", label: "Active" },
12403
+ { value: "inactive", label: "Inactive" },
12404
+ { value: "uninstalled", label: "Available to Install" },
12405
+ { value: "error", label: "Error" }
12406
+ ];
12407
+ const categoryCounts = {};
12408
+ categories.forEach((cat) => {
12409
+ categoryCounts[cat.value] = data.plugins.filter((p) => p.category === cat.value).length;
12410
+ });
12411
+ categories.sort((a, b) => (categoryCounts[b.value] || 0) - (categoryCounts[a.value] || 0));
12412
+ const statusCounts = {};
12413
+ statuses.forEach((status) => {
12414
+ statusCounts[status.value] = data.plugins.filter((p) => p.status === status.value).length;
12415
+ });
12416
+ statuses.sort((a, b) => (statusCounts[b.value] || 0) - (statusCounts[a.value] || 0));
12341
12417
  const pageContent = `
12342
12418
  <div>
12343
12419
  <!-- Header -->
12344
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
12420
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
12345
12421
  <div>
12346
12422
  <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Plugins</h1>
12347
12423
  <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage and extend functionality with plugins</p>
@@ -12349,7 +12425,7 @@ function renderPluginsListPage(data) {
12349
12425
  </div>
12350
12426
 
12351
12427
  <!-- Experimental Notice -->
12352
- <div class="mb-6 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 p-4">
12428
+ <div class="mb-8 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 p-4">
12353
12429
  <div class="flex items-start">
12354
12430
  <div class="flex-shrink-0">
12355
12431
  <svg class="h-5 w-5 text-amber-600 dark:text-amber-400" viewBox="0 0 20 20" fill="currentColor">
@@ -12370,176 +12446,174 @@ function renderPluginsListPage(data) {
12370
12446
  </div>
12371
12447
  </div>
12372
12448
 
12373
- <!-- Stats -->
12374
- <div class="mb-6">
12375
- <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
12376
- <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-5 md:divide-x md:divide-y-0">
12377
- <div class="px-4 py-5 sm:p-6">
12378
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
12379
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12380
- <div class="flex items-baseline text-2xl font-semibold text-cyan-400">
12381
- ${data.stats?.total || 0}
12382
- </div>
12383
- <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">
12384
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12385
- <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" />
12386
- </svg>
12387
- <span class="sr-only">Increased by</span>
12388
- 8.5%
12389
- </div>
12390
- </dd>
12391
- </div>
12392
- <div class="px-4 py-5 sm:p-6">
12393
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Active Plugins</dt>
12394
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12395
- <div class="flex items-baseline text-2xl font-semibold text-lime-400">
12396
- ${data.stats?.active || 0}
12397
- </div>
12398
- <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">
12399
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12400
- <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" />
12401
- </svg>
12402
- <span class="sr-only">Increased by</span>
12403
- 12.3%
12404
- </div>
12405
- </dd>
12449
+ <div class="flex flex-col lg:flex-row gap-8">
12450
+ <!-- Sidebar Filters -->
12451
+ <aside class="w-full lg:w-48 flex-shrink-0 space-y-8 lg:sticky lg:top-6 lg:self-start">
12452
+ <!-- Categories Filter -->
12453
+ <div>
12454
+ <h3 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Categories</h3>
12455
+ <div class="space-y-3">
12456
+ ${categories.map((cat) => {
12457
+ const count = categoryCounts[cat.value] || 0;
12458
+ const isDisabled = count === 0;
12459
+ return `
12460
+ <div class="flex items-center ${isDisabled ? "opacity-50" : ""}">
12461
+ <input
12462
+ id="category-${cat.value}"
12463
+ name="category"
12464
+ value="${cat.value}"
12465
+ type="checkbox"
12466
+ onchange="filterAndSortPlugins()"
12467
+ class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-700 text-zinc-900 focus:ring-zinc-600 dark:bg-zinc-900 disabled:cursor-not-allowed"
12468
+ ${isDisabled ? "disabled" : ""}
12469
+ >
12470
+ <label for="category-${cat.value}" class="ml-3 text-sm text-zinc-600 dark:text-zinc-400 select-none ${isDisabled ? "cursor-not-allowed" : ""}">
12471
+ ${cat.label} <span class="text-zinc-400 dark:text-zinc-500">(${count})</span>
12472
+ </label>
12473
+ </div>
12474
+ `;
12475
+ }).join("")}
12476
+ </div>
12406
12477
  </div>
12407
- <div class="px-4 py-5 sm:p-6">
12408
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Inactive Plugins</dt>
12409
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12410
- <div class="flex items-baseline text-2xl font-semibold text-purple-400">
12411
- ${data.stats?.inactive || 0}
12412
- </div>
12413
- <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">
12414
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12415
- <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" />
12416
- </svg>
12417
- <span class="sr-only">Decreased by</span>
12418
- 3.2%
12419
- </div>
12420
- </dd>
12478
+
12479
+ <div class="h-px bg-zinc-200 dark:bg-zinc-800 lg:hidden"></div>
12480
+
12481
+ <!-- Status Filter -->
12482
+ <div>
12483
+ <h3 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Status</h3>
12484
+ <div class="space-y-3">
12485
+ ${statuses.map((status) => {
12486
+ const count = statusCounts[status.value] || 0;
12487
+ const isDisabled = count === 0;
12488
+ let colorClass = "";
12489
+ let ringClass = "";
12490
+ let dotClass = "";
12491
+ switch (status.value) {
12492
+ case "active":
12493
+ colorClass = "text-emerald-700 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10";
12494
+ ringClass = "ring-emerald-600/20";
12495
+ dotClass = "bg-emerald-500 dark:bg-emerald-400";
12496
+ break;
12497
+ case "inactive":
12498
+ colorClass = "text-zinc-700 dark:text-zinc-400 bg-zinc-50 dark:bg-zinc-500/10";
12499
+ ringClass = "ring-zinc-600/20";
12500
+ dotClass = "bg-zinc-500 dark:bg-zinc-400";
12501
+ break;
12502
+ case "error":
12503
+ colorClass = "text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10";
12504
+ ringClass = "ring-red-600/20";
12505
+ dotClass = "bg-red-500 dark:bg-red-400";
12506
+ break;
12507
+ case "uninstalled":
12508
+ colorClass = "text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-500/10";
12509
+ ringClass = "ring-yellow-600/20";
12510
+ dotClass = "bg-yellow-500 dark:bg-yellow-400";
12511
+ break;
12512
+ default:
12513
+ colorClass = "text-zinc-700 dark:text-zinc-400 bg-zinc-50 dark:bg-zinc-500/10";
12514
+ ringClass = "ring-zinc-600/20";
12515
+ dotClass = "bg-zinc-500 dark:bg-zinc-400";
12516
+ }
12517
+ return `
12518
+ <div class="flex items-center ${isDisabled ? "opacity-50" : ""}">
12519
+ <input
12520
+ id="status-${status.value}"
12521
+ name="status"
12522
+ value="${status.value}"
12523
+ type="checkbox"
12524
+ onchange="filterAndSortPlugins()"
12525
+ class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-700 text-zinc-900 focus:ring-zinc-600 dark:bg-zinc-900 disabled:cursor-not-allowed"
12526
+ ${isDisabled ? "disabled" : ""}
12527
+ >
12528
+ <label for="status-${status.value}" class="ml-3 cursor-pointer select-none flex items-center ${isDisabled ? "cursor-not-allowed" : ""}">
12529
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${colorClass} ${ringClass}">
12530
+ <span class="mr-1.5 h-1.5 w-1.5 rounded-full ${dotClass}"></span>
12531
+ ${status.label}
12532
+ </span>
12533
+ <span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400">(${count})</span>
12534
+ </label>
12535
+ </div>
12536
+ `;
12537
+ }).join("")}
12538
+ </div>
12421
12539
  </div>
12422
- <div class="px-4 py-5 sm:p-6">
12423
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Plugin Errors</dt>
12424
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12425
- <div class="flex items-baseline text-2xl font-semibold text-pink-400">
12426
- ${data.stats?.errors || 0}
12427
- </div>
12428
- <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">
12429
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12430
- <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" />
12431
- </svg>
12432
- <span class="sr-only">Decreased by</span>
12433
- 1.5%
12434
- </div>
12435
- </dd>
12540
+ </aside>
12541
+
12542
+ <!-- Main Content -->
12543
+ <div class="flex-1 min-w-0">
12544
+ <!-- Stats Row (Compact) -->
12545
+ <div class="flex flex-wrap gap-4 mb-6">
12546
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12547
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Total</div>
12548
+ <div class="mt-1 text-lg font-semibold text-zinc-900 dark:text-white">${data.stats?.total || 0}</div>
12549
+ </div>
12550
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12551
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Active</div>
12552
+ <div class="mt-1 text-lg font-semibold text-emerald-600 dark:text-emerald-400">${data.stats?.active || 0}</div>
12553
+ </div>
12554
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12555
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Available</div>
12556
+ <div class="mt-1 text-lg font-semibold text-zinc-600 dark:text-zinc-400">${data.stats?.uninstalled || 0}</div>
12557
+ </div>
12558
+ <div class="min-w-[140px] rounded-lg bg-zinc-50 dark:bg-zinc-800/50 p-3 ring-1 ring-inset ring-zinc-950/5 dark:ring-white/5">
12559
+ <div class="text-xs font-medium text-zinc-500 dark:text-zinc-400">Errors</div>
12560
+ <div class="mt-1 text-lg font-semibold text-red-600 dark:text-red-400">${data.stats?.errors || 0}</div>
12561
+ </div>
12436
12562
  </div>
12437
- <div class="px-4 py-5 sm:p-6">
12438
- <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Available to Install</dt>
12439
- <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12440
- <div class="flex items-baseline text-2xl font-semibold text-zinc-400">
12441
- ${data.stats?.uninstalled || 0}
12442
- </div>
12443
- <div class="inline-flex items-baseline rounded-full bg-zinc-400/10 text-zinc-600 dark:text-zinc-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
12444
- <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12445
- <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" />
12563
+
12564
+ <!-- Toolbar -->
12565
+ <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-6">
12566
+ <div class="relative flex-1 w-full">
12567
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
12568
+ <svg class="h-4 w-4 text-zinc-400" viewBox="0 0 20 20" fill="currentColor">
12569
+ <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
12446
12570
  </svg>
12447
- <span class="sr-only">Available</span>
12448
- Ready
12449
12571
  </div>
12450
- </dd>
12451
- </div>
12452
- </dl>
12453
- </div>
12572
+ <input
12573
+ id="search-input"
12574
+ type="text"
12575
+ placeholder="Search plugins..."
12576
+ oninput="filterAndSortPlugins()"
12577
+ class="block w-full h-9 rounded-md border-0 py-1.5 pl-10 text-zinc-900 ring-1 ring-inset ring-zinc-300 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-zinc-600 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700 dark:focus:ring-zinc-500 sm:text-sm sm:leading-6"
12578
+ >
12579
+ </div>
12454
12580
 
12455
- <!-- Filters -->
12456
- <div class="relative rounded-xl overflow-hidden mb-6">
12457
- <!-- Gradient Background -->
12458
- <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>
12581
+ <div class="flex items-center gap-3 w-full sm:w-auto">
12582
+ <select id="sort-filter" onchange="filterAndSortPlugins()" class="block w-full sm:w-auto h-9 rounded-md border-0 py-1.5 pl-3 pr-8 text-zinc-900 ring-1 ring-inset ring-zinc-300 focus:ring-2 focus:ring-inset focus:ring-zinc-600 dark:bg-zinc-900 dark:text-white dark:ring-zinc-700 dark:focus:ring-zinc-500 sm:text-sm sm:leading-6">
12583
+ <option value="name-asc">Name (A-Z)</option>
12584
+ <option value="name-desc">Name (Z-A)</option>
12585
+ <option value="newest">Newest Installed</option>
12586
+ <option value="updated">Recently Updated</option>
12587
+ <option value="popular">Popularity</option>
12588
+ <option value="rating">Highest Rated</option>
12589
+ </select>
12459
12590
 
12460
- <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">
12461
- <div class="px-6 py-5">
12462
- <div class="flex items-center justify-between">
12463
- <div class="flex items-center space-x-4 flex-1">
12464
- <div>
12465
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
12466
- <div class="mt-2 grid grid-cols-1">
12467
- <select id="category-filter" onchange="filterPlugins()" 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">
12468
- <option value="">All Categories</option>
12469
- <option value="content">Content Management</option>
12470
- <option value="media">Media</option>
12471
- <option value="seo">SEO & Analytics</option>
12472
- <option value="security">Security</option>
12473
- <option value="utilities">Utilities</option>
12474
- <option value="system">System</option>
12475
- <option value="development">Development</option>
12476
- <option value="demo">Demo</option>
12477
- </select>
12478
- <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">
12479
- <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" />
12480
- </svg>
12481
- </div>
12482
- </div>
12483
- <div>
12484
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
12485
- <div class="mt-2 grid grid-cols-1">
12486
- <select id="status-filter" onchange="filterPlugins()" 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">
12487
- <option value="">All Status</option>
12488
- <option value="active">Active</option>
12489
- <option value="inactive">Inactive</option>
12490
- <option value="uninstalled">Available to Install</option>
12491
- <option value="error">Error</option>
12492
- </select>
12493
- <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">
12494
- <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" />
12495
- </svg>
12496
- </div>
12497
- </div>
12498
- <div class="flex-1 max-w-md">
12499
- <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
12500
- <div class="relative group">
12501
- <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">
12502
- <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
12503
- <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
12504
- </svg>
12505
- </div>
12506
- <input
12507
- id="search-input"
12508
- type="text"
12509
- placeholder="Search plugins..."
12510
- oninput="filterPlugins()"
12511
- 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"
12512
- />
12513
- </div>
12514
- </div>
12515
- </div>
12516
- <div class="flex items-center gap-x-3 ml-4">
12517
- <button
12518
- onclick="location.reload()"
12519
- 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"
12520
- >
12521
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12522
- <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"/>
12523
- </svg>
12524
- Refresh
12525
- </button>
12526
- </div>
12591
+ <button
12592
+ onclick="location.reload()"
12593
+ class="inline-flex items-center gap-x-1.5 rounded-md bg-white dark:bg-zinc-900 px-3 py-1.5 h-9 text-sm font-semibold text-zinc-900 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800"
12594
+ >
12595
+ <svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12596
+ <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"/>
12597
+ </svg>
12598
+ </button>
12527
12599
  </div>
12528
12600
  </div>
12601
+
12602
+ <!-- Plugins Grid -->
12603
+ <div id="plugins-grid" class="grid gap-6" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));">
12604
+ ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
12605
+ </div>
12529
12606
  </div>
12530
12607
  </div>
12531
-
12532
- <!-- Plugins Grid -->
12533
- <div id="plugins-grid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
12534
- ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
12535
12608
  </div>
12536
12609
 
12537
12610
  <script>
12538
- async function togglePlugin(pluginId, action) {
12539
- const button = event.target;
12540
- const originalText = button.textContent;
12611
+ async function togglePlugin(pluginId, action, event) {
12612
+ const button = event.target.closest('button');
12613
+ if (!button) return;
12614
+
12541
12615
  button.disabled = true;
12542
- button.textContent = action === 'activate' ? 'Activating...' : 'Deactivating...';
12616
+ button.classList.add('opacity-50', 'cursor-wait');
12543
12617
 
12544
12618
  try {
12545
12619
  const response = await fetch(\`/admin/plugins/\${pluginId}/\${action}\`, {
@@ -12555,27 +12629,36 @@ function renderPluginsListPage(data) {
12555
12629
  // Update UI
12556
12630
  const card = button.closest('.plugin-card');
12557
12631
  const statusBadge = card.querySelector('.status-badge');
12632
+ const knob = button.querySelector('.toggle-knob');
12558
12633
 
12559
12634
  if (action === 'activate') {
12560
12635
  // Update status badge
12561
- 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';
12562
- statusBadge.innerHTML = '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>Active';
12563
- // Update card border to green
12564
- 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';
12565
- // Update button
12566
- button.textContent = 'Deactivate';
12567
- button.onclick = () => togglePlugin(pluginId, 'deactivate');
12568
- 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';
12636
+ statusBadge.className = 'status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 ring-emerald-600/20';
12637
+ statusBadge.innerHTML = '<div class="w-1.5 h-1.5 bg-emerald-500 dark:bg-emerald-400 rounded-full mr-1.5"></div>Active';
12638
+
12639
+ // Update button state to Active
12640
+ button.className = 'bg-emerald-600 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button';
12641
+ button.setAttribute('aria-checked', 'true');
12642
+ button.onclick = (event) => togglePlugin(pluginId, 'deactivate', event);
12643
+
12644
+ // Update knob position
12645
+ if (knob) {
12646
+ knob.className = 'translate-x-5 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob';
12647
+ }
12569
12648
  } else {
12570
12649
  // Update status badge
12571
- 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';
12572
- statusBadge.innerHTML = '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>Inactive';
12573
- // Update card border to pink
12574
- 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';
12575
- // Update button
12576
- button.textContent = 'Activate';
12577
- button.onclick = () => togglePlugin(pluginId, 'activate');
12578
- 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';
12650
+ statusBadge.className = 'status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-600/20';
12651
+ statusBadge.innerHTML = '<div class="w-1.5 h-1.5 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-1.5"></div>Inactive';
12652
+
12653
+ // Update button state to Inactive
12654
+ button.className = 'bg-zinc-200 dark:bg-zinc-700 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button';
12655
+ button.setAttribute('aria-checked', 'false');
12656
+ button.onclick = (event) => togglePlugin(pluginId, 'activate', event);
12657
+
12658
+ // Update knob position
12659
+ if (knob) {
12660
+ knob.className = 'translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob';
12661
+ }
12579
12662
  }
12580
12663
 
12581
12664
  showNotification(\`Plugin \${action}d successfully\`, 'success');
@@ -12584,9 +12667,9 @@ function renderPluginsListPage(data) {
12584
12667
  }
12585
12668
  } catch (error) {
12586
12669
  showNotification(error.message, 'error');
12587
- button.textContent = originalText;
12588
12670
  } finally {
12589
12671
  button.disabled = false;
12672
+ button.classList.remove('opacity-50', 'cursor-wait');
12590
12673
  }
12591
12674
  }
12592
12675
 
@@ -12670,81 +12753,92 @@ function renderPluginsListPage(data) {
12670
12753
  showNotification('Plugin details coming soon!', 'info');
12671
12754
  }
12672
12755
 
12673
- function showNotification(message, type) {
12674
- const notification = document.createElement('div');
12675
- const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-blue-600';
12676
- notification.className = \`fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 \${bgColor}\`;
12677
- notification.textContent = message;
12678
- document.body.appendChild(notification);
12679
-
12680
- setTimeout(() => {
12681
- notification.remove();
12682
- }, 3000);
12683
- }
12684
-
12685
- function filterPlugins() {
12686
- const categoryFilter = document.getElementById('category-filter').value.toLowerCase();
12687
- const statusFilter = document.getElementById('status-filter').value.toLowerCase();
12756
+ function filterAndSortPlugins() {
12757
+ // Get checked categories
12758
+ const checkedCategories = Array.from(document.querySelectorAll('input[name="category"]:checked'))
12759
+ .map(cb => cb.value.toLowerCase());
12760
+
12761
+ // Get checked statuses
12762
+ const checkedStatuses = Array.from(document.querySelectorAll('input[name="status"]:checked'))
12763
+ .map(cb => cb.value.toLowerCase());
12764
+
12688
12765
  const searchInput = document.getElementById('search-input').value.toLowerCase();
12766
+ const sortValue = document.getElementById('sort-filter').value;
12689
12767
 
12690
- const pluginCards = document.querySelectorAll('.plugin-card');
12691
- let visibleCount = 0;
12692
-
12693
- pluginCards.forEach(card => {
12694
- // Get plugin data from card attributes
12768
+ const pluginsGrid = document.getElementById('plugins-grid');
12769
+ const pluginCards = Array.from(pluginsGrid.querySelectorAll('.plugin-card'));
12770
+
12771
+ // Filter
12772
+ const visibleCards = pluginCards.filter(card => {
12695
12773
  const category = card.getAttribute('data-category')?.toLowerCase() || '';
12696
12774
  const status = card.getAttribute('data-status')?.toLowerCase() || '';
12697
12775
  const name = card.getAttribute('data-name')?.toLowerCase() || '';
12698
12776
  const description = card.getAttribute('data-description')?.toLowerCase() || '';
12699
12777
 
12700
- // Check if plugin matches all filters
12701
- let matches = true;
12702
-
12703
- // Category filter
12704
- if (categoryFilter && category !== categoryFilter) {
12705
- matches = false;
12706
- }
12707
-
12708
- // Status filter
12709
- if (statusFilter && status !== statusFilter) {
12710
- matches = false;
12711
- }
12712
-
12713
- // Search filter - check if search term is in name or description
12714
- if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) {
12715
- matches = false;
12716
- }
12778
+ // Category filter: if any selected, must match one of them
12779
+ if (checkedCategories.length > 0 && !checkedCategories.includes(category)) return false;
12780
+
12781
+ // Status filter: if any selected, must match one of them
12782
+ if (checkedStatuses.length > 0 && !checkedStatuses.includes(status)) return false;
12783
+
12784
+ // Search filter
12785
+ if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) return false;
12786
+
12787
+ return true;
12788
+ });
12717
12789
 
12718
- // Show/hide card
12719
- if (matches) {
12720
- card.style.display = '';
12721
- visibleCount++;
12722
- } else {
12723
- card.style.display = 'none';
12790
+ // Sort
12791
+ visibleCards.sort((a, b) => {
12792
+ const aName = a.getAttribute('data-name') || '';
12793
+ const bName = b.getAttribute('data-name') || '';
12794
+ const aInstalled = parseInt(a.getAttribute('data-installed') || '0');
12795
+ const bInstalled = parseInt(b.getAttribute('data-installed') || '0');
12796
+ const aUpdated = parseInt(a.getAttribute('data-updated') || '0');
12797
+ const bUpdated = parseInt(b.getAttribute('data-updated') || '0');
12798
+ const aDownloads = parseInt(a.getAttribute('data-downloads') || '0');
12799
+ const bDownloads = parseInt(b.getAttribute('data-downloads') || '0');
12800
+ const aRating = parseFloat(a.getAttribute('data-rating') || '0');
12801
+ const bRating = parseFloat(b.getAttribute('data-rating') || '0');
12802
+
12803
+ switch (sortValue) {
12804
+ case 'name-desc': return bName.localeCompare(aName);
12805
+ case 'newest': return bInstalled - aInstalled;
12806
+ case 'updated': return bUpdated - aUpdated;
12807
+ case 'popular': return bDownloads - aDownloads;
12808
+ case 'rating': return bRating - aRating;
12809
+ case 'name-asc':
12810
+ default: return aName.localeCompare(bName);
12724
12811
  }
12725
12812
  });
12726
12813
 
12727
- // Show/hide "no results" message
12814
+ // Re-append
12815
+ pluginCards.forEach(card => card.style.display = 'none'); // Hide all first
12816
+
12817
+ // If no results
12728
12818
  let noResultsMsg = document.getElementById('no-results-message');
12729
- if (visibleCount === 0) {
12819
+ if (visibleCards.length === 0) {
12730
12820
  if (!noResultsMsg) {
12731
12821
  noResultsMsg = document.createElement('div');
12732
12822
  noResultsMsg.id = 'no-results-message';
12733
- noResultsMsg.className = 'col-span-full text-center py-12';
12823
+ noResultsMsg.className = 'col-span-full text-center py-12 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-dashed border-zinc-300 dark:border-zinc-700';
12734
12824
  noResultsMsg.innerHTML = \`
12735
12825
  <div class="flex flex-col items-center">
12736
- <svg class="w-16 h-16 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12826
+ <svg class="w-12 h-12 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12737
12827
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
12738
12828
  </svg>
12739
- <h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-2">No plugins found</h3>
12829
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-1">No plugins found</h3>
12740
12830
  <p class="text-sm text-zinc-500 dark:text-zinc-400">Try adjusting your filters or search terms</p>
12741
12831
  </div>
12742
12832
  \`;
12743
- document.getElementById('plugins-grid').appendChild(noResultsMsg);
12833
+ pluginsGrid.appendChild(noResultsMsg);
12744
12834
  }
12745
12835
  noResultsMsg.style.display = '';
12746
- } else if (noResultsMsg) {
12747
- noResultsMsg.style.display = 'none';
12836
+ } else {
12837
+ if (noResultsMsg) noResultsMsg.style.display = 'none';
12838
+ visibleCards.forEach(card => {
12839
+ card.style.display = '';
12840
+ pluginsGrid.appendChild(card); // Re-appending moves it to the end, effectively sorting
12841
+ });
12748
12842
  }
12749
12843
  }
12750
12844
  </script>
@@ -12775,116 +12869,111 @@ function renderPluginsListPage(data) {
12775
12869
  }
12776
12870
  function renderPluginCard(plugin) {
12777
12871
  const statusColors = {
12778
- 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",
12779
- 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",
12780
- 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",
12781
- uninstalled: "bg-zinc-100 dark:bg-zinc-600/10 text-zinc-600 dark:text-zinc-500 ring-zinc-600/10 dark:ring-zinc-500/20"
12872
+ active: "bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 ring-emerald-600/20",
12873
+ inactive: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-600/20",
12874
+ error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-600/20",
12875
+ uninstalled: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-600 dark:text-zinc-500 ring-zinc-600/20"
12782
12876
  };
12783
12877
  const statusIcons = {
12784
- active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
12785
- inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
12786
- error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>',
12787
- uninstalled: '<div class="w-2 h-2 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-2"></div>'
12788
- };
12789
- const borderColors = {
12790
- active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
12791
- inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
12792
- error: "ring-[3px] ring-red-500 dark:ring-red-400",
12793
- uninstalled: "ring-[3px] ring-zinc-400 dark:ring-zinc-600"
12878
+ active: '<div class="w-1.5 h-1.5 bg-emerald-500 dark:bg-emerald-400 rounded-full mr-1.5"></div>',
12879
+ inactive: '<div class="w-1.5 h-1.5 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-1.5"></div>',
12880
+ error: '<div class="w-1.5 h-1.5 bg-red-500 dark:bg-red-400 rounded-full mr-1.5"></div>',
12881
+ uninstalled: '<div class="w-1.5 h-1.5 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-1.5"></div>'
12794
12882
  };
12795
12883
  const criticalCorePlugins = ["core-auth", "core-media"];
12796
12884
  const canToggle = !criticalCorePlugins.includes(plugin.id);
12797
12885
  let actionButton = "";
12798
12886
  if (plugin.status === "uninstalled") {
12799
- actionButton = `<button onclick="installPlugin('${plugin.name}')" class="bg-cyan-600 dark:bg-cyan-700 hover:bg-cyan-700 dark:hover:bg-cyan-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Install</button>`;
12800
- } else if (plugin.status === "active") {
12801
- actionButton = `<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>`;
12887
+ actionButton = `<button onclick="installPlugin('${plugin.name}')" class="w-full sm:w-auto bg-zinc-900 dark:bg-white hover:bg-zinc-800 dark:hover:bg-zinc-100 text-white dark:text-zinc-900 px-3 py-1.5 rounded-md text-xs font-medium transition-colors shadow-sm">Install</button>`;
12802
12888
  } else {
12803
- actionButton = `<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>`;
12889
+ const isActive = plugin.status === "active";
12890
+ const action = isActive ? "deactivate" : "activate";
12891
+ const bgClass = isActive ? "bg-emerald-600" : "bg-zinc-200 dark:bg-zinc-700";
12892
+ const translateClass = isActive ? "translate-x-5" : "translate-x-0";
12893
+ if (canToggle) {
12894
+ actionButton = `
12895
+ <button onclick="togglePlugin('${plugin.id}', '${action}', event)" type="button" class="${bgClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-emerald-600 focus:ring-offset-2 toggle-button" role="switch" aria-checked="${isActive}">
12896
+ <span class="sr-only">Toggle plugin</span>
12897
+ <span aria-hidden="true" class="${translateClass} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out toggle-knob"></span>
12898
+ </button>
12899
+ `;
12900
+ } else {
12901
+ actionButton = `
12902
+ <div class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-not-allowed rounded-full border-2 border-transparent bg-emerald-600/50 opacity-50" title="Core plugin cannot be disabled">
12903
+ <span class="translate-x-5 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0"></span>
12904
+ </div>
12905
+ `;
12906
+ }
12804
12907
  }
12805
12908
  return `
12806
- <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" data-category="${plugin.category}" data-status="${plugin.status}" data-name="${plugin.displayName}" data-description="${plugin.description}">
12909
+ <div class="plugin-card flex flex-col h-full rounded-md bg-white dark:bg-zinc-900 ring-1 ring-zinc-950/10 dark:ring-white/10 p-5 transition-all hover:shadow-md"
12910
+ data-category="${plugin.category}"
12911
+ data-status="${plugin.status}"
12912
+ data-name="${plugin.displayName}"
12913
+ data-description="${plugin.description}"
12914
+ data-downloads="${plugin.downloadCount || 0}"
12915
+ data-rating="${plugin.rating || 0}">
12807
12916
  <div class="flex items-start justify-between mb-4">
12808
12917
  <div class="flex items-center gap-3">
12809
- <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">
12918
+ <div class="w-10 h-10 rounded-md flex items-center justify-center bg-zinc-50 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 ring-1 ring-inset ring-zinc-200 dark:ring-zinc-700/50">
12810
12919
  ${plugin.icon || getDefaultPluginIcon(plugin.category)}
12811
12920
  </div>
12812
12921
  <div>
12813
- <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">${plugin.displayName}</h3>
12814
- <p class="text-sm text-zinc-500 dark:text-zinc-400">v${plugin.version} by ${plugin.author}</p>
12922
+ <div class="flex items-center gap-2">
12923
+ <h3 class="text-sm font-semibold text-zinc-900 dark:text-white">${plugin.displayName}</h3>
12924
+ <span class="status-badge inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset ${statusColors[plugin.status]}">
12925
+ ${statusIcons[plugin.status]}${plugin.status.charAt(0).toUpperCase() + plugin.status.slice(1)}
12926
+ </span>
12927
+ </div>
12928
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">v${plugin.version} \u2022 ${plugin.author}</p>
12815
12929
  </div>
12816
12930
  </div>
12817
- <div class="flex flex-col items-end gap-2">
12818
- <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]}">
12819
- ${statusIcons[plugin.status]}${plugin.status.charAt(0).toUpperCase() + plugin.status.slice(1)}
12820
- </span>
12821
- ${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>' : ""}
12931
+
12932
+ <div class="flex items-center gap-1">
12933
+ ${plugin.status !== "uninstalled" ? `
12934
+ <button onclick="showPluginDetails('${plugin.id}')" class="text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Plugin Details">
12935
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12936
+ <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"/>
12937
+ </svg>
12938
+ </button>
12939
+ ` : ""}
12940
+
12941
+ ${!plugin.isCore && plugin.status !== "uninstalled" ? `
12942
+ <button onclick="uninstallPlugin('${plugin.id}')" class="text-zinc-400 hover:text-red-600 dark:text-zinc-500 dark:hover:text-red-400 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Uninstall Plugin">
12943
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12944
+ <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"/>
12945
+ </svg>
12946
+ </button>
12947
+ ` : ""}
12822
12948
  </div>
12823
12949
  </div>
12824
12950
 
12825
- <p class="text-zinc-600 dark:text-zinc-300 text-sm mb-4 line-clamp-3">${plugin.description}</p>
12951
+ <p class="text-zinc-600 dark:text-zinc-400 text-sm mb-4 line-clamp-2 flex-grow">${plugin.description}</p>
12826
12952
 
12827
- <div class="flex items-center gap-4 mb-4 text-xs text-zinc-500 dark:text-zinc-400">
12828
- <span class="flex items-center gap-1">
12829
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12830
- <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"/>
12831
- </svg>
12953
+ <div class="flex flex-wrap items-center gap-2 mb-5">
12954
+ <span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">
12832
12955
  ${plugin.category}
12833
12956
  </span>
12834
-
12835
- ${plugin.downloadCount ? `
12836
- <span class="flex items-center gap-1">
12837
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12838
- <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"/>
12839
- </svg>
12840
- ${plugin.downloadCount.toLocaleString()}
12841
- </span>
12842
- ` : ""}
12843
-
12844
- ${plugin.rating ? `
12845
- <span class="flex items-center gap-1">
12846
- <svg class="w-4 h-4 text-yellow-500 dark:text-yellow-400 fill-current" viewBox="0 0 24 24">
12847
- <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"/>
12848
- </svg>
12849
- ${plugin.rating}
12850
- </span>
12851
- ` : ""}
12852
-
12853
- <span>${plugin.lastUpdated}</span>
12854
- </div>
12855
-
12856
- ${plugin.dependencies && plugin.dependencies.length > 0 ? `
12857
- <div class="mb-4">
12858
- <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">Dependencies:</p>
12859
- <div class="flex flex-wrap gap-1">
12860
- ${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("")}
12861
- </div>
12957
+ ${plugin.isCore ? '<span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">Core</span>' : ""}
12958
+
12959
+ ${plugin.dependencies && plugin.dependencies.map((dep) => `
12960
+ <span class="inline-flex items-center rounded-md bg-zinc-100 dark:bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-400">
12961
+ ${dep}
12962
+ </span>
12963
+ `).join("") || ""}
12862
12964
  </div>
12863
- ` : ""}
12864
12965
 
12865
- <div class="flex items-center justify-between">
12966
+ <div class="flex items-center justify-between pt-4 border-t border-zinc-100 dark:border-zinc-800 mt-auto">
12866
12967
  <div class="flex gap-2">
12867
- ${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
12868
- ${plugin.status !== "uninstalled" ? `
12869
- <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">
12870
- Settings
12871
- </button>
12872
- ` : ""}
12968
+ ${actionButton}
12873
12969
  </div>
12874
12970
 
12875
12971
  <div class="flex items-center gap-2">
12876
12972
  ${plugin.status !== "uninstalled" ? `
12877
- <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">
12878
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12879
- <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"/>
12880
- </svg>
12881
- </button>
12882
- ` : ""}
12883
-
12884
- ${!plugin.isCore && plugin.status !== "uninstalled" ? `
12885
- <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">
12886
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12887
- <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"/>
12973
+ <button onclick="openPluginSettings('${plugin.id}')" class="text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 p-1.5 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Settings">
12974
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12975
+ <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"/>
12976
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
12888
12977
  </svg>
12889
12978
  </button>
12890
12979
  ` : ""}
@@ -18957,6 +19046,8 @@ function renderCollectionFormPage(data) {
18957
19046
  })
18958
19047
  .then(data => {
18959
19048
  if (data.success) {
19049
+ // Close modal before reloading
19050
+ closeFieldModal();
18960
19051
  location.reload();
18961
19052
  } else {
18962
19053
  alert('Error saving field: ' + (data.error || 'Unknown error'));
@@ -19507,11 +19598,65 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
19507
19598
  return c.json({ success: false, error: "Field name must contain only lowercase letters, numbers, and underscores." });
19508
19599
  }
19509
19600
  const db = c.env.DB;
19601
+ const getCollectionStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
19602
+ const collection = await getCollectionStmt.bind(collectionId).first();
19603
+ if (!collection) {
19604
+ return c.json({ success: false, error: "Collection not found." });
19605
+ }
19606
+ let schema = collection.schema ? typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema : null;
19607
+ if (schema && schema.properties && schema.properties[fieldName]) {
19608
+ return c.json({ success: false, error: "A field with this name already exists." });
19609
+ }
19510
19610
  const existingStmt = db.prepare("SELECT id FROM content_fields WHERE collection_id = ? AND field_name = ?");
19511
19611
  const existing = await existingStmt.bind(collectionId, fieldName).first();
19512
19612
  if (existing) {
19513
19613
  return c.json({ success: false, error: "A field with this name already exists." });
19514
19614
  }
19615
+ let parsedOptions = {};
19616
+ try {
19617
+ parsedOptions = fieldOptions ? JSON.parse(fieldOptions) : {};
19618
+ } catch (e) {
19619
+ console.error("Error parsing field options:", e);
19620
+ }
19621
+ if (schema) {
19622
+ if (!schema.properties) {
19623
+ schema.properties = {};
19624
+ }
19625
+ if (!schema.required) {
19626
+ schema.required = [];
19627
+ }
19628
+ const fieldConfig = {
19629
+ type: fieldType === "number" ? "number" : fieldType === "boolean" ? "boolean" : "string",
19630
+ title: fieldLabel,
19631
+ searchable: isSearchable,
19632
+ ...parsedOptions
19633
+ };
19634
+ if (fieldType === "richtext") {
19635
+ fieldConfig.format = "richtext";
19636
+ } else if (fieldType === "date") {
19637
+ fieldConfig.format = "date-time";
19638
+ } else if (fieldType === "select") {
19639
+ fieldConfig.enum = parsedOptions.options || [];
19640
+ } else if (fieldType === "media") {
19641
+ fieldConfig.format = "media";
19642
+ } else if (fieldType === "quill") {
19643
+ fieldConfig.type = "quill";
19644
+ } else if (fieldType === "mdxeditor") {
19645
+ fieldConfig.type = "mdxeditor";
19646
+ }
19647
+ schema.properties[fieldName] = fieldConfig;
19648
+ if (isRequired && !schema.required.includes(fieldName)) {
19649
+ schema.required.push(fieldName);
19650
+ }
19651
+ const updateSchemaStmt = db.prepare(`
19652
+ UPDATE collections
19653
+ SET schema = ?, updated_at = ?
19654
+ WHERE id = ?
19655
+ `);
19656
+ await updateSchemaStmt.bind(JSON.stringify(schema), Date.now(), collectionId).run();
19657
+ console.log("[Add Field] Added field to schema:", fieldName, fieldConfig);
19658
+ return c.json({ success: true, fieldId: `schema-${fieldName}` });
19659
+ }
19515
19660
  const orderStmt = db.prepare("SELECT MAX(field_order) as max_order FROM content_fields WHERE collection_id = ?");
19516
19661
  const orderResult = await orderStmt.bind(collectionId).first();
19517
19662
  const nextOrder = (orderResult?.max_order || 0) + 1;
@@ -19652,7 +19797,39 @@ adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
19652
19797
  adminCollectionsRoutes.delete("/:collectionId/fields/:fieldId", async (c) => {
19653
19798
  try {
19654
19799
  const fieldId = c.req.param("fieldId");
19800
+ const collectionId = c.req.param("collectionId");
19655
19801
  const db = c.env.DB;
19802
+ if (fieldId.startsWith("schema-")) {
19803
+ const fieldName = fieldId.replace("schema-", "");
19804
+ const getCollectionStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
19805
+ const collection = await getCollectionStmt.bind(collectionId).first();
19806
+ if (!collection) {
19807
+ return c.json({ success: false, error: "Collection not found." });
19808
+ }
19809
+ let schema = typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema;
19810
+ if (!schema || !schema.properties) {
19811
+ return c.json({ success: false, error: "Field not found in schema." });
19812
+ }
19813
+ if (schema.properties[fieldName]) {
19814
+ delete schema.properties[fieldName];
19815
+ if (schema.required && Array.isArray(schema.required)) {
19816
+ const requiredIndex = schema.required.indexOf(fieldName);
19817
+ if (requiredIndex !== -1) {
19818
+ schema.required.splice(requiredIndex, 1);
19819
+ }
19820
+ }
19821
+ const updateCollectionStmt = db.prepare(`
19822
+ UPDATE collections
19823
+ SET schema = ?, updated_at = ?
19824
+ WHERE id = ?
19825
+ `);
19826
+ await updateCollectionStmt.bind(JSON.stringify(schema), Date.now(), collectionId).run();
19827
+ console.log("[Delete Field] Removed field from schema:", fieldName);
19828
+ return c.json({ success: true });
19829
+ } else {
19830
+ return c.json({ success: false, error: "Field not found in schema." });
19831
+ }
19832
+ }
19656
19833
  const deleteStmt = db.prepare("DELETE FROM content_fields WHERE id = ?");
19657
19834
  await deleteStmt.bind(fieldId).run();
19658
19835
  return c.json({ success: true });
@@ -21580,5 +21757,5 @@ var ROUTES_INFO = {
21580
21757
  };
21581
21758
 
21582
21759
  export { PluginBuilder, ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, test_cleanup_default, userRoutes };
21583
- //# sourceMappingURL=chunk-HDSRB23N.js.map
21584
- //# sourceMappingURL=chunk-HDSRB23N.js.map
21760
+ //# sourceMappingURL=chunk-T4XRPNX2.js.map
21761
+ //# sourceMappingURL=chunk-T4XRPNX2.js.map