@sonicjs-cms/core 2.0.10 → 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-LW33AOBF.js → chunk-5RKQB2JG.js} +6 -222
  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-Z4H6DBVF.js → chunk-CLLJFZ5U.js} +1965 -1043
  6. package/dist/chunk-CLLJFZ5U.js.map +1 -0
  7. package/dist/{chunk-MXJJN4IA.js → chunk-DU7JJZN7.js} +5 -4
  8. package/dist/chunk-DU7JJZN7.js.map +1 -0
  9. package/dist/{chunk-YHG45LMU.js → chunk-FYWJMETG.js} +20 -4
  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-Q7SL7U43.cjs → chunk-IM2LGCYD.cjs} +2114 -1192
  14. package/dist/chunk-IM2LGCYD.cjs.map +1 -0
  15. package/dist/{chunk-3PHG75W4.cjs → chunk-NNXPAPUD.cjs} +5 -4
  16. package/dist/chunk-NNXPAPUD.cjs.map +1 -0
  17. package/dist/{chunk-FTMKKKNH.js → chunk-QNWYQZ55.js} +3 -3
  18. package/dist/{chunk-FTMKKKNH.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-HXA5QSI3.cjs → chunk-X2VADBA4.cjs} +22 -6
  22. package/dist/chunk-X2VADBA4.cjs.map +1 -0
  23. package/dist/{chunk-MU3MR2QR.cjs → chunk-YU6QFFI4.cjs} +5 -222
  24. package/dist/chunk-YU6QFFI4.cjs.map +1 -0
  25. package/dist/{chunk-CAP6QQR2.cjs → chunk-ZMSYKV62.cjs} +5 -5
  26. package/dist/{chunk-CAP6QQR2.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 +385 -10
  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-3PHG75W4.cjs.map +0 -1
  58. package/dist/chunk-COBUPOMD.js.map +0 -1
  59. package/dist/chunk-HXA5QSI3.cjs.map +0 -1
  60. package/dist/chunk-LW33AOBF.js.map +0 -1
  61. package/dist/chunk-MU3MR2QR.cjs.map +0 -1
  62. package/dist/chunk-MXJJN4IA.js.map +0 -1
  63. package/dist/chunk-NBDPIRQS.cjs.map +0 -1
  64. package/dist/chunk-Q7SL7U43.cjs.map +0 -1
  65. package/dist/chunk-YHG45LMU.js.map +0 -1
  66. package/dist/chunk-Z4H6DBVF.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-YHG45LMU.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-LW33AOBF.js';
5
- import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-MXJJN4IA.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();
@@ -4767,16 +5678,25 @@ async function getCollectionFields(db, collectionId) {
4767
5678
  const schema = typeof collectionRow.schema === "string" ? JSON.parse(collectionRow.schema) : collectionRow.schema;
4768
5679
  if (schema && schema.properties) {
4769
5680
  let fieldOrder = 0;
4770
- return Object.entries(schema.properties).map(([fieldName, fieldConfig]) => ({
4771
- id: `schema-${fieldName}`,
4772
- field_name: fieldName,
4773
- field_type: fieldConfig.type || "string",
4774
- field_label: fieldConfig.title || fieldName,
4775
- field_options: fieldConfig,
4776
- field_order: fieldOrder++,
4777
- is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
4778
- is_searchable: false
4779
- }));
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
+ });
4780
5700
  }
4781
5701
  } catch (e) {
4782
5702
  console.error("Error parsing collection schema:", e);
@@ -5024,11 +5944,44 @@ adminContentRoutes.get("/new", async (c) => {
5024
5944
  }
5025
5945
  const fields = await getCollectionFields(db, collectionId);
5026
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
+ });
5027
5974
  const formData = {
5028
5975
  collection,
5029
5976
  fields,
5030
5977
  isEdit: false,
5031
5978
  workflowEnabled,
5979
+ tinymceEnabled,
5980
+ tinymceSettings,
5981
+ quillEnabled,
5982
+ quillSettings,
5983
+ mdxeditorEnabled,
5984
+ mdxeditorSettings,
5032
5985
  user: user ? {
5033
5986
  name: user.email,
5034
5987
  email: user.email,
@@ -5096,6 +6049,27 @@ adminContentRoutes.get("/:id/edit", async (c) => {
5096
6049
  const fields = await getCollectionFields(db, content.collection_id);
5097
6050
  const contentData = content.data ? JSON.parse(content.data) : {};
5098
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
+ }
5099
6073
  const formData = {
5100
6074
  id: content.id,
5101
6075
  title: content.title,
@@ -5111,6 +6085,12 @@ adminContentRoutes.get("/:id/edit", async (c) => {
5111
6085
  fields,
5112
6086
  isEdit: true,
5113
6087
  workflowEnabled,
6088
+ tinymceEnabled,
6089
+ tinymceSettings,
6090
+ quillEnabled,
6091
+ quillSettings,
6092
+ mdxeditorEnabled,
6093
+ mdxeditorSettings,
5114
6094
  referrerParams,
5115
6095
  user: user ? {
5116
6096
  name: user.email,
@@ -5161,41 +6141,31 @@ adminContentRoutes.post("/", async (c) => {
5161
6141
  const data = {};
5162
6142
  const errors = {};
5163
6143
  for (const field of fields) {
5164
- const value2 = formData.get(field.field_name);
5165
- if (field.field_type === "guid") {
5166
- const options = field.field_options || {};
5167
- if (options.autoGenerate) {
5168
- data[field.field_name] = crypto.randomUUID();
5169
- continue;
5170
- }
5171
- }
5172
- 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() === "")) {
5173
6146
  errors[field.field_name] = [`${field.field_label} is required`];
5174
6147
  continue;
5175
6148
  }
5176
6149
  switch (field.field_type) {
5177
6150
  case "number":
5178
- if (value2 && isNaN(Number(value2))) {
6151
+ if (value && isNaN(Number(value))) {
5179
6152
  errors[field.field_name] = [`${field.field_label} must be a valid number`];
5180
6153
  } else {
5181
- data[field.field_name] = value2 ? Number(value2) : null;
6154
+ data[field.field_name] = value ? Number(value) : null;
5182
6155
  }
5183
6156
  break;
5184
6157
  case "boolean":
5185
- 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;
5186
6159
  break;
5187
6160
  case "select":
5188
6161
  if (field.field_options?.multiple) {
5189
6162
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5190
6163
  } else {
5191
- data[field.field_name] = value2;
6164
+ data[field.field_name] = value;
5192
6165
  }
5193
6166
  break;
5194
- case "guid":
5195
- data[field.field_name] = value2 || null;
5196
- break;
5197
6167
  default:
5198
- data[field.field_name] = value2;
6168
+ data[field.field_name] = value;
5199
6169
  }
5200
6170
  }
5201
6171
  if (Object.keys(errors).length > 0) {
@@ -5322,31 +6292,31 @@ adminContentRoutes.put("/:id", async (c) => {
5322
6292
  const data = {};
5323
6293
  const errors = {};
5324
6294
  for (const field of fields) {
5325
- const value2 = formData.get(field.field_name);
5326
- 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() === "")) {
5327
6297
  errors[field.field_name] = [`${field.field_label} is required`];
5328
6298
  continue;
5329
6299
  }
5330
6300
  switch (field.field_type) {
5331
6301
  case "number":
5332
- if (value2 && isNaN(Number(value2))) {
6302
+ if (value && isNaN(Number(value))) {
5333
6303
  errors[field.field_name] = [`${field.field_label} must be a valid number`];
5334
6304
  } else {
5335
- data[field.field_name] = value2 ? Number(value2) : null;
6305
+ data[field.field_name] = value ? Number(value) : null;
5336
6306
  }
5337
6307
  break;
5338
6308
  case "boolean":
5339
- 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;
5340
6310
  break;
5341
6311
  case "select":
5342
6312
  if (field.field_options?.multiple) {
5343
6313
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5344
6314
  } else {
5345
- data[field.field_name] = value2;
6315
+ data[field.field_name] = value;
5346
6316
  }
5347
6317
  break;
5348
6318
  default:
5349
- data[field.field_name] = value2;
6319
+ data[field.field_name] = value;
5350
6320
  }
5351
6321
  }
5352
6322
  if (Object.keys(errors).length > 0) {
@@ -5463,23 +6433,23 @@ adminContentRoutes.post("/preview", async (c) => {
5463
6433
  const fields = await getCollectionFields(db, collectionId);
5464
6434
  const data = {};
5465
6435
  for (const field of fields) {
5466
- const value2 = formData.get(field.field_name);
6436
+ const value = formData.get(field.field_name);
5467
6437
  switch (field.field_type) {
5468
6438
  case "number":
5469
- data[field.field_name] = value2 ? Number(value2) : null;
6439
+ data[field.field_name] = value ? Number(value) : null;
5470
6440
  break;
5471
6441
  case "boolean":
5472
- data[field.field_name] = value2 === "true";
6442
+ data[field.field_name] = value === "true";
5473
6443
  break;
5474
6444
  case "select":
5475
6445
  if (field.field_options?.multiple) {
5476
6446
  data[field.field_name] = formData.getAll(`${field.field_name}[]`);
5477
6447
  } else {
5478
- data[field.field_name] = value2;
6448
+ data[field.field_name] = value;
5479
6449
  }
5480
6450
  break;
5481
6451
  default:
5482
- data[field.field_name] = value2;
6452
+ data[field.field_name] = value;
5483
6453
  }
5484
6454
  }
5485
6455
  const previewHTML = `
@@ -7335,10 +8305,10 @@ function renderUsersListPage(data) {
7335
8305
  label: "",
7336
8306
  className: "w-12",
7337
8307
  sortable: false,
7338
- render: (value2, row) => {
8308
+ render: (value, row) => {
7339
8309
  const initials = `${row.firstName.charAt(0)}${row.lastName.charAt(0)}`.toUpperCase();
7340
- if (value2) {
7341
- 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">`;
7342
8312
  }
7343
8313
  return `
7344
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">
@@ -7353,7 +8323,7 @@ function renderUsersListPage(data) {
7353
8323
  sortable: true,
7354
8324
  sortType: "string",
7355
8325
  render: (_value, row) => {
7356
- const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
8326
+ const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
7357
8327
  "&": "&amp;",
7358
8328
  "<": "&lt;",
7359
8329
  ">": "&gt;",
@@ -7362,9 +8332,9 @@ function renderUsersListPage(data) {
7362
8332
  })[char] || char);
7363
8333
  const truncatedFirstName = row.firstName.length > 25 ? row.firstName.substring(0, 25) + "..." : row.firstName;
7364
8334
  const truncatedLastName = row.lastName.length > 25 ? row.lastName.substring(0, 25) + "..." : row.lastName;
7365
- const fullName = escapeHtml7(`${truncatedFirstName} ${truncatedLastName}`);
8335
+ const fullName = escapeHtml6(`${truncatedFirstName} ${truncatedLastName}`);
7366
8336
  const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
7367
- const username = escapeHtml7(truncatedUsername);
8337
+ const username = escapeHtml6(truncatedUsername);
7368
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>';
7369
8339
  return `
7370
8340
  <div>
@@ -7379,15 +8349,15 @@ function renderUsersListPage(data) {
7379
8349
  label: "Email",
7380
8350
  sortable: true,
7381
8351
  sortType: "string",
7382
- render: (value2) => {
7383
- const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
8352
+ render: (value) => {
8353
+ const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
7384
8354
  "&": "&amp;",
7385
8355
  "<": "&lt;",
7386
8356
  ">": "&gt;",
7387
8357
  '"': "&quot;",
7388
8358
  "'": "&#39;"
7389
8359
  })[char] || char);
7390
- const escapedEmail = escapeHtml7(value2);
8360
+ const escapedEmail = escapeHtml6(value);
7391
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>`;
7392
8362
  }
7393
8363
  },
@@ -7396,7 +8366,7 @@ function renderUsersListPage(data) {
7396
8366
  label: "Role",
7397
8367
  sortable: true,
7398
8368
  sortType: "string",
7399
- render: (_value) => {
8369
+ render: (value) => {
7400
8370
  const roleColors = {
7401
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",
7402
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",
@@ -7412,7 +8382,7 @@ function renderUsersListPage(data) {
7412
8382
  label: "Last Login",
7413
8383
  sortable: true,
7414
8384
  sortType: "date",
7415
- render: (_value) => {
8385
+ render: (value) => {
7416
8386
  if (!value) return '<span class="text-zinc-500 dark:text-zinc-400">Never</span>';
7417
8387
  return `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`;
7418
8388
  }
@@ -7422,7 +8392,7 @@ function renderUsersListPage(data) {
7422
8392
  label: "Created",
7423
8393
  sortable: true,
7424
8394
  sortType: "date",
7425
- 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>`
7426
8396
  },
7427
8397
  {
7428
8398
  key: "actions",
@@ -7920,7 +8890,7 @@ userRoutes.post("/profile/avatar", async (c) => {
7920
8890
  try {
7921
8891
  const formData = await c.req.formData();
7922
8892
  const avatarFile = formData.get("avatar");
7923
- if (!avatarFile || !(avatarFile instanceof File) || !avatarFile.name) {
8893
+ if (!avatarFile || typeof avatarFile === "string" || !avatarFile.name) {
7924
8894
  return c.html(renderAlert2({
7925
8895
  type: "error",
7926
8896
  message: "Please select an image file.",
@@ -8503,6 +9473,46 @@ userRoutes.put("/users/:id", async (c) => {
8503
9473
  }));
8504
9474
  }
8505
9475
  });
9476
+ userRoutes.post("/users/:id/toggle", async (c) => {
9477
+ const db = c.env.DB;
9478
+ const user = c.get("user");
9479
+ const userId = c.req.param("id");
9480
+ try {
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);
9485
+ }
9486
+ const userStmt = db.prepare(`
9487
+ SELECT id, email FROM users WHERE id = ?
9488
+ `);
9489
+ const userToToggle = await userStmt.bind(userId).first();
9490
+ if (!userToToggle) {
9491
+ return c.json({ error: "User not found" }, 404);
9492
+ }
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
+ });
8506
9516
  userRoutes.delete("/users/:id", async (c) => {
8507
9517
  const db = c.env.DB;
8508
9518
  const user = c.get("user");
@@ -11135,7 +12145,7 @@ function renderPluginsListPage(data) {
11135
12145
  <!-- Stats -->
11136
12146
  <div class="mb-6">
11137
12147
  <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
11138
- <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">
11139
12149
  <div class="px-4 py-5 sm:p-6">
11140
12150
  <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
11141
12151
  <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
@@ -11196,10 +12206,25 @@ function renderPluginsListPage(data) {
11196
12206
  </div>
11197
12207
  </dd>
11198
12208
  </div>
11199
- </dl>
11200
- </div>
11201
-
11202
- <!-- Filters -->
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>
12224
+ </dl>
12225
+ </div>
12226
+
12227
+ <!-- Filters -->
11203
12228
  <div class="relative rounded-xl overflow-hidden mb-6">
11204
12229
  <!-- Gradient Background -->
11205
12230
  <div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10 dark:from-cyan-400/20 dark:via-blue-400/20 dark:to-purple-400/20"></div>
@@ -11211,13 +12236,16 @@ function renderPluginsListPage(data) {
11211
12236
  <div>
11212
12237
  <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
11213
12238
  <div class="mt-2 grid grid-cols-1">
11214
- <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">
11215
12240
  <option value="">All Categories</option>
11216
12241
  <option value="content">Content Management</option>
11217
12242
  <option value="media">Media</option>
11218
12243
  <option value="seo">SEO & Analytics</option>
11219
12244
  <option value="security">Security</option>
11220
12245
  <option value="utilities">Utilities</option>
12246
+ <option value="system">System</option>
12247
+ <option value="development">Development</option>
12248
+ <option value="demo">Demo</option>
11221
12249
  </select>
11222
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">
11223
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" />
@@ -11227,10 +12255,11 @@ function renderPluginsListPage(data) {
11227
12255
  <div>
11228
12256
  <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
11229
12257
  <div class="mt-2 grid grid-cols-1">
11230
- <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">
11231
12259
  <option value="">All Status</option>
11232
12260
  <option value="active">Active</option>
11233
12261
  <option value="inactive">Inactive</option>
12262
+ <option value="uninstalled">Available to Install</option>
11234
12263
  <option value="error">Error</option>
11235
12264
  </select>
11236
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">
@@ -11247,8 +12276,10 @@ function renderPluginsListPage(data) {
11247
12276
  </svg>
11248
12277
  </div>
11249
12278
  <input
12279
+ id="search-input"
11250
12280
  type="text"
11251
12281
  placeholder="Search plugins..."
12282
+ oninput="filterPlugins()"
11252
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"
11253
12284
  />
11254
12285
  </div>
@@ -11271,7 +12302,7 @@ function renderPluginsListPage(data) {
11271
12302
  </div>
11272
12303
 
11273
12304
  <!-- Plugins Grid -->
11274
- <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">
11275
12306
  ${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
11276
12307
  </div>
11277
12308
 
@@ -11398,7 +12429,12 @@ function renderPluginsListPage(data) {
11398
12429
  }
11399
12430
 
11400
12431
  function openPluginSettings(pluginId) {
11401
- 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
+ }
11402
12438
  }
11403
12439
 
11404
12440
  function showPluginDetails(pluginId) {
@@ -11422,12 +12458,78 @@ function renderPluginsListPage(data) {
11422
12458
  const dropdown = document.getElementById('plugin-dropdown');
11423
12459
  dropdown.classList.toggle('hidden');
11424
12460
  }
11425
-
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
+
11426
12528
  // Close dropdown when clicking outside
11427
12529
  document.addEventListener('click', (event) => {
11428
12530
  const dropdown = document.getElementById('plugin-dropdown');
11429
12531
  const button = event.target.closest('button[onclick="toggleDropdown()"]');
11430
-
12532
+
11431
12533
  if (!button && !dropdown.contains(event.target)) {
11432
12534
  dropdown.classList.add('hidden');
11433
12535
  }
@@ -11462,23 +12564,33 @@ function renderPluginCard(plugin) {
11462
12564
  const statusColors = {
11463
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",
11464
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",
11465
- 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"
11466
12569
  };
11467
12570
  const statusIcons = {
11468
12571
  active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
11469
12572
  inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
11470
- 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>'
11471
12575
  };
11472
12576
  const borderColors = {
11473
12577
  active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
11474
12578
  inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
11475
- 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"
11476
12581
  };
11477
12582
  const criticalCorePlugins = ["core-auth", "core-media"];
11478
12583
  const canToggle = !criticalCorePlugins.includes(plugin.id);
11479
- 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
+ }
11480
12592
  return `
11481
- <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}">
11482
12594
  <div class="flex items-start justify-between mb-4">
11483
12595
  <div class="flex items-center gap-3">
11484
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">
@@ -11539,20 +12651,24 @@ function renderPluginCard(plugin) {
11539
12651
 
11540
12652
  <div class="flex items-center justify-between">
11541
12653
  <div class="flex gap-2">
11542
- ${canToggle ? actionButton : ""}
12654
+ ${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
12655
+ ${plugin.status !== "uninstalled" ? `
11543
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">
11544
12657
  Settings
11545
12658
  </button>
12659
+ ` : ""}
11546
12660
  </div>
11547
12661
 
11548
12662
  <div class="flex items-center gap-2">
12663
+ ${plugin.status !== "uninstalled" ? `
11549
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">
11550
12665
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11551
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"/>
11552
12667
  </svg>
11553
12668
  </button>
12669
+ ` : ""}
11554
12670
 
11555
- ${!plugin.isCore ? `
12671
+ ${!plugin.isCore && plugin.status !== "uninstalled" ? `
11556
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">
11557
12673
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11558
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"/>
@@ -11855,28 +12971,22 @@ function renderPluginSettingsPage(data) {
11855
12971
  const { plugin, activity = [], user } = data;
11856
12972
  const pageContent = `
11857
12973
  <div class="w-full px-4 sm:px-6 lg:px-8 py-6">
11858
- <!-- Header with breadcrumb -->
11859
- <div class="flex items-center mb-6">
11860
- <nav class="flex" aria-label="Breadcrumb">
11861
- <ol class="flex items-center space-x-2">
11862
- <li>
11863
- <a href="/admin/plugins" class="text-gray-400 hover:text-white transition-colors">
11864
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11865
- <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"/>
11866
- </svg>
11867
- Plugins
11868
- </a>
11869
- </li>
11870
- <li>
11871
- <svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
11872
- <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"/>
11873
- </svg>
11874
- </li>
11875
- <li>
11876
- <span class="text-gray-300">${plugin.displayName}</span>
11877
- </li>
11878
- </ol>
11879
- </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>
11880
12990
  </div>
11881
12991
 
11882
12992
  <!-- Plugin Header -->
@@ -11887,9 +12997,8 @@ function renderPluginSettingsPage(data) {
11887
12997
  ${plugin.icon || plugin.displayName.charAt(0).toUpperCase()}
11888
12998
  </div>
11889
12999
  <div>
11890
- <h1 class="text-2xl font-semibold text-white mb-1">${plugin.displayName}</h1>
11891
- <p class="text-gray-300 mb-2">${plugin.description}</p>
11892
- <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">
11893
13002
  <span>v${plugin.version}</span>
11894
13003
  <span>by ${plugin.author}</span>
11895
13004
  <span>${plugin.category}</span>
@@ -11898,7 +13007,7 @@ function renderPluginSettingsPage(data) {
11898
13007
  </div>
11899
13008
  </div>
11900
13009
  </div>
11901
-
13010
+
11902
13011
  <div class="flex items-center gap-3">
11903
13012
  ${renderStatusBadge(plugin.status)}
11904
13013
  ${renderToggleButton(plugin)}
@@ -12185,10 +13294,10 @@ function renderSettingsTab(plugin) {
12185
13294
  `;
12186
13295
  }
12187
13296
  function renderSettingsFields(settings) {
12188
- return Object.entries(settings).map(([key, value2]) => {
13297
+ return Object.entries(settings).map(([key, value]) => {
12189
13298
  const fieldId = `setting_${key}`;
12190
13299
  const displayName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
12191
- if (typeof value2 === "boolean") {
13300
+ if (typeof value === "boolean") {
12192
13301
  return `
12193
13302
  <div class="flex items-center justify-between">
12194
13303
  <div>
@@ -12196,12 +13305,12 @@ function renderSettingsFields(settings) {
12196
13305
  <p class="text-xs text-gray-400">Enable or disable this feature</p>
12197
13306
  </div>
12198
13307
  <label class="relative inline-flex items-center cursor-pointer">
12199
- <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">
12200
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>
12201
13310
  </label>
12202
13311
  </div>
12203
13312
  `;
12204
- } else if (typeof value2 === "number") {
13313
+ } else if (typeof value === "number") {
12205
13314
  return `
12206
13315
  <div>
12207
13316
  <label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
@@ -12209,7 +13318,7 @@ function renderSettingsFields(settings) {
12209
13318
  type="number"
12210
13319
  name="${fieldId}"
12211
13320
  id="${fieldId}"
12212
- value="${value2}"
13321
+ value="${value}"
12213
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"
12214
13323
  >
12215
13324
  </div>
@@ -12222,7 +13331,7 @@ function renderSettingsFields(settings) {
12222
13331
  type="text"
12223
13332
  name="${fieldId}"
12224
13333
  id="${fieldId}"
12225
- value="${value2}"
13334
+ value="${value}"
12226
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"
12227
13336
  >
12228
13337
  </div>
@@ -12370,6 +13479,86 @@ function formatTimestamp(timestamp) {
12370
13479
  // src/routes/admin-plugins.ts
12371
13480
  var adminPluginRoutes = new Hono();
12372
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
+ ];
12373
13562
  adminPluginRoutes.get("/", async (c) => {
12374
13563
  try {
12375
13564
  const user = c.get("user");
@@ -12378,15 +13567,17 @@ adminPluginRoutes.get("/", async (c) => {
12378
13567
  return c.text("Access denied", 403);
12379
13568
  }
12380
13569
  const pluginService = new PluginService(db);
12381
- let plugins = [];
12382
- 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 };
12383
13572
  try {
12384
- plugins = await pluginService.getAllPlugins();
13573
+ installedPlugins = await pluginService.getAllPlugins();
12385
13574
  stats = await pluginService.getPluginStats();
12386
13575
  } catch (error) {
12387
13576
  console.error("Error loading plugins:", error);
12388
13577
  }
12389
- 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) => ({
12390
13581
  id: p.id,
12391
13582
  name: p.name,
12392
13583
  displayName: p.display_name,
@@ -12403,8 +13594,28 @@ adminPluginRoutes.get("/", async (c) => {
12403
13594
  permissions: p.permissions,
12404
13595
  isCore: p.is_core
12405
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;
12406
13617
  const pageData = {
12407
- plugins: templatePlugins,
13618
+ plugins: allPlugins,
12408
13619
  stats,
12409
13620
  user: {
12410
13621
  name: user?.email || "User",
@@ -12541,7 +13752,7 @@ adminPluginRoutes.post("/install", async (c) => {
12541
13752
  id: "demo-login-prefill",
12542
13753
  name: "demo-login-plugin",
12543
13754
  display_name: "Demo Login Prefill",
12544
- 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",
12545
13756
  version: "1.0.0-beta.1",
12546
13757
  author: "SonicJS",
12547
13758
  category: "demo",
@@ -12551,7 +13762,7 @@ adminPluginRoutes.post("/install", async (c) => {
12551
13762
  settings: {
12552
13763
  enableNotice: true,
12553
13764
  demoEmail: "admin@sonicjs.com",
12554
- demoPassword: "admin123"
13765
+ demoPassword: "sonicjs!"
12555
13766
  }
12556
13767
  });
12557
13768
  return c.json({ success: true, plugin: demoPlugin });
@@ -12650,6 +13861,50 @@ adminPluginRoutes.post("/install", async (c) => {
12650
13861
  });
12651
13862
  return c.json({ success: true, plugin: seedDataPlugin });
12652
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
+ }
12653
13908
  return c.json({ error: "Plugin not found in registry" }, 404);
12654
13909
  } catch (error) {
12655
13910
  console.error("Error installing plugin:", error);
@@ -13725,751 +14980,146 @@ adminLogsRoutes.get("/export", async (c) => {
13725
14980
  return c.json({ error: "Failed to export logs" }, 500);
13726
14981
  }
13727
14982
  });
13728
- adminLogsRoutes.post("/cleanup", async (c) => {
13729
- try {
13730
- const user = c.get("user");
13731
- if (!user || user.role !== "admin") {
13732
- return c.json({
13733
- success: false,
13734
- error: "Unauthorized. Admin access required."
13735
- }, 403);
13736
- }
13737
- const logger = getLogger(c.env.DB);
13738
- await logger.cleanupByRetention();
13739
- return c.html(html`
13740
- <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
13741
- Log cleanup completed successfully!
13742
- </div>
13743
- `);
13744
- } catch (error) {
13745
- console.error("Error cleaning up logs:", error);
13746
- return c.html(html`
13747
- <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
13748
- Failed to clean up logs. Please try again.
13749
- </div>
13750
- `);
13751
- }
13752
- });
13753
- adminLogsRoutes.post("/search", async (c) => {
13754
- try {
13755
- const formData = await c.req.formData();
13756
- const search = formData.get("search");
13757
- const level = formData.get("level");
13758
- const category = formData.get("category");
13759
- const logger = getLogger(c.env.DB);
13760
- const filter = {
13761
- limit: 20,
13762
- offset: 0,
13763
- sortBy: "created_at",
13764
- sortOrder: "desc"
13765
- };
13766
- if (search) filter.search = search;
13767
- if (level) filter.level = [level];
13768
- if (category) filter.category = [category];
13769
- const { logs } = await logger.getLogs(filter);
13770
- const rows = logs.map((log) => {
13771
- const formattedLog = {
13772
- ...log,
13773
- formattedDate: new Date(log.createdAt).toLocaleString(),
13774
- levelClass: getLevelClass(log.level),
13775
- categoryClass: getCategoryClass(log.category)
13776
- };
13777
- return `
13778
- <tr class="hover:bg-gray-50">
13779
- <td class="px-6 py-4 whitespace-nowrap">
13780
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
13781
- ${formattedLog.level}
13782
- </span>
13783
- </td>
13784
- <td class="px-6 py-4 whitespace-nowrap">
13785
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
13786
- ${formattedLog.category}
13787
- </span>
13788
- </td>
13789
- <td class="px-6 py-4">
13790
- <div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
13791
- </td>
13792
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
13793
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
13794
- <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
13795
- <a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
13796
- </td>
13797
- </tr>
13798
- `;
13799
- }).join("");
13800
- return c.html(rows);
13801
- } catch (error) {
13802
- console.error("Error searching logs:", error);
13803
- return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
13804
- }
13805
- });
13806
- function getLevelClass(level) {
13807
- switch (level) {
13808
- case "debug":
13809
- return "bg-gray-100 text-gray-800";
13810
- case "info":
13811
- return "bg-blue-100 text-blue-800";
13812
- case "warn":
13813
- return "bg-yellow-100 text-yellow-800";
13814
- case "error":
13815
- return "bg-red-100 text-red-800";
13816
- case "fatal":
13817
- return "bg-purple-100 text-purple-800";
13818
- default:
13819
- return "bg-gray-100 text-gray-800";
13820
- }
13821
- }
13822
- function getCategoryClass(category) {
13823
- switch (category) {
13824
- case "auth":
13825
- return "bg-green-100 text-green-800";
13826
- case "api":
13827
- return "bg-blue-100 text-blue-800";
13828
- case "workflow":
13829
- return "bg-purple-100 text-purple-800";
13830
- case "plugin":
13831
- return "bg-indigo-100 text-indigo-800";
13832
- case "media":
13833
- return "bg-pink-100 text-pink-800";
13834
- case "system":
13835
- return "bg-gray-100 text-gray-800";
13836
- case "security":
13837
- return "bg-red-100 text-red-800";
13838
- case "error":
13839
- return "bg-red-100 text-red-800";
13840
- default:
13841
- return "bg-gray-100 text-gray-800";
13842
- }
13843
- }
13844
- var adminDesignRoutes = new Hono();
13845
- adminDesignRoutes.get("/", (c) => {
13846
- const user = c.get("user");
13847
- const pageData = {
13848
- user: user ? {
13849
- name: user.email,
13850
- email: user.email,
13851
- role: user.role
13852
- } : void 0
13853
- };
13854
- return c.html(renderDesignPage(pageData));
13855
- });
13856
- var adminCheckboxRoutes = new Hono();
13857
- adminCheckboxRoutes.get("/", (c) => {
13858
- const user = c.get("user");
13859
- const pageData = {
13860
- user: user ? {
13861
- name: user.email,
13862
- email: user.email,
13863
- role: user.role
13864
- } : void 0
13865
- };
13866
- return c.html(renderCheckboxPage(pageData));
13867
- });
13868
-
13869
- // src/templates/pages/admin-faq-form.template.ts
13870
- function renderFAQForm(data) {
13871
- const { faq, isEdit, errors, message, messageType } = data;
13872
- const pageTitle = isEdit ? "Edit FAQ" : "New FAQ";
13873
- const pageContent = `
13874
- <div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
13875
- <!-- Header -->
13876
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
13877
- <div>
13878
- <h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
13879
- <p class="mt-2 text-sm text-gray-300">
13880
- ${isEdit ? "Update the FAQ details below" : "Create a new frequently asked question"}
13881
- </p>
13882
- </div>
13883
- <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
13884
- <a href="/admin/faq"
13885
- 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">
13886
- <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13887
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
13888
- </svg>
13889
- Back to List
13890
- </a>
13891
- </div>
13892
- </div>
13893
-
13894
- ${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
13895
-
13896
- <!-- Form -->
13897
- <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
13898
- <form ${isEdit ? `hx-put="/admin/faq/${faq?.id}"` : 'hx-post="/admin/faq"'}
13899
- hx-target="body"
13900
- hx-swap="outerHTML"
13901
- class="space-y-6 p-6">
13902
-
13903
- <!-- Question -->
13904
- <div>
13905
- <label for="question" class="block text-sm font-medium text-white">
13906
- Question <span class="text-red-400">*</span>
13907
- </label>
13908
- <div class="mt-1">
13909
- <textarea name="question"
13910
- id="question"
13911
- rows="3"
13912
- required
13913
- maxlength="500"
13914
- 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"
13915
- placeholder="Enter the frequently asked question...">${faq?.question || ""}</textarea>
13916
- <p class="mt-1 text-sm text-gray-300">
13917
- <span id="question-count">0</span>/500 characters
13918
- </p>
13919
- </div>
13920
- ${errors?.question ? `
13921
- <div class="mt-1">
13922
- ${errors.question.map((error) => `
13923
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13924
- `).join("")}
13925
- </div>
13926
- ` : ""}
13927
- </div>
13928
-
13929
- <!-- Answer -->
13930
- <div>
13931
- <label for="answer" class="block text-sm font-medium text-white">
13932
- Answer <span class="text-red-400">*</span>
13933
- </label>
13934
- <div class="mt-1">
13935
- <textarea name="answer"
13936
- id="answer"
13937
- rows="6"
13938
- required
13939
- maxlength="2000"
13940
- 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"
13941
- placeholder="Enter the detailed answer...">${faq?.answer || ""}</textarea>
13942
- <p class="mt-1 text-sm text-gray-300">
13943
- <span id="answer-count">0</span>/2000 characters. You can use basic HTML for formatting.
13944
- </p>
13945
- </div>
13946
- ${errors?.answer ? `
13947
- <div class="mt-1">
13948
- ${errors.answer.map((error) => `
13949
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13950
- `).join("")}
13951
- </div>
13952
- ` : ""}
13953
- </div>
13954
-
13955
- <!-- Category and Tags Row -->
13956
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
13957
- <!-- Category -->
13958
- <div>
13959
- <label for="category" class="block text-sm font-medium text-white">Category</label>
13960
- <div class="mt-1">
13961
- <select name="category"
13962
- id="category"
13963
- 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">
13964
- <option value="">Select a category</option>
13965
- <option value="general" ${faq?.category === "general" ? "selected" : ""}>General</option>
13966
- <option value="technical" ${faq?.category === "technical" ? "selected" : ""}>Technical</option>
13967
- <option value="billing" ${faq?.category === "billing" ? "selected" : ""}>Billing</option>
13968
- <option value="support" ${faq?.category === "support" ? "selected" : ""}>Support</option>
13969
- <option value="account" ${faq?.category === "account" ? "selected" : ""}>Account</option>
13970
- <option value="features" ${faq?.category === "features" ? "selected" : ""}>Features</option>
13971
- </select>
13972
- </div>
13973
- ${errors?.category ? `
13974
- <div class="mt-1">
13975
- ${errors.category.map((error) => `
13976
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13977
- `).join("")}
13978
- </div>
13979
- ` : ""}
13980
- </div>
13981
-
13982
- <!-- Tags -->
13983
- <div>
13984
- <label for="tags" class="block text-sm font-medium text-white">Tags</label>
13985
- <div class="mt-1">
13986
- <input type="text"
13987
- name="tags"
13988
- id="tags"
13989
- value="${faq?.tags || ""}"
13990
- 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"
13991
- placeholder="e.g., payment, setup, troubleshooting">
13992
- <p class="mt-1 text-sm text-gray-300">Separate multiple tags with commas</p>
13993
- </div>
13994
- ${errors?.tags ? `
13995
- <div class="mt-1">
13996
- ${errors.tags.map((error) => `
13997
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13998
- `).join("")}
13999
- </div>
14000
- ` : ""}
14001
- </div>
14002
- </div>
14003
-
14004
- <!-- Status and Sort Order Row -->
14005
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
14006
- <!-- Published Status -->
14007
- <div>
14008
- <label class="block text-sm font-medium text-white">Status</label>
14009
- <div class="mt-2 space-y-2">
14010
- <div class="flex items-center">
14011
- <input id="published"
14012
- name="isPublished"
14013
- type="radio"
14014
- value="true"
14015
- ${!faq || faq.isPublished ? "checked" : ""}
14016
- class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
14017
- <label for="published" class="ml-2 block text-sm text-white">
14018
- Published <span class="text-gray-300">(visible to users)</span>
14019
- </label>
14020
- </div>
14021
- <div class="flex items-center">
14022
- <input id="draft"
14023
- name="isPublished"
14024
- type="radio"
14025
- value="false"
14026
- ${faq && !faq.isPublished ? "checked" : ""}
14027
- class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
14028
- <label for="draft" class="ml-2 block text-sm text-white">
14029
- Draft <span class="text-gray-300">(not visible to users)</span>
14030
- </label>
14031
- </div>
14032
- </div>
14033
- </div>
14034
-
14035
- <!-- Sort Order -->
14036
- <div>
14037
- <label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
14038
- <div class="mt-1">
14039
- <input type="number"
14040
- name="sortOrder"
14041
- id="sortOrder"
14042
- value="${faq?.sortOrder || 0}"
14043
- min="0"
14044
- step="1"
14045
- 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">
14046
- <p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
14047
- </div>
14048
- ${errors?.sortOrder ? `
14049
- <div class="mt-1">
14050
- ${errors.sortOrder.map((error) => `
14051
- <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14052
- `).join("")}
14053
- </div>
14054
- ` : ""}
14055
- </div>
14056
- </div>
14057
-
14058
- <!-- Form Actions -->
14059
- <div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
14060
- <a href="/admin/faq"
14061
- 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">
14062
- Cancel
14063
- </a>
14064
- <button type="submit"
14065
- 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">
14066
- <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14067
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
14068
- </svg>
14069
- ${isEdit ? "Update FAQ" : "Create FAQ"}
14070
- </button>
14071
- </div>
14072
- </form>
14073
- </div>
14074
- </div>
14075
-
14076
- <script>
14077
- // Character count for question
14078
- const questionTextarea = document.getElementById('question');
14079
- const questionCount = document.getElementById('question-count');
14080
-
14081
- function updateQuestionCount() {
14082
- questionCount.textContent = questionTextarea.value.length;
14083
- }
14084
-
14085
- questionTextarea.addEventListener('input', updateQuestionCount);
14086
- updateQuestionCount(); // Initial count
14087
-
14088
- // Character count for answer
14089
- const answerTextarea = document.getElementById('answer');
14090
- const answerCount = document.getElementById('answer-count');
14091
-
14092
- function updateAnswerCount() {
14093
- answerCount.textContent = answerTextarea.value.length;
14094
- }
14095
-
14096
- answerTextarea.addEventListener('input', updateAnswerCount);
14097
- updateAnswerCount(); // Initial count
14098
- </script>
14099
- `;
14100
- const layoutData = {
14101
- title: `${pageTitle} - Admin`,
14102
- pageTitle,
14103
- currentPath: isEdit ? `/admin/faq/${faq?.id}` : "/admin/faq/new",
14104
- user: data.user,
14105
- content: pageContent
14106
- };
14107
- return renderAdminLayout(layoutData);
14108
- }
14109
- function escapeHtml4(unsafe) {
14110
- return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14111
- }
14112
-
14113
- // src/routes/admin-faq.ts
14114
- var faqSchema = z.object({
14115
- question: z.string().min(1, "Question is required").max(500, "Question must be under 500 characters"),
14116
- answer: z.string().min(1, "Answer is required").max(2e3, "Answer must be under 2000 characters"),
14117
- category: z.string().optional(),
14118
- tags: z.string().optional(),
14119
- isPublished: z.string().transform((val) => val === "true"),
14120
- sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
14121
- });
14122
- var adminFAQRoutes = new Hono();
14123
- adminFAQRoutes.get("/", async (c) => {
14124
- try {
14125
- const user = c.get("user");
14126
- const { category, published, search, page = "1" } = c.req.query();
14127
- const currentPage = parseInt(page, 10) || 1;
14128
- const limit = 20;
14129
- const offset = (currentPage - 1) * limit;
14130
- const db = c.env?.DB;
14131
- if (!db) {
14132
- return c.html(renderFAQList({
14133
- faqs: [],
14134
- totalCount: 0,
14135
- currentPage: 1,
14136
- totalPages: 1,
14137
- user: user ? {
14138
- name: user.email,
14139
- email: user.email,
14140
- role: user.role
14141
- } : void 0,
14142
- message: "Database not available",
14143
- messageType: "error"
14144
- }));
14145
- }
14146
- let whereClause = "WHERE 1=1";
14147
- const params = [];
14148
- if (category) {
14149
- whereClause += " AND category = ?";
14150
- params.push(category);
14151
- }
14152
- if (published !== void 0) {
14153
- whereClause += " AND isPublished = ?";
14154
- params.push(published === "true" ? 1 : 0);
14155
- }
14156
- if (search) {
14157
- whereClause += " AND (question LIKE ? OR answer LIKE ? OR tags LIKE ?)";
14158
- const searchTerm = `%${search}%`;
14159
- params.push(searchTerm, searchTerm, searchTerm);
14160
- }
14161
- const countQuery = `SELECT COUNT(*) as count FROM faqs ${whereClause}`;
14162
- const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
14163
- const totalCount = countResults?.[0]?.count || 0;
14164
- const dataQuery = `
14165
- SELECT * FROM faqs
14166
- ${whereClause}
14167
- ORDER BY sortOrder ASC, created_at DESC
14168
- LIMIT ? OFFSET ?
14169
- `;
14170
- const { results: faqs } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
14171
- const totalPages = Math.ceil(totalCount / limit);
14172
- return c.html(renderFAQList({
14173
- faqs: faqs || [],
14174
- totalCount,
14175
- currentPage,
14176
- totalPages,
14177
- user: user ? {
14178
- name: user.email,
14179
- email: user.email,
14180
- role: user.role
14181
- } : void 0
14182
- }));
14183
- } catch (error) {
14184
- console.error("Error fetching FAQs:", error);
14185
- const user = c.get("user");
14186
- return c.html(renderFAQList({
14187
- faqs: [],
14188
- totalCount: 0,
14189
- currentPage: 1,
14190
- totalPages: 1,
14191
- user: user ? {
14192
- name: user.email,
14193
- email: user.email,
14194
- role: user.role
14195
- } : void 0,
14196
- message: "Failed to load FAQs",
14197
- messageType: "error"
14198
- }));
14199
- }
14200
- });
14201
- adminFAQRoutes.get("/new", async (c) => {
14202
- const user = c.get("user");
14203
- return c.html(renderFAQForm({
14204
- isEdit: false,
14205
- user: user ? {
14206
- name: user.email,
14207
- email: user.email,
14208
- role: user.role
14209
- } : void 0
14210
- }));
14211
- });
14212
- adminFAQRoutes.post("/", async (c) => {
14213
- try {
14214
- const formData = await c.req.formData();
14215
- const data = Object.fromEntries(formData.entries());
14216
- const validatedData = faqSchema.parse(data);
14217
- const user = c.get("user");
14218
- const db = c.env?.DB;
14219
- if (!db) {
14220
- return c.html(renderFAQForm({
14221
- isEdit: false,
14222
- user: user ? {
14223
- name: user.email,
14224
- email: user.email,
14225
- role: user.role
14226
- } : void 0,
14227
- message: "Database not available",
14228
- messageType: "error"
14229
- }));
14230
- }
14231
- const { results } = await db.prepare(`
14232
- INSERT INTO faqs (question, answer, category, tags, isPublished, sortOrder)
14233
- VALUES (?, ?, ?, ?, ?, ?)
14234
- RETURNING *
14235
- `).bind(
14236
- validatedData.question,
14237
- validatedData.answer,
14238
- validatedData.category || null,
14239
- validatedData.tags || null,
14240
- validatedData.isPublished ? 1 : 0,
14241
- validatedData.sortOrder
14242
- ).all();
14243
- if (results && results.length > 0) {
14244
- return c.redirect("/admin/faq?message=FAQ created successfully");
14245
- } else {
14246
- return c.html(renderFAQForm({
14247
- isEdit: false,
14248
- user: user ? {
14249
- name: user.email,
14250
- email: user.email,
14251
- role: user.role
14252
- } : void 0,
14253
- message: "Failed to create FAQ",
14254
- messageType: "error"
14255
- }));
14256
- }
14257
- } catch (error) {
14258
- console.error("Error creating FAQ:", error);
14259
- const user = c.get("user");
14260
- if (error instanceof z.ZodError) {
14261
- const errors = {};
14262
- error.errors.forEach((err) => {
14263
- const field = err.path[0];
14264
- if (!errors[field]) errors[field] = [];
14265
- errors[field].push(err.message);
14266
- });
14267
- return c.html(renderFAQForm({
14268
- isEdit: false,
14269
- user: user ? {
14270
- name: user.email,
14271
- email: user.email,
14272
- role: user.role
14273
- } : void 0,
14274
- errors,
14275
- message: "Please correct the errors below",
14276
- messageType: "error"
14277
- }));
14278
- }
14279
- return c.html(renderFAQForm({
14280
- isEdit: false,
14281
- user: user ? {
14282
- name: user.email,
14283
- email: user.email,
14284
- role: user.role
14285
- } : void 0,
14286
- message: "Failed to create FAQ",
14287
- messageType: "error"
14288
- }));
14289
- }
14290
- });
14291
- adminFAQRoutes.get("/:id", async (c) => {
14292
- try {
14293
- const id = parseInt(c.req.param("id"));
14294
- const user = c.get("user");
14295
- const db = c.env?.DB;
14296
- if (!db) {
14297
- return c.html(renderFAQForm({
14298
- isEdit: true,
14299
- user: user ? {
14300
- name: user.email,
14301
- email: user.email,
14302
- role: user.role
14303
- } : void 0,
14304
- message: "Database not available",
14305
- messageType: "error"
14306
- }));
14307
- }
14308
- const { results } = await db.prepare("SELECT * FROM faqs WHERE id = ?").bind(id).all();
14309
- if (!results || results.length === 0) {
14310
- return c.redirect("/admin/faq?message=FAQ not found&type=error");
14311
- }
14312
- const faq = results[0];
14313
- return c.html(renderFAQForm({
14314
- faq: {
14315
- id: faq.id,
14316
- question: faq.question,
14317
- answer: faq.answer,
14318
- category: faq.category,
14319
- tags: faq.tags,
14320
- isPublished: Boolean(faq.isPublished),
14321
- sortOrder: faq.sortOrder
14322
- },
14323
- isEdit: true,
14324
- user: user ? {
14325
- name: user.email,
14326
- email: user.email,
14327
- role: user.role
14328
- } : void 0
14329
- }));
14330
- } catch (error) {
14331
- console.error("Error fetching FAQ:", error);
14332
- const user = c.get("user");
14333
- return c.html(renderFAQForm({
14334
- isEdit: true,
14335
- user: user ? {
14336
- name: user.email,
14337
- email: user.email,
14338
- role: user.role
14339
- } : void 0,
14340
- message: "Failed to load FAQ",
14341
- messageType: "error"
14342
- }));
14343
- }
14344
- });
14345
- adminFAQRoutes.put("/:id", async (c) => {
14346
- try {
14347
- const id = parseInt(c.req.param("id"));
14348
- const formData = await c.req.formData();
14349
- const data = Object.fromEntries(formData.entries());
14350
- const validatedData = faqSchema.parse(data);
14351
- const user = c.get("user");
14352
- const db = c.env?.DB;
14353
- if (!db) {
14354
- return c.html(renderFAQForm({
14355
- isEdit: true,
14356
- user: user ? {
14357
- name: user.email,
14358
- email: user.email,
14359
- role: user.role
14360
- } : void 0,
14361
- message: "Database not available",
14362
- messageType: "error"
14363
- }));
14364
- }
14365
- const { results } = await db.prepare(`
14366
- UPDATE faqs
14367
- SET question = ?, answer = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
14368
- WHERE id = ?
14369
- RETURNING *
14370
- `).bind(
14371
- validatedData.question,
14372
- validatedData.answer,
14373
- validatedData.category || null,
14374
- validatedData.tags || null,
14375
- validatedData.isPublished ? 1 : 0,
14376
- validatedData.sortOrder,
14377
- id
14378
- ).all();
14379
- if (results && results.length > 0) {
14380
- return c.redirect("/admin/faq?message=FAQ updated successfully");
14381
- } else {
14382
- return c.html(renderFAQForm({
14383
- faq: {
14384
- id,
14385
- question: validatedData.question,
14386
- answer: validatedData.answer,
14387
- category: validatedData.category,
14388
- tags: validatedData.tags,
14389
- isPublished: validatedData.isPublished,
14390
- sortOrder: validatedData.sortOrder
14391
- },
14392
- isEdit: true,
14393
- user: user ? {
14394
- name: user.email,
14395
- email: user.email,
14396
- role: user.role
14397
- } : void 0,
14398
- message: "FAQ not found",
14399
- messageType: "error"
14400
- }));
14401
- }
14402
- } catch (error) {
14403
- console.error("Error updating FAQ:", error);
14404
- const user = c.get("user");
14405
- const id = parseInt(c.req.param("id"));
14406
- if (error instanceof z.ZodError) {
14407
- const errors = {};
14408
- error.errors.forEach((err) => {
14409
- const field = err.path[0];
14410
- if (!errors[field]) errors[field] = [];
14411
- errors[field].push(err.message);
14412
- });
14413
- return c.html(renderFAQForm({
14414
- faq: {
14415
- id,
14416
- question: "",
14417
- answer: "",
14418
- category: "",
14419
- tags: "",
14420
- isPublished: true,
14421
- sortOrder: 0
14422
- },
14423
- isEdit: true,
14424
- user: user ? {
14425
- name: user.email,
14426
- email: user.email,
14427
- role: user.role
14428
- } : void 0,
14429
- errors,
14430
- message: "Please correct the errors below",
14431
- messageType: "error"
14432
- }));
14433
- }
14434
- return c.html(renderFAQForm({
14435
- faq: {
14436
- id,
14437
- question: "",
14438
- answer: "",
14439
- category: "",
14440
- tags: "",
14441
- isPublished: true,
14442
- sortOrder: 0
14443
- },
14444
- isEdit: true,
14445
- user: user ? {
14446
- name: user.email,
14447
- email: user.email,
14448
- role: user.role
14449
- } : void 0,
14450
- message: "Failed to update FAQ",
14451
- messageType: "error"
14452
- }));
14453
- }
14454
- });
14455
- adminFAQRoutes.delete("/:id", async (c) => {
14983
+ adminLogsRoutes.post("/cleanup", async (c) => {
14456
14984
  try {
14457
- const id = parseInt(c.req.param("id"));
14458
- const db = c.env?.DB;
14459
- if (!db) {
14460
- return c.json({ error: "Database not available" }, 500);
14461
- }
14462
- const { changes } = await db.prepare("DELETE FROM faqs WHERE id = ?").bind(id).run();
14463
- if (changes === 0) {
14464
- 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);
14465
14991
  }
14466
- 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);
14467
15056
  } catch (error) {
14468
- console.error("Error deleting FAQ:", error);
14469
- 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";
14470
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));
14471
15122
  });
14472
- var admin_faq_default = adminFAQRoutes;
14473
15123
 
14474
15124
  // src/templates/pages/admin-testimonials-form.template.ts
14475
15125
  function renderTestimonialsForm(data) {
@@ -14527,7 +15177,7 @@ function renderTestimonialsForm(data) {
14527
15177
  ${errors?.authorName ? `
14528
15178
  <div class="mt-1">
14529
15179
  ${errors.authorName.map((error) => `
14530
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15180
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14531
15181
  `).join("")}
14532
15182
  </div>
14533
15183
  ` : ""}
@@ -14549,7 +15199,7 @@ function renderTestimonialsForm(data) {
14549
15199
  ${errors?.authorTitle ? `
14550
15200
  <div class="mt-1">
14551
15201
  ${errors.authorTitle.map((error) => `
14552
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15202
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14553
15203
  `).join("")}
14554
15204
  </div>
14555
15205
  ` : ""}
@@ -14570,7 +15220,7 @@ function renderTestimonialsForm(data) {
14570
15220
  ${errors?.authorCompany ? `
14571
15221
  <div class="mt-1">
14572
15222
  ${errors.authorCompany.map((error) => `
14573
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15223
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14574
15224
  `).join("")}
14575
15225
  </div>
14576
15226
  ` : ""}
@@ -14602,7 +15252,7 @@ function renderTestimonialsForm(data) {
14602
15252
  ${errors?.testimonialText ? `
14603
15253
  <div class="mt-1">
14604
15254
  ${errors.testimonialText.map((error) => `
14605
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15255
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14606
15256
  `).join("")}
14607
15257
  </div>
14608
15258
  ` : ""}
@@ -14626,7 +15276,7 @@ function renderTestimonialsForm(data) {
14626
15276
  ${errors?.rating ? `
14627
15277
  <div class="mt-1">
14628
15278
  ${errors.rating.map((error) => `
14629
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15279
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14630
15280
  `).join("")}
14631
15281
  </div>
14632
15282
  ` : ""}
@@ -14680,7 +15330,7 @@ function renderTestimonialsForm(data) {
14680
15330
  ${errors?.sortOrder ? `
14681
15331
  <div class="mt-1">
14682
15332
  ${errors.sortOrder.map((error) => `
14683
- <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15333
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14684
15334
  `).join("")}
14685
15335
  </div>
14686
15336
  ` : ""}
@@ -14727,7 +15377,7 @@ function renderTestimonialsForm(data) {
14727
15377
  };
14728
15378
  return renderAdminLayout(layoutData);
14729
15379
  }
14730
- function escapeHtml5(unsafe) {
15380
+ function escapeHtml4(unsafe) {
14731
15381
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14732
15382
  }
14733
15383
 
@@ -15155,7 +15805,7 @@ function renderCodeExamplesForm(data) {
15155
15805
  ${errors?.title ? `
15156
15806
  <div class="mt-1">
15157
15807
  ${errors.title.map((error) => `
15158
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15808
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15159
15809
  `).join("")}
15160
15810
  </div>
15161
15811
  ` : ""}
@@ -15178,7 +15828,7 @@ function renderCodeExamplesForm(data) {
15178
15828
  ${errors?.description ? `
15179
15829
  <div class="mt-1">
15180
15830
  ${errors.description.map((error) => `
15181
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15831
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15182
15832
  `).join("")}
15183
15833
  </div>
15184
15834
  ` : ""}
@@ -15210,7 +15860,7 @@ function renderCodeExamplesForm(data) {
15210
15860
  ${errors?.language ? `
15211
15861
  <div class="mt-1">
15212
15862
  ${errors.language.map((error) => `
15213
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15863
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15214
15864
  `).join("")}
15215
15865
  </div>
15216
15866
  ` : ""}
@@ -15231,7 +15881,7 @@ function renderCodeExamplesForm(data) {
15231
15881
  ${errors?.category ? `
15232
15882
  <div class="mt-1">
15233
15883
  ${errors.category.map((error) => `
15234
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15884
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15235
15885
  `).join("")}
15236
15886
  </div>
15237
15887
  ` : ""}
@@ -15253,7 +15903,7 @@ function renderCodeExamplesForm(data) {
15253
15903
  ${errors?.tags ? `
15254
15904
  <div class="mt-1">
15255
15905
  ${errors.tags.map((error) => `
15256
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15906
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15257
15907
  `).join("")}
15258
15908
  </div>
15259
15909
  ` : ""}
@@ -15284,7 +15934,7 @@ function renderCodeExamplesForm(data) {
15284
15934
  ${errors?.code ? `
15285
15935
  <div class="mt-1">
15286
15936
  ${errors.code.map((error) => `
15287
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15937
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15288
15938
  `).join("")}
15289
15939
  </div>
15290
15940
  ` : ""}
@@ -15338,7 +15988,7 @@ function renderCodeExamplesForm(data) {
15338
15988
  ${errors?.sortOrder ? `
15339
15989
  <div class="mt-1">
15340
15990
  ${errors.sortOrder.map((error) => `
15341
- <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15991
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
15342
15992
  `).join("")}
15343
15993
  </div>
15344
15994
  ` : ""}
@@ -15396,7 +16046,7 @@ function renderCodeExamplesForm(data) {
15396
16046
  };
15397
16047
  return renderAdminLayout(layoutData);
15398
16048
  }
15399
- function escapeHtml6(unsafe) {
16049
+ function escapeHtml5(unsafe) {
15400
16050
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
15401
16051
  }
15402
16052
 
@@ -16730,8 +17380,8 @@ function renderTable2(data) {
16730
17380
  </td>
16731
17381
  ` : ""}
16732
17382
  ${data.columns.map((column, colIndex) => {
16733
- const value2 = row[column.key];
16734
- 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;
16735
17385
  const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
16736
17386
  const isFirst = colIndex === 0 && !data.selectable;
16737
17387
  const isLast = colIndex === data.columns.length - 1;
@@ -17118,10 +17768,41 @@ function renderCollectionsListPage(data) {
17118
17768
 
17119
17769
  // src/templates/pages/admin-collections-form.template.ts
17120
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
+ }
17121
17798
  function renderCollectionFormPage(data) {
17122
17799
  const isEdit = data.isEdit || !!data.id;
17123
17800
  const title = isEdit ? "Edit Collection" : "Create New Collection";
17124
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
+ }));
17125
17806
  const fields = [
17126
17807
  {
17127
17808
  name: "displayName",
@@ -17358,21 +18039,24 @@ function renderCollectionFormPage(data) {
17358
18039
 
17359
18040
  <!-- Fields List (Read-Only) -->
17360
18041
  <div class="space-y-3">
17361
- ${(data.fields || []).map((field) => `
18042
+ ${fieldsWithData.map((field) => `
17362
18043
  <div class="bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4">
17363
18044
  <div class="flex items-center justify-between">
17364
18045
  <div class="flex items-center gap-x-4">
17365
18046
  <div>
17366
18047
  <div class="flex items-center gap-x-2">
17367
18048
  <span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
17368
- <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">
17369
- ${field.field_type}
17370
- </span>
18049
+ ${getFieldTypeBadge(field.field_type)}
17371
18050
  ${field.is_required ? `
17372
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">
17373
18052
  Required
17374
18053
  </span>
17375
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
+ ` : ""}
17376
18060
  </div>
17377
18061
  <div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
17378
18062
  <code class="px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">${field.field_name}</code>
@@ -17418,8 +18102,10 @@ function renderCollectionFormPage(data) {
17418
18102
 
17419
18103
  <!-- Fields List -->
17420
18104
  <div id="fields-list" class="space-y-3">
17421
- ${(data.fields || []).map((field) => `
17422
- <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}">
17423
18109
  <div class="flex items-center justify-between">
17424
18110
  <div class="flex items-center gap-x-4">
17425
18111
  <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
@@ -17430,14 +18116,17 @@ function renderCollectionFormPage(data) {
17430
18116
  <div>
17431
18117
  <div class="flex items-center gap-x-2">
17432
18118
  <span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
17433
- <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">
17434
- ${field.field_type}
17435
- </span>
18119
+ ${getFieldTypeBadge(field.field_type)}
17436
18120
  ${field.is_required ? `
17437
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">
17438
18122
  Required
17439
18123
  </span>
17440
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
+ ` : ""}
17441
18130
  </div>
17442
18131
  <div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
17443
18132
  Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
@@ -17548,7 +18237,7 @@ function renderCollectionFormPage(data) {
17548
18237
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
17549
18238
  <input
17550
18239
  type="text"
17551
- id="field-name"
18240
+ id="modal-field-name"
17552
18241
  name="field_name"
17553
18242
  required
17554
18243
  pattern="[a-z0-9_]+"
@@ -17569,13 +18258,14 @@ function renderCollectionFormPage(data) {
17569
18258
  >
17570
18259
  <option value="">Select field type...</option>
17571
18260
  <option value="text">Text</option>
17572
- <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>' : ""}
17573
18264
  <option value="number">Number</option>
17574
18265
  <option value="boolean">Boolean</option>
17575
18266
  <option value="date">Date</option>
17576
18267
  <option value="select">Select</option>
17577
18268
  <option value="media">Media</option>
17578
- <option value="guid">GUID (Auto-generated)</option>
17579
18269
  </select>
17580
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">
17581
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" />
@@ -17600,6 +18290,7 @@ function renderCollectionFormPage(data) {
17600
18290
  <div class="flex gap-3">
17601
18291
  <div class="flex h-6 shrink-0 items-center">
17602
18292
  <div class="group grid size-4 grid-cols-1">
18293
+ <input type="hidden" name="is_required" value="0">
17603
18294
  <input
17604
18295
  type="checkbox"
17605
18296
  id="field-required"
@@ -17621,6 +18312,7 @@ function renderCollectionFormPage(data) {
17621
18312
  <div class="flex gap-3">
17622
18313
  <div class="flex h-6 shrink-0 items-center">
17623
18314
  <div class="group grid size-4 grid-cols-1">
18315
+ <input type="hidden" name="is_searchable" value="0">
17624
18316
  <input
17625
18317
  type="checkbox"
17626
18318
  id="field-searchable"
@@ -17683,18 +18375,52 @@ function renderCollectionFormPage(data) {
17683
18375
  document.getElementById('submit-text').textContent = 'Add Field';
17684
18376
  document.getElementById('field-form').reset();
17685
18377
  document.getElementById('field-id').value = '';
17686
- document.getElementById('field-name').disabled = false;
18378
+ document.getElementById('modal-field-name').disabled = false;
17687
18379
  currentEditingField = null;
18380
+ isEditingField = false; // Allow change handlers for add mode
17688
18381
  document.getElementById('field-modal').classList.remove('hidden');
17689
18382
  }
17690
18383
 
17691
18384
  function editField(fieldId) {
17692
18385
  const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
17693
- 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
+ }
17694
18419
 
17695
- // Find the field data from the collection's fields array
17696
- const field = ${JSON.stringify(data.fields || [])}.find(f => f.id === fieldId);
17697
- if (!field) return;
18420
+ if (!field) {
18421
+ console.error('Field data not found for id:', fieldId);
18422
+ return;
18423
+ }
17698
18424
 
17699
18425
  // Set up the modal for editing
17700
18426
  document.getElementById('modal-title').textContent = 'Edit Field';
@@ -17702,18 +18428,102 @@ function renderCollectionFormPage(data) {
17702
18428
  document.getElementById('field-id').value = fieldId;
17703
18429
  currentEditingField = fieldId;
17704
18430
 
17705
- // Populate form with existing field data
17706
- document.getElementById('field-name').value = field.field_name || '';
17707
- document.getElementById('field-name').disabled = true;
17708
- document.getElementById('field-label').value = field.field_label || '';
17709
- document.getElementById('field-type').value = field.field_type || '';
17710
- document.getElementById('field-required').checked = Boolean(field.is_required);
17711
- document.getElementById('field-searchable').checked = Boolean(field.is_searchable);
17712
-
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
+
17713
18523
  // Handle field options - serialize object back to JSON string
17714
18524
  if (field.field_options) {
17715
- document.getElementById('field-options').value = typeof field.field_options === 'string'
17716
- ? field.field_options
18525
+ document.getElementById('field-options').value = typeof field.field_options === 'string'
18526
+ ? field.field_options
17717
18527
  : JSON.stringify(field.field_options, null, 2);
17718
18528
  } else {
17719
18529
  document.getElementById('field-options').value = '';
@@ -17724,7 +18534,7 @@ function renderCollectionFormPage(data) {
17724
18534
  const optionsContainer = document.getElementById('field-options-container');
17725
18535
  const helpText = document.getElementById('field-type-help');
17726
18536
 
17727
- if (['select', 'media', 'richtext', 'guid'].includes(fieldType)) {
18537
+ if (['select', 'media', 'richtext'].includes(fieldType)) {
17728
18538
  optionsContainer.classList.remove('hidden');
17729
18539
 
17730
18540
  // Set help text based on type
@@ -17738,9 +18548,6 @@ function renderCollectionFormPage(data) {
17738
18548
  case 'richtext':
17739
18549
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17740
18550
  break;
17741
- case 'guid':
17742
- helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17743
- break;
17744
18551
  }
17745
18552
  } else {
17746
18553
  optionsContainer.classList.add('hidden');
@@ -17764,12 +18571,19 @@ function renderCollectionFormPage(data) {
17764
18571
  }
17765
18572
  }
17766
18573
 
17767
- 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
17768
18581
  }
17769
18582
 
17770
18583
  function closeFieldModal() {
17771
18584
  document.getElementById('field-modal').classList.add('hidden');
17772
18585
  currentEditingField = null;
18586
+ isEditingField = false; // Clear the flag when closing
17773
18587
  }
17774
18588
 
17775
18589
  let fieldToDelete = null;
@@ -17843,12 +18657,21 @@ function renderCollectionFormPage(data) {
17843
18657
  });
17844
18658
  });
17845
18659
 
18660
+ // Flag to prevent change handler during programmatic edits
18661
+ let isEditingField = false;
18662
+
17846
18663
  // Field type change handler
17847
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
+
17848
18671
  const optionsContainer = document.getElementById('field-options-container');
17849
18672
  const fieldOptions = document.getElementById('field-options');
17850
18673
  const helpText = document.getElementById('field-type-help');
17851
- const fieldNameInput = document.getElementById('field-name');
18674
+ const fieldNameInput = document.getElementById('modal-field-name');
17852
18675
 
17853
18676
  // Show/hide options based on field type
17854
18677
  if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
@@ -17868,14 +18691,6 @@ function renderCollectionFormPage(data) {
17868
18691
  fieldOptions.value = '{"toolbar": "full", "height": 400}';
17869
18692
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17870
18693
  break;
17871
- case 'guid':
17872
- fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
17873
- helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17874
- // Suggest 'id' as field name for GUID fields
17875
- if (!fieldNameInput.value || fieldNameInput.value === '') {
17876
- fieldNameInput.value = 'id';
17877
- }
17878
- break;
17879
18694
  }
17880
18695
  } else {
17881
18696
  optionsContainer.classList.add('hidden');
@@ -18012,8 +18827,19 @@ adminCollectionsRoutes.get("/", async (c) => {
18012
18827
  return c.html(html`<p>Error loading collections</p>`);
18013
18828
  }
18014
18829
  });
18015
- adminCollectionsRoutes.get("/new", (c) => {
18830
+ adminCollectionsRoutes.get("/new", async (c) => {
18016
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
+ });
18017
18843
  const formData = {
18018
18844
  isEdit: false,
18019
18845
  user: user ? {
@@ -18021,7 +18847,12 @@ adminCollectionsRoutes.get("/new", (c) => {
18021
18847
  email: user.email,
18022
18848
  role: user.role
18023
18849
  } : void 0,
18024
- version: c.get("appVersion")
18850
+ version: c.get("appVersion"),
18851
+ editorPlugins: {
18852
+ tinymce: tinymceActive,
18853
+ quill: quillActive,
18854
+ mdxeditor: mdxeditorActive
18855
+ }
18025
18856
  };
18026
18857
  return c.html(renderCollectionFormPage(formData));
18027
18858
  });
@@ -18121,16 +18952,16 @@ adminCollectionsRoutes.post("/", async (c) => {
18121
18952
  if (isHtmx) {
18122
18953
  return c.html(html`
18123
18954
  <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
18124
- Collection created successfully! Redirecting...
18955
+ Collection created successfully! Redirecting to edit mode...
18125
18956
  <script>
18126
18957
  setTimeout(() => {
18127
- window.location.href = '/admin/collections';
18958
+ window.location.href = '/admin/collections/${collectionId}';
18128
18959
  }, 1500);
18129
18960
  </script>
18130
18961
  </div>
18131
18962
  `);
18132
18963
  } else {
18133
- return c.redirect("/admin/collections");
18964
+ return c.redirect(`/admin/collections/${collectionId}`);
18134
18965
  }
18135
18966
  } catch (error) {
18136
18967
  console.error("Error creating collection:", error);
@@ -18180,7 +19011,7 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18180
19011
  field_options: fieldConfig,
18181
19012
  field_order: fieldOrder++,
18182
19013
  is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
18183
- is_searchable: false
19014
+ is_searchable: fieldConfig.searchable === true || false
18184
19015
  }));
18185
19016
  }
18186
19017
  } catch (e) {
@@ -18205,6 +19036,11 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18205
19036
  is_searchable: row.is_searchable === 1
18206
19037
  }));
18207
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
+ ]);
18208
19044
  const formData = {
18209
19045
  id: collection.id,
18210
19046
  name: collection.name,
@@ -18218,7 +19054,12 @@ adminCollectionsRoutes.get("/:id", async (c) => {
18218
19054
  email: user.email,
18219
19055
  role: user.role
18220
19056
  } : void 0,
18221
- version: c.get("appVersion")
19057
+ version: c.get("appVersion"),
19058
+ editorPlugins: {
19059
+ tinymce: tinymceActive,
19060
+ quill: quillActive,
19061
+ mdxeditor: mdxeditorActive
19062
+ }
18222
19063
  };
18223
19064
  return c.html(renderCollectionFormPage(formData));
18224
19065
  } catch (error) {
@@ -18358,21 +19199,103 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
18358
19199
  adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
18359
19200
  try {
18360
19201
  const fieldId = c.req.param("fieldId");
19202
+ const collectionId = c.req.param("collectionId");
18361
19203
  const formData = await c.req.formData();
18362
19204
  const fieldLabel = formData.get("field_label");
18363
- const isRequired = formData.get("is_required") === "1";
18364
- 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";
18365
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
+ });
18366
19219
  if (!fieldLabel) {
18367
19220
  return c.json({ success: false, error: "Field label is required." });
18368
19221
  }
18369
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
+ }
18370
19283
  const updateStmt = db.prepare(`
18371
19284
  UPDATE content_fields
18372
- 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 = ?
18373
19286
  WHERE id = ?
18374
19287
  `);
18375
- 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);
18376
19299
  return c.json({ success: true });
18377
19300
  } catch (error) {
18378
19301
  console.error("Error updating field:", error);
@@ -19633,7 +20556,7 @@ function renderMigrationSettings(settings) {
19633
20556
  btn.innerHTML = 'Running...';
19634
20557
 
19635
20558
  try {
19636
- const response = await fetch('/admin/api/migrations/run', {
20559
+ const response = await fetch('/admin/settings/api/migrations/run', {
19637
20560
  method: 'POST'
19638
20561
  });
19639
20562
  const result = await response.json();
@@ -19654,7 +20577,7 @@ function renderMigrationSettings(settings) {
19654
20577
 
19655
20578
  window.validateSchema = async function() {
19656
20579
  try {
19657
- const response = await fetch('/admin/api/migrations/validate');
20580
+ const response = await fetch('/admin/settings/api/migrations/validate');
19658
20581
  const result = await response.json();
19659
20582
 
19660
20583
  if (result.success) {
@@ -20282,7 +21205,6 @@ var ROUTES_INFO = {
20282
21205
  "adminLogsRoutes",
20283
21206
  "adminDesignRoutes",
20284
21207
  "adminCheckboxRoutes",
20285
- "adminFAQRoutes",
20286
21208
  "adminTestimonialsRoutes",
20287
21209
  "adminCodeExamplesRoutes",
20288
21210
  "adminDashboardRoutes",
@@ -20293,6 +21215,6 @@ var ROUTES_INFO = {
20293
21215
  reference: "https://github.com/sonicjs/sonicjs"
20294
21216
  };
20295
21217
 
20296
- 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 };
20297
- //# sourceMappingURL=chunk-Z4H6DBVF.js.map
20298
- //# sourceMappingURL=chunk-Z4H6DBVF.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