@sonicjs-cms/core 2.0.9 → 2.0.11

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 (67) hide show
  1. package/dist/{chunk-VEC5MLT3.js → chunk-5RKQB2JG.js} +10 -228
  2. package/dist/chunk-5RKQB2JG.js.map +1 -0
  3. package/dist/chunk-AMSTLQFI.cjs +801 -0
  4. package/dist/chunk-AMSTLQFI.cjs.map +1 -0
  5. package/dist/{chunk-ABYMIXRN.js → chunk-CLLJFZ5U.js} +2018 -1054
  6. package/dist/chunk-CLLJFZ5U.js.map +1 -0
  7. package/dist/{chunk-WRRLB6KG.js → chunk-DU7JJZN7.js} +5 -4
  8. package/dist/chunk-DU7JJZN7.js.map +1 -0
  9. package/dist/{chunk-OKPDQO2Y.js → chunk-FYWJMETG.js} +30 -10
  10. package/dist/chunk-FYWJMETG.js.map +1 -0
  11. package/dist/chunk-I5ZPYKNX.js +787 -0
  12. package/dist/chunk-I5ZPYKNX.js.map +1 -0
  13. package/dist/{chunk-4I25AGUR.cjs → chunk-IM2LGCYD.cjs} +2166 -1202
  14. package/dist/chunk-IM2LGCYD.cjs.map +1 -0
  15. package/dist/{chunk-TMIRVVQ7.cjs → chunk-NNXPAPUD.cjs} +5 -4
  16. package/dist/chunk-NNXPAPUD.cjs.map +1 -0
  17. package/dist/{chunk-OPGDMS7L.js → chunk-QNWYQZ55.js} +3 -3
  18. package/dist/{chunk-OPGDMS7L.js.map → chunk-QNWYQZ55.js.map} +1 -1
  19. package/dist/{chunk-COBUPOMD.js → chunk-T7IYBGGO.cjs} +5 -770
  20. package/dist/chunk-T7IYBGGO.cjs.map +1 -0
  21. package/dist/{chunk-DYYAXDXI.cjs → chunk-X2VADBA4.cjs} +31 -11
  22. package/dist/chunk-X2VADBA4.cjs.map +1 -0
  23. package/dist/{chunk-EYMHWJTW.cjs → chunk-YU6QFFI4.cjs} +9 -228
  24. package/dist/chunk-YU6QFFI4.cjs.map +1 -0
  25. package/dist/{chunk-MABBKINE.cjs → chunk-ZMSYKV62.cjs} +5 -5
  26. package/dist/{chunk-MABBKINE.cjs.map → chunk-ZMSYKV62.cjs.map} +1 -1
  27. package/dist/{chunk-NBDPIRQS.cjs → chunk-ZPMFT2JW.js} +4 -786
  28. package/dist/chunk-ZPMFT2JW.js.map +1 -0
  29. package/dist/index.cjs +475 -104
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +386 -11
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.cjs +24 -23
  34. package/dist/middleware.js +3 -2
  35. package/dist/migrations-IHERIQVD.js +4 -0
  36. package/dist/migrations-IHERIQVD.js.map +1 -0
  37. package/dist/migrations-POFD5KNG.cjs +13 -0
  38. package/dist/migrations-POFD5KNG.cjs.map +1 -0
  39. package/dist/routes.cjs +25 -28
  40. package/dist/routes.js +6 -5
  41. package/dist/services.cjs +19 -18
  42. package/dist/services.js +2 -1
  43. package/dist/templates.cjs +17 -21
  44. package/dist/templates.js +2 -2
  45. package/dist/utils.cjs +11 -11
  46. package/dist/utils.js +1 -1
  47. package/migrations/001_initial_schema.sql +2 -2
  48. package/migrations/007_demo_login_plugin.sql +1 -1
  49. package/migrations/020_add_email_plugin.sql +22 -0
  50. package/migrations/021_add_magic_link_auth_plugin.sql +42 -0
  51. package/migrations/021_add_otp_login.sql +42 -0
  52. package/migrations/022_add_tinymce_plugin.sql +25 -0
  53. package/migrations/023_add_mdxeditor_plugin.sql +25 -0
  54. package/migrations/024_add_quill_editor_plugin.sql +25 -0
  55. package/migrations/025_add_easymde_plugin.sql +25 -0
  56. package/package.json +3 -2
  57. package/dist/chunk-4I25AGUR.cjs.map +0 -1
  58. package/dist/chunk-ABYMIXRN.js.map +0 -1
  59. package/dist/chunk-COBUPOMD.js.map +0 -1
  60. package/dist/chunk-DYYAXDXI.cjs.map +0 -1
  61. package/dist/chunk-EYMHWJTW.cjs.map +0 -1
  62. package/dist/chunk-NBDPIRQS.cjs.map +0 -1
  63. package/dist/chunk-OKPDQO2Y.js.map +0 -1
  64. package/dist/chunk-TMIRVVQ7.cjs.map +0 -1
  65. package/dist/chunk-VEC5MLT3.js.map +0 -1
  66. package/dist/chunk-WRRLB6KG.js.map +0 -1
  67. package/migrations/002_faq_plugin.sql +0 -86
@@ -1,8 +1,9 @@
1
1
  import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-6FR25MPC.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-OKPDQO2Y.js';
3
- import { PluginService, MigrationService } from './chunk-COBUPOMD.js';
4
- import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-VEC5MLT3.js';
5
- import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-WRRLB6KG.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-FYWJMETG.js';
3
+ import { PluginService } from './chunk-I5ZPYKNX.js';
4
+ import { MigrationService } from './chunk-ZPMFT2JW.js';
5
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-5RKQB2JG.js';
6
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-DU7JJZN7.js';
6
7
  import { metricsTracker } from './chunk-FICTAGD4.js';
7
8
  import { Hono } from 'hono';
8
9
  import { cors } from 'hono/cors';
@@ -337,8 +338,8 @@ apiRoutes.get("/content", async (c) => {
337
338
  filter.limit = 50;
338
339
  }
339
340
  filter.limit = Math.min(filter.limit, 1e3);
340
- const builder = new QueryFilterBuilder();
341
- const queryResult = builder.build("content", filter);
341
+ const builder3 = new QueryFilterBuilder();
342
+ const queryResult = builder3.build("content", filter);
342
343
  if (queryResult.errors.length > 0) {
343
344
  return c.json({
344
345
  error: "Invalid filter parameters",
@@ -440,8 +441,8 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
440
441
  filter.limit = 50;
441
442
  }
442
443
  filter.limit = Math.min(filter.limit, 1e3);
443
- const builder = new QueryFilterBuilder();
444
- const queryResult = builder.build("content", filter);
444
+ const builder3 = new QueryFilterBuilder();
445
+ const queryResult = builder3.build("content", filter);
445
446
  if (queryResult.errors.length > 0) {
446
447
  return c.json({
447
448
  error: "Invalid filter parameters",
@@ -1084,10 +1085,10 @@ apiMediaRoutes.patch("/:id", async (c) => {
1084
1085
  const allowedFields = ["alt", "caption", "tags", "folder"];
1085
1086
  const updates = [];
1086
1087
  const values = [];
1087
- for (const [key, value2] of Object.entries(body)) {
1088
+ for (const [key, value] of Object.entries(body)) {
1088
1089
  if (allowedFields.includes(key)) {
1089
1090
  updates.push(`${key} = ?`);
1090
- values.push(key === "tags" ? JSON.stringify(value2) : value2);
1091
+ values.push(key === "tags" ? JSON.stringify(value) : value);
1091
1092
  }
1092
1093
  }
1093
1094
  if (updates.length === 0) {
@@ -1717,6 +1718,68 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1717
1718
  return c.json({ error: "Failed to delete collection" }, 500);
1718
1719
  }
1719
1720
  });
1721
+ adminApiRoutes.get("/migrations/status", async (c) => {
1722
+ try {
1723
+ const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
1724
+ const db = c.env.DB;
1725
+ const migrationService = new MigrationService2(db);
1726
+ const status = await migrationService.getMigrationStatus();
1727
+ return c.json({
1728
+ success: true,
1729
+ data: status
1730
+ });
1731
+ } catch (error) {
1732
+ console.error("Error fetching migration status:", error);
1733
+ return c.json({
1734
+ success: false,
1735
+ error: "Failed to fetch migration status"
1736
+ }, 500);
1737
+ }
1738
+ });
1739
+ adminApiRoutes.post("/migrations/run", async (c) => {
1740
+ try {
1741
+ const user = c.get("user");
1742
+ if (!user || user.role !== "admin") {
1743
+ return c.json({
1744
+ success: false,
1745
+ error: "Unauthorized. Admin access required."
1746
+ }, 403);
1747
+ }
1748
+ const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
1749
+ const db = c.env.DB;
1750
+ const migrationService = new MigrationService2(db);
1751
+ const result = await migrationService.runPendingMigrations();
1752
+ return c.json({
1753
+ success: result.success,
1754
+ message: result.message,
1755
+ applied: result.applied
1756
+ });
1757
+ } catch (error) {
1758
+ console.error("Error running migrations:", error);
1759
+ return c.json({
1760
+ success: false,
1761
+ error: "Failed to run migrations"
1762
+ }, 500);
1763
+ }
1764
+ });
1765
+ adminApiRoutes.get("/migrations/validate", async (c) => {
1766
+ try {
1767
+ const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
1768
+ const db = c.env.DB;
1769
+ const migrationService = new MigrationService2(db);
1770
+ const validation = await migrationService.validateSchema();
1771
+ return c.json({
1772
+ success: true,
1773
+ data: validation
1774
+ });
1775
+ } catch (error) {
1776
+ console.error("Error validating schema:", error);
1777
+ return c.json({
1778
+ success: false,
1779
+ error: "Failed to validate schema"
1780
+ }, 500);
1781
+ }
1782
+ });
1720
1783
  var admin_api_default = adminApiRoutes;
1721
1784
 
1722
1785
  // src/templates/pages/auth-login.template.ts
@@ -1861,7 +1924,7 @@ function renderLoginPage(data, demoLoginActive = false) {
1861
1924
 
1862
1925
  if (emailInput && passwordInput) {
1863
1926
  emailInput.value = 'admin@sonicjs.com';
1864
- passwordInput.value = 'admin123';
1927
+ passwordInput.value = 'sonicjs!';
1865
1928
 
1866
1929
  // Add visual indication that form is prefilled (only if not already present)
1867
1930
  const form = emailInput.closest('form');
@@ -2120,16 +2183,22 @@ authRoutes.post(
2120
2183
  async (c) => {
2121
2184
  try {
2122
2185
  const db = c.env.DB;
2123
- const requestData = await c.req.json();
2186
+ let requestData;
2187
+ try {
2188
+ requestData = await c.req.json();
2189
+ } catch (parseError) {
2190
+ return c.json({ error: "Invalid JSON in request body" }, 400);
2191
+ }
2124
2192
  const validationSchema = await authValidationService.buildRegistrationSchema(db);
2125
- const validationResult = await validationSchema.safeParseAsync(requestData);
2126
- if (!validationResult.success) {
2193
+ let validatedData;
2194
+ try {
2195
+ validatedData = await validationSchema.parseAsync(requestData);
2196
+ } catch (validationError) {
2127
2197
  return c.json({
2128
2198
  error: "Validation failed",
2129
- details: validationResult.error.errors.map((e) => e.message)
2199
+ details: validationError.errors?.map((e) => e.message) || [validationError.message || "Invalid request data"]
2130
2200
  }, 400);
2131
2201
  }
2132
- const validatedData = validationResult.data;
2133
2202
  const email = validatedData.email;
2134
2203
  const password = validatedData.password;
2135
2204
  const username = validatedData.username || authValidationService.generateDefaultValue("username", validatedData);
@@ -2181,7 +2250,13 @@ authRoutes.post(
2181
2250
  }, 201);
2182
2251
  } catch (error) {
2183
2252
  console.error("Registration error:", error);
2184
- return c.json({ error: "Registration failed" }, 500);
2253
+ if (error instanceof Error && error.message.includes("validation")) {
2254
+ return c.json({ error: error.message }, 400);
2255
+ }
2256
+ return c.json({
2257
+ error: "Registration failed",
2258
+ details: error instanceof Error ? error.message : String(error)
2259
+ }, 500);
2185
2260
  }
2186
2261
  }
2187
2262
  );
@@ -2227,8 +2302,8 @@ authRoutes.post("/login", async (c) => {
2227
2302
  id: user.id,
2228
2303
  email: user.email,
2229
2304
  username: user.username,
2230
- firstName: user.firstName,
2231
- lastName: user.lastName,
2305
+ firstName: user.first_name,
2306
+ lastName: user.last_name,
2232
2307
  role: user.role
2233
2308
  },
2234
2309
  token
@@ -2470,8 +2545,10 @@ authRoutes.post("/seed-admin", async (c) => {
2470
2545
  `).run();
2471
2546
  const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
2472
2547
  if (existingAdmin) {
2548
+ const passwordHash2 = await AuthManager.hashPassword("sonicjs!");
2549
+ await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
2473
2550
  return c.json({
2474
- message: "Admin user already exists",
2551
+ message: "Admin user already exists (password updated)",
2475
2552
  user: {
2476
2553
  id: existingAdmin.id,
2477
2554
  email: "admin@sonicjs.com",
@@ -2480,7 +2557,7 @@ authRoutes.post("/seed-admin", async (c) => {
2480
2557
  }
2481
2558
  });
2482
2559
  }
2483
- const passwordHash = await AuthManager.hashPassword("admin123");
2560
+ const passwordHash = await AuthManager.hashPassword("sonicjs!");
2484
2561
  const userId = "admin-user-id";
2485
2562
  const now = Date.now();
2486
2563
  const adminEmail = "admin@sonicjs.com".toLowerCase();
@@ -2990,7 +3067,7 @@ init_admin_layout_catalyst_template();
2990
3067
 
2991
3068
  // src/templates/components/dynamic-field.template.ts
2992
3069
  function renderDynamicField(field, options = {}) {
2993
- const { value: value2 = "", errors = [], disabled = false, className = "" } = options;
3070
+ const { value = "", errors = [], disabled = false, className = "" } = options;
2994
3071
  const opts = field.field_options || {};
2995
3072
  const required = field.is_required ? "required" : "";
2996
3073
  const baseClasses = `w-full rounded-lg px-3 py-2 text-sm text-zinc-950 dark:text-white bg-white dark:bg-zinc-800 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow ${className}`;
@@ -3047,7 +3124,7 @@ function renderDynamicField(field, options = {}) {
3047
3124
  type="text"
3048
3125
  id="${fieldId}"
3049
3126
  name="${fieldName}"
3050
- value="${escapeHtml2(value2)}"
3127
+ value="${escapeHtml2(value)}"
3051
3128
  placeholder="${opts.placeholder || ""}"
3052
3129
  maxlength="${opts.maxLength || ""}"
3053
3130
  ${opts.pattern ? `data-pattern="${opts.pattern}"` : ""}
@@ -3083,40 +3160,66 @@ function renderDynamicField(field, options = {}) {
3083
3160
  ` : ""}
3084
3161
  `;
3085
3162
  break;
3163
+ case "textarea":
3164
+ fieldHTML = `
3165
+ <textarea
3166
+ id="${fieldId}"
3167
+ name="${fieldName}"
3168
+ rows="${opts.rows || 6}"
3169
+ placeholder="${opts.placeholder || ""}"
3170
+ maxlength="${opts.maxLength || ""}"
3171
+ class="${baseClasses} ${errorClasses} resize-y"
3172
+ ${required}
3173
+ ${disabled ? "disabled" : ""}
3174
+ >${escapeHtml2(value)}</textarea>
3175
+ `;
3176
+ break;
3086
3177
  case "richtext":
3087
3178
  fieldHTML = `
3088
- <div class="richtext-container">
3089
- <textarea
3179
+ <div class="richtext-container" data-height="${opts.height || 300}" data-toolbar="${opts.toolbar || "full"}">
3180
+ <textarea
3090
3181
  id="${fieldId}"
3091
3182
  name="${fieldName}"
3092
3183
  class="${baseClasses} ${errorClasses} min-h-[${opts.height || 300}px]"
3093
3184
  ${required}
3094
3185
  ${disabled ? "disabled" : ""}
3095
- >${escapeHtml2(value2)}</textarea>
3096
- <script>
3097
- // Initialize TinyMCE for this field
3098
- if (typeof tinymce !== 'undefined') {
3099
- tinymce.init({
3100
- selector: '#${fieldId}',
3101
- skin: 'oxide-dark',
3102
- content_css: 'dark',
3103
- height: ${opts.height || 300},
3104
- menubar: false,
3105
- plugins: [
3106
- 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
3107
- 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
3108
- 'insertdatetime', 'media', 'table', 'help', 'wordcount'
3109
- ],
3110
- toolbar: '${opts.toolbar === "simple" ? "bold italic underline | bullist numlist | link" : "undo redo | blocks | bold italic backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help"}',
3111
- content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif; font-size: 14px; color: #fff; background-color: #1f2937; }',
3112
- setup: function(editor) {
3113
- editor.on('change', function() {
3114
- editor.save();
3115
- });
3116
- }
3117
- });
3118
- }
3119
- </script>
3186
+ >${escapeHtml2(value)}</textarea>
3187
+ </div>
3188
+ `;
3189
+ break;
3190
+ case "quill":
3191
+ fieldHTML = `
3192
+ <div class="quill-editor-container" data-field-id="${fieldId}">
3193
+ <!-- Quill Editor Container -->
3194
+ <div
3195
+ id="${fieldId}-editor"
3196
+ class="quill-editor bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100"
3197
+ data-theme="${opts.theme || "snow"}"
3198
+ data-toolbar="${opts.toolbar || "full"}"
3199
+ data-placeholder="${opts.placeholder || "Enter content..."}"
3200
+ data-height="${opts.height || 300}"
3201
+ >${value}</div>
3202
+
3203
+ <!-- Hidden input to store the actual content for form submission -->
3204
+ <input
3205
+ type="hidden"
3206
+ id="${fieldId}"
3207
+ name="${fieldName}"
3208
+ value="${escapeHtml2(value)}"
3209
+ >
3210
+ </div>
3211
+ `;
3212
+ break;
3213
+ case "mdxeditor":
3214
+ fieldHTML = `
3215
+ <div class="richtext-container" data-height="${opts.height || 300}" data-toolbar="${opts.toolbar || "full"}">
3216
+ <textarea
3217
+ id="${fieldId}"
3218
+ name="${fieldName}"
3219
+ class="${baseClasses} ${errorClasses} min-h-[${opts.height || 300}px]"
3220
+ ${required}
3221
+ ${disabled ? "disabled" : ""}
3222
+ >${escapeHtml2(value)}</textarea>
3120
3223
  </div>
3121
3224
  `;
3122
3225
  break;
@@ -3126,7 +3229,7 @@ function renderDynamicField(field, options = {}) {
3126
3229
  type="number"
3127
3230
  id="${fieldId}"
3128
3231
  name="${fieldName}"
3129
- value="${value2}"
3232
+ value="${value}"
3130
3233
  min="${opts.min || ""}"
3131
3234
  max="${opts.max || ""}"
3132
3235
  step="${opts.step || ""}"
@@ -3138,7 +3241,7 @@ function renderDynamicField(field, options = {}) {
3138
3241
  `;
3139
3242
  break;
3140
3243
  case "boolean":
3141
- const checked = value2 === true || value2 === "true" || value2 === "1" ? "checked" : "";
3244
+ const checked = value === true || value === "true" || value === "1" ? "checked" : "";
3142
3245
  fieldHTML = `
3143
3246
  <div class="flex items-center space-x-3">
3144
3247
  <input
@@ -3159,11 +3262,26 @@ function renderDynamicField(field, options = {}) {
3159
3262
  break;
3160
3263
  case "date":
3161
3264
  fieldHTML = `
3162
- <input
3163
- type="date"
3265
+ <input
3266
+ type="date"
3267
+ id="${fieldId}"
3268
+ name="${fieldName}"
3269
+ value="${value}"
3270
+ min="${opts.min || ""}"
3271
+ max="${opts.max || ""}"
3272
+ class="${baseClasses} ${errorClasses}"
3273
+ ${required}
3274
+ ${disabled ? "disabled" : ""}
3275
+ >
3276
+ `;
3277
+ break;
3278
+ case "datetime":
3279
+ fieldHTML = `
3280
+ <input
3281
+ type="datetime-local"
3164
3282
  id="${fieldId}"
3165
3283
  name="${fieldName}"
3166
- value="${value2}"
3284
+ value="${value}"
3167
3285
  min="${opts.min || ""}"
3168
3286
  max="${opts.max || ""}"
3169
3287
  class="${baseClasses} ${errorClasses}"
@@ -3172,10 +3290,75 @@ function renderDynamicField(field, options = {}) {
3172
3290
  >
3173
3291
  `;
3174
3292
  break;
3293
+ case "slug":
3294
+ let slugPattern = opts.pattern || "^[a-z0-9-]+$";
3295
+ let slugHelp = '<p class="mt-2 text-xs text-zinc-500 dark:text-zinc-400">Use lowercase letters, numbers, and hyphens only</p>';
3296
+ slugHelp += `<button type="button" class="mt-1 text-xs text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300" onclick="generateSlugFromTitle('\${fieldId}')">Generate from title</button>`;
3297
+ fieldHTML = `
3298
+ <input
3299
+ type="text"
3300
+ id="${fieldId}"
3301
+ name="${fieldName}"
3302
+ value="${escapeHtml2(value)}"
3303
+ placeholder="${opts.placeholder || "url-friendly-slug"}"
3304
+ maxlength="${opts.maxLength || ""}"
3305
+ data-pattern="${slugPattern}"
3306
+ class="${baseClasses} ${errorClasses}"
3307
+ ${required}
3308
+ ${disabled ? "disabled" : ""}
3309
+ >
3310
+ ${slugHelp}
3311
+ <script>
3312
+ (function() {
3313
+ const field = document.getElementById('${fieldId}');
3314
+ const pattern = new RegExp('${slugPattern}');
3315
+
3316
+ field.addEventListener('input', function() {
3317
+ if (this.value && !pattern.test(this.value)) {
3318
+ this.setCustomValidity('Please use only lowercase letters, numbers, and hyphens.');
3319
+ } else {
3320
+ this.setCustomValidity('');
3321
+ }
3322
+ });
3323
+
3324
+ field.addEventListener('blur', function() {
3325
+ this.reportValidity();
3326
+ });
3327
+ })();
3328
+
3329
+ function generateSlugFromTitle(slugFieldId) {
3330
+ const titleField = document.querySelector('input[name="title"]');
3331
+ const slugField = document.getElementById(slugFieldId);
3332
+ if (titleField && slugField) {
3333
+ const slug = titleField.value
3334
+ .toLowerCase()
3335
+ .replace(/[^a-z0-9\\s_-]/g, '')
3336
+ .replace(/\\s+/g, '-')
3337
+ .replace(/[-_]+/g, '-')
3338
+ .replace(/^[-_]|[-_]$/g, '');
3339
+ slugField.value = slug;
3340
+ }
3341
+ }
3342
+
3343
+ // Auto-generate slug when title changes
3344
+ document.addEventListener('DOMContentLoaded', function() {
3345
+ const titleField = document.querySelector('input[name="title"]');
3346
+ const slugField = document.getElementById('${fieldId}');
3347
+ if (titleField && slugField && !slugField.value) {
3348
+ titleField.addEventListener('input', function() {
3349
+ if (!slugField.value) {
3350
+ generateSlugFromTitle('${fieldId}');
3351
+ }
3352
+ });
3353
+ }
3354
+ });
3355
+ </script>
3356
+ `;
3357
+ break;
3175
3358
  case "select":
3176
3359
  const options2 = opts.options || [];
3177
3360
  const multiple = opts.multiple ? "multiple" : "";
3178
- const selectedValues = Array.isArray(value2) ? value2 : [value2];
3361
+ const selectedValues = Array.isArray(value) ? value : [value];
3179
3362
  fieldHTML = `
3180
3363
  <select
3181
3364
  id="${fieldId}"
@@ -3208,9 +3391,9 @@ function renderDynamicField(field, options = {}) {
3208
3391
  case "media":
3209
3392
  fieldHTML = `
3210
3393
  <div class="media-field-container">
3211
- <input type="hidden" id="${fieldId}" name="${fieldName}" value="${value2}">
3212
- <div class="media-preview ${value2 ? "" : "hidden"}" id="${fieldId}-preview">
3213
- ${value2 ? `<img src="${value2}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
3394
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${value}">
3395
+ <div class="media-preview ${value ? "" : "hidden"}" id="${fieldId}-preview">
3396
+ ${value ? `<img src="${value}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
3214
3397
  </div>
3215
3398
  <div class="media-actions mt-2 space-x-2">
3216
3399
  <button
@@ -3224,7 +3407,7 @@ function renderDynamicField(field, options = {}) {
3224
3407
  </svg>
3225
3408
  Select Media
3226
3409
  </button>
3227
- ${value2 ? `
3410
+ ${value ? `
3228
3411
  <button
3229
3412
  type="button"
3230
3413
  onclick="clearMediaField('${fieldId}')"
@@ -3238,36 +3421,13 @@ function renderDynamicField(field, options = {}) {
3238
3421
  </div>
3239
3422
  `;
3240
3423
  break;
3241
- case "guid":
3242
- fieldHTML = `
3243
- <div class="guid-field-container">
3244
- <input
3245
- type="text"
3246
- id="${fieldId}"
3247
- name="${fieldName}"
3248
- value="${escapeHtml2(value2)}"
3249
- class="${baseClasses} bg-zinc-100 dark:bg-zinc-800/50 cursor-not-allowed"
3250
- readonly
3251
- disabled
3252
- >
3253
- <div class="mt-2 flex items-start gap-x-2">
3254
- <svg class="h-5 w-5 text-cyan-600 dark:text-cyan-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
3255
- <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"/>
3256
- </svg>
3257
- <div class="text-xs text-zinc-600 dark:text-zinc-400">
3258
- ${value2 ? "This unique identifier was automatically generated and cannot be changed." : "A unique identifier (UUID) will be automatically generated when you save this content."}
3259
- </div>
3260
- </div>
3261
- </div>
3262
- `;
3263
- break;
3264
3424
  default:
3265
3425
  fieldHTML = `
3266
3426
  <input
3267
3427
  type="text"
3268
3428
  id="${fieldId}"
3269
3429
  name="${fieldName}"
3270
- value="${escapeHtml2(value2)}"
3430
+ value="${escapeHtml2(value)}"
3271
3431
  class="${baseClasses} ${errorClasses}"
3272
3432
  ${required}
3273
3433
  ${disabled ? "disabled" : ""}
@@ -3324,34 +3484,750 @@ function escapeHtml2(text) {
3324
3484
  "'": "&#39;"
3325
3485
  })[char] || char);
3326
3486
  }
3487
+ var PluginBuilder = class _PluginBuilder {
3488
+ plugin;
3489
+ constructor(options) {
3490
+ this.plugin = {
3491
+ name: options.name,
3492
+ version: options.version,
3493
+ description: options.description,
3494
+ author: options.author,
3495
+ dependencies: options.dependencies,
3496
+ routes: [],
3497
+ middleware: [],
3498
+ models: [],
3499
+ services: [],
3500
+ adminPages: [],
3501
+ adminComponents: [],
3502
+ menuItems: [],
3503
+ hooks: []
3504
+ };
3505
+ }
3506
+ /**
3507
+ * Create a new plugin builder
3508
+ */
3509
+ static create(options) {
3510
+ return new _PluginBuilder(options);
3511
+ }
3512
+ /**
3513
+ * Add metadata to the plugin
3514
+ */
3515
+ metadata(metadata) {
3516
+ Object.assign(this.plugin, metadata);
3517
+ return this;
3518
+ }
3519
+ /**
3520
+ * Add routes to plugin
3521
+ */
3522
+ addRoutes(routes) {
3523
+ this.plugin.routes = [...this.plugin.routes || [], ...routes];
3524
+ return this;
3525
+ }
3526
+ /**
3527
+ * Add a single route to plugin
3528
+ */
3529
+ addRoute(path, handler, options) {
3530
+ const route = {
3531
+ path,
3532
+ handler,
3533
+ ...options
3534
+ };
3535
+ this.plugin.routes = [...this.plugin.routes || [], route];
3536
+ return this;
3537
+ }
3538
+ /**
3539
+ * Add middleware to plugin
3540
+ */
3541
+ addMiddleware(middleware) {
3542
+ this.plugin.middleware = [...this.plugin.middleware || [], ...middleware];
3543
+ return this;
3544
+ }
3545
+ /**
3546
+ * Add a single middleware to plugin
3547
+ */
3548
+ addSingleMiddleware(name, handler, options) {
3549
+ const middleware = {
3550
+ name,
3551
+ handler,
3552
+ ...options
3553
+ };
3554
+ this.plugin.middleware = [...this.plugin.middleware || [], middleware];
3555
+ return this;
3556
+ }
3557
+ /**
3558
+ * Add models to plugin
3559
+ */
3560
+ addModels(models) {
3561
+ this.plugin.models = [...this.plugin.models || [], ...models];
3562
+ return this;
3563
+ }
3564
+ /**
3565
+ * Add a single model to plugin
3566
+ */
3567
+ addModel(name, options) {
3568
+ const model = {
3569
+ name,
3570
+ ...options
3571
+ };
3572
+ this.plugin.models = [...this.plugin.models || [], model];
3573
+ return this;
3574
+ }
3575
+ /**
3576
+ * Add services to plugin
3577
+ */
3578
+ addServices(services) {
3579
+ this.plugin.services = [...this.plugin.services || [], ...services];
3580
+ return this;
3581
+ }
3582
+ /**
3583
+ * Add a single service to plugin
3584
+ */
3585
+ addService(name, implementation, options) {
3586
+ const service = {
3587
+ name,
3588
+ implementation,
3589
+ ...options
3590
+ };
3591
+ this.plugin.services = [...this.plugin.services || [], service];
3592
+ return this;
3593
+ }
3594
+ /**
3595
+ * Add admin pages to plugin
3596
+ */
3597
+ addAdminPages(pages) {
3598
+ this.plugin.adminPages = [...this.plugin.adminPages || [], ...pages];
3599
+ return this;
3600
+ }
3601
+ /**
3602
+ * Add a single admin page to plugin
3603
+ */
3604
+ addAdminPage(path, title, component, options) {
3605
+ const page = {
3606
+ path,
3607
+ title,
3608
+ component,
3609
+ ...options
3610
+ };
3611
+ this.plugin.adminPages = [...this.plugin.adminPages || [], page];
3612
+ return this;
3613
+ }
3614
+ /**
3615
+ * Add admin components to plugin
3616
+ */
3617
+ addComponents(components) {
3618
+ this.plugin.adminComponents = [...this.plugin.adminComponents || [], ...components];
3619
+ return this;
3620
+ }
3621
+ /**
3622
+ * Add a single admin component to plugin
3623
+ */
3624
+ addComponent(name, template, options) {
3625
+ const component = {
3626
+ name,
3627
+ template,
3628
+ ...options
3629
+ };
3630
+ this.plugin.adminComponents = [...this.plugin.adminComponents || [], component];
3631
+ return this;
3632
+ }
3633
+ /**
3634
+ * Add menu items to plugin
3635
+ */
3636
+ addMenuItems(items) {
3637
+ this.plugin.menuItems = [...this.plugin.menuItems || [], ...items];
3638
+ return this;
3639
+ }
3640
+ /**
3641
+ * Add a single menu item to plugin
3642
+ */
3643
+ addMenuItem(label, path, options) {
3644
+ const menuItem = {
3645
+ label,
3646
+ path,
3647
+ ...options
3648
+ };
3649
+ this.plugin.menuItems = [...this.plugin.menuItems || [], menuItem];
3650
+ return this;
3651
+ }
3652
+ /**
3653
+ * Add hooks to plugin
3654
+ */
3655
+ addHooks(hooks) {
3656
+ this.plugin.hooks = [...this.plugin.hooks || [], ...hooks];
3657
+ return this;
3658
+ }
3659
+ /**
3660
+ * Add a single hook to plugin
3661
+ */
3662
+ addHook(name, handler, options) {
3663
+ const hook = {
3664
+ name,
3665
+ handler,
3666
+ ...options
3667
+ };
3668
+ this.plugin.hooks = [...this.plugin.hooks || [], hook];
3669
+ return this;
3670
+ }
3671
+ /**
3672
+ * Add lifecycle hooks
3673
+ */
3674
+ lifecycle(hooks) {
3675
+ Object.assign(this.plugin, hooks);
3676
+ return this;
3677
+ }
3678
+ /**
3679
+ * Build the plugin
3680
+ */
3681
+ build() {
3682
+ if (!this.plugin.name || !this.plugin.version) {
3683
+ throw new Error("Plugin name and version are required");
3684
+ }
3685
+ return this.plugin;
3686
+ }
3687
+ };
3327
3688
 
3328
- // src/templates/pages/admin-content-form.template.ts
3329
- function renderContentFormPage(data) {
3330
- const isEdit = data.isEdit || !!data.id;
3331
- const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
3332
- const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
3333
- const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
3334
- const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
3335
- const metaFields = data.fields.filter((f) => f.field_name.startsWith("meta_"));
3336
- const getFieldValue = (fieldName) => {
3337
- if (fieldName === "title") return data.title || data.data?.[fieldName] || "";
3338
- if (fieldName === "slug") return data.slug || data.data?.[fieldName] || "";
3339
- return data.data?.[fieldName] || "";
3340
- };
3341
- const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
3342
- value: getFieldValue(field.field_name),
3343
- errors: data.validationErrors?.[field.field_name] || []
3344
- }));
3345
- const contentFieldsHTML = contentFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
3346
- value: getFieldValue(field.field_name),
3347
- errors: data.validationErrors?.[field.field_name] || []
3348
- }));
3349
- const metaFieldsHTML = metaFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
3350
- value: getFieldValue(field.field_name),
3351
- errors: data.validationErrors?.[field.field_name] || []
3352
- }));
3353
- const pageContent = `
3354
- <div class="space-y-6">
3689
+ // src/plugins/available/tinymce-plugin/index.ts
3690
+ var builder = PluginBuilder.create({
3691
+ name: "tinymce-plugin",
3692
+ version: "1.0.0",
3693
+ description: "Powerful WYSIWYG rich text editor for content creation"
3694
+ });
3695
+ builder.metadata({
3696
+ author: {
3697
+ name: "SonicJS Team",
3698
+ email: "team@sonicjs.com"
3699
+ },
3700
+ license: "MIT",
3701
+ compatibility: "^2.0.0"
3702
+ });
3703
+ builder.lifecycle({
3704
+ activate: async () => {
3705
+ console.info("\u2705 TinyMCE plugin activated");
3706
+ },
3707
+ deactivate: async () => {
3708
+ console.info("\u274C TinyMCE plugin deactivated");
3709
+ }
3710
+ });
3711
+ builder.build();
3712
+ function getTinyMCEScript(apiKey = "no-api-key") {
3713
+ return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
3714
+ }
3715
+ function getTinyMCEInitScript(config) {
3716
+ const skin = config?.skin || "oxide-dark";
3717
+ const contentCss = skin.includes("dark") ? "dark" : "default";
3718
+ const defaultHeight = config?.defaultHeight || 300;
3719
+ return `
3720
+ // Initialize TinyMCE for all richtext fields
3721
+ function initializeTinyMCE() {
3722
+ if (typeof tinymce !== 'undefined') {
3723
+ // Find all textareas that need TinyMCE
3724
+ document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
3725
+ // Skip if already initialized
3726
+ if (tinymce.get(textarea.id)) {
3727
+ return;
3728
+ }
3729
+
3730
+ // Get configuration from data attributes
3731
+ const container = textarea.closest('.richtext-container');
3732
+ const height = container?.dataset.height || ${defaultHeight};
3733
+ const toolbar = container?.dataset.toolbar || 'full';
3734
+
3735
+ tinymce.init({
3736
+ selector: '#' + textarea.id,
3737
+ skin: '${skin}',
3738
+ content_css: '${contentCss}',
3739
+ height: parseInt(height),
3740
+ menubar: false,
3741
+ plugins: [
3742
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
3743
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
3744
+ 'insertdatetime', 'media', 'table', 'help', 'wordcount'
3745
+ ],
3746
+ toolbar: toolbar === 'simple'
3747
+ ? 'bold italic underline | bullist numlist | link'
3748
+ : toolbar === 'minimal'
3749
+ ? 'bold italic | link'
3750
+ : 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
3751
+ content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-size: 14px }'
3752
+ });
3753
+ });
3754
+ }
3755
+ }
3756
+
3757
+ // Initialize on DOMContentLoaded
3758
+ if (document.readyState === 'loading') {
3759
+ document.addEventListener('DOMContentLoaded', initializeTinyMCE);
3760
+ } else {
3761
+ // DOM already loaded, initialize immediately
3762
+ initializeTinyMCE();
3763
+ }
3764
+
3765
+ // Also reinitialize after HTMX swaps (for dynamic content)
3766
+ document.addEventListener('htmx:afterSwap', function(event) {
3767
+ // Give the DOM a moment to settle
3768
+ setTimeout(initializeTinyMCE, 100);
3769
+ });
3770
+ `;
3771
+ }
3772
+
3773
+ // src/plugins/core-plugins/quill-editor/index.ts
3774
+ var QUILL_TOOLBARS = {
3775
+ full: [
3776
+ [{ "header": [1, 2, 3, 4, 5, 6, false] }],
3777
+ ["bold", "italic", "underline", "strike"],
3778
+ [{ "color": [] }, { "background": [] }],
3779
+ [{ "align": [] }],
3780
+ [{ "list": "ordered" }, { "list": "bullet" }],
3781
+ [{ "indent": "-1" }, { "indent": "+1" }],
3782
+ ["blockquote", "code-block"],
3783
+ ["link", "image", "video"],
3784
+ ["clean"]
3785
+ ],
3786
+ simple: [
3787
+ ["bold", "italic", "underline"],
3788
+ [{ "list": "ordered" }, { "list": "bullet" }],
3789
+ ["link"]
3790
+ ],
3791
+ minimal: [
3792
+ ["bold", "italic"],
3793
+ ["link"]
3794
+ ]
3795
+ };
3796
+ function getQuillInitScript() {
3797
+ return `
3798
+ <script>
3799
+ // Global Quill initialization function
3800
+ window.initializeQuillEditors = function() {
3801
+ console.log('[Quill] initializeQuillEditors called');
3802
+ if (typeof Quill === 'undefined') {
3803
+ console.warn('[Quill] Quill is not loaded yet. Retrying...');
3804
+ setTimeout(window.initializeQuillEditors, 100);
3805
+ return;
3806
+ }
3807
+
3808
+ console.log('[Quill] Quill is loaded, searching for editors...');
3809
+ // Find all Quill editor containers that haven't been initialized
3810
+ const containers = document.querySelectorAll('.quill-editor-container');
3811
+ console.log('[Quill] Found', containers.length, 'editor containers');
3812
+
3813
+ containers.forEach((container, index) => {
3814
+ console.log('[Quill] Processing container', index);
3815
+ try {
3816
+ const editorDiv = container.querySelector('.quill-editor');
3817
+ if (!editorDiv || editorDiv.classList.contains('ql-container')) {
3818
+ return; // Already initialized or invalid
3819
+ }
3820
+
3821
+ const fieldId = container.getAttribute('data-field-id');
3822
+ const hiddenInput = document.getElementById(fieldId);
3823
+ const theme = editorDiv.getAttribute('data-theme') || 'snow';
3824
+ const toolbarPreset = editorDiv.getAttribute('data-toolbar') || 'full';
3825
+ const placeholder = editorDiv.getAttribute('data-placeholder') || 'Enter content...';
3826
+ const height = parseInt(editorDiv.getAttribute('data-height') || '300');
3827
+
3828
+ // Get toolbar configuration
3829
+ const toolbarConfig = ${JSON.stringify(QUILL_TOOLBARS)};
3830
+ const toolbar = toolbarConfig[toolbarPreset] || toolbarConfig.full;
3831
+
3832
+ // Initialize Quill
3833
+ const quill = new Quill('#' + editorDiv.id, {
3834
+ theme: theme,
3835
+ placeholder: placeholder,
3836
+ modules: {
3837
+ toolbar: toolbar
3838
+ },
3839
+ formats: [
3840
+ 'header', 'bold', 'italic', 'underline', 'strike',
3841
+ 'color', 'background', 'align',
3842
+ 'list', 'bullet', 'indent',
3843
+ 'blockquote', 'code-block',
3844
+ 'link', 'image', 'video'
3845
+ ]
3846
+ });
3847
+
3848
+ // Set editor height
3849
+ const editorElement = editorDiv.querySelector('.ql-editor');
3850
+ if (editorElement) {
3851
+ editorElement.style.minHeight = height + 'px';
3852
+ }
3853
+
3854
+ // Sync content to hidden input on change
3855
+ quill.on('text-change', function() {
3856
+ const html = quill.root.innerHTML;
3857
+ if (hiddenInput) {
3858
+ hiddenInput.value = html;
3859
+ }
3860
+ });
3861
+
3862
+ // Store quill instance for potential later access
3863
+ editorDiv.quillInstance = quill;
3864
+ console.log('[Quill] Successfully initialized editor', index);
3865
+ } catch (error) {
3866
+ console.error('[Quill] Error initializing editor', index, ':', error);
3867
+ }
3868
+ });
3869
+ console.log('[Quill] Initialization complete');
3870
+ };
3871
+
3872
+ // Initialize on DOM ready
3873
+ if (document.readyState === 'loading') {
3874
+ document.addEventListener('DOMContentLoaded', window.initializeQuillEditors);
3875
+ } else {
3876
+ window.initializeQuillEditors();
3877
+ }
3878
+
3879
+ // Re-initialize after HTMX swaps
3880
+ if (typeof htmx !== 'undefined') {
3881
+ document.body.addEventListener('htmx:afterSwap', window.initializeQuillEditors);
3882
+ }
3883
+ </script>
3884
+ `;
3885
+ }
3886
+ function getQuillCDN(version = "2.0.2") {
3887
+ return `
3888
+ <!-- Quill Editor CSS -->
3889
+ <link href="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.snow.css" rel="stylesheet">
3890
+ <link href="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.bubble.css" rel="stylesheet">
3891
+
3892
+ <!-- Quill Editor JS -->
3893
+ <script src="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.js"></script>
3894
+
3895
+ <!-- Quill Dark Mode Styles -->
3896
+ <style>
3897
+ /* Dark mode styles for Quill editor */
3898
+ .dark .ql-toolbar {
3899
+ background-color: #18181b !important;
3900
+ border-color: #3f3f46 !important;
3901
+ }
3902
+
3903
+ .dark .ql-toolbar button,
3904
+ .dark .ql-toolbar .ql-picker-label {
3905
+ color: #e4e4e7 !important;
3906
+ }
3907
+
3908
+ .dark .ql-toolbar button:hover,
3909
+ .dark .ql-toolbar .ql-picker-label:hover {
3910
+ color: #fff !important;
3911
+ }
3912
+
3913
+ .dark .ql-toolbar button.ql-active,
3914
+ .dark .ql-toolbar .ql-picker-label.ql-active {
3915
+ color: #3b82f6 !important;
3916
+ }
3917
+
3918
+ .dark .ql-stroke {
3919
+ stroke: #e4e4e7 !important;
3920
+ }
3921
+
3922
+ .dark .ql-fill {
3923
+ fill: #e4e4e7 !important;
3924
+ }
3925
+
3926
+ .dark .ql-picker-options {
3927
+ background-color: #18181b !important;
3928
+ border-color: #3f3f46 !important;
3929
+ }
3930
+
3931
+ .dark .ql-picker-item:hover {
3932
+ background-color: #27272a !important;
3933
+ }
3934
+
3935
+ .dark .ql-container {
3936
+ background-color: #09090b !important;
3937
+ border-color: #3f3f46 !important;
3938
+ color: #e4e4e7 !important;
3939
+ }
3940
+
3941
+ .dark .ql-editor {
3942
+ color: #e4e4e7 !important;
3943
+ }
3944
+
3945
+ .dark .ql-editor.ql-blank::before {
3946
+ color: #71717a !important;
3947
+ }
3948
+
3949
+ /* Improve contrast for dark mode */
3950
+ .dark .ql-snow .ql-stroke {
3951
+ stroke: #e4e4e7 !important;
3952
+ }
3953
+
3954
+ .dark .ql-snow .ql-stroke.ql-fill {
3955
+ fill: #e4e4e7 !important;
3956
+ }
3957
+
3958
+ .dark .ql-snow .ql-fill {
3959
+ fill: #e4e4e7 !important;
3960
+ }
3961
+
3962
+ .dark .ql-snow .ql-picker-options {
3963
+ background-color: #18181b !important;
3964
+ }
3965
+
3966
+ .dark .ql-snow .ql-picker-item:hover {
3967
+ color: #3b82f6 !important;
3968
+ }
3969
+ </style>
3970
+ `;
3971
+ }
3972
+ function createQuillEditorPlugin() {
3973
+ const builder3 = PluginBuilder.create({
3974
+ name: "quill-editor",
3975
+ version: "1.0.0",
3976
+ description: "Quill rich text editor integration for SonicJS"
3977
+ });
3978
+ builder3.metadata({
3979
+ author: {
3980
+ name: "SonicJS Team",
3981
+ email: "team@sonicjs.com"
3982
+ },
3983
+ license: "MIT",
3984
+ compatibility: "^2.0.0",
3985
+ tags: ["editor", "rich-text", "wysiwyg", "quill"]
3986
+ });
3987
+ builder3.lifecycle({
3988
+ activate: async () => {
3989
+ console.info("\u2705 Quill Editor plugin activated");
3990
+ },
3991
+ deactivate: async () => {
3992
+ console.info("\u274C Quill Editor plugin deactivated");
3993
+ }
3994
+ });
3995
+ return builder3.build();
3996
+ }
3997
+ createQuillEditorPlugin();
3998
+
3999
+ // src/plugins/available/mdxeditor-plugin/index.ts
4000
+ var builder2 = PluginBuilder.create({
4001
+ name: "mdxeditor-plugin",
4002
+ version: "1.0.0",
4003
+ description: "Lightweight markdown editor with live preview"
4004
+ });
4005
+ builder2.metadata({
4006
+ author: {
4007
+ name: "SonicJS Team",
4008
+ email: "team@sonicjs.com"
4009
+ },
4010
+ license: "MIT",
4011
+ compatibility: "^2.0.0"
4012
+ });
4013
+ builder2.lifecycle({
4014
+ activate: async () => {
4015
+ console.info("\u2705 EasyMDE editor plugin activated");
4016
+ },
4017
+ deactivate: async () => {
4018
+ console.info("\u274C EasyMDE editor plugin deactivated");
4019
+ }
4020
+ });
4021
+ builder2.build();
4022
+ function getMDXEditorScripts() {
4023
+ return `
4024
+ <!-- EasyMDE Markdown Editor -->
4025
+ <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
4026
+ <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
4027
+ <style>
4028
+ /* Dark mode styling for EasyMDE */
4029
+ .EasyMDEContainer {
4030
+ background-color: #1e293b;
4031
+ }
4032
+
4033
+ .EasyMDEContainer .CodeMirror {
4034
+ background-color: #1e293b;
4035
+ color: #e2e8f0;
4036
+ border-color: #334155;
4037
+ }
4038
+
4039
+ .EasyMDEContainer .CodeMirror-scroll {
4040
+ background-color: #1e293b;
4041
+ }
4042
+
4043
+ .EasyMDEContainer .CodeMirror-cursor {
4044
+ border-left-color: #e2e8f0;
4045
+ }
4046
+
4047
+ .EasyMDEContainer .CodeMirror-gutters {
4048
+ background-color: #0f172a;
4049
+ border-right-color: #334155;
4050
+ }
4051
+
4052
+ .EasyMDEContainer .CodeMirror-linenumber {
4053
+ color: #64748b;
4054
+ }
4055
+
4056
+ .editor-toolbar {
4057
+ background-color: #0f172a;
4058
+ border-color: #334155;
4059
+ }
4060
+
4061
+ .editor-toolbar button {
4062
+ color: #94a3b8 !important;
4063
+ }
4064
+
4065
+ .editor-toolbar button:hover,
4066
+ .editor-toolbar button.active {
4067
+ background-color: #334155;
4068
+ border-color: #475569;
4069
+ color: #e2e8f0 !important;
4070
+ }
4071
+
4072
+ .editor-toolbar i.separator {
4073
+ border-left-color: #334155;
4074
+ border-right-color: #334155;
4075
+ }
4076
+
4077
+ .editor-statusbar {
4078
+ background-color: #0f172a;
4079
+ color: #64748b;
4080
+ border-top-color: #334155;
4081
+ }
4082
+
4083
+ .editor-preview,
4084
+ .editor-preview-side {
4085
+ background-color: #1e293b;
4086
+ color: #e2e8f0;
4087
+ }
4088
+
4089
+ .CodeMirror-selected {
4090
+ background-color: #334155 !important;
4091
+ }
4092
+
4093
+ .CodeMirror-focused .CodeMirror-selected {
4094
+ background-color: #475569 !important;
4095
+ }
4096
+
4097
+ /* Syntax highlighting for dark mode */
4098
+ .cm-header {
4099
+ color: #60a5fa;
4100
+ }
4101
+
4102
+ .cm-strong {
4103
+ color: #fbbf24;
4104
+ }
4105
+
4106
+ .cm-em {
4107
+ color: #a78bfa;
4108
+ }
4109
+
4110
+ .cm-link {
4111
+ color: #34d399;
4112
+ }
4113
+
4114
+ .cm-url {
4115
+ color: #34d399;
4116
+ }
4117
+
4118
+ .cm-quote {
4119
+ color: #94a3b8;
4120
+ font-style: italic;
4121
+ }
4122
+
4123
+ .cm-comment {
4124
+ color: #64748b;
4125
+ }
4126
+ </style>
4127
+ `;
4128
+ }
4129
+ function getMDXEditorInitScript(config) {
4130
+ const defaultHeight = config?.defaultHeight || 400;
4131
+ const toolbar = config?.toolbar || "full";
4132
+ const placeholder = config?.placeholder || "Start writing your content...";
4133
+ return `
4134
+ // Initialize EasyMDE (Markdown Editor) for all richtext fields
4135
+ function initializeMDXEditor() {
4136
+ if (typeof EasyMDE === 'undefined') {
4137
+ console.warn('EasyMDE not loaded yet, retrying...');
4138
+ setTimeout(initializeMDXEditor, 100);
4139
+ return;
4140
+ }
4141
+
4142
+ // Find all textareas that need EasyMDE
4143
+ document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
4144
+ // Skip if already initialized
4145
+ if (textarea.dataset.mdxeditorInitialized === 'true') {
4146
+ return;
4147
+ }
4148
+
4149
+ // Mark as initialized
4150
+ textarea.dataset.mdxeditorInitialized = 'true';
4151
+
4152
+ // Get configuration from data attributes
4153
+ const container = textarea.closest('.richtext-container');
4154
+ const height = container?.dataset.height || ${defaultHeight};
4155
+ const editorToolbar = container?.dataset.toolbar || '${toolbar}';
4156
+
4157
+ // Initialize EasyMDE
4158
+ try {
4159
+ const toolbarButtons = editorToolbar === 'minimal'
4160
+ ? ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'preview']
4161
+ : ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', '|', 'preview', 'side-by-side', 'fullscreen', '|', 'guide'];
4162
+
4163
+ const easyMDE = new EasyMDE({
4164
+ element: textarea,
4165
+ placeholder: '${placeholder}',
4166
+ spellChecker: false,
4167
+ minHeight: height + 'px',
4168
+ toolbar: toolbarButtons,
4169
+ status: ['lines', 'words', 'cursor'],
4170
+ renderingConfig: {
4171
+ singleLineBreaks: false,
4172
+ codeSyntaxHighlighting: true
4173
+ }
4174
+ });
4175
+
4176
+ // Store reference to editor instance
4177
+ textarea.easyMDEInstance = easyMDE;
4178
+
4179
+ console.log('EasyMDE initialized for field:', textarea.id || textarea.name);
4180
+ } catch (error) {
4181
+ console.error('Error initializing EasyMDE:', error);
4182
+ // Show textarea as fallback
4183
+ textarea.style.display = 'block';
4184
+ }
4185
+ });
4186
+ }
4187
+
4188
+ // Initialize on DOMContentLoaded
4189
+ if (document.readyState === 'loading') {
4190
+ document.addEventListener('DOMContentLoaded', initializeMDXEditor);
4191
+ } else {
4192
+ // DOM already loaded, initialize immediately
4193
+ initializeMDXEditor();
4194
+ }
4195
+
4196
+ // Also reinitialize after HTMX swaps (for dynamic content)
4197
+ document.addEventListener('htmx:afterSwap', function(event) {
4198
+ // Give the DOM a moment to settle
4199
+ setTimeout(initializeMDXEditor, 100);
4200
+ });
4201
+ `;
4202
+ }
4203
+
4204
+ // src/templates/pages/admin-content-form.template.ts
4205
+ function renderContentFormPage(data) {
4206
+ const isEdit = data.isEdit || !!data.id;
4207
+ const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
4208
+ const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
4209
+ const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
4210
+ const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
4211
+ const metaFields = data.fields.filter((f) => f.field_name.startsWith("meta_"));
4212
+ const getFieldValue = (fieldName) => {
4213
+ if (fieldName === "title") return data.title || data.data?.[fieldName] || "";
4214
+ if (fieldName === "slug") return data.slug || data.data?.[fieldName] || "";
4215
+ return data.data?.[fieldName] || "";
4216
+ };
4217
+ const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4218
+ value: getFieldValue(field.field_name),
4219
+ errors: data.validationErrors?.[field.field_name] || []
4220
+ }));
4221
+ const contentFieldsHTML = contentFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4222
+ value: getFieldValue(field.field_name),
4223
+ errors: data.validationErrors?.[field.field_name] || []
4224
+ }));
4225
+ const metaFieldsHTML = metaFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
4226
+ value: getFieldValue(field.field_name),
4227
+ errors: data.validationErrors?.[field.field_name] || []
4228
+ }));
4229
+ const pageContent = `
4230
+ <div class="space-y-6">
3355
4231
  <!-- Header -->
3356
4232
  <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
3357
4233
  <div>
@@ -3650,8 +4526,13 @@ function renderContentFormPage(data) {
3650
4526
 
3651
4527
  ${getConfirmationDialogScript()}
3652
4528
 
3653
- <!-- TinyMCE CDN -->
3654
- <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
4529
+ ${data.tinymceEnabled ? getTinyMCEScript(data.tinymceSettings?.apiKey) : "<!-- TinyMCE plugin not active -->"}
4530
+
4531
+ ${data.quillEnabled ? getQuillCDN(data.quillSettings?.version) : "<!-- Quill plugin not active -->"}
4532
+
4533
+ ${data.quillEnabled ? getQuillInitScript() : "<!-- Quill init script not needed -->"}
4534
+
4535
+ ${data.mdxeditorEnabled ? getMDXEditorScripts() : "<!-- MDXEditor plugin not active -->"}
3655
4536
 
3656
4537
  <!-- Dynamic Field Scripts -->
3657
4538
  <script>
@@ -3926,6 +4807,19 @@ function renderContentFormPage(data) {
3926
4807
  form.addEventListener('input', scheduleAutoSave);
3927
4808
  form.addEventListener('change', scheduleAutoSave);
3928
4809
  });
4810
+
4811
+ ${data.tinymceEnabled ? `<script>${getTinyMCEInitScript({
4812
+ skin: data.tinymceSettings?.skin,
4813
+ defaultHeight: data.tinymceSettings?.defaultHeight,
4814
+ defaultToolbar: data.tinymceSettings?.defaultToolbar
4815
+ })}</script>` : ""}
4816
+
4817
+ ${data.mdxeditorEnabled ? `<script>${getMDXEditorInitScript({
4818
+ theme: data.mdxeditorSettings?.theme,
4819
+ defaultHeight: data.mdxeditorSettings?.defaultHeight,
4820
+ toolbar: data.mdxeditorSettings?.toolbar,
4821
+ placeholder: data.mdxeditorSettings?.placeholder
4822
+ })}</script>` : ""}
3929
4823
  </script>
3930
4824
  `;
3931
4825
  const layoutData = {
@@ -3996,11 +4890,11 @@ function renderContentListPage(data) {
3996
4890
  label: "Title",
3997
4891
  sortable: true,
3998
4892
  sortType: "string",
3999
- render: (value2, row) => `
4893
+ render: (value, row) => `
4000
4894
  <div class="flex items-center">
4001
4895
  <div>
4002
4896
  <div class="text-sm font-medium text-zinc-950 dark:text-white">
4003
- <a href="/content/${row.id}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${row.title}</a>
4897
+ <a href="/admin/content/${row.id}/edit${currentParams ? `?ref=${encodeURIComponent(currentParams)}` : ""}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${row.title}</a>
4004
4898
  </div>
4005
4899
  <div class="text-sm text-zinc-500 dark:text-zinc-400">${row.slug}</div>
4006
4900
  </div>
@@ -4019,7 +4913,7 @@ function renderContentListPage(data) {
4019
4913
  label: "Status",
4020
4914
  sortable: true,
4021
4915
  sortType: "string",
4022
- render: (value2) => value2
4916
+ render: (value) => value
4023
4917
  },
4024
4918
  {
4025
4919
  key: "authorName",
@@ -4040,7 +4934,7 @@ function renderContentListPage(data) {
4040
4934
  label: "Actions",
4041
4935
  sortable: false,
4042
4936
  className: "text-sm font-medium",
4043
- render: (value2, row) => `
4937
+ render: (value, row) => `
4044
4938
  <div class="flex space-x-2">
4045
4939
  <button
4046
4940
  class="inline-flex items-center justify-center p-1.5 rounded-lg bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 ring-1 ring-inset ring-cyan-600/20 dark:ring-cyan-500/20 hover:bg-cyan-100 dark:hover:bg-cyan-500/20 transition-colors"
@@ -4051,6 +4945,15 @@ function renderContentListPage(data) {
4051
4945
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
4052
4946
  </svg>
4053
4947
  </button>
4948
+ <button
4949
+ class="inline-flex items-center justify-center p-1.5 rounded-lg bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-400 ring-1 ring-inset ring-purple-600/20 dark:ring-purple-500/20 hover:bg-purple-100 dark:hover:bg-purple-500/20 transition-colors"
4950
+ onclick="window.open('/api/content/${row.id}', '_blank')"
4951
+ title="View API"
4952
+ >
4953
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4954
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
4955
+ </svg>
4956
+ </button>
4054
4957
  <button
4055
4958
  class="inline-flex items-center justify-center p-1.5 rounded-lg bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 hover:bg-red-100 dark:hover:bg-red-500/20 transition-colors"
4056
4959
  hx-delete="/admin/content/${row.id}"
@@ -4230,7 +5133,7 @@ function renderContentListPage(data) {
4230
5133
  class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 ${filter.name === "status" ? "pl-8" : "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"
4231
5134
  >
4232
5135
  ${filter.options.map((opt) => `
4233
- <option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
5136
+ <option value="${opt.__value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
4234
5137
  `).join("")}
4235
5138
  </select>
4236
5139
  <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">
@@ -4750,7 +5653,15 @@ function escapeHtml3(text) {
4750
5653
  }
4751
5654
 
4752
5655
  // src/middleware/plugin-middleware.ts
4753
- var isPluginActive2 = () => false;
5656
+ async function isPluginActive2(db, pluginId) {
5657
+ try {
5658
+ const result = await db.prepare("SELECT status FROM plugins WHERE id = ?").bind(pluginId).first();
5659
+ return result?.status === "active";
5660
+ } catch (error) {
5661
+ console.error(`[isPluginActive] Error checking plugin status for ${pluginId}:`, error);
5662
+ return false;
5663
+ }
5664
+ }
4754
5665
 
4755
5666
  // src/routes/admin-content.ts
4756
5667
  var adminContentRoutes = new Hono();
@@ -4760,6 +5671,37 @@ async function getCollectionFields(db, collectionId) {
4760
5671
  return cache.getOrSet(
4761
5672
  cache.generateKey("fields", collectionId),
4762
5673
  async () => {
5674
+ const collectionStmt = db.prepare("SELECT schema FROM collections WHERE id = ?");
5675
+ const collectionRow = await collectionStmt.bind(collectionId).first();
5676
+ if (collectionRow && collectionRow.schema) {
5677
+ try {
5678
+ const schema = typeof collectionRow.schema === "string" ? JSON.parse(collectionRow.schema) : collectionRow.schema;
5679
+ if (schema && schema.properties) {
5680
+ let fieldOrder = 0;
5681
+ return Object.entries(schema.properties).map(([fieldName, fieldConfig]) => {
5682
+ let fieldOptions = { ...fieldConfig };
5683
+ if (fieldConfig.type === "select" && fieldConfig.enum) {
5684
+ fieldOptions.options = fieldConfig.enum.map((value, index) => ({
5685
+ value,
5686
+ label: fieldConfig.enumLabels?.[index] || value
5687
+ }));
5688
+ }
5689
+ return {
5690
+ id: `schema-${fieldName}`,
5691
+ field_name: fieldName,
5692
+ field_type: fieldConfig.type || "string",
5693
+ field_label: fieldConfig.title || fieldName,
5694
+ field_options: fieldOptions,
5695
+ field_order: fieldOrder++,
5696
+ is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
5697
+ is_searchable: false
5698
+ };
5699
+ });
5700
+ }
5701
+ } catch (e) {
5702
+ console.error("Error parsing collection schema:", e);
5703
+ }
5704
+ }
4763
5705
  const stmt = db.prepare(`
4764
5706
  SELECT * FROM content_fields
4765
5707
  WHERE collection_id = ?
@@ -5002,11 +5944,44 @@ adminContentRoutes.get("/new", async (c) => {
5002
5944
  }
5003
5945
  const fields = await getCollectionFields(db, collectionId);
5004
5946
  const workflowEnabled = await isPluginActive2(db, "workflow");
5947
+ const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
5948
+ let tinymceSettings;
5949
+ if (tinymceEnabled) {
5950
+ const pluginService = new PluginService(db);
5951
+ const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
5952
+ tinymceSettings = tinymcePlugin2?.settings;
5953
+ }
5954
+ const quillEnabled = await isPluginActive2(db, "quill-editor");
5955
+ let quillSettings;
5956
+ if (quillEnabled) {
5957
+ const pluginService = new PluginService(db);
5958
+ const quillPlugin = await pluginService.getPlugin("quill-editor");
5959
+ quillSettings = quillPlugin?.settings;
5960
+ }
5961
+ const mdxeditorEnabled = await isPluginActive2(db, "mdxeditor-plugin");
5962
+ let mdxeditorSettings;
5963
+ if (mdxeditorEnabled) {
5964
+ const pluginService = new PluginService(db);
5965
+ const mdxeditorPlugin2 = await pluginService.getPlugin("mdxeditor-plugin");
5966
+ mdxeditorSettings = mdxeditorPlugin2?.settings;
5967
+ }
5968
+ console.log("[Content Form /new] Editor plugins status:", {
5969
+ tinymce: tinymceEnabled,
5970
+ quill: quillEnabled,
5971
+ mdxeditor: mdxeditorEnabled,
5972
+ mdxeditorSettings
5973
+ });
5005
5974
  const formData = {
5006
5975
  collection,
5007
5976
  fields,
5008
5977
  isEdit: false,
5009
5978
  workflowEnabled,
5979
+ tinymceEnabled,
5980
+ tinymceSettings,
5981
+ quillEnabled,
5982
+ quillSettings,
5983
+ mdxeditorEnabled,
5984
+ mdxeditorSettings,
5010
5985
  user: user ? {
5011
5986
  name: user.email,
5012
5987
  email: user.email,
@@ -5074,6 +6049,27 @@ adminContentRoutes.get("/:id/edit", async (c) => {
5074
6049
  const fields = await getCollectionFields(db, content.collection_id);
5075
6050
  const contentData = content.data ? JSON.parse(content.data) : {};
5076
6051
  const workflowEnabled = await isPluginActive2(db, "workflow");
6052
+ const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
6053
+ let tinymceSettings;
6054
+ if (tinymceEnabled) {
6055
+ const pluginService = new PluginService(db);
6056
+ const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
6057
+ tinymceSettings = tinymcePlugin2?.settings;
6058
+ }
6059
+ const quillEnabled = await isPluginActive2(db, "quill-editor");
6060
+ let quillSettings;
6061
+ if (quillEnabled) {
6062
+ const pluginService = new PluginService(db);
6063
+ const quillPlugin = await pluginService.getPlugin("quill-editor");
6064
+ quillSettings = quillPlugin?.settings;
6065
+ }
6066
+ const mdxeditorEnabled = await isPluginActive2(db, "mdxeditor-plugin");
6067
+ let mdxeditorSettings;
6068
+ if (mdxeditorEnabled) {
6069
+ const pluginService = new PluginService(db);
6070
+ const mdxeditorPlugin2 = await pluginService.getPlugin("mdxeditor-plugin");
6071
+ mdxeditorSettings = mdxeditorPlugin2?.settings;
6072
+ }
5077
6073
  const formData = {
5078
6074
  id: content.id,
5079
6075
  title: content.title,
@@ -5089,6 +6085,12 @@ adminContentRoutes.get("/:id/edit", async (c) => {
5089
6085
  fields,
5090
6086
  isEdit: true,
5091
6087
  workflowEnabled,
6088
+ tinymceEnabled,
6089
+ tinymceSettings,
6090
+ quillEnabled,
6091
+ quillSettings,
6092
+ mdxeditorEnabled,
6093
+ mdxeditorSettings,
5092
6094
  referrerParams,
5093
6095
  user: user ? {
5094
6096
  name: user.email,
@@ -5139,41 +6141,31 @@ adminContentRoutes.post("/", async (c) => {
5139
6141
  const data = {};
5140
6142
  const errors = {};
5141
6143
  for (const field of fields) {
5142
- const value2 = formData.get(field.field_name);
5143
- if (field.field_type === "guid") {
5144
- const options = field.field_options || {};
5145
- if (options.autoGenerate) {
5146
- data[field.field_name] = crypto.randomUUID();
5147
- continue;
5148
- }
5149
- }
5150
- if (field.is_required && (!value2 || value2.toString().trim() === "")) {
6144
+ const value = formData.get(field.field_name);
6145
+ if (field.is_required && (!value || value.toString().trim() === "")) {
5151
6146
  errors[field.field_name] = [`${field.field_label} is required`];
5152
6147
  continue;
5153
6148
  }
5154
6149
  switch (field.field_type) {
5155
6150
  case "number":
5156
- if (value2 && isNaN(Number(value2))) {
6151
+ if (value && isNaN(Number(value))) {
5157
6152
  errors[field.field_name] = [`${field.field_label} must be a valid number`];
5158
6153
  } else {
5159
- data[field.field_name] = value2 ? Number(value2) : null;
6154
+ data[field.field_name] = value ? Number(value) : null;
5160
6155
  }
5161
6156
  break;
5162
6157
  case "boolean":
5163
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value2 === "true" : false;
6158
+ data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
5164
6159
  break;
5165
6160
  case "select":
5166
6161
  if (field.field_options?.multiple) {
5167
6162
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5168
6163
  } else {
5169
- data[field.field_name] = value2;
6164
+ data[field.field_name] = value;
5170
6165
  }
5171
6166
  break;
5172
- case "guid":
5173
- data[field.field_name] = value2 || null;
5174
- break;
5175
6167
  default:
5176
- data[field.field_name] = value2;
6168
+ data[field.field_name] = value;
5177
6169
  }
5178
6170
  }
5179
6171
  if (Object.keys(errors).length > 0) {
@@ -5207,9 +6199,9 @@ adminContentRoutes.post("/", async (c) => {
5207
6199
  INSERT INTO content (
5208
6200
  id, collection_id, slug, title, data, status,
5209
6201
  scheduled_publish_at, scheduled_unpublish_at,
5210
- meta_title, meta_description, author_id, created_by, created_at, updated_at
6202
+ meta_title, meta_description, author_id, created_at, updated_at
5211
6203
  )
5212
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6204
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5213
6205
  `);
5214
6206
  await insertStmt.bind(
5215
6207
  contentId,
@@ -5223,7 +6215,6 @@ adminContentRoutes.post("/", async (c) => {
5223
6215
  data.meta_title || null,
5224
6216
  data.meta_description || null,
5225
6217
  user?.userId || "unknown",
5226
- user?.userId || "unknown",
5227
6218
  now,
5228
6219
  now
5229
6220
  ).run();
@@ -5301,31 +6292,31 @@ adminContentRoutes.put("/:id", async (c) => {
5301
6292
  const data = {};
5302
6293
  const errors = {};
5303
6294
  for (const field of fields) {
5304
- const value2 = formData.get(field.field_name);
5305
- if (field.is_required && (!value2 || value2.toString().trim() === "")) {
6295
+ const value = formData.get(field.field_name);
6296
+ if (field.is_required && (!value || value.toString().trim() === "")) {
5306
6297
  errors[field.field_name] = [`${field.field_label} is required`];
5307
6298
  continue;
5308
6299
  }
5309
6300
  switch (field.field_type) {
5310
6301
  case "number":
5311
- if (value2 && isNaN(Number(value2))) {
6302
+ if (value && isNaN(Number(value))) {
5312
6303
  errors[field.field_name] = [`${field.field_label} must be a valid number`];
5313
6304
  } else {
5314
- data[field.field_name] = value2 ? Number(value2) : null;
6305
+ data[field.field_name] = value ? Number(value) : null;
5315
6306
  }
5316
6307
  break;
5317
6308
  case "boolean":
5318
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value2 === "true" : false;
6309
+ data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
5319
6310
  break;
5320
6311
  case "select":
5321
6312
  if (field.field_options?.multiple) {
5322
6313
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5323
6314
  } else {
5324
- data[field.field_name] = value2;
6315
+ data[field.field_name] = value;
5325
6316
  }
5326
6317
  break;
5327
6318
  default:
5328
- data[field.field_name] = value2;
6319
+ data[field.field_name] = value;
5329
6320
  }
5330
6321
  }
5331
6322
  if (Object.keys(errors).length > 0) {
@@ -5442,23 +6433,23 @@ adminContentRoutes.post("/preview", async (c) => {
5442
6433
  const fields = await getCollectionFields(db, collectionId);
5443
6434
  const data = {};
5444
6435
  for (const field of fields) {
5445
- const value2 = formData.get(field.field_name);
6436
+ const value = formData.get(field.field_name);
5446
6437
  switch (field.field_type) {
5447
6438
  case "number":
5448
- data[field.field_name] = value2 ? Number(value2) : null;
6439
+ data[field.field_name] = value ? Number(value) : null;
5449
6440
  break;
5450
6441
  case "boolean":
5451
- data[field.field_name] = value2 === "true";
6442
+ data[field.field_name] = value === "true";
5452
6443
  break;
5453
6444
  case "select":
5454
6445
  if (field.field_options?.multiple) {
5455
6446
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5456
6447
  } else {
5457
- data[field.field_name] = value2;
6448
+ data[field.field_name] = value;
5458
6449
  }
5459
6450
  break;
5460
6451
  default:
5461
- data[field.field_name] = value2;
6452
+ data[field.field_name] = value;
5462
6453
  }
5463
6454
  }
5464
6455
  const previewHTML = `
@@ -5526,9 +6517,9 @@ adminContentRoutes.post("/duplicate", async (c) => {
5526
6517
  const insertStmt = db.prepare(`
5527
6518
  INSERT INTO content (
5528
6519
  id, collection_id, slug, title, data, status,
5529
- author_id, created_by, created_at, updated_at
6520
+ author_id, created_at, updated_at
5530
6521
  )
5531
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6522
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5532
6523
  `);
5533
6524
  await insertStmt.bind(
5534
6525
  newId,
@@ -5539,7 +6530,6 @@ adminContentRoutes.post("/duplicate", async (c) => {
5539
6530
  "draft",
5540
6531
  // Always start as draft
5541
6532
  user?.userId || "unknown",
5542
- user?.userId || "unknown",
5543
6533
  now,
5544
6534
  now
5545
6535
  ).run();
@@ -7315,10 +8305,10 @@ function renderUsersListPage(data) {
7315
8305
  label: "",
7316
8306
  className: "w-12",
7317
8307
  sortable: false,
7318
- render: (value2, row) => {
8308
+ render: (value, row) => {
7319
8309
  const initials = `${row.firstName.charAt(0)}${row.lastName.charAt(0)}`.toUpperCase();
7320
- if (value2) {
7321
- return `<img src="${value2}" alt="${row.firstName} ${row.lastName}" class="w-8 h-8 rounded-full">`;
8310
+ if (value) {
8311
+ return `<img src="${value}" alt="${row.firstName} ${row.lastName}" class="w-8 h-8 rounded-full">`;
7322
8312
  }
7323
8313
  return `
7324
8314
  <div class="w-8 h-8 bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 rounded-full flex items-center justify-center">
@@ -7333,7 +8323,7 @@ function renderUsersListPage(data) {
7333
8323
  sortable: true,
7334
8324
  sortType: "string",
7335
8325
  render: (_value, row) => {
7336
- const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
8326
+ const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
7337
8327
  "&": "&amp;",
7338
8328
  "<": "&lt;",
7339
8329
  ">": "&gt;",
@@ -7342,9 +8332,9 @@ function renderUsersListPage(data) {
7342
8332
  })[char] || char);
7343
8333
  const truncatedFirstName = row.firstName.length > 25 ? row.firstName.substring(0, 25) + "..." : row.firstName;
7344
8334
  const truncatedLastName = row.lastName.length > 25 ? row.lastName.substring(0, 25) + "..." : row.lastName;
7345
- const fullName = escapeHtml7(`${truncatedFirstName} ${truncatedLastName}`);
8335
+ const fullName = escapeHtml6(`${truncatedFirstName} ${truncatedLastName}`);
7346
8336
  const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
7347
- const username = escapeHtml7(truncatedUsername);
8337
+ const username = escapeHtml6(truncatedUsername);
7348
8338
  const statusBadge = row.isActive ? '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-1 ring-inset ring-lime-700/10 dark:ring-lime-400/20 ml-2">Active</span>' : '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-700/10 dark:ring-red-500/20 ml-2">Inactive</span>';
7349
8339
  return `
7350
8340
  <div>
@@ -7359,15 +8349,15 @@ function renderUsersListPage(data) {
7359
8349
  label: "Email",
7360
8350
  sortable: true,
7361
8351
  sortType: "string",
7362
- render: (value2) => {
7363
- const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
8352
+ render: (value) => {
8353
+ const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
7364
8354
  "&": "&amp;",
7365
8355
  "<": "&lt;",
7366
8356
  ">": "&gt;",
7367
8357
  '"': "&quot;",
7368
8358
  "'": "&#39;"
7369
8359
  })[char] || char);
7370
- const escapedEmail = escapeHtml7(value2);
8360
+ const escapedEmail = escapeHtml6(value);
7371
8361
  return `<a href="mailto:${escapedEmail}" class="text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 transition-colors">${escapedEmail}</a>`;
7372
8362
  }
7373
8363
  },
@@ -7376,7 +8366,7 @@ function renderUsersListPage(data) {
7376
8366
  label: "Role",
7377
8367
  sortable: true,
7378
8368
  sortType: "string",
7379
- render: (_value) => {
8369
+ render: (value) => {
7380
8370
  const roleColors = {
7381
8371
  admin: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-700/10 dark:ring-red-500/20",
7382
8372
  editor: "bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-500/20",
@@ -7392,7 +8382,7 @@ function renderUsersListPage(data) {
7392
8382
  label: "Last Login",
7393
8383
  sortable: true,
7394
8384
  sortType: "date",
7395
- render: (_value) => {
8385
+ render: (value) => {
7396
8386
  if (!value) return '<span class="text-zinc-500 dark:text-zinc-400">Never</span>';
7397
8387
  return `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`;
7398
8388
  }
@@ -7402,7 +8392,7 @@ function renderUsersListPage(data) {
7402
8392
  label: "Created",
7403
8393
  sortable: true,
7404
8394
  sortType: "date",
7405
- render: (_value) => `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`
8395
+ render: (value) => `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`
7406
8396
  },
7407
8397
  {
7408
8398
  key: "actions",
@@ -7900,7 +8890,7 @@ userRoutes.post("/profile/avatar", async (c) => {
7900
8890
  try {
7901
8891
  const formData = await c.req.formData();
7902
8892
  const avatarFile = formData.get("avatar");
7903
- if (!avatarFile || !(avatarFile instanceof File) || !avatarFile.name) {
8893
+ if (!avatarFile || typeof avatarFile === "string" || !avatarFile.name) {
7904
8894
  return c.html(renderAlert2({
7905
8895
  type: "error",
7906
8896
  message: "Please select an image file.",
@@ -8483,35 +9473,75 @@ userRoutes.put("/users/:id", async (c) => {
8483
9473
  }));
8484
9474
  }
8485
9475
  });
8486
- userRoutes.delete("/users/:id", async (c) => {
9476
+ userRoutes.post("/users/:id/toggle", async (c) => {
8487
9477
  const db = c.env.DB;
8488
9478
  const user = c.get("user");
8489
9479
  const userId = c.req.param("id");
8490
9480
  try {
8491
- const body = await c.req.json().catch(() => ({ hardDelete: false }));
8492
- const hardDelete = body.hardDelete === true;
8493
- if (userId === user.userId) {
8494
- return c.json({ error: "You cannot delete your own account" }, 400);
9481
+ const body = await c.req.json().catch(() => ({ active: true }));
9482
+ const active = body.active === true;
9483
+ if (userId === user.userId && !active) {
9484
+ return c.json({ error: "You cannot deactivate your own account" }, 400);
8495
9485
  }
8496
9486
  const userStmt = db.prepare(`
8497
9487
  SELECT id, email FROM users WHERE id = ?
8498
9488
  `);
8499
- const userToDelete = await userStmt.bind(userId).first();
8500
- if (!userToDelete) {
9489
+ const userToToggle = await userStmt.bind(userId).first();
9490
+ if (!userToToggle) {
8501
9491
  return c.json({ error: "User not found" }, 404);
8502
9492
  }
8503
- if (hardDelete) {
8504
- const deleteStmt = db.prepare(`
8505
- DELETE FROM users WHERE id = ?
8506
- `);
8507
- await deleteStmt.bind(userId).run();
8508
- await logActivity(
8509
- db,
8510
- user.userId,
8511
- "user!.hard_delete",
8512
- "users",
8513
- userId,
8514
- { email: userToDelete.email, permanent: true },
9493
+ const toggleStmt = db.prepare(`
9494
+ UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
9495
+ `);
9496
+ await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
9497
+ await logActivity(
9498
+ db,
9499
+ user.userId,
9500
+ active ? "user.activate" : "user.deactivate",
9501
+ "users",
9502
+ userId,
9503
+ { email: userToToggle.email },
9504
+ c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
9505
+ c.req.header("user-agent")
9506
+ );
9507
+ return c.json({
9508
+ success: true,
9509
+ message: active ? "User activated successfully" : "User deactivated successfully"
9510
+ });
9511
+ } catch (error) {
9512
+ console.error("User toggle error:", error);
9513
+ return c.json({ error: "Failed to toggle user status" }, 500);
9514
+ }
9515
+ });
9516
+ userRoutes.delete("/users/:id", async (c) => {
9517
+ const db = c.env.DB;
9518
+ const user = c.get("user");
9519
+ const userId = c.req.param("id");
9520
+ try {
9521
+ const body = await c.req.json().catch(() => ({ hardDelete: false }));
9522
+ const hardDelete = body.hardDelete === true;
9523
+ if (userId === user.userId) {
9524
+ return c.json({ error: "You cannot delete your own account" }, 400);
9525
+ }
9526
+ const userStmt = db.prepare(`
9527
+ SELECT id, email FROM users WHERE id = ?
9528
+ `);
9529
+ const userToDelete = await userStmt.bind(userId).first();
9530
+ if (!userToDelete) {
9531
+ return c.json({ error: "User not found" }, 404);
9532
+ }
9533
+ if (hardDelete) {
9534
+ const deleteStmt = db.prepare(`
9535
+ DELETE FROM users WHERE id = ?
9536
+ `);
9537
+ await deleteStmt.bind(userId).run();
9538
+ await logActivity(
9539
+ db,
9540
+ user.userId,
9541
+ "user!.hard_delete",
9542
+ "users",
9543
+ userId,
9544
+ { email: userToDelete.email, permanent: true },
8515
9545
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
8516
9546
  c.req.header("user-agent")
8517
9547
  );
@@ -11090,10 +12120,32 @@ function renderPluginsListPage(data) {
11090
12120
  </div>
11091
12121
  </div>
11092
12122
 
12123
+ <!-- Experimental Notice -->
12124
+ <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">
12125
+ <div class="flex items-start">
12126
+ <div class="flex-shrink-0">
12127
+ <svg class="h-5 w-5 text-amber-600 dark:text-amber-400" viewBox="0 0 20 20" fill="currentColor">
12128
+ <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
12129
+ </svg>
12130
+ </div>
12131
+ <div class="ml-3 flex-1">
12132
+ <h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">
12133
+ Experimental Feature
12134
+ </h3>
12135
+ <div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
12136
+ <p>
12137
+ Plugin management is currently under active development. While functional, some features may change or have limitations.
12138
+ Please report any issues you encounter on our <a href="https://discord.gg/8bMy6bv3sZ" target="_blank" class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100">Discord community</a>.
12139
+ </p>
12140
+ </div>
12141
+ </div>
12142
+ </div>
12143
+ </div>
12144
+
11093
12145
  <!-- Stats -->
11094
12146
  <div class="mb-6">
11095
12147
  <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
11096
- <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">
12148
+ <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">
11097
12149
  <div class="px-4 py-5 sm:p-6">
11098
12150
  <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
11099
12151
  <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
@@ -11154,6 +12206,21 @@ function renderPluginsListPage(data) {
11154
12206
  </div>
11155
12207
  </dd>
11156
12208
  </div>
12209
+ <div class="px-4 py-5 sm:p-6">
12210
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Available to Install</dt>
12211
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
12212
+ <div class="flex items-baseline text-2xl font-semibold text-zinc-400">
12213
+ ${data.stats?.uninstalled || 0}
12214
+ </div>
12215
+ <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">
12216
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
12217
+ <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" />
12218
+ </svg>
12219
+ <span class="sr-only">Available</span>
12220
+ Ready
12221
+ </div>
12222
+ </dd>
12223
+ </div>
11157
12224
  </dl>
11158
12225
  </div>
11159
12226
 
@@ -11169,13 +12236,16 @@ function renderPluginsListPage(data) {
11169
12236
  <div>
11170
12237
  <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
11171
12238
  <div class="mt-2 grid grid-cols-1">
11172
- <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">
12239
+ <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">
11173
12240
  <option value="">All Categories</option>
11174
12241
  <option value="content">Content Management</option>
11175
12242
  <option value="media">Media</option>
11176
12243
  <option value="seo">SEO & Analytics</option>
11177
12244
  <option value="security">Security</option>
11178
12245
  <option value="utilities">Utilities</option>
12246
+ <option value="system">System</option>
12247
+ <option value="development">Development</option>
12248
+ <option value="demo">Demo</option>
11179
12249
  </select>
11180
12250
  <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">
11181
12251
  <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" />
@@ -11185,10 +12255,11 @@ function renderPluginsListPage(data) {
11185
12255
  <div>
11186
12256
  <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
11187
12257
  <div class="mt-2 grid grid-cols-1">
11188
- <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">
12258
+ <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">
11189
12259
  <option value="">All Status</option>
11190
12260
  <option value="active">Active</option>
11191
12261
  <option value="inactive">Inactive</option>
12262
+ <option value="uninstalled">Available to Install</option>
11192
12263
  <option value="error">Error</option>
11193
12264
  </select>
11194
12265
  <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">
@@ -11205,8 +12276,10 @@ function renderPluginsListPage(data) {
11205
12276
  </svg>
11206
12277
  </div>
11207
12278
  <input
12279
+ id="search-input"
11208
12280
  type="text"
11209
12281
  placeholder="Search plugins..."
12282
+ oninput="filterPlugins()"
11210
12283
  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"
11211
12284
  />
11212
12285
  </div>
@@ -11229,7 +12302,7 @@ function renderPluginsListPage(data) {
11229
12302
  </div>
11230
12303
 
11231
12304
  <!-- Plugins Grid -->
11232
- <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
12305
+ <div id="plugins-grid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
11233
12306
  ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
11234
12307
  </div>
11235
12308
 
@@ -11356,7 +12429,12 @@ function renderPluginsListPage(data) {
11356
12429
  }
11357
12430
 
11358
12431
  function openPluginSettings(pluginId) {
11359
- window.location.href = \`/admin/plugins/\${pluginId}\`;
12432
+ // Email plugin has a custom settings page
12433
+ if (pluginId === 'email') {
12434
+ window.location.href = '/admin/plugins/email/settings';
12435
+ } else {
12436
+ window.location.href = \`/admin/plugins/\${pluginId}\`;
12437
+ }
11360
12438
  }
11361
12439
 
11362
12440
  function showPluginDetails(pluginId) {
@@ -11380,12 +12458,78 @@ function renderPluginsListPage(data) {
11380
12458
  const dropdown = document.getElementById('plugin-dropdown');
11381
12459
  dropdown.classList.toggle('hidden');
11382
12460
  }
11383
-
12461
+
12462
+ function filterPlugins() {
12463
+ const categoryFilter = document.getElementById('category-filter').value.toLowerCase();
12464
+ const statusFilter = document.getElementById('status-filter').value.toLowerCase();
12465
+ const searchInput = document.getElementById('search-input').value.toLowerCase();
12466
+
12467
+ const pluginCards = document.querySelectorAll('.plugin-card');
12468
+ let visibleCount = 0;
12469
+
12470
+ pluginCards.forEach(card => {
12471
+ // Get plugin data from card attributes
12472
+ const category = card.getAttribute('data-category')?.toLowerCase() || '';
12473
+ const status = card.getAttribute('data-status')?.toLowerCase() || '';
12474
+ const name = card.getAttribute('data-name')?.toLowerCase() || '';
12475
+ const description = card.getAttribute('data-description')?.toLowerCase() || '';
12476
+
12477
+ // Check if plugin matches all filters
12478
+ let matches = true;
12479
+
12480
+ // Category filter
12481
+ if (categoryFilter && category !== categoryFilter) {
12482
+ matches = false;
12483
+ }
12484
+
12485
+ // Status filter
12486
+ if (statusFilter && status !== statusFilter) {
12487
+ matches = false;
12488
+ }
12489
+
12490
+ // Search filter - check if search term is in name or description
12491
+ if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) {
12492
+ matches = false;
12493
+ }
12494
+
12495
+ // Show/hide card
12496
+ if (matches) {
12497
+ card.style.display = '';
12498
+ visibleCount++;
12499
+ } else {
12500
+ card.style.display = 'none';
12501
+ }
12502
+ });
12503
+
12504
+ // Show/hide "no results" message
12505
+ let noResultsMsg = document.getElementById('no-results-message');
12506
+ if (visibleCount === 0) {
12507
+ if (!noResultsMsg) {
12508
+ noResultsMsg = document.createElement('div');
12509
+ noResultsMsg.id = 'no-results-message';
12510
+ noResultsMsg.className = 'col-span-full text-center py-12';
12511
+ noResultsMsg.innerHTML = \`
12512
+ <div class="flex flex-col items-center">
12513
+ <svg class="w-16 h-16 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12514
+ <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" />
12515
+ </svg>
12516
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-2">No plugins found</h3>
12517
+ <p class="text-sm text-zinc-500 dark:text-zinc-400">Try adjusting your filters or search terms</p>
12518
+ </div>
12519
+ \`;
12520
+ document.getElementById('plugins-grid').appendChild(noResultsMsg);
12521
+ }
12522
+ noResultsMsg.style.display = '';
12523
+ } else if (noResultsMsg) {
12524
+ noResultsMsg.style.display = 'none';
12525
+ }
12526
+ }
12527
+
11384
12528
  // Close dropdown when clicking outside
11385
12529
  document.addEventListener('click', (event) => {
11386
12530
  const dropdown = document.getElementById('plugin-dropdown');
11387
12531
  const button = event.target.closest('button[onclick="toggleDropdown()"]');
11388
-
12532
+
11389
12533
  if (!button && !dropdown.contains(event.target)) {
11390
12534
  dropdown.classList.add('hidden');
11391
12535
  }
@@ -11420,23 +12564,33 @@ function renderPluginCard(plugin) {
11420
12564
  const statusColors = {
11421
12565
  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",
11422
12566
  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",
11423
- 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"
12567
+ 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",
12568
+ 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"
11424
12569
  };
11425
12570
  const statusIcons = {
11426
12571
  active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
11427
12572
  inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
11428
- error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>'
12573
+ error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>',
12574
+ uninstalled: '<div class="w-2 h-2 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-2"></div>'
11429
12575
  };
11430
12576
  const borderColors = {
11431
12577
  active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
11432
12578
  inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
11433
- error: "ring-[3px] ring-red-500 dark:ring-red-400"
12579
+ error: "ring-[3px] ring-red-500 dark:ring-red-400",
12580
+ uninstalled: "ring-[3px] ring-zinc-400 dark:ring-zinc-600"
11434
12581
  };
11435
12582
  const criticalCorePlugins = ["core-auth", "core-media"];
11436
12583
  const canToggle = !criticalCorePlugins.includes(plugin.id);
11437
- 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>`;
12584
+ let actionButton = "";
12585
+ if (plugin.status === "uninstalled") {
12586
+ 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>`;
12587
+ } else if (plugin.status === "active") {
12588
+ 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>`;
12589
+ } else {
12590
+ 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>`;
12591
+ }
11438
12592
  return `
11439
- <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">
12593
+ <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}">
11440
12594
  <div class="flex items-start justify-between mb-4">
11441
12595
  <div class="flex items-center gap-3">
11442
12596
  <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">
@@ -11497,20 +12651,24 @@ function renderPluginCard(plugin) {
11497
12651
 
11498
12652
  <div class="flex items-center justify-between">
11499
12653
  <div class="flex gap-2">
11500
- ${canToggle ? actionButton : ""}
12654
+ ${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
12655
+ ${plugin.status !== "uninstalled" ? `
11501
12656
  <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">
11502
12657
  Settings
11503
12658
  </button>
12659
+ ` : ""}
11504
12660
  </div>
11505
12661
 
11506
12662
  <div class="flex items-center gap-2">
12663
+ ${plugin.status !== "uninstalled" ? `
11507
12664
  <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">
11508
12665
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11509
12666
  <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"/>
11510
12667
  </svg>
11511
12668
  </button>
12669
+ ` : ""}
11512
12670
 
11513
- ${!plugin.isCore ? `
12671
+ ${!plugin.isCore && plugin.status !== "uninstalled" ? `
11514
12672
  <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">
11515
12673
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11516
12674
  <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"/>
@@ -11813,28 +12971,22 @@ function renderPluginSettingsPage(data) {
11813
12971
  const { plugin, activity = [], user } = data;
11814
12972
  const pageContent = `
11815
12973
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
11816
- <!-- Header with breadcrumb -->
11817
- <div class="flex items-center mb-6">
11818
- <nav class="flex" aria-label="Breadcrumb">
11819
- <ol class="flex items-center space-x-2">
11820
- <li>
11821
- <a href="/admin/plugins" class="text-gray-400 hover:text-white transition-colors">
11822
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11823
- <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"/>
11824
- </svg>
11825
- Plugins
11826
- </a>
11827
- </li>
11828
- <li>
11829
- <svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
11830
- <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"/>
11831
- </svg>
11832
- </li>
11833
- <li>
11834
- <span class="text-gray-300">${plugin.displayName}</span>
11835
- </li>
11836
- </ol>
11837
- </nav>
12974
+ <!-- Header with Back Button -->
12975
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
12976
+ <div>
12977
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Plugin Settings</h1>
12978
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
12979
+ ${plugin.description}
12980
+ </p>
12981
+ </div>
12982
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
12983
+ <a href="/admin/plugins" class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
12984
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12985
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
12986
+ </svg>
12987
+ Back to Plugins
12988
+ </a>
12989
+ </div>
11838
12990
  </div>
11839
12991
 
11840
12992
  <!-- Plugin Header -->
@@ -11845,9 +12997,8 @@ function renderPluginSettingsPage(data) {
11845
12997
  ${plugin.icon || plugin.displayName.charAt(0).toUpperCase()}
11846
12998
  </div>
11847
12999
  <div>
11848
- <h1 class="text-2xl font-semibold text-white mb-1">${plugin.displayName}</h1>
11849
- <p class="text-gray-300 mb-2">${plugin.description}</p>
11850
- <div class="flex items-center gap-4 text-sm text-gray-400">
13000
+ <h2 class="text-2xl font-semibold text-white mb-1">${plugin.displayName}</h2>
13001
+ <div class="flex items-center gap-4 text-sm text-gray-400 mt-2">
11851
13002
  <span>v${plugin.version}</span>
11852
13003
  <span>by ${plugin.author}</span>
11853
13004
  <span>${plugin.category}</span>
@@ -11856,7 +13007,7 @@ function renderPluginSettingsPage(data) {
11856
13007
  </div>
11857
13008
  </div>
11858
13009
  </div>
11859
-
13010
+
11860
13011
  <div class="flex items-center gap-3">
11861
13012
  ${renderStatusBadge(plugin.status)}
11862
13013
  ${renderToggleButton(plugin)}
@@ -12143,10 +13294,10 @@ function renderSettingsTab(plugin) {
12143
13294
  `;
12144
13295
  }
12145
13296
  function renderSettingsFields(settings) {
12146
- return Object.entries(settings).map(([key, value2]) => {
13297
+ return Object.entries(settings).map(([key, value]) => {
12147
13298
  const fieldId = `setting_${key}`;
12148
13299
  const displayName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
12149
- if (typeof value2 === "boolean") {
13300
+ if (typeof value === "boolean") {
12150
13301
  return `
12151
13302
  <div class="flex items-center justify-between">
12152
13303
  <div>
@@ -12154,12 +13305,12 @@ function renderSettingsFields(settings) {
12154
13305
  <p class="text-xs text-gray-400">Enable or disable this feature</p>
12155
13306
  </div>
12156
13307
  <label class="relative inline-flex items-center cursor-pointer">
12157
- <input type="checkbox" name="${fieldId}" id="${fieldId}" ${value2 ? "checked" : ""} class="sr-only peer">
13308
+ <input type="checkbox" name="${fieldId}" id="${fieldId}" ${value ? "checked" : ""} class="sr-only peer">
12158
13309
  <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>
12159
13310
  </label>
12160
13311
  </div>
12161
13312
  `;
12162
- } else if (typeof value2 === "number") {
13313
+ } else if (typeof value === "number") {
12163
13314
  return `
12164
13315
  <div>
12165
13316
  <label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
@@ -12167,7 +13318,7 @@ function renderSettingsFields(settings) {
12167
13318
  type="number"
12168
13319
  name="${fieldId}"
12169
13320
  id="${fieldId}"
12170
- value="${value2}"
13321
+ value="${value}"
12171
13322
  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"
12172
13323
  >
12173
13324
  </div>
@@ -12180,7 +13331,7 @@ function renderSettingsFields(settings) {
12180
13331
  type="text"
12181
13332
  name="${fieldId}"
12182
13333
  id="${fieldId}"
12183
- value="${value2}"
13334
+ value="${value}"
12184
13335
  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"
12185
13336
  >
12186
13337
  </div>
@@ -12328,6 +13479,86 @@ function formatTimestamp(timestamp) {
12328
13479
  // src/routes/admin-plugins.ts
12329
13480
  var adminPluginRoutes = new Hono();
12330
13481
  adminPluginRoutes.use("*", requireAuth());
13482
+ var AVAILABLE_PLUGINS = [
13483
+ {
13484
+ id: "third-party-faq",
13485
+ name: "faq-plugin",
13486
+ display_name: "FAQ System",
13487
+ description: "Frequently Asked Questions management system with categories, search, and custom styling",
13488
+ version: "2.0.0",
13489
+ author: "Community Developer",
13490
+ category: "content",
13491
+ icon: "\u2753",
13492
+ permissions: ["manage:faqs"],
13493
+ dependencies: [],
13494
+ is_core: false
13495
+ },
13496
+ {
13497
+ id: "demo-login-prefill",
13498
+ name: "demo-login-plugin",
13499
+ display_name: "Demo Login Prefill",
13500
+ description: "Prefills login form with demo credentials (admin@sonicjs.com/sonicjs!) for easy site demonstration",
13501
+ version: "1.0.0-beta.1",
13502
+ author: "SonicJS",
13503
+ category: "demo",
13504
+ icon: "\u{1F3AF}",
13505
+ permissions: [],
13506
+ dependencies: [],
13507
+ is_core: false
13508
+ },
13509
+ {
13510
+ id: "database-tools",
13511
+ name: "database-tools",
13512
+ display_name: "Database Tools",
13513
+ description: "Database management tools including truncate, backup, and validation",
13514
+ version: "1.0.0-beta.1",
13515
+ author: "SonicJS Team",
13516
+ category: "system",
13517
+ icon: "\u{1F5C4}\uFE0F",
13518
+ permissions: ["manage:database", "admin"],
13519
+ dependencies: [],
13520
+ is_core: false
13521
+ },
13522
+ {
13523
+ id: "seed-data",
13524
+ name: "seed-data",
13525
+ display_name: "Seed Data",
13526
+ description: "Generate realistic example users and content for testing and development",
13527
+ version: "1.0.0-beta.1",
13528
+ author: "SonicJS Team",
13529
+ category: "development",
13530
+ icon: "\u{1F331}",
13531
+ permissions: ["admin"],
13532
+ dependencies: [],
13533
+ is_core: false
13534
+ },
13535
+ {
13536
+ id: "quill-editor",
13537
+ name: "quill-editor",
13538
+ display_name: "Quill Rich Text Editor",
13539
+ description: "Quill WYSIWYG editor integration for rich text editing. Lightweight, modern editor with customizable toolbars and dark mode support.",
13540
+ version: "1.0.0",
13541
+ author: "SonicJS Team",
13542
+ category: "editor",
13543
+ icon: "\u270D\uFE0F",
13544
+ permissions: [],
13545
+ dependencies: [],
13546
+ is_core: true
13547
+ },
13548
+ {
13549
+ id: "tinymce-plugin",
13550
+ name: "tinymce-plugin",
13551
+ display_name: "TinyMCE Rich Text Editor",
13552
+ description: "Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.",
13553
+ version: "1.0.0",
13554
+ author: "SonicJS Team",
13555
+ category: "editor",
13556
+ icon: "\u{1F4DD}",
13557
+ permissions: [],
13558
+ dependencies: [],
13559
+ is_core: false
13560
+ }
13561
+ ];
12331
13562
  adminPluginRoutes.get("/", async (c) => {
12332
13563
  try {
12333
13564
  const user = c.get("user");
@@ -12336,15 +13567,17 @@ adminPluginRoutes.get("/", async (c) => {
12336
13567
  return c.text("Access denied", 403);
12337
13568
  }
12338
13569
  const pluginService = new PluginService(db);
12339
- let plugins = [];
12340
- let stats = { total: 0, active: 0, inactive: 0, errors: 0 };
13570
+ let installedPlugins = [];
13571
+ let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
12341
13572
  try {
12342
- plugins = await pluginService.getAllPlugins();
13573
+ installedPlugins = await pluginService.getAllPlugins();
12343
13574
  stats = await pluginService.getPluginStats();
12344
13575
  } catch (error) {
12345
13576
  console.error("Error loading plugins:", error);
12346
13577
  }
12347
- const templatePlugins = plugins.map((p) => ({
13578
+ const installedPluginIds = new Set(installedPlugins.map((p) => p.id));
13579
+ const uninstalledPlugins = AVAILABLE_PLUGINS.filter((p) => !installedPluginIds.has(p.id));
13580
+ const templatePlugins = installedPlugins.map((p) => ({
12348
13581
  id: p.id,
12349
13582
  name: p.name,
12350
13583
  displayName: p.display_name,
@@ -12361,8 +13594,28 @@ adminPluginRoutes.get("/", async (c) => {
12361
13594
  permissions: p.permissions,
12362
13595
  isCore: p.is_core
12363
13596
  }));
13597
+ const uninstalledTemplatePlugins = uninstalledPlugins.map((p) => ({
13598
+ id: p.id,
13599
+ name: p.name,
13600
+ displayName: p.display_name,
13601
+ description: p.description,
13602
+ version: p.version,
13603
+ author: p.author,
13604
+ status: "uninstalled",
13605
+ category: p.category,
13606
+ icon: p.icon,
13607
+ downloadCount: 0,
13608
+ rating: 0,
13609
+ lastUpdated: "Not installed",
13610
+ dependencies: p.dependencies,
13611
+ permissions: p.permissions,
13612
+ isCore: p.is_core
13613
+ }));
13614
+ const allPlugins = [...templatePlugins, ...uninstalledTemplatePlugins];
13615
+ stats.uninstalled = uninstalledPlugins.length;
13616
+ stats.total = installedPlugins.length + uninstalledPlugins.length;
12364
13617
  const pageData = {
12365
- plugins: templatePlugins,
13618
+ plugins: allPlugins,
12366
13619
  stats,
12367
13620
  user: {
12368
13621
  name: user?.email || "User",
@@ -12499,7 +13752,7 @@ adminPluginRoutes.post("/install", async (c) => {
12499
13752
  id: "demo-login-prefill",
12500
13753
  name: "demo-login-plugin",
12501
13754
  display_name: "Demo Login Prefill",
12502
- description: "Prefills login form with demo credentials (admin@sonicjs.com/admin123) for easy site demonstration",
13755
+ description: "Prefills login form with demo credentials (admin@sonicjs.com/sonicjs!) for easy site demonstration",
12503
13756
  version: "1.0.0-beta.1",
12504
13757
  author: "SonicJS",
12505
13758
  category: "demo",
@@ -12509,7 +13762,7 @@ adminPluginRoutes.post("/install", async (c) => {
12509
13762
  settings: {
12510
13763
  enableNotice: true,
12511
13764
  demoEmail: "admin@sonicjs.com",
12512
- demoPassword: "admin123"
13765
+ demoPassword: "sonicjs!"
12513
13766
  }
12514
13767
  });
12515
13768
  return c.json({ success: true, plugin: demoPlugin });
@@ -12608,6 +13861,50 @@ adminPluginRoutes.post("/install", async (c) => {
12608
13861
  });
12609
13862
  return c.json({ success: true, plugin: seedDataPlugin });
12610
13863
  }
13864
+ if (body.name === "quill-editor") {
13865
+ const quillPlugin = await pluginService.installPlugin({
13866
+ id: "quill-editor",
13867
+ name: "quill-editor",
13868
+ display_name: "Quill Rich Text Editor",
13869
+ description: "Quill WYSIWYG editor integration for rich text editing. Lightweight, modern editor with customizable toolbars and dark mode support.",
13870
+ version: "1.0.0",
13871
+ author: "SonicJS Team",
13872
+ category: "editor",
13873
+ icon: "\u270D\uFE0F",
13874
+ permissions: [],
13875
+ dependencies: [],
13876
+ is_core: true,
13877
+ settings: {
13878
+ version: "2.0.2",
13879
+ defaultHeight: 300,
13880
+ defaultToolbar: "full",
13881
+ theme: "snow"
13882
+ }
13883
+ });
13884
+ return c.json({ success: true, plugin: quillPlugin });
13885
+ }
13886
+ if (body.name === "tinymce-plugin") {
13887
+ const tinymcePlugin2 = await pluginService.installPlugin({
13888
+ id: "tinymce-plugin",
13889
+ name: "tinymce-plugin",
13890
+ display_name: "TinyMCE Rich Text Editor",
13891
+ description: "Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.",
13892
+ version: "1.0.0",
13893
+ author: "SonicJS Team",
13894
+ category: "editor",
13895
+ icon: "\u{1F4DD}",
13896
+ permissions: [],
13897
+ dependencies: [],
13898
+ is_core: false,
13899
+ settings: {
13900
+ apiKey: "no-api-key",
13901
+ defaultHeight: 300,
13902
+ defaultToolbar: "full",
13903
+ skin: "oxide-dark"
13904
+ }
13905
+ });
13906
+ return c.json({ success: true, plugin: tinymcePlugin2 });
13907
+ }
12611
13908
  return c.json({ error: "Plugin not found in registry" }, 404);
12612
13909
  } catch (error) {
12613
13910
  console.error("Error installing plugin:", error);
@@ -13683,751 +14980,146 @@ adminLogsRoutes.get("/export", async (c) => {
13683
14980
  return c.json({ error: "Failed to export logs" }, 500);
13684
14981
  }
13685
14982
  });
13686
- adminLogsRoutes.post("/cleanup", async (c) => {
13687
- try {
13688
- const user = c.get("user");
13689
- if (!user || user.role !== "admin") {
13690
- return c.json({
13691
- success: false,
13692
- error: "Unauthorized. Admin access required."
13693
- }, 403);
13694
- }
13695
- const logger = getLogger(c.env.DB);
13696
- await logger.cleanupByRetention();
13697
- return c.html(html`
13698
- <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
13699
- Log cleanup completed successfully!
13700
- </div>
13701
- `);
13702
- } catch (error) {
13703
- console.error("Error cleaning up logs:", error);
13704
- return c.html(html`
13705
- <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
13706
- Failed to clean up logs. Please try again.
13707
- </div>
13708
- `);
13709
- }
13710
- });
13711
- adminLogsRoutes.post("/search", async (c) => {
13712
- try {
13713
- const formData = await c.req.formData();
13714
- const search = formData.get("search");
13715
- const level = formData.get("level");
13716
- const category = formData.get("category");
13717
- const logger = getLogger(c.env.DB);
13718
- const filter = {
13719
- limit: 20,
13720
- offset: 0,
13721
- sortBy: "created_at",
13722
- sortOrder: "desc"
13723
- };
13724
- if (search) filter.search = search;
13725
- if (level) filter.level = [level];
13726
- if (category) filter.category = [category];
13727
- const { logs } = await logger.getLogs(filter);
13728
- const rows = logs.map((log) => {
13729
- const formattedLog = {
13730
- ...log,
13731
- formattedDate: new Date(log.createdAt).toLocaleString(),
13732
- levelClass: getLevelClass(log.level),
13733
- categoryClass: getCategoryClass(log.category)
13734
- };
13735
- return `
13736
- <tr class="hover:bg-gray-50">
13737
- <td class="px-6 py-4 whitespace-nowrap">
13738
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
13739
- ${formattedLog.level}
13740
- </span>
13741
- </td>
13742
- <td class="px-6 py-4 whitespace-nowrap">
13743
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
13744
- ${formattedLog.category}
13745
- </span>
13746
- </td>
13747
- <td class="px-6 py-4">
13748
- <div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
13749
- </td>
13750
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
13751
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
13752
- <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
13753
- <a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
13754
- </td>
13755
- </tr>
13756
- `;
13757
- }).join("");
13758
- return c.html(rows);
13759
- } catch (error) {
13760
- console.error("Error searching logs:", error);
13761
- return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
13762
- }
13763
- });
13764
- function getLevelClass(level) {
13765
- switch (level) {
13766
- case "debug":
13767
- return "bg-gray-100 text-gray-800";
13768
- case "info":
13769
- return "bg-blue-100 text-blue-800";
13770
- case "warn":
13771
- return "bg-yellow-100 text-yellow-800";
13772
- case "error":
13773
- return "bg-red-100 text-red-800";
13774
- case "fatal":
13775
- return "bg-purple-100 text-purple-800";
13776
- default:
13777
- return "bg-gray-100 text-gray-800";
13778
- }
13779
- }
13780
- function getCategoryClass(category) {
13781
- switch (category) {
13782
- case "auth":
13783
- return "bg-green-100 text-green-800";
13784
- case "api":
13785
- return "bg-blue-100 text-blue-800";
13786
- case "workflow":
13787
- return "bg-purple-100 text-purple-800";
13788
- case "plugin":
13789
- return "bg-indigo-100 text-indigo-800";
13790
- case "media":
13791
- return "bg-pink-100 text-pink-800";
13792
- case "system":
13793
- return "bg-gray-100 text-gray-800";
13794
- case "security":
13795
- return "bg-red-100 text-red-800";
13796
- case "error":
13797
- return "bg-red-100 text-red-800";
13798
- default:
13799
- return "bg-gray-100 text-gray-800";
13800
- }
13801
- }
13802
- var adminDesignRoutes = new Hono();
13803
- adminDesignRoutes.get("/", (c) => {
13804
- const user = c.get("user");
13805
- const pageData = {
13806
- user: user ? {
13807
- name: user.email,
13808
- email: user.email,
13809
- role: user.role
13810
- } : void 0
13811
- };
13812
- return c.html(renderDesignPage(pageData));
13813
- });
13814
- var adminCheckboxRoutes = new Hono();
13815
- adminCheckboxRoutes.get("/", (c) => {
13816
- const user = c.get("user");
13817
- const pageData = {
13818
- user: user ? {
13819
- name: user.email,
13820
- email: user.email,
13821
- role: user.role
13822
- } : void 0
13823
- };
13824
- return c.html(renderCheckboxPage(pageData));
13825
- });
13826
-
13827
- // src/templates/pages/admin-faq-form.template.ts
13828
- function renderFAQForm(data) {
13829
- const { faq, isEdit, errors, message, messageType } = data;
13830
- const pageTitle = isEdit ? "Edit FAQ" : "New FAQ";
13831
- const pageContent = `
13832
- <div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
13833
- <!-- Header -->
13834
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
13835
- <div>
13836
- <h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
13837
- <p class="mt-2 text-sm text-gray-300">
13838
- ${isEdit ? "Update the FAQ details below" : "Create a new frequently asked question"}
13839
- </p>
13840
- </div>
13841
- <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
13842
- <a href="/admin/faq"
13843
- class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
13844
- <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13845
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
13846
- </svg>
13847
- Back to List
13848
- </a>
13849
- </div>
13850
- </div>
13851
-
13852
- ${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
13853
-
13854
- <!-- Form -->
13855
- <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
13856
- <form ${isEdit ? `hx-put="/admin/faq/${faq?.id}"` : 'hx-post="/admin/faq"'}
13857
- hx-target="body"
13858
- hx-swap="outerHTML"
13859
- class="space-y-6 p-6">
13860
-
13861
- <!-- Question -->
13862
- <div>
13863
- <label for="question" class="block text-sm font-medium text-white">
13864
- Question <span class="text-red-400">*</span>
13865
- </label>
13866
- <div class="mt-1">
13867
- <textarea name="question"
13868
- id="question"
13869
- rows="3"
13870
- required
13871
- maxlength="500"
13872
- class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13873
- placeholder="Enter the frequently asked question...">${faq?.question || ""}</textarea>
13874
- <p class="mt-1 text-sm text-gray-300">
13875
- <span id="question-count">0</span>/500 characters
13876
- </p>
13877
- </div>
13878
- ${errors?.question ? `
13879
- <div class="mt-1">
13880
- ${errors.question.map((error) => `
13881
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13882
- `).join("")}
13883
- </div>
13884
- ` : ""}
13885
- </div>
13886
-
13887
- <!-- Answer -->
13888
- <div>
13889
- <label for="answer" class="block text-sm font-medium text-white">
13890
- Answer <span class="text-red-400">*</span>
13891
- </label>
13892
- <div class="mt-1">
13893
- <textarea name="answer"
13894
- id="answer"
13895
- rows="6"
13896
- required
13897
- maxlength="2000"
13898
- class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13899
- placeholder="Enter the detailed answer...">${faq?.answer || ""}</textarea>
13900
- <p class="mt-1 text-sm text-gray-300">
13901
- <span id="answer-count">0</span>/2000 characters. You can use basic HTML for formatting.
13902
- </p>
13903
- </div>
13904
- ${errors?.answer ? `
13905
- <div class="mt-1">
13906
- ${errors.answer.map((error) => `
13907
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13908
- `).join("")}
13909
- </div>
13910
- ` : ""}
13911
- </div>
13912
-
13913
- <!-- Category and Tags Row -->
13914
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
13915
- <!-- Category -->
13916
- <div>
13917
- <label for="category" class="block text-sm font-medium text-white">Category</label>
13918
- <div class="mt-1">
13919
- <select name="category"
13920
- id="category"
13921
- class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
13922
- <option value="">Select a category</option>
13923
- <option value="general" ${faq?.category === "general" ? "selected" : ""}>General</option>
13924
- <option value="technical" ${faq?.category === "technical" ? "selected" : ""}>Technical</option>
13925
- <option value="billing" ${faq?.category === "billing" ? "selected" : ""}>Billing</option>
13926
- <option value="support" ${faq?.category === "support" ? "selected" : ""}>Support</option>
13927
- <option value="account" ${faq?.category === "account" ? "selected" : ""}>Account</option>
13928
- <option value="features" ${faq?.category === "features" ? "selected" : ""}>Features</option>
13929
- </select>
13930
- </div>
13931
- ${errors?.category ? `
13932
- <div class="mt-1">
13933
- ${errors.category.map((error) => `
13934
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13935
- `).join("")}
13936
- </div>
13937
- ` : ""}
13938
- </div>
13939
-
13940
- <!-- Tags -->
13941
- <div>
13942
- <label for="tags" class="block text-sm font-medium text-white">Tags</label>
13943
- <div class="mt-1">
13944
- <input type="text"
13945
- name="tags"
13946
- id="tags"
13947
- value="${faq?.tags || ""}"
13948
- class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
13949
- placeholder="e.g., payment, setup, troubleshooting">
13950
- <p class="mt-1 text-sm text-gray-300">Separate multiple tags with commas</p>
13951
- </div>
13952
- ${errors?.tags ? `
13953
- <div class="mt-1">
13954
- ${errors.tags.map((error) => `
13955
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13956
- `).join("")}
13957
- </div>
13958
- ` : ""}
13959
- </div>
13960
- </div>
13961
-
13962
- <!-- Status and Sort Order Row -->
13963
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
13964
- <!-- Published Status -->
13965
- <div>
13966
- <label class="block text-sm font-medium text-white">Status</label>
13967
- <div class="mt-2 space-y-2">
13968
- <div class="flex items-center">
13969
- <input id="published"
13970
- name="isPublished"
13971
- type="radio"
13972
- value="true"
13973
- ${!faq || faq.isPublished ? "checked" : ""}
13974
- class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
13975
- <label for="published" class="ml-2 block text-sm text-white">
13976
- Published <span class="text-gray-300">(visible to users)</span>
13977
- </label>
13978
- </div>
13979
- <div class="flex items-center">
13980
- <input id="draft"
13981
- name="isPublished"
13982
- type="radio"
13983
- value="false"
13984
- ${faq && !faq.isPublished ? "checked" : ""}
13985
- class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
13986
- <label for="draft" class="ml-2 block text-sm text-white">
13987
- Draft <span class="text-gray-300">(not visible to users)</span>
13988
- </label>
13989
- </div>
13990
- </div>
13991
- </div>
13992
-
13993
- <!-- Sort Order -->
13994
- <div>
13995
- <label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
13996
- <div class="mt-1">
13997
- <input type="number"
13998
- name="sortOrder"
13999
- id="sortOrder"
14000
- value="${faq?.sortOrder || 0}"
14001
- min="0"
14002
- step="1"
14003
- class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
14004
- <p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
14005
- </div>
14006
- ${errors?.sortOrder ? `
14007
- <div class="mt-1">
14008
- ${errors.sortOrder.map((error) => `
14009
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14010
- `).join("")}
14011
- </div>
14012
- ` : ""}
14013
- </div>
14014
- </div>
14015
-
14016
- <!-- Form Actions -->
14017
- <div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
14018
- <a href="/admin/faq"
14019
- class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
14020
- Cancel
14021
- </a>
14022
- <button type="submit"
14023
- class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-blue-500/80 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-blue-500 transition-all">
14024
- <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14025
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
14026
- </svg>
14027
- ${isEdit ? "Update FAQ" : "Create FAQ"}
14028
- </button>
14029
- </div>
14030
- </form>
14031
- </div>
14032
- </div>
14033
-
14034
- <script>
14035
- // Character count for question
14036
- const questionTextarea = document.getElementById('question');
14037
- const questionCount = document.getElementById('question-count');
14038
-
14039
- function updateQuestionCount() {
14040
- questionCount.textContent = questionTextarea.value.length;
14041
- }
14042
-
14043
- questionTextarea.addEventListener('input', updateQuestionCount);
14044
- updateQuestionCount(); // Initial count
14045
-
14046
- // Character count for answer
14047
- const answerTextarea = document.getElementById('answer');
14048
- const answerCount = document.getElementById('answer-count');
14049
-
14050
- function updateAnswerCount() {
14051
- answerCount.textContent = answerTextarea.value.length;
14052
- }
14053
-
14054
- answerTextarea.addEventListener('input', updateAnswerCount);
14055
- updateAnswerCount(); // Initial count
14056
- </script>
14057
- `;
14058
- const layoutData = {
14059
- title: `${pageTitle} - Admin`,
14060
- pageTitle,
14061
- currentPath: isEdit ? `/admin/faq/${faq?.id}` : "/admin/faq/new",
14062
- user: data.user,
14063
- content: pageContent
14064
- };
14065
- return renderAdminLayout(layoutData);
14066
- }
14067
- function escapeHtml4(unsafe) {
14068
- return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14069
- }
14070
-
14071
- // src/routes/admin-faq.ts
14072
- var faqSchema = z.object({
14073
- question: z.string().min(1, "Question is required").max(500, "Question must be under 500 characters"),
14074
- answer: z.string().min(1, "Answer is required").max(2e3, "Answer must be under 2000 characters"),
14075
- category: z.string().optional(),
14076
- tags: z.string().optional(),
14077
- isPublished: z.string().transform((val) => val === "true"),
14078
- sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
14079
- });
14080
- var adminFAQRoutes = new Hono();
14081
- adminFAQRoutes.get("/", async (c) => {
14082
- try {
14083
- const user = c.get("user");
14084
- const { category, published, search, page = "1" } = c.req.query();
14085
- const currentPage = parseInt(page, 10) || 1;
14086
- const limit = 20;
14087
- const offset = (currentPage - 1) * limit;
14088
- const db = c.env?.DB;
14089
- if (!db) {
14090
- return c.html(renderFAQList({
14091
- faqs: [],
14092
- totalCount: 0,
14093
- currentPage: 1,
14094
- totalPages: 1,
14095
- user: user ? {
14096
- name: user.email,
14097
- email: user.email,
14098
- role: user.role
14099
- } : void 0,
14100
- message: "Database not available",
14101
- messageType: "error"
14102
- }));
14103
- }
14104
- let whereClause = "WHERE 1=1";
14105
- const params = [];
14106
- if (category) {
14107
- whereClause += " AND category = ?";
14108
- params.push(category);
14109
- }
14110
- if (published !== void 0) {
14111
- whereClause += " AND isPublished = ?";
14112
- params.push(published === "true" ? 1 : 0);
14113
- }
14114
- if (search) {
14115
- whereClause += " AND (question LIKE ? OR answer LIKE ? OR tags LIKE ?)";
14116
- const searchTerm = `%${search}%`;
14117
- params.push(searchTerm, searchTerm, searchTerm);
14118
- }
14119
- const countQuery = `SELECT COUNT(*) as count FROM faqs ${whereClause}`;
14120
- const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
14121
- const totalCount = countResults?.[0]?.count || 0;
14122
- const dataQuery = `
14123
- SELECT * FROM faqs
14124
- ${whereClause}
14125
- ORDER BY sortOrder ASC, created_at DESC
14126
- LIMIT ? OFFSET ?
14127
- `;
14128
- const { results: faqs } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
14129
- const totalPages = Math.ceil(totalCount / limit);
14130
- return c.html(renderFAQList({
14131
- faqs: faqs || [],
14132
- totalCount,
14133
- currentPage,
14134
- totalPages,
14135
- user: user ? {
14136
- name: user.email,
14137
- email: user.email,
14138
- role: user.role
14139
- } : void 0
14140
- }));
14141
- } catch (error) {
14142
- console.error("Error fetching FAQs:", error);
14143
- const user = c.get("user");
14144
- return c.html(renderFAQList({
14145
- faqs: [],
14146
- totalCount: 0,
14147
- currentPage: 1,
14148
- totalPages: 1,
14149
- user: user ? {
14150
- name: user.email,
14151
- email: user.email,
14152
- role: user.role
14153
- } : void 0,
14154
- message: "Failed to load FAQs",
14155
- messageType: "error"
14156
- }));
14157
- }
14158
- });
14159
- adminFAQRoutes.get("/new", async (c) => {
14160
- const user = c.get("user");
14161
- return c.html(renderFAQForm({
14162
- isEdit: false,
14163
- user: user ? {
14164
- name: user.email,
14165
- email: user.email,
14166
- role: user.role
14167
- } : void 0
14168
- }));
14169
- });
14170
- adminFAQRoutes.post("/", async (c) => {
14171
- try {
14172
- const formData = await c.req.formData();
14173
- const data = Object.fromEntries(formData.entries());
14174
- const validatedData = faqSchema.parse(data);
14175
- const user = c.get("user");
14176
- const db = c.env?.DB;
14177
- if (!db) {
14178
- return c.html(renderFAQForm({
14179
- isEdit: false,
14180
- user: user ? {
14181
- name: user.email,
14182
- email: user.email,
14183
- role: user.role
14184
- } : void 0,
14185
- message: "Database not available",
14186
- messageType: "error"
14187
- }));
14188
- }
14189
- const { results } = await db.prepare(`
14190
- INSERT INTO faqs (question, answer, category, tags, isPublished, sortOrder)
14191
- VALUES (?, ?, ?, ?, ?, ?)
14192
- RETURNING *
14193
- `).bind(
14194
- validatedData.question,
14195
- validatedData.answer,
14196
- validatedData.category || null,
14197
- validatedData.tags || null,
14198
- validatedData.isPublished ? 1 : 0,
14199
- validatedData.sortOrder
14200
- ).all();
14201
- if (results && results.length > 0) {
14202
- return c.redirect("/admin/faq?message=FAQ created successfully");
14203
- } else {
14204
- return c.html(renderFAQForm({
14205
- isEdit: false,
14206
- user: user ? {
14207
- name: user.email,
14208
- email: user.email,
14209
- role: user.role
14210
- } : void 0,
14211
- message: "Failed to create FAQ",
14212
- messageType: "error"
14213
- }));
14214
- }
14215
- } catch (error) {
14216
- console.error("Error creating FAQ:", error);
14217
- const user = c.get("user");
14218
- if (error instanceof z.ZodError) {
14219
- const errors = {};
14220
- error.errors.forEach((err) => {
14221
- const field = err.path[0];
14222
- if (!errors[field]) errors[field] = [];
14223
- errors[field].push(err.message);
14224
- });
14225
- return c.html(renderFAQForm({
14226
- isEdit: false,
14227
- user: user ? {
14228
- name: user.email,
14229
- email: user.email,
14230
- role: user.role
14231
- } : void 0,
14232
- errors,
14233
- message: "Please correct the errors below",
14234
- messageType: "error"
14235
- }));
14236
- }
14237
- return c.html(renderFAQForm({
14238
- isEdit: false,
14239
- user: user ? {
14240
- name: user.email,
14241
- email: user.email,
14242
- role: user.role
14243
- } : void 0,
14244
- message: "Failed to create FAQ",
14245
- messageType: "error"
14246
- }));
14247
- }
14248
- });
14249
- adminFAQRoutes.get("/:id", async (c) => {
14250
- try {
14251
- const id = parseInt(c.req.param("id"));
14252
- const user = c.get("user");
14253
- const db = c.env?.DB;
14254
- if (!db) {
14255
- return c.html(renderFAQForm({
14256
- isEdit: true,
14257
- user: user ? {
14258
- name: user.email,
14259
- email: user.email,
14260
- role: user.role
14261
- } : void 0,
14262
- message: "Database not available",
14263
- messageType: "error"
14264
- }));
14265
- }
14266
- const { results } = await db.prepare("SELECT * FROM faqs WHERE id = ?").bind(id).all();
14267
- if (!results || results.length === 0) {
14268
- return c.redirect("/admin/faq?message=FAQ not found&type=error");
14269
- }
14270
- const faq = results[0];
14271
- return c.html(renderFAQForm({
14272
- faq: {
14273
- id: faq.id,
14274
- question: faq.question,
14275
- answer: faq.answer,
14276
- category: faq.category,
14277
- tags: faq.tags,
14278
- isPublished: Boolean(faq.isPublished),
14279
- sortOrder: faq.sortOrder
14280
- },
14281
- isEdit: true,
14282
- user: user ? {
14283
- name: user.email,
14284
- email: user.email,
14285
- role: user.role
14286
- } : void 0
14287
- }));
14288
- } catch (error) {
14289
- console.error("Error fetching FAQ:", error);
14290
- const user = c.get("user");
14291
- return c.html(renderFAQForm({
14292
- isEdit: true,
14293
- user: user ? {
14294
- name: user.email,
14295
- email: user.email,
14296
- role: user.role
14297
- } : void 0,
14298
- message: "Failed to load FAQ",
14299
- messageType: "error"
14300
- }));
14301
- }
14302
- });
14303
- adminFAQRoutes.put("/:id", async (c) => {
14304
- try {
14305
- const id = parseInt(c.req.param("id"));
14306
- const formData = await c.req.formData();
14307
- const data = Object.fromEntries(formData.entries());
14308
- const validatedData = faqSchema.parse(data);
14309
- const user = c.get("user");
14310
- const db = c.env?.DB;
14311
- if (!db) {
14312
- return c.html(renderFAQForm({
14313
- isEdit: true,
14314
- user: user ? {
14315
- name: user.email,
14316
- email: user.email,
14317
- role: user.role
14318
- } : void 0,
14319
- message: "Database not available",
14320
- messageType: "error"
14321
- }));
14322
- }
14323
- const { results } = await db.prepare(`
14324
- UPDATE faqs
14325
- SET question = ?, answer = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
14326
- WHERE id = ?
14327
- RETURNING *
14328
- `).bind(
14329
- validatedData.question,
14330
- validatedData.answer,
14331
- validatedData.category || null,
14332
- validatedData.tags || null,
14333
- validatedData.isPublished ? 1 : 0,
14334
- validatedData.sortOrder,
14335
- id
14336
- ).all();
14337
- if (results && results.length > 0) {
14338
- return c.redirect("/admin/faq?message=FAQ updated successfully");
14339
- } else {
14340
- return c.html(renderFAQForm({
14341
- faq: {
14342
- id,
14343
- question: validatedData.question,
14344
- answer: validatedData.answer,
14345
- category: validatedData.category,
14346
- tags: validatedData.tags,
14347
- isPublished: validatedData.isPublished,
14348
- sortOrder: validatedData.sortOrder
14349
- },
14350
- isEdit: true,
14351
- user: user ? {
14352
- name: user.email,
14353
- email: user.email,
14354
- role: user.role
14355
- } : void 0,
14356
- message: "FAQ not found",
14357
- messageType: "error"
14358
- }));
14359
- }
14360
- } catch (error) {
14361
- console.error("Error updating FAQ:", error);
14362
- const user = c.get("user");
14363
- const id = parseInt(c.req.param("id"));
14364
- if (error instanceof z.ZodError) {
14365
- const errors = {};
14366
- error.errors.forEach((err) => {
14367
- const field = err.path[0];
14368
- if (!errors[field]) errors[field] = [];
14369
- errors[field].push(err.message);
14370
- });
14371
- return c.html(renderFAQForm({
14372
- faq: {
14373
- id,
14374
- question: "",
14375
- answer: "",
14376
- category: "",
14377
- tags: "",
14378
- isPublished: true,
14379
- sortOrder: 0
14380
- },
14381
- isEdit: true,
14382
- user: user ? {
14383
- name: user.email,
14384
- email: user.email,
14385
- role: user.role
14386
- } : void 0,
14387
- errors,
14388
- message: "Please correct the errors below",
14389
- messageType: "error"
14390
- }));
14391
- }
14392
- return c.html(renderFAQForm({
14393
- faq: {
14394
- id,
14395
- question: "",
14396
- answer: "",
14397
- category: "",
14398
- tags: "",
14399
- isPublished: true,
14400
- sortOrder: 0
14401
- },
14402
- isEdit: true,
14403
- user: user ? {
14404
- name: user.email,
14405
- email: user.email,
14406
- role: user.role
14407
- } : void 0,
14408
- message: "Failed to update FAQ",
14409
- messageType: "error"
14410
- }));
14411
- }
14412
- });
14413
- adminFAQRoutes.delete("/:id", async (c) => {
14983
+ adminLogsRoutes.post("/cleanup", async (c) => {
14414
14984
  try {
14415
- const id = parseInt(c.req.param("id"));
14416
- const db = c.env?.DB;
14417
- if (!db) {
14418
- return c.json({ error: "Database not available" }, 500);
14419
- }
14420
- const { changes } = await db.prepare("DELETE FROM faqs WHERE id = ?").bind(id).run();
14421
- if (changes === 0) {
14422
- return c.json({ error: "FAQ not found" }, 404);
14985
+ const user = c.get("user");
14986
+ if (!user || user.role !== "admin") {
14987
+ return c.json({
14988
+ success: false,
14989
+ error: "Unauthorized. Admin access required."
14990
+ }, 403);
14423
14991
  }
14424
- return c.redirect("/admin/faq?message=FAQ deleted successfully");
14992
+ const logger = getLogger(c.env.DB);
14993
+ await logger.cleanupByRetention();
14994
+ return c.html(html`
14995
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
14996
+ Log cleanup completed successfully!
14997
+ </div>
14998
+ `);
14999
+ } catch (error) {
15000
+ console.error("Error cleaning up logs:", error);
15001
+ return c.html(html`
15002
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
15003
+ Failed to clean up logs. Please try again.
15004
+ </div>
15005
+ `);
15006
+ }
15007
+ });
15008
+ adminLogsRoutes.post("/search", async (c) => {
15009
+ try {
15010
+ const formData = await c.req.formData();
15011
+ const search = formData.get("search");
15012
+ const level = formData.get("level");
15013
+ const category = formData.get("category");
15014
+ const logger = getLogger(c.env.DB);
15015
+ const filter = {
15016
+ limit: 20,
15017
+ offset: 0,
15018
+ sortBy: "created_at",
15019
+ sortOrder: "desc"
15020
+ };
15021
+ if (search) filter.search = search;
15022
+ if (level) filter.level = [level];
15023
+ if (category) filter.category = [category];
15024
+ const { logs } = await logger.getLogs(filter);
15025
+ const rows = logs.map((log) => {
15026
+ const formattedLog = {
15027
+ ...log,
15028
+ formattedDate: new Date(log.createdAt).toLocaleString(),
15029
+ levelClass: getLevelClass(log.level),
15030
+ categoryClass: getCategoryClass(log.category)
15031
+ };
15032
+ return `
15033
+ <tr class="hover:bg-gray-50">
15034
+ <td class="px-6 py-4 whitespace-nowrap">
15035
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
15036
+ ${formattedLog.level}
15037
+ </span>
15038
+ </td>
15039
+ <td class="px-6 py-4 whitespace-nowrap">
15040
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
15041
+ ${formattedLog.category}
15042
+ </span>
15043
+ </td>
15044
+ <td class="px-6 py-4">
15045
+ <div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
15046
+ </td>
15047
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
15048
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
15049
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
15050
+ <a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
15051
+ </td>
15052
+ </tr>
15053
+ `;
15054
+ }).join("");
15055
+ return c.html(rows);
14425
15056
  } catch (error) {
14426
- console.error("Error deleting FAQ:", error);
14427
- return c.json({ error: "Failed to delete FAQ" }, 500);
15057
+ console.error("Error searching logs:", error);
15058
+ return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
15059
+ }
15060
+ });
15061
+ function getLevelClass(level) {
15062
+ switch (level) {
15063
+ case "debug":
15064
+ return "bg-gray-100 text-gray-800";
15065
+ case "info":
15066
+ return "bg-blue-100 text-blue-800";
15067
+ case "warn":
15068
+ return "bg-yellow-100 text-yellow-800";
15069
+ case "error":
15070
+ return "bg-red-100 text-red-800";
15071
+ case "fatal":
15072
+ return "bg-purple-100 text-purple-800";
15073
+ default:
15074
+ return "bg-gray-100 text-gray-800";
15075
+ }
15076
+ }
15077
+ function getCategoryClass(category) {
15078
+ switch (category) {
15079
+ case "auth":
15080
+ return "bg-green-100 text-green-800";
15081
+ case "api":
15082
+ return "bg-blue-100 text-blue-800";
15083
+ case "workflow":
15084
+ return "bg-purple-100 text-purple-800";
15085
+ case "plugin":
15086
+ return "bg-indigo-100 text-indigo-800";
15087
+ case "media":
15088
+ return "bg-pink-100 text-pink-800";
15089
+ case "system":
15090
+ return "bg-gray-100 text-gray-800";
15091
+ case "security":
15092
+ return "bg-red-100 text-red-800";
15093
+ case "error":
15094
+ return "bg-red-100 text-red-800";
15095
+ default:
15096
+ return "bg-gray-100 text-gray-800";
14428
15097
  }
15098
+ }
15099
+ var adminDesignRoutes = new Hono();
15100
+ adminDesignRoutes.get("/", (c) => {
15101
+ const user = c.get("user");
15102
+ const pageData = {
15103
+ user: user ? {
15104
+ name: user.email,
15105
+ email: user.email,
15106
+ role: user.role
15107
+ } : void 0
15108
+ };
15109
+ return c.html(renderDesignPage(pageData));
15110
+ });
15111
+ var adminCheckboxRoutes = new Hono();
15112
+ adminCheckboxRoutes.get("/", (c) => {
15113
+ const user = c.get("user");
15114
+ const pageData = {
15115
+ user: user ? {
15116
+ name: user.email,
15117
+ email: user.email,
15118
+ role: user.role
15119
+ } : void 0
15120
+ };
15121
+ return c.html(renderCheckboxPage(pageData));
14429
15122
  });
14430
- var admin_faq_default = adminFAQRoutes;
14431
15123
 
14432
15124
  // src/templates/pages/admin-testimonials-form.template.ts
14433
15125
  function renderTestimonialsForm(data) {
@@ -14485,7 +15177,7 @@ function renderTestimonialsForm(data) {
14485
15177
  ${errors?.authorName ? `
14486
15178
  <div class="mt-1">
14487
15179
  ${errors.authorName.map((error) => `
14488
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15180
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14489
15181
  `).join("")}
14490
15182
  </div>
14491
15183
  ` : ""}
@@ -14507,7 +15199,7 @@ function renderTestimonialsForm(data) {
14507
15199
  ${errors?.authorTitle ? `
14508
15200
  <div class="mt-1">
14509
15201
  ${errors.authorTitle.map((error) => `
14510
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15202
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14511
15203
  `).join("")}
14512
15204
  </div>
14513
15205
  ` : ""}
@@ -14528,7 +15220,7 @@ function renderTestimonialsForm(data) {
14528
15220
  ${errors?.authorCompany ? `
14529
15221
  <div class="mt-1">
14530
15222
  ${errors.authorCompany.map((error) => `
14531
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15223
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14532
15224
  `).join("")}
14533
15225
  </div>
14534
15226
  ` : ""}
@@ -14560,7 +15252,7 @@ function renderTestimonialsForm(data) {
14560
15252
  ${errors?.testimonialText ? `
14561
15253
  <div class="mt-1">
14562
15254
  ${errors.testimonialText.map((error) => `
14563
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15255
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14564
15256
  `).join("")}
14565
15257
  </div>
14566
15258
  ` : ""}
@@ -14584,7 +15276,7 @@ function renderTestimonialsForm(data) {
14584
15276
  ${errors?.rating ? `
14585
15277
  <div class="mt-1">
14586
15278
  ${errors.rating.map((error) => `
14587
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15279
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14588
15280
  `).join("")}
14589
15281
  </div>
14590
15282
  ` : ""}
@@ -14638,7 +15330,7 @@ function renderTestimonialsForm(data) {
14638
15330
  ${errors?.sortOrder ? `
14639
15331
  <div class="mt-1">
14640
15332
  ${errors.sortOrder.map((error) => `
14641
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15333
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14642
15334
  `).join("")}
14643
15335
  </div>
14644
15336
  ` : ""}
@@ -14685,7 +15377,7 @@ function renderTestimonialsForm(data) {
14685
15377
  };
14686
15378
  return renderAdminLayout(layoutData);
14687
15379
  }
14688
- function escapeHtml5(unsafe) {
15380
+ function escapeHtml4(unsafe) {
14689
15381
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14690
15382
  }
14691
15383
 
@@ -15113,7 +15805,7 @@ function renderCodeExamplesForm(data) {
15113
15805
  ${errors?.title ? `
15114
15806
  <div class="mt-1">
15115
15807
  ${errors.title.map((error) => `
15116
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15808
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15117
15809
  `).join("")}
15118
15810
  </div>
15119
15811
  ` : ""}
@@ -15136,7 +15828,7 @@ function renderCodeExamplesForm(data) {
15136
15828
  ${errors?.description ? `
15137
15829
  <div class="mt-1">
15138
15830
  ${errors.description.map((error) => `
15139
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15831
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15140
15832
  `).join("")}
15141
15833
  </div>
15142
15834
  ` : ""}
@@ -15168,7 +15860,7 @@ function renderCodeExamplesForm(data) {
15168
15860
  ${errors?.language ? `
15169
15861
  <div class="mt-1">
15170
15862
  ${errors.language.map((error) => `
15171
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15863
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15172
15864
  `).join("")}
15173
15865
  </div>
15174
15866
  ` : ""}
@@ -15189,7 +15881,7 @@ function renderCodeExamplesForm(data) {
15189
15881
  ${errors?.category ? `
15190
15882
  <div class="mt-1">
15191
15883
  ${errors.category.map((error) => `
15192
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15884
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15193
15885
  `).join("")}
15194
15886
  </div>
15195
15887
  ` : ""}
@@ -15211,7 +15903,7 @@ function renderCodeExamplesForm(data) {
15211
15903
  ${errors?.tags ? `
15212
15904
  <div class="mt-1">
15213
15905
  ${errors.tags.map((error) => `
15214
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15906
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15215
15907
  `).join("")}
15216
15908
  </div>
15217
15909
  ` : ""}
@@ -15242,7 +15934,7 @@ function renderCodeExamplesForm(data) {
15242
15934
  ${errors?.code ? `
15243
15935
  <div class="mt-1">
15244
15936
  ${errors.code.map((error) => `
15245
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15937
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15246
15938
  `).join("")}
15247
15939
  </div>
15248
15940
  ` : ""}
@@ -15296,7 +15988,7 @@ function renderCodeExamplesForm(data) {
15296
15988
  ${errors?.sortOrder ? `
15297
15989
  <div class="mt-1">
15298
15990
  ${errors.sortOrder.map((error) => `
15299
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15991
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15300
15992
  `).join("")}
15301
15993
  </div>
15302
15994
  ` : ""}
@@ -15354,7 +16046,7 @@ function renderCodeExamplesForm(data) {
15354
16046
  };
15355
16047
  return renderAdminLayout(layoutData);
15356
16048
  }
15357
- function escapeHtml6(unsafe) {
16049
+ function escapeHtml5(unsafe) {
15358
16050
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
15359
16051
  }
15360
16052
 
@@ -16688,8 +17380,8 @@ function renderTable2(data) {
16688
17380
  </td>
16689
17381
  ` : ""}
16690
17382
  ${data.columns.map((column, colIndex) => {
16691
- const value2 = row[column.key];
16692
- const displayValue = column.render ? column.render(value2, row) : value2;
17383
+ const value = row[column.key];
17384
+ const displayValue = column.render ? column.render(value, row) : value;
16693
17385
  const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
16694
17386
  const isFirst = colIndex === 0 && !data.selectable;
16695
17387
  const isLast = colIndex === data.columns.length - 1;
@@ -17076,10 +17768,41 @@ function renderCollectionsListPage(data) {
17076
17768
 
17077
17769
  // src/templates/pages/admin-collections-form.template.ts
17078
17770
  init_admin_layout_catalyst_template();
17771
+ function getFieldTypeBadge(fieldType) {
17772
+ const typeLabels = {
17773
+ "text": "Text",
17774
+ "richtext": "Rich Text (TinyMCE)",
17775
+ "quill": "Rich Text (Quill)",
17776
+ "mdxeditor": "Rich Text (MDXEditor)",
17777
+ "number": "Number",
17778
+ "boolean": "Boolean",
17779
+ "date": "Date",
17780
+ "select": "Select",
17781
+ "media": "Media"
17782
+ };
17783
+ const typeColors = {
17784
+ "text": "bg-blue-500/10 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300 ring-blue-500/20 dark:ring-blue-400/20",
17785
+ "richtext": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
17786
+ "quill": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
17787
+ "mdxeditor": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
17788
+ "number": "bg-green-500/10 dark:bg-green-400/10 text-green-700 dark:text-green-300 ring-green-500/20 dark:ring-green-400/20",
17789
+ "boolean": "bg-amber-500/10 dark:bg-amber-400/10 text-amber-700 dark:text-amber-300 ring-amber-500/20 dark:ring-amber-400/20",
17790
+ "date": "bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-cyan-500/20 dark:ring-cyan-400/20",
17791
+ "select": "bg-indigo-500/10 dark:bg-indigo-400/10 text-indigo-700 dark:text-indigo-300 ring-indigo-500/20 dark:ring-indigo-400/20",
17792
+ "media": "bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-rose-500/20 dark:ring-rose-400/20"
17793
+ };
17794
+ const label = typeLabels[fieldType] || fieldType;
17795
+ const color = typeColors[fieldType] || "bg-zinc-500/10 dark:bg-zinc-400/10 text-zinc-700 dark:text-zinc-300 ring-zinc-500/20 dark:ring-zinc-400/20";
17796
+ return `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${color} ring-1 ring-inset">${label}</span>`;
17797
+ }
17079
17798
  function renderCollectionFormPage(data) {
17080
17799
  const isEdit = data.isEdit || !!data.id;
17081
17800
  const title = isEdit ? "Edit Collection" : "Create New Collection";
17082
17801
  const subtitle = isEdit ? `Update collection: ${data.display_name}` : "Define a new content collection with custom fields and settings.";
17802
+ const fieldsWithData = (data.fields || []).map((field) => ({
17803
+ ...field,
17804
+ dataFieldJSON: JSON.stringify(JSON.stringify(field))
17805
+ }));
17083
17806
  const fields = [
17084
17807
  {
17085
17808
  name: "displayName",
@@ -17316,21 +18039,24 @@ function renderCollectionFormPage(data) {
17316
18039
 
17317
18040
  <!-- Fields List (Read-Only) -->
17318
18041
  <div class="space-y-3">
17319
- ${(data.fields || []).map((field) => `
18042
+ ${fieldsWithData.map((field) => `
17320
18043
  <div class="bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4">
17321
18044
  <div class="flex items-center justify-between">
17322
18045
  <div class="flex items-center gap-x-4">
17323
18046
  <div>
17324
18047
  <div class="flex items-center gap-x-2">
17325
18048
  <span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
17326
- <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-500/20 dark:ring-cyan-400/20">
17327
- ${field.field_type}
17328
- </span>
18049
+ ${getFieldTypeBadge(field.field_type)}
17329
18050
  ${field.is_required ? `
17330
18051
  <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-1 ring-inset ring-rose-500/20 dark:ring-rose-400/20">
17331
18052
  Required
17332
18053
  </span>
17333
18054
  ` : ""}
18055
+ ${field.is_searchable ? `
18056
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-emerald-500/10 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300 ring-1 ring-inset ring-emerald-500/20 dark:ring-emerald-400/20">
18057
+ Searchable
18058
+ </span>
18059
+ ` : ""}
17334
18060
  </div>
17335
18061
  <div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
17336
18062
  <code class="px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">${field.field_name}</code>
@@ -17376,8 +18102,10 @@ function renderCollectionFormPage(data) {
17376
18102
 
17377
18103
  <!-- Fields List -->
17378
18104
  <div id="fields-list" class="space-y-3">
17379
- ${(data.fields || []).map((field) => `
17380
- <div class="field-item bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4" data-field-id="${field.id}">
18105
+ ${fieldsWithData.map((field) => `
18106
+ <div class="field-item bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4"
18107
+ data-field-id="${field.id}"
18108
+ data-field-data="${field.dataFieldJSON}">
17381
18109
  <div class="flex items-center justify-between">
17382
18110
  <div class="flex items-center gap-x-4">
17383
18111
  <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
@@ -17388,14 +18116,17 @@ function renderCollectionFormPage(data) {
17388
18116
  <div>
17389
18117
  <div class="flex items-center gap-x-2">
17390
18118
  <span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
17391
- <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-500/20 dark:ring-cyan-400/20">
17392
- ${field.field_type}
17393
- </span>
18119
+ ${getFieldTypeBadge(field.field_type)}
17394
18120
  ${field.is_required ? `
17395
18121
  <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-pink-500/10 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300 ring-1 ring-inset ring-pink-500/20 dark:ring-pink-400/20">
17396
18122
  Required
17397
18123
  </span>
17398
18124
  ` : ""}
18125
+ ${field.is_searchable ? `
18126
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-emerald-500/10 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300 ring-1 ring-inset ring-emerald-500/20 dark:ring-emerald-400/20">
18127
+ Searchable
18128
+ </span>
18129
+ ` : ""}
17399
18130
  </div>
17400
18131
  <div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
17401
18132
  Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
@@ -17506,7 +18237,7 @@ function renderCollectionFormPage(data) {
17506
18237
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
17507
18238
  <input
17508
18239
  type="text"
17509
- id="field-name"
18240
+ id="modal-field-name"
17510
18241
  name="field_name"
17511
18242
  required
17512
18243
  pattern="[a-z0-9_]+"
@@ -17527,13 +18258,14 @@ function renderCollectionFormPage(data) {
17527
18258
  >
17528
18259
  <option value="">Select field type...</option>
17529
18260
  <option value="text">Text</option>
17530
- <option value="richtext">Rich Text</option>
18261
+ ${data.editorPlugins?.tinymce ? '<option value="richtext">Rich Text (TinyMCE)</option>' : ""}
18262
+ ${data.editorPlugins?.quill ? '<option value="quill">Rich Text (Quill)</option>' : ""}
18263
+ ${data.editorPlugins?.mdxeditor ? '<option value="mdxeditor">Rich Text (MDXEditor)</option>' : ""}
17531
18264
  <option value="number">Number</option>
17532
18265
  <option value="boolean">Boolean</option>
17533
18266
  <option value="date">Date</option>
17534
18267
  <option value="select">Select</option>
17535
18268
  <option value="media">Media</option>
17536
- <option value="guid">GUID (Auto-generated)</option>
17537
18269
  </select>
17538
18270
  <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-blue-600 dark:text-blue-400 sm:size-4">
17539
18271
  <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" />
@@ -17558,6 +18290,7 @@ function renderCollectionFormPage(data) {
17558
18290
  <div class="flex gap-3">
17559
18291
  <div class="flex h-6 shrink-0 items-center">
17560
18292
  <div class="group grid size-4 grid-cols-1">
18293
+ <input type="hidden" name="is_required" value="0">
17561
18294
  <input
17562
18295
  type="checkbox"
17563
18296
  id="field-required"
@@ -17579,6 +18312,7 @@ function renderCollectionFormPage(data) {
17579
18312
  <div class="flex gap-3">
17580
18313
  <div class="flex h-6 shrink-0 items-center">
17581
18314
  <div class="group grid size-4 grid-cols-1">
18315
+ <input type="hidden" name="is_searchable" value="0">
17582
18316
  <input
17583
18317
  type="checkbox"
17584
18318
  id="field-searchable"
@@ -17641,18 +18375,52 @@ function renderCollectionFormPage(data) {
17641
18375
  document.getElementById('submit-text').textContent = 'Add Field';
17642
18376
  document.getElementById('field-form').reset();
17643
18377
  document.getElementById('field-id').value = '';
17644
- document.getElementById('field-name').disabled = false;
18378
+ document.getElementById('modal-field-name').disabled = false;
17645
18379
  currentEditingField = null;
18380
+ isEditingField = false; // Allow change handlers for add mode
17646
18381
  document.getElementById('field-modal').classList.remove('hidden');
17647
18382
  }
17648
18383
 
17649
18384
  function editField(fieldId) {
17650
18385
  const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
17651
- if (!fieldItem) return;
18386
+ if (!fieldItem) {
18387
+ console.error('Field item not found:', fieldId);
18388
+ return;
18389
+ }
18390
+
18391
+ // Get field data from data attribute (primary source) or embedded array (fallback)
18392
+ let field = null;
18393
+
18394
+ // Try to get from data attribute first
18395
+ const fieldDataAttr = fieldItem.getAttribute('data-field-data');
18396
+ if (fieldDataAttr) {
18397
+ try {
18398
+ // Data is double-JSON encoded to properly escape all special characters
18399
+ field = JSON.parse(JSON.parse(fieldDataAttr));
18400
+ console.log('Loaded field data from data attribute:', field);
18401
+ } catch (e) {
18402
+ console.error('Error parsing field data from attribute:', e);
18403
+ // Try single parse as fallback for backwards compatibility
18404
+ try {
18405
+ field = JSON.parse(fieldDataAttr);
18406
+ console.log('Loaded field data from data attribute (single parse):', field);
18407
+ } catch (e2) {
18408
+ console.error('Error parsing field data (fallback):', e2);
18409
+ }
18410
+ }
18411
+ }
18412
+
18413
+ // Fallback to embedded array
18414
+ if (!field) {
18415
+ const fields = ${JSON.stringify(data.fields || [])};
18416
+ field = fields.find(f => f.id === fieldId);
18417
+ console.log('Loaded field data from embedded array:', field);
18418
+ }
17652
18419
 
17653
- // Find the field data from the collection's fields array
17654
- const field = ${JSON.stringify(data.fields || [])}.find(f => f.id === fieldId);
17655
- if (!field) return;
18420
+ if (!field) {
18421
+ console.error('Field data not found for id:', fieldId);
18422
+ return;
18423
+ }
17656
18424
 
17657
18425
  // Set up the modal for editing
17658
18426
  document.getElementById('modal-title').textContent = 'Edit Field';
@@ -17660,18 +18428,102 @@ function renderCollectionFormPage(data) {
17660
18428
  document.getElementById('field-id').value = fieldId;
17661
18429
  currentEditingField = fieldId;
17662
18430
 
17663
- // Populate form with existing field data
17664
- document.getElementById('field-name').value = field.field_name || '';
17665
- document.getElementById('field-name').disabled = true;
17666
- document.getElementById('field-label').value = field.field_label || '';
17667
- document.getElementById('field-type').value = field.field_type || '';
17668
- document.getElementById('field-required').checked = Boolean(field.is_required);
17669
- document.getElementById('field-searchable').checked = Boolean(field.is_searchable);
17670
-
18431
+ // Show modal FIRST before populating fields
18432
+ document.getElementById('field-modal').classList.remove('hidden');
18433
+
18434
+ // Set flag to prevent change event handlers from interfering
18435
+ isEditingField = true;
18436
+
18437
+ // Use setTimeout to ensure modal is rendered before setting values
18438
+ setTimeout(() => {
18439
+ // Populate form with existing field data
18440
+ console.log('Field object for editing:', field);
18441
+ console.log('field.field_name:', field.field_name);
18442
+ console.log('field.field_type:', field.field_type);
18443
+ console.log('field.field_label:', field.field_label);
18444
+
18445
+ const fieldNameInput = document.getElementById('modal-field-name');
18446
+ const fieldTypeSelect = document.getElementById('field-type');
18447
+ const fieldLabelInput = document.getElementById('field-label');
18448
+
18449
+ console.log('Field name input element:', fieldNameInput);
18450
+ console.log('Field type select element:', fieldTypeSelect);
18451
+ console.log('Field label input element:', fieldLabelInput);
18452
+
18453
+ if (fieldNameInput) {
18454
+ console.log('Before setting - field-name value:', fieldNameInput.value);
18455
+ console.log('Before setting - field-name disabled:', fieldNameInput.disabled);
18456
+
18457
+ fieldNameInput.disabled = false; // Enable first to ensure value can be set
18458
+ fieldNameInput.value = field.field_name || '';
18459
+ fieldNameInput.disabled = true; // Then disable
18460
+
18461
+ console.log('After setting - field-name value:', fieldNameInput.value);
18462
+ console.log('After setting - field-name disabled:', fieldNameInput.disabled);
18463
+
18464
+ // Verify the value stuck
18465
+ setTimeout(() => {
18466
+ console.log('After 100ms - field-name value:', fieldNameInput.value);
18467
+ }, 100);
18468
+ } else {
18469
+ console.error('field-name input not found!');
18470
+ }
18471
+
18472
+ if (fieldLabelInput) {
18473
+ fieldLabelInput.value = field.field_label || '';
18474
+ console.log('Set field-label to:', fieldLabelInput.value);
18475
+ } else {
18476
+ console.error('field-label input not found!');
18477
+ }
18478
+
18479
+ if (fieldTypeSelect) {
18480
+ // Map schema types to UI field types
18481
+ let uiFieldType = field.field_type || '';
18482
+
18483
+ // Check if it's a schema field with field_options that might indicate the actual type
18484
+ if (field.field_options && typeof field.field_options === 'object') {
18485
+ // Check for richtext format
18486
+ if (field.field_options.format === 'richtext') {
18487
+ uiFieldType = 'richtext';
18488
+ }
18489
+ // Check for other format indicators
18490
+ else if (field.field_options.type) {
18491
+ uiFieldType = field.field_options.type;
18492
+ }
18493
+ }
18494
+
18495
+ // Map common schema types to UI types
18496
+ const typeMapping = {
18497
+ 'string': 'text',
18498
+ 'integer': 'number',
18499
+ 'bool': 'boolean'
18500
+ };
18501
+
18502
+ if (typeMapping[uiFieldType]) {
18503
+ uiFieldType = typeMapping[uiFieldType];
18504
+ }
18505
+
18506
+ fieldTypeSelect.value = uiFieldType;
18507
+ console.log('Set field-type to:', fieldTypeSelect.value, '(original:', field.field_type, ')');
18508
+ } else {
18509
+ console.error('field-type select not found!');
18510
+ }
18511
+
18512
+ const requiredCheckbox = document.getElementById('field-required');
18513
+ const searchableCheckbox = document.getElementById('field-searchable');
18514
+
18515
+ if (requiredCheckbox) {
18516
+ requiredCheckbox.checked = Boolean(field.is_required);
18517
+ }
18518
+
18519
+ if (searchableCheckbox) {
18520
+ searchableCheckbox.checked = Boolean(field.is_searchable);
18521
+ }
18522
+
17671
18523
  // Handle field options - serialize object back to JSON string
17672
18524
  if (field.field_options) {
17673
- document.getElementById('field-options').value = typeof field.field_options === 'string'
17674
- ? field.field_options
18525
+ document.getElementById('field-options').value = typeof field.field_options === 'string'
18526
+ ? field.field_options
17675
18527
  : JSON.stringify(field.field_options, null, 2);
17676
18528
  } else {
17677
18529
  document.getElementById('field-options').value = '';
@@ -17682,7 +18534,7 @@ function renderCollectionFormPage(data) {
17682
18534
  const optionsContainer = document.getElementById('field-options-container');
17683
18535
  const helpText = document.getElementById('field-type-help');
17684
18536
 
17685
- if (['select', 'media', 'richtext', 'guid'].includes(fieldType)) {
18537
+ if (['select', 'media', 'richtext'].includes(fieldType)) {
17686
18538
  optionsContainer.classList.remove('hidden');
17687
18539
 
17688
18540
  // Set help text based on type
@@ -17696,9 +18548,6 @@ function renderCollectionFormPage(data) {
17696
18548
  case 'richtext':
17697
18549
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17698
18550
  break;
17699
- case 'guid':
17700
- helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17701
- break;
17702
18551
  }
17703
18552
  } else {
17704
18553
  optionsContainer.classList.add('hidden');
@@ -17722,12 +18571,19 @@ function renderCollectionFormPage(data) {
17722
18571
  }
17723
18572
  }
17724
18573
 
17725
- document.getElementById('field-modal').classList.remove('hidden');
18574
+ // Clear the flag after a short delay to allow all events to settle
18575
+ setTimeout(() => {
18576
+ isEditingField = false;
18577
+ console.log('Cleared isEditingField flag');
18578
+ }, 100);
18579
+
18580
+ }, 10); // Small delay to ensure modal is fully rendered
17726
18581
  }
17727
18582
 
17728
18583
  function closeFieldModal() {
17729
18584
  document.getElementById('field-modal').classList.add('hidden');
17730
18585
  currentEditingField = null;
18586
+ isEditingField = false; // Clear the flag when closing
17731
18587
  }
17732
18588
 
17733
18589
  let fieldToDelete = null;
@@ -17801,12 +18657,21 @@ function renderCollectionFormPage(data) {
17801
18657
  });
17802
18658
  });
17803
18659
 
18660
+ // Flag to prevent change handler during programmatic edits
18661
+ let isEditingField = false;
18662
+
17804
18663
  // Field type change handler
17805
18664
  document.getElementById('field-type').addEventListener('change', function() {
18665
+ // Skip if we're programmatically setting values during edit
18666
+ if (isEditingField) {
18667
+ console.log('Skipping change handler - field is being edited');
18668
+ return;
18669
+ }
18670
+
17806
18671
  const optionsContainer = document.getElementById('field-options-container');
17807
18672
  const fieldOptions = document.getElementById('field-options');
17808
18673
  const helpText = document.getElementById('field-type-help');
17809
- const fieldNameInput = document.getElementById('field-name');
18674
+ const fieldNameInput = document.getElementById('modal-field-name');
17810
18675
 
17811
18676
  // Show/hide options based on field type
17812
18677
  if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
@@ -17826,14 +18691,6 @@ function renderCollectionFormPage(data) {
17826
18691
  fieldOptions.value = '{"toolbar": "full", "height": 400}';
17827
18692
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17828
18693
  break;
17829
- case 'guid':
17830
- fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
17831
- helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17832
- // Suggest 'id' as field name for GUID fields
17833
- if (!fieldNameInput.value || fieldNameInput.value === '') {
17834
- fieldNameInput.value = 'id';
17835
- }
17836
- break;
17837
18694
  }
17838
18695
  } else {
17839
18696
  optionsContainer.classList.add('hidden');
@@ -17970,8 +18827,19 @@ adminCollectionsRoutes.get("/", async (c) => {
17970
18827
  return c.html(html`<p>Error loading collections</p>`);
17971
18828
  }
17972
18829
  });
17973
- adminCollectionsRoutes.get("/new", (c) => {
18830
+ adminCollectionsRoutes.get("/new", async (c) => {
17974
18831
  const user = c.get("user");
18832
+ const db = c.env.DB;
18833
+ const [tinymceActive, quillActive, mdxeditorActive] = await Promise.all([
18834
+ isPluginActive2(db, "tinymce-plugin"),
18835
+ isPluginActive2(db, "quill-editor"),
18836
+ isPluginActive2(db, "mdxeditor-plugin")
18837
+ ]);
18838
+ console.log("[Collections /new] Editor plugins status:", {
18839
+ tinymce: tinymceActive,
18840
+ quill: quillActive,
18841
+ mdxeditor: mdxeditorActive
18842
+ });
17975
18843
  const formData = {
17976
18844
  isEdit: false,
17977
18845
  user: user ? {
@@ -17979,7 +18847,12 @@ adminCollectionsRoutes.get("/new", (c) => {
17979
18847
  email: user.email,
17980
18848
  role: user.role
17981
18849
  } : void 0,
17982
- version: c.get("appVersion")
18850
+ version: c.get("appVersion"),
18851
+ editorPlugins: {
18852
+ tinymce: tinymceActive,
18853
+ quill: quillActive,
18854
+ mdxeditor: mdxeditorActive
18855
+ }
17983
18856
  };
17984
18857
  return c.html(renderCollectionFormPage(formData));
17985
18858
  });
@@ -18079,16 +18952,16 @@ adminCollectionsRoutes.post("/", async (c) => {
18079
18952
  if (isHtmx) {
18080
18953
  return c.html(html`
18081
18954
  <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
18082
- Collection created successfully! Redirecting...
18955
+ Collection created successfully! Redirecting to edit mode...
18083
18956
  <script>
18084
18957
  setTimeout(() => {
18085
- window.location.href = '/admin/collections';
18958
+ window.location.href = '/admin/collections/${collectionId}';
18086
18959
  }, 1500);
18087
18960
  </script>
18088
18961
  </div>
18089
18962
  `);
18090
18963
  } else {
18091
- return c.redirect("/admin/collections");
18964
+ return c.redirect(`/admin/collections/${collectionId}`);
18092
18965
  }
18093
18966
  } catch (error) {
18094
18967
  console.error("Error creating collection:", error);
@@ -18138,7 +19011,7 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18138
19011
  field_options: fieldConfig,
18139
19012
  field_order: fieldOrder++,
18140
19013
  is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
18141
- is_searchable: false
19014
+ is_searchable: fieldConfig.searchable === true || false
18142
19015
  }));
18143
19016
  }
18144
19017
  } catch (e) {
@@ -18163,6 +19036,11 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18163
19036
  is_searchable: row.is_searchable === 1
18164
19037
  }));
18165
19038
  }
19039
+ const [tinymceActive, quillActive, mdxeditorActive] = await Promise.all([
19040
+ isPluginActive2(db, "tinymce-plugin"),
19041
+ isPluginActive2(db, "quill-editor"),
19042
+ isPluginActive2(db, "mdxeditor-plugin")
19043
+ ]);
18166
19044
  const formData = {
18167
19045
  id: collection.id,
18168
19046
  name: collection.name,
@@ -18176,7 +19054,12 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18176
19054
  email: user.email,
18177
19055
  role: user.role
18178
19056
  } : void 0,
18179
- version: c.get("appVersion")
19057
+ version: c.get("appVersion"),
19058
+ editorPlugins: {
19059
+ tinymce: tinymceActive,
19060
+ quill: quillActive,
19061
+ mdxeditor: mdxeditorActive
19062
+ }
18180
19063
  };
18181
19064
  return c.html(renderCollectionFormPage(formData));
18182
19065
  } catch (error) {
@@ -18316,21 +19199,103 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
18316
19199
  adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
18317
19200
  try {
18318
19201
  const fieldId = c.req.param("fieldId");
19202
+ const collectionId = c.req.param("collectionId");
18319
19203
  const formData = await c.req.formData();
18320
19204
  const fieldLabel = formData.get("field_label");
18321
- const isRequired = formData.get("is_required") === "1";
18322
- const isSearchable = formData.get("is_searchable") === "1";
19205
+ const fieldType = formData.get("field_type");
19206
+ const isRequiredValues = formData.getAll("is_required");
19207
+ const isSearchableValues = formData.getAll("is_searchable");
19208
+ const isRequired = isRequiredValues[isRequiredValues.length - 1] === "1";
19209
+ const isSearchable = isSearchableValues[isSearchableValues.length - 1] === "1";
18323
19210
  const fieldOptions = formData.get("field_options") || "{}";
19211
+ console.log("[Field Update] Field ID:", fieldId);
19212
+ console.log("[Field Update] Form data received:", {
19213
+ field_label: fieldLabel,
19214
+ field_type: fieldType,
19215
+ is_required: formData.get("is_required"),
19216
+ is_searchable: formData.get("is_searchable"),
19217
+ field_options: fieldOptions
19218
+ });
18324
19219
  if (!fieldLabel) {
18325
19220
  return c.json({ success: false, error: "Field label is required." });
18326
19221
  }
18327
19222
  const db = c.env.DB;
19223
+ if (fieldId.startsWith("schema-")) {
19224
+ const fieldName = fieldId.replace("schema-", "");
19225
+ console.log("[Field Update] Updating schema field:", fieldName);
19226
+ const getCollectionStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
19227
+ const collection = await getCollectionStmt.bind(collectionId).first();
19228
+ if (!collection) {
19229
+ return c.json({ success: false, error: "Collection not found." });
19230
+ }
19231
+ let schema = typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema;
19232
+ if (!schema) {
19233
+ schema = { type: "object", properties: {}, required: [] };
19234
+ }
19235
+ if (!schema.properties) {
19236
+ schema.properties = {};
19237
+ }
19238
+ if (!schema.required) {
19239
+ schema.required = [];
19240
+ }
19241
+ if (schema.properties[fieldName]) {
19242
+ const updatedFieldConfig = {
19243
+ ...schema.properties[fieldName],
19244
+ type: fieldType,
19245
+ title: fieldLabel,
19246
+ searchable: isSearchable
19247
+ };
19248
+ if (isRequired) {
19249
+ updatedFieldConfig.required = true;
19250
+ } else {
19251
+ delete updatedFieldConfig.required;
19252
+ }
19253
+ schema.properties[fieldName] = updatedFieldConfig;
19254
+ const requiredIndex = schema.required.indexOf(fieldName);
19255
+ console.log("[Field Update] Required field handling:", {
19256
+ fieldName,
19257
+ isRequired,
19258
+ currentRequiredArray: schema.required,
19259
+ requiredIndex
19260
+ });
19261
+ if (isRequired && requiredIndex === -1) {
19262
+ schema.required.push(fieldName);
19263
+ console.log("[Field Update] Added field to required array");
19264
+ } else if (!isRequired && requiredIndex !== -1) {
19265
+ schema.required.splice(requiredIndex, 1);
19266
+ console.log("[Field Update] Removed field from required array");
19267
+ }
19268
+ console.log("[Field Update] Final required array:", schema.required);
19269
+ console.log("[Field Update] Final field config:", schema.properties[fieldName]);
19270
+ }
19271
+ const updateCollectionStmt = db.prepare(`
19272
+ UPDATE collections
19273
+ SET schema = ?, updated_at = ?
19274
+ WHERE id = ?
19275
+ `);
19276
+ const result2 = await updateCollectionStmt.bind(JSON.stringify(schema), Date.now(), collectionId).run();
19277
+ console.log("[Field Update] Schema update result:", {
19278
+ success: result2.success,
19279
+ changes: result2.meta?.changes
19280
+ });
19281
+ return c.json({ success: true });
19282
+ }
18328
19283
  const updateStmt = db.prepare(`
18329
19284
  UPDATE content_fields
18330
- SET field_label = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
19285
+ SET field_label = ?, field_type = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
18331
19286
  WHERE id = ?
18332
19287
  `);
18333
- await updateStmt.bind(fieldLabel, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
19288
+ const result = await updateStmt.bind(fieldLabel, fieldType, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
19289
+ console.log("[Field Update] Update result:", {
19290
+ success: result.success,
19291
+ meta: result.meta,
19292
+ changes: result.meta?.changes,
19293
+ last_row_id: result.meta?.last_row_id
19294
+ });
19295
+ const verifyStmt = db.prepare("SELECT * FROM content_fields WHERE id = ?");
19296
+ const verifyResult = await verifyStmt.bind(fieldId).first();
19297
+ console.log("[Field Update] Verification - field after update:", verifyResult);
19298
+ console.log("[Field Update] Successfully updated field with type:", fieldType);
18334
19299
  return c.json({ success: true });
18335
19300
  } catch (error) {
18336
19301
  console.error("Error updating field:", error);
@@ -19591,7 +20556,7 @@ function renderMigrationSettings(settings) {
19591
20556
  btn.innerHTML = 'Running...';
19592
20557
 
19593
20558
  try {
19594
- const response = await fetch('/admin/api/migrations/run', {
20559
+ const response = await fetch('/admin/settings/api/migrations/run', {
19595
20560
  method: 'POST'
19596
20561
  });
19597
20562
  const result = await response.json();
@@ -19612,7 +20577,7 @@ function renderMigrationSettings(settings) {
19612
20577
 
19613
20578
  window.validateSchema = async function() {
19614
20579
  try {
19615
- const response = await fetch('/admin/api/migrations/validate');
20580
+ const response = await fetch('/admin/settings/api/migrations/validate');
19616
20581
  const result = await response.json();
19617
20582
 
19618
20583
  if (result.success) {
@@ -20240,7 +21205,6 @@ var ROUTES_INFO = {
20240
21205
  "adminLogsRoutes",
20241
21206
  "adminDesignRoutes",
20242
21207
  "adminCheckboxRoutes",
20243
- "adminFAQRoutes",
20244
21208
  "adminTestimonialsRoutes",
20245
21209
  "adminCodeExamplesRoutes",
20246
21210
  "adminDashboardRoutes",
@@ -20251,6 +21215,6 @@ var ROUTES_INFO = {
20251
21215
  reference: "https://github.com/sonicjs/sonicjs"
20252
21216
  };
20253
21217
 
20254
- export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
20255
- //# sourceMappingURL=chunk-ABYMIXRN.js.map
20256
- //# sourceMappingURL=chunk-ABYMIXRN.js.map
21218
+ 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, userRoutes };
21219
+ //# sourceMappingURL=chunk-CLLJFZ5U.js.map
21220
+ //# sourceMappingURL=chunk-CLLJFZ5U.js.map