@sonicjs-cms/core 2.5.0 → 2.7.0

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 (92) hide show
  1. package/dist/{app-Db0AfT5F.d.cts → app-DV27cjPy.d.cts} +1 -1
  2. package/dist/{app-Db0AfT5F.d.ts → app-DV27cjPy.d.ts} +1 -1
  3. package/dist/{chunk-YIXSSJWD.cjs → chunk-AYPF6C4D.cjs} +5 -5
  4. package/dist/{chunk-YIXSSJWD.cjs.map → chunk-AYPF6C4D.cjs.map} +1 -1
  5. package/dist/chunk-CLIH2T74.js +403 -0
  6. package/dist/chunk-CLIH2T74.js.map +1 -0
  7. package/dist/{chunk-BHNDALCA.js → chunk-DNHJS6RN.js} +6 -4
  8. package/dist/chunk-DNHJS6RN.js.map +1 -0
  9. package/dist/{chunk-YYV3XQOQ.cjs → chunk-E2BXLXPW.cjs} +7 -7
  10. package/dist/{chunk-YYV3XQOQ.cjs.map → chunk-E2BXLXPW.cjs.map} +1 -1
  11. package/dist/{chunk-AZLU3ROK.cjs → chunk-EHSZ6TAN.cjs} +11 -4
  12. package/dist/chunk-EHSZ6TAN.cjs.map +1 -0
  13. package/dist/{chunk-3YUHXWSG.js → chunk-F332TENF.js} +3 -3
  14. package/dist/{chunk-3YUHXWSG.js.map → chunk-F332TENF.js.map} +1 -1
  15. package/dist/{chunk-V5LBQN3I.js → chunk-GRN3GHUG.js} +11 -4
  16. package/dist/chunk-GRN3GHUG.js.map +1 -0
  17. package/dist/{chunk-UAQL2VWX.cjs → chunk-J7F3NPAP.cjs} +2436 -707
  18. package/dist/chunk-J7F3NPAP.cjs.map +1 -0
  19. package/dist/{chunk-VEL7QRYI.js → chunk-L2IDZI7F.js} +9 -2
  20. package/dist/chunk-L2IDZI7F.js.map +1 -0
  21. package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
  22. package/dist/chunk-MPT5PA6U.cjs.map +1 -0
  23. package/dist/{chunk-ZWV3EBZ7.cjs → chunk-MYB5RY7H.cjs} +6 -4
  24. package/dist/chunk-MYB5RY7H.cjs.map +1 -0
  25. package/dist/{chunk-OJZ45OJD.js → chunk-UISZ2MBW.js} +2272 -544
  26. package/dist/chunk-UISZ2MBW.js.map +1 -0
  27. package/dist/{chunk-AVPUX57O.js → chunk-V3KVSEG6.js} +3 -3
  28. package/dist/{chunk-AVPUX57O.js.map → chunk-V3KVSEG6.js.map} +1 -1
  29. package/dist/{chunk-TJTWRO4G.js → chunk-Y3EWJQ4D.js} +4 -4
  30. package/dist/{chunk-TJTWRO4G.js.map → chunk-Y3EWJQ4D.js.map} +1 -1
  31. package/dist/{chunk-LWG2MWDA.cjs → chunk-Y72M3MVX.cjs} +4 -4
  32. package/dist/{chunk-LWG2MWDA.cjs.map → chunk-Y72M3MVX.cjs.map} +1 -1
  33. package/dist/{chunk-SGAG6FD3.js → chunk-YFJJU26H.js} +24 -2
  34. package/dist/chunk-YFJJU26H.js.map +1 -0
  35. package/dist/chunk-YHW27CBV.cjs +406 -0
  36. package/dist/chunk-YHW27CBV.cjs.map +1 -0
  37. package/dist/{chunk-I4V3VZWF.cjs → chunk-YRFAQ6MI.cjs} +9 -2
  38. package/dist/chunk-YRFAQ6MI.cjs.map +1 -0
  39. package/dist/{collection-config-B6gMPunn.d.cts → collection-config-BF95LgQb.d.cts} +1 -1
  40. package/dist/{collection-config-B6gMPunn.d.ts → collection-config-BF95LgQb.d.ts} +1 -1
  41. package/dist/index.cjs +4098 -424
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +503 -8
  44. package/dist/index.d.ts +503 -8
  45. package/dist/index.js +4008 -341
  46. package/dist/index.js.map +1 -1
  47. package/dist/middleware.cjs +24 -24
  48. package/dist/middleware.d.cts +1 -1
  49. package/dist/middleware.d.ts +1 -1
  50. package/dist/middleware.js +3 -3
  51. package/dist/migrations-LEMFV2ND.cjs +13 -0
  52. package/dist/{migrations-NIEUFG44.cjs.map → migrations-LEMFV2ND.cjs.map} +1 -1
  53. package/dist/migrations-RKQES6XY.js +4 -0
  54. package/dist/{migrations-TGZKJKV4.js.map → migrations-RKQES6XY.js.map} +1 -1
  55. package/dist/{plugin-bootstrap-dYhD9fQR.d.ts → plugin-bootstrap-CB-xaBfK.d.ts} +2 -2
  56. package/dist/{plugin-bootstrap-SHsdjE6X.d.cts → plugin-bootstrap-U-cw9jn3.d.cts} +2 -2
  57. package/dist/plugins.cjs +11 -11
  58. package/dist/plugins.js +2 -2
  59. package/dist/routes.cjs +27 -27
  60. package/dist/routes.d.cts +1 -1
  61. package/dist/routes.d.ts +1 -1
  62. package/dist/routes.js +7 -7
  63. package/dist/services.cjs +16 -16
  64. package/dist/services.d.cts +2 -2
  65. package/dist/services.d.ts +2 -2
  66. package/dist/services.js +2 -2
  67. package/dist/templates.cjs +17 -17
  68. package/dist/templates.js +2 -2
  69. package/dist/types.d.cts +1 -1
  70. package/dist/types.d.ts +1 -1
  71. package/dist/utils.cjs +14 -14
  72. package/dist/utils.d.cts +1 -1
  73. package/dist/utils.d.ts +1 -1
  74. package/dist/utils.js +1 -1
  75. package/migrations/029_ai_search_plugin.sql +45 -0
  76. package/package.json +4 -2
  77. package/dist/chunk-AI2JJIJX.cjs +0 -211
  78. package/dist/chunk-AI2JJIJX.cjs.map +0 -1
  79. package/dist/chunk-AZLU3ROK.cjs.map +0 -1
  80. package/dist/chunk-BHNDALCA.js.map +0 -1
  81. package/dist/chunk-I4V3VZWF.cjs.map +0 -1
  82. package/dist/chunk-ILZ3DP4I.cjs.map +0 -1
  83. package/dist/chunk-OJZ45OJD.js.map +0 -1
  84. package/dist/chunk-QDBNW7KQ.js +0 -209
  85. package/dist/chunk-QDBNW7KQ.js.map +0 -1
  86. package/dist/chunk-SGAG6FD3.js.map +0 -1
  87. package/dist/chunk-UAQL2VWX.cjs.map +0 -1
  88. package/dist/chunk-V5LBQN3I.js.map +0 -1
  89. package/dist/chunk-VEL7QRYI.js.map +0 -1
  90. package/dist/chunk-ZWV3EBZ7.cjs.map +0 -1
  91. package/dist/migrations-NIEUFG44.cjs +0 -13
  92. package/dist/migrations-TGZKJKV4.js +0 -4
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var chunk7FOAMNTI_cjs = require('./chunk-7FOAMNTI.cjs');
4
- var chunkYYV3XQOQ_cjs = require('./chunk-YYV3XQOQ.cjs');
5
- var chunkILZ3DP4I_cjs = require('./chunk-ILZ3DP4I.cjs');
6
- var chunkI4V3VZWF_cjs = require('./chunk-I4V3VZWF.cjs');
7
- var chunkAZLU3ROK_cjs = require('./chunk-AZLU3ROK.cjs');
8
- var chunkAI2JJIJX_cjs = require('./chunk-AI2JJIJX.cjs');
9
- var chunkZWV3EBZ7_cjs = require('./chunk-ZWV3EBZ7.cjs');
4
+ var chunkE2BXLXPW_cjs = require('./chunk-E2BXLXPW.cjs');
5
+ var chunkMPT5PA6U_cjs = require('./chunk-MPT5PA6U.cjs');
6
+ var chunkYRFAQ6MI_cjs = require('./chunk-YRFAQ6MI.cjs');
7
+ var chunkEHSZ6TAN_cjs = require('./chunk-EHSZ6TAN.cjs');
8
+ var chunkYHW27CBV_cjs = require('./chunk-YHW27CBV.cjs');
9
+ var chunkMYB5RY7H_cjs = require('./chunk-MYB5RY7H.cjs');
10
10
  var chunkRCQ2HIQD_cjs = require('./chunk-RCQ2HIQD.cjs');
11
11
  var hono = require('hono');
12
12
  var cors = require('hono/cors');
@@ -76,7 +76,7 @@ apiContentCrudRoutes.get("/:id", async (c) => {
76
76
  }, 500);
77
77
  }
78
78
  });
79
- apiContentCrudRoutes.post("/", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
79
+ apiContentCrudRoutes.post("/", chunkE2BXLXPW_cjs.requireAuth(), async (c) => {
80
80
  try {
81
81
  const db = c.env.DB;
82
82
  const user = c.get("user");
@@ -142,7 +142,7 @@ apiContentCrudRoutes.post("/", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
142
142
  }, 500);
143
143
  }
144
144
  });
145
- apiContentCrudRoutes.put("/:id", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
145
+ apiContentCrudRoutes.put("/:id", chunkE2BXLXPW_cjs.requireAuth(), async (c) => {
146
146
  try {
147
147
  const id = c.req.param("id");
148
148
  const db = c.env.DB;
@@ -206,7 +206,7 @@ apiContentCrudRoutes.put("/:id", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
206
206
  }, 500);
207
207
  }
208
208
  });
209
- apiContentCrudRoutes.delete("/:id", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
209
+ apiContentCrudRoutes.delete("/:id", chunkE2BXLXPW_cjs.requireAuth(), async (c) => {
210
210
  try {
211
211
  const id = c.req.param("id");
212
212
  const db = c.env.DB;
@@ -242,7 +242,7 @@ apiRoutes.use("*", async (c, next) => {
242
242
  c.header("X-Response-Time", `${totalTime}ms`);
243
243
  });
244
244
  apiRoutes.use("*", async (c, next) => {
245
- const cacheEnabled = await chunkYYV3XQOQ_cjs.isPluginActive(c.env.DB, "core-cache");
245
+ const cacheEnabled = await chunkE2BXLXPW_cjs.isPluginActive(c.env.DB, "core-cache");
246
246
  c.set("cacheEnabled", cacheEnabled);
247
247
  await next();
248
248
  });
@@ -367,12 +367,12 @@ apiRoutes.get("/content", async (c) => {
367
367
  });
368
368
  }
369
369
  }
370
- const filter = chunkZWV3EBZ7_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
370
+ const filter = chunkMYB5RY7H_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
371
371
  if (!filter.limit) {
372
372
  filter.limit = 50;
373
373
  }
374
374
  filter.limit = Math.min(filter.limit, 1e3);
375
- const builder3 = new chunkZWV3EBZ7_cjs.QueryFilterBuilder();
375
+ const builder3 = new chunkMYB5RY7H_cjs.QueryFilterBuilder();
376
376
  const queryResult = builder3.build("content", filter);
377
377
  if (queryResult.errors.length > 0) {
378
378
  return c.json({
@@ -459,7 +459,7 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
459
459
  if (!collectionResult) {
460
460
  return c.json({ error: "Collection not found" }, 404);
461
461
  }
462
- const filter = chunkZWV3EBZ7_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
462
+ const filter = chunkMYB5RY7H_cjs.QueryFilterBuilder.parseFromQuery(queryParams);
463
463
  if (!filter.where) {
464
464
  filter.where = { and: [] };
465
465
  }
@@ -475,7 +475,7 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
475
475
  filter.limit = 50;
476
476
  }
477
477
  filter.limit = Math.min(filter.limit, 1e3);
478
- const builder3 = new chunkZWV3EBZ7_cjs.QueryFilterBuilder();
478
+ const builder3 = new chunkMYB5RY7H_cjs.QueryFilterBuilder();
479
479
  const queryResult = builder3.build("content", filter);
480
480
  if (queryResult.errors.length > 0) {
481
481
  return c.json({
@@ -600,7 +600,7 @@ var fileValidationSchema = zod.z.object({
600
600
  // 50MB max
601
601
  });
602
602
  var apiMediaRoutes = new hono.Hono();
603
- apiMediaRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
603
+ apiMediaRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
604
604
  apiMediaRoutes.post("/upload", async (c) => {
605
605
  try {
606
606
  const user = c.get("user");
@@ -1344,8 +1344,8 @@ apiSystemRoutes.get("/env", (c) => {
1344
1344
  });
1345
1345
  var api_system_default = apiSystemRoutes;
1346
1346
  var adminApiRoutes = new hono.Hono();
1347
- adminApiRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
1348
- adminApiRoutes.use("*", chunkYYV3XQOQ_cjs.requireRole(["admin", "editor"]));
1347
+ adminApiRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
1348
+ adminApiRoutes.use("*", chunkE2BXLXPW_cjs.requireRole(["admin", "editor"]));
1349
1349
  adminApiRoutes.get("/stats", async (c) => {
1350
1350
  try {
1351
1351
  const db = c.env.DB;
@@ -1585,6 +1585,107 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1585
1585
  return c.json({ error: "Failed to fetch collection" }, 500);
1586
1586
  }
1587
1587
  });
1588
+ adminApiRoutes.get("/references", async (c) => {
1589
+ try {
1590
+ const db = c.env.DB;
1591
+ const url = new URL(c.req.url);
1592
+ const collectionParams = url.searchParams.getAll("collection").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
1593
+ const search = c.req.query("search") || "";
1594
+ const id = c.req.query("id") || "";
1595
+ const limit = Math.min(Number.parseInt(c.req.query("limit") || "20", 10) || 20, 100);
1596
+ if (collectionParams.length === 0) {
1597
+ return c.json({ error: "Collection is required" }, 400);
1598
+ }
1599
+ const placeholders = collectionParams.map(() => "?").join(", ");
1600
+ const collectionStmt = db.prepare(`
1601
+ SELECT id, name, display_name
1602
+ FROM collections
1603
+ WHERE id IN (${placeholders}) OR name IN (${placeholders})
1604
+ `);
1605
+ const collectionResults = await collectionStmt.bind(...collectionParams, ...collectionParams).all();
1606
+ const collections = collectionResults.results || [];
1607
+ if (collections.length === 0) {
1608
+ return c.json({ error: "Collection not found" }, 404);
1609
+ }
1610
+ const collectionById = Object.fromEntries(
1611
+ collections.map((entry) => [
1612
+ entry.id,
1613
+ {
1614
+ id: entry.id,
1615
+ name: entry.name,
1616
+ display_name: entry.display_name
1617
+ }
1618
+ ])
1619
+ );
1620
+ const collectionIds = collections.map((entry) => entry.id);
1621
+ if (id) {
1622
+ const idPlaceholders = collectionIds.map(() => "?").join(", ");
1623
+ const itemStmt = db.prepare(`
1624
+ SELECT id, title, slug, collection_id
1625
+ FROM content
1626
+ WHERE id = ? AND collection_id IN (${idPlaceholders})
1627
+ LIMIT 1
1628
+ `);
1629
+ const item = await itemStmt.bind(id, ...collectionIds).first();
1630
+ if (!item) {
1631
+ return c.json({ error: "Reference not found" }, 404);
1632
+ }
1633
+ return c.json({
1634
+ data: {
1635
+ id: item.id,
1636
+ title: item.title,
1637
+ slug: item.slug,
1638
+ collection: collectionById[item.collection_id]
1639
+ }
1640
+ });
1641
+ }
1642
+ let stmt;
1643
+ let results;
1644
+ const listPlaceholders = collectionIds.map(() => "?").join(", ");
1645
+ const statusFilterValues = ["published"];
1646
+ const statusClause = ` AND status IN (${statusFilterValues.map(() => "?").join(", ")})`;
1647
+ if (search) {
1648
+ const searchParam = `%${search}%`;
1649
+ stmt = db.prepare(`
1650
+ SELECT id, title, slug, status, updated_at, collection_id
1651
+ FROM content
1652
+ WHERE collection_id IN (${listPlaceholders})
1653
+ AND (title LIKE ? OR slug LIKE ?)
1654
+ ${statusClause}
1655
+ ORDER BY updated_at DESC
1656
+ LIMIT ?
1657
+ `);
1658
+ const queryResults = await stmt.bind(...collectionIds, searchParam, searchParam, ...statusFilterValues, limit).all();
1659
+ results = queryResults.results;
1660
+ } else {
1661
+ stmt = db.prepare(`
1662
+ SELECT id, title, slug, status, updated_at, collection_id
1663
+ FROM content
1664
+ WHERE collection_id IN (${listPlaceholders})
1665
+ ${statusClause}
1666
+ ORDER BY updated_at DESC
1667
+ LIMIT ?
1668
+ `);
1669
+ const queryResults = await stmt.bind(...collectionIds, ...statusFilterValues, limit).all();
1670
+ results = queryResults.results;
1671
+ }
1672
+ const items = (results || []).map((row) => ({
1673
+ id: row.id,
1674
+ title: row.title,
1675
+ slug: row.slug,
1676
+ status: row.status,
1677
+ updated_at: row.updated_at ? Number(row.updated_at) : null,
1678
+ collection: collectionById[row.collection_id]
1679
+ }));
1680
+ return c.json({
1681
+ data: items,
1682
+ count: items.length
1683
+ });
1684
+ } catch (error) {
1685
+ console.error("Error fetching reference options:", error);
1686
+ return c.json({ error: "Failed to fetch references" }, 500);
1687
+ }
1688
+ });
1588
1689
  adminApiRoutes.post("/collections", async (c) => {
1589
1690
  try {
1590
1691
  const contentType = c.req.header("Content-Type");
@@ -1754,7 +1855,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1754
1855
  });
1755
1856
  adminApiRoutes.get("/migrations/status", async (c) => {
1756
1857
  try {
1757
- const { MigrationService: MigrationService2 } = await import('./migrations-NIEUFG44.cjs');
1858
+ const { MigrationService: MigrationService2 } = await import('./migrations-LEMFV2ND.cjs');
1758
1859
  const db = c.env.DB;
1759
1860
  const migrationService = new MigrationService2(db);
1760
1861
  const status = await migrationService.getMigrationStatus();
@@ -1779,7 +1880,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1779
1880
  error: "Unauthorized. Admin access required."
1780
1881
  }, 403);
1781
1882
  }
1782
- const { MigrationService: MigrationService2 } = await import('./migrations-NIEUFG44.cjs');
1883
+ const { MigrationService: MigrationService2 } = await import('./migrations-LEMFV2ND.cjs');
1783
1884
  const db = c.env.DB;
1784
1885
  const migrationService = new MigrationService2(db);
1785
1886
  const result = await migrationService.runPendingMigrations();
@@ -1798,7 +1899,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1798
1899
  });
1799
1900
  adminApiRoutes.get("/migrations/validate", async (c) => {
1800
1901
  try {
1801
- const { MigrationService: MigrationService2 } = await import('./migrations-NIEUFG44.cjs');
1902
+ const { MigrationService: MigrationService2 } = await import('./migrations-LEMFV2ND.cjs');
1802
1903
  const db = c.env.DB;
1803
1904
  const migrationService = new MigrationService2(db);
1804
1905
  const validation = await migrationService.validateSchema();
@@ -1825,7 +1926,7 @@ function renderLoginPage(data, demoLoginActive = false) {
1825
1926
  <meta charset="UTF-8">
1826
1927
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1827
1928
  <title>Login - SonicJS AI</title>
1828
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
1929
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
1829
1930
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
1830
1931
  <script src="https://cdn.tailwindcss.com"></script>
1831
1932
  <script>
@@ -1873,8 +1974,8 @@ function renderLoginPage(data, demoLoginActive = false) {
1873
1974
  <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
1874
1975
  <div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
1875
1976
  <!-- Alerts -->
1876
- ${data.error ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
1877
- ${data.message ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.message })}</div>` : ""}
1977
+ ${data.error ? `<div class="mb-6">${chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
1978
+ ${data.message ? `<div class="mb-6">${chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.message })}</div>` : ""}
1878
1979
 
1879
1980
  <!-- Form Response (HTMX target) -->
1880
1981
  <div id="form-response" class="mb-6"></div>
@@ -2002,7 +2103,7 @@ function renderRegisterPage(data) {
2002
2103
  <meta charset="UTF-8">
2003
2104
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2004
2105
  <title>Register - SonicJS AI</title>
2005
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
2106
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
2006
2107
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
2007
2108
  <script src="https://cdn.tailwindcss.com"></script>
2008
2109
  <script>
@@ -2025,42 +2126,20 @@ function renderRegisterPage(data) {
2025
2126
  <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
2026
2127
  <!-- Logo Section -->
2027
2128
  <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
2028
- <div class="mx-auto w-64 mb-8">
2029
- <svg class="w-full h-auto" viewBox="380 1300 2250 400" aria-hidden="true">
2030
- <path fill="#F1F2F2" d="M476.851,1404.673h168.536c4.714,0,8.695-1.618,11.944-4.866c3.241-3.241,4.866-7.222,4.866-11.943 c0-2.357-0.443-4.569-1.327-6.636c-0.885-2.06-2.067-3.829-3.539-5.308c-1.479-1.472-3.249-2.654-5.308-3.538 c-2.067-0.885-4.279-1.327-6.635-1.327H476.851c-20.057,0-37.158,7.154-51.313,21.454c-14.155,14.308-21.233,31.483-21.233,51.534 c0,20.058,7.078,37.234,21.233,51.534c14.155,14.308,31.255,21.454,51.313,21.454h112.357c10.907,0,20.196,3.837,27.868,11.502 c7.666,7.672,11.502,16.885,11.502,27.646c0,10.769-3.836,19.982-11.502,27.647c-7.672,7.673-16.961,11.502-27.868,11.502H421.115 c-4.721,0-8.702,1.624-11.944,4.865c-3.248,3.249-4.866,7.23-4.866,11.944c0,3.248,0.733,6.123,2.212,8.626 c1.472,2.509,3.462,4.499,5.971,5.972c2.502,1.472,5.378,2.212,8.626,2.212h168.094c20.052,0,37.227-7.078,51.534-21.234 c14.3-14.155,21.454-31.331,21.454-51.534c0-20.196-7.154-37.379-21.454-51.534c-14.308-14.156-31.483-21.234-51.534-21.234 H476.851c-10.616,0-19.76-3.905-27.426-11.721c-7.672-7.811-11.501-17.101-11.501-27.87c0-10.761,3.829-19.975,11.501-27.647 C457.091,1408.508,466.235,1404.673,476.851,1404.673z"></path>
2031
- <path fill="#F1F2F2" d="M974.78,1398.211c-5.016,6.574-10.034,13.146-15.048,19.721c-1.828,2.398-3.657,4.796-5.487,7.194 c1.994,1.719,3.958,3.51,5.873,5.424c18.724,18.731,28.089,41.216,28.089,67.459c0,26.251-9.366,48.658-28.089,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-9.848,0-19.156-1.308-27.923-3.923l-4.185,3.354 c-8.587,6.885-17.154,13.796-25.725,20.702c17.52,8.967,36.86,13.487,58.054,13.487c35.533,0,65.91-12.608,91.124-37.821 c25.214-25.215,37.821-55.584,37.821-91.125c0-35.534-12.607-65.911-37.821-91.126 C981.004,1403.663,977.926,1400.854,974.78,1398.211z"></path>
2032
- <path fill="#F1F2F2" d="M1364.644,1439.619c-4.72,0-8.702,1.624-11.943,4.865c-3.249,3.249-4.866,7.23-4.866,11.944v138.014 l-167.651-211.003c-0.297-0.586-0.74-1.03-1.327-1.326c-4.721-4.714-10.249-7.742-16.588-9.069 c-6.346-1.326-12.608-0.732-18.801,1.77c-6.192,2.509-11.059,6.49-14.598,11.944c-3.539,5.46-5.308,11.577-5.308,18.357v208.348 c0,4.721,1.618,8.703,4.866,11.944c3.241,3.241,7.222,4.865,11.943,4.865c2.945,0,5.751-0.738,8.405-2.211 c2.654-1.472,4.713-3.463,6.193-5.971c1.473-2.503,2.212-5.378,2.212-8.627v-205.251l166.325,209.675 c2.06,2.654,4.423,4.865,7.078,6.635c5.308,3.829,11.349,5.75,18.137,5.75c5.308,0,10.464-1.182,15.482-3.538 c3.539-1.769,6.56-4.127,9.069-7.078c2.502-2.945,4.491-6.338,5.971-10.175c1.473-3.829,2.212-7.664,2.212-11.501v-141.552 c0-4.714-1.624-8.695-4.865-11.944C1373.339,1441.243,1369.359,1439.619,1364.644,1439.619z"></path>
2033
- <path fill="#F1F2F2" d="M1508.406,1432.983c-2.654-1.472-5.46-2.212-8.404-2.212c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.395-4.865,7.3-4.865,11.723v163.228c0,4.721,1.616,8.702,4.865,11.943c3.241,3.249,7.223,4.866,11.944,4.866 c2.944,0,5.751-0.732,8.404-2.212c2.655-1.472,4.714-3.539,6.193-6.194c1.473-2.654,2.213-5.453,2.213-8.404V1447.58 c0-2.945-0.74-5.75-2.213-8.405C1513.12,1436.522,1511.06,1434.462,1508.406,1432.983z"></path>
2034
- <path fill="#F1F2F2" d="M1499.78,1367.957c-4.575,0-8.481,1.625-11.722,4.866c-3.249,3.249-4.865,7.23-4.865,11.943 c0,2.951,0.732,5.75,2.212,8.405c1.472,2.654,3.463,4.721,5.971,6.193c2.503,1.479,5.378,2.212,8.627,2.212 c4.423,0,8.328-1.618,11.721-4.865c3.387-3.243,5.088-7.224,5.088-11.944c0-4.713-1.701-8.694-5.088-11.943 C1508.33,1369.582,1504.349,1367.957,1499.78,1367.957z"></path>
2035
- <path fill="#F1F2F2" d="M1859.627,1369.727H1747.27c-35.388,0-65.69,12.607-90.904,37.821 c-25.213,25.215-37.82,55.591-37.82,91.125c0,35.54,12.607,65.911,37.82,91.125c25.215,25.215,55.516,37.821,90.904,37.821h56.178 c4.714,0,8.695-1.618,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943c0-4.714-1.624-8.695-4.865-11.943 c-3.249-3.243-7.23-4.866-11.944-4.866h-56.178c-26.251,0-48.659-9.359-67.237-28.09c-18.579-18.723-27.868-41.207-27.868-67.459 c0-26.243,9.29-48.659,27.868-67.237c18.579-18.579,40.987-27.868,67.237-27.868h112.357c4.714,0,8.696-1.693,11.944-5.087 c3.241-3.387,4.865-7.368,4.865-11.943c0-4.569-1.624-8.475-4.865-11.723C1868.322,1371.351,1864.341,1369.727,1859.627,1369.727z "></path>
2036
- <path fill="#06b6d4" d="M2219.256,1371.054h-112.357c-4.423,0-8.336,1.624-11.723,4.865c-3.393,3.249-5.087,7.23-5.087,11.944 c0,4.721,1.694,8.702,5.087,11.943c3.387,3.249,7.3,4.866,11.723,4.866h95.547v95.105c0,26.251-9.365,48.659-28.088,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-26.251,0-48.659-9.289-67.237-27.868c-18.579-18.579-27.868-40.987-27.868-67.237 c0-4.713-1.701-8.771-5.088-12.165c-3.393-3.387-7.374-5.087-11.943-5.087c-4.575,0-8.481,1.7-11.722,5.087 c-3.249,3.393-4.865,7.451-4.865,12.165c0,35.388,12.607,65.69,37.82,90.904c25.215,25.213,55.584,37.82,91.126,37.82 c35.532,0,65.91-12.607,91.125-37.82c25.214-25.215,37.82-55.516,37.82-90.904v-111.915c0-4.714-1.624-8.695-4.865-11.944 C2227.951,1372.678,2223.971,1371.054,2219.256,1371.054z"></path>
2037
- <path fill="#06b6d4" d="M2574.24,1502.875c-14.306-14.156-31.483-21.234-51.533-21.234H2410.35 c-10.617,0-19.762-3.829-27.426-11.501c-7.672-7.664-11.501-16.954-11.501-27.868c0-10.907,3.829-20.196,11.501-27.868 c7.664-7.664,16.809-11.501,27.426-11.501h112.357c4.714,0,8.695-1.617,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943 c0-4.714-1.624-8.695-4.865-11.944c-3.249-3.241-7.23-4.865-11.944-4.865H2410.35c-20.058,0-37.158,7.154-51.313,21.454 c-14.156,14.308-21.232,31.483-21.232,51.534c0,20.058,7.077,37.234,21.232,51.534c14.156,14.308,31.255,21.454,51.313,21.454 h112.357c7.078,0,13.637,1.77,19.684,5.308c6.042,3.539,10.838,8.336,14.377,14.377c3.538,6.047,5.307,12.607,5.307,19.685 c0,10.616-3.835,19.76-11.501,27.425c-7.672,7.673-16.961,11.502-27.868,11.502h-168.094c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.393-4.865,7.374-4.865,11.943c0,4.576,1.616,8.481,4.865,11.723c3.241,3.249,7.223,4.866,11.944,4.866h168.094 c20.051,0,37.227-7.078,51.533-21.234c14.302-14.155,21.454-31.331,21.454-51.534 C2595.695,1534.213,2588.542,1517.03,2574.24,1502.875z"></path>
2038
- <path fill="#06b6d4" d="M854.024,1585.195l20.001-16.028c16.616-13.507,33.04-27.265,50.086-40.251 c1.13-0.861,2.9-1.686,2.003-3.516c-0.843-1.716-2.481-2.302-4.484-2.123c-8.514,0.765-17.016-0.538-25.537-0.353 c-1.124,0.024-2.768,0.221-3.163-1.25c-0.371-1.369,1.088-2.063,1.919-2.894c6.26-6.242,12.574-12.43,18.816-18.691 c9.303-9.327,18.565-18.714,27.851-28.066c1.848-1.859,3.701-3.713,5.549-5.572c2.655-2.661,5.309-5.315,7.958-7.982 c0.574-0.579,1.259-1.141,1.246-1.94c-0.004-0.257-0.078-0.538-0.254-0.853c-0.556-0.981-1.441-1.1-2.469-0.957 c-0.658,0.096-1.315,0.185-1.973,0.275c-3.844,0.538-7.689,1.076-11.533,1.608c-3.641,0.505-7.281,1.02-10.922,1.529 c-4.162,0.582-8.324,1.158-12.486,1.748c-1.142,0.161-2.409,1.662-3.354,0.508c-0.419-0.508-0.431-1.028-0.251-1.531 c0.269-0.741,0.957-1.441,1.387-2.021c3.414-4.58,6.882-9.124,10.356-13.662c1.74-2.272,3.48-4.544,5.214-6.822 c4.682-6.141,9.369-12.281,14.051-18.422c0.09-0.119,0.181-0.237,0.271-0.355c6.848-8.98,13.7-17.958,20.553-26.936 c0.488-0.64,0.977-1.28,1.465-1.92c2.159-2.828,4.315-5.658,6.476-8.486c4.197-5.501,8.454-10.954,12.67-16.442 c0.263-0.347,0.538-0.718,0.717-1.106c0.269-0.586,0.299-1.196-0.335-1.776c-0.825-0.753-1.8-0.15-2.595,0.419 c-0.67,0.472-1.333,0.957-1.955,1.489c-2.206,1.889-4.401,3.797-6.595,5.698c-3.958,3.438-7.922,6.876-11.976,10.194 c-2.443,2.003-4.865,4.028-7.301,6.038c-18.689-10.581-39.53-15.906-62.549-15.906c-35.54,0-65.911,12.607-91.125,37.82 c-25.214,25.215-37.821,55.592-37.821,91.126c0,35.54,12.607,65.91,37.821,91.125c4.146,4.146,8.445,7.916,12.87,11.381 c-9.015,11.14-18.036,22.277-27.034,33.429c-1.208,1.489-3.755,3.151-2.745,4.891c0.078,0.144,0.173,0.281,0.305,0.425 c1.321,1.429,3.492-1.303,4.933-2.457c6.673-5.333,13.333-10.685,19.982-16.042c3.707-2.984,7.417-5.965,11.124-8.952 c1.474-1.188,2.951-2.373,4.425-3.561c6.41-5.164,12.816-10.333,19.238-15.481L854.024,1585.195z M797.552,1498.009 c0-26.243,9.29-48.728,27.868-67.459c18.579-18.723,40.987-28.089,67.238-28.089c12.273,0,23.712,2.075,34.34,6.171 c-3.37,2.905-6.734,5.816-10.069,8.762c-6.075,5.351-12.365,10.469-18.667,15.564c-4.179,3.378-8.371,6.744-12.514,10.164 c-7.54,6.23-15.037,12.52-22.529,18.804c-7.091,5.955-14.182,11.904-21.19,17.949c-1.136,0.974-3.055,1.907-2.135,3.94 c0.831,1.836,2.774,1.417,4.341,1.578l12.145-0.599l14.151-0.698c1.031-0.102,2.192-0.257,2.89,0.632 c0.034,0.044,0.073,0.078,0.106,0.127c1.017,1.561-0.67,2.105-1.387,2.942c-6.308,7.318-12.616,14.637-18.978,21.907 c-8.161,9.339-16.353,18.649-24.544,27.958c-2.146,2.433-4.275,4.879-6.422,7.312c-1.034,1.172-2.129,2.272-1.238,3.922 c0.933,1.728,2.685,1.752,4.323,1.602c4.134-0.367,8.263-0.489,12.396-0.492c0.242,0,0.485-0.005,0.728-0.004 c2.711,0.009,5.422,0.068,8.134,0.145c2.582,0.074,5.166,0.165,7.752,0.249c0.275,1.62-0.879,2.356-1.62,3.259 c-1.333,1.626-2.667,3.247-4,4.867c-4.315,5.252-8.62,10.514-12.928,15.772c-3.562-2.725-7.007-5.733-10.324-9.051 C806.842,1546.667,797.552,1524.26,797.552,1498.009z"></path>
2129
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-white">
2130
+ <svg class="h-7 w-7 text-zinc-950" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
2039
2132
  </svg>
2040
2133
  </div>
2041
- <h2 class="mt-6 text-xl font-medium text-white">${data.isSetup ? "Welcome to SonicJS" : "Create Account"}</h2>
2042
- ${data.isSetup ? `<p class="mt-2 text-sm text-zinc-400">Create your admin account to get started.</p>` : `<p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>`}
2134
+ <h1 class="mt-6 text-3xl font-semibold tracking-tight text-white">SonicJS AI</h1>
2135
+ <p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>
2043
2136
  </div>
2044
2137
 
2045
2138
  <!-- Form Container -->
2046
2139
  <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
2047
2140
  <div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
2048
- <!-- Setup Banner -->
2049
- ${data.isSetup ? `
2050
- <div class="mb-6 rounded-lg bg-blue-500/10 p-4 ring-1 ring-blue-500/20">
2051
- <div class="flex items-start gap-x-3">
2052
- <svg class="h-5 w-5 text-blue-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2053
- <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"/>
2054
- </svg>
2055
- <div class="flex-1">
2056
- <p class="text-sm font-medium text-blue-300">First-time Setup</p>
2057
- <p class="text-sm text-blue-400/80 mt-1">This account will be the administrator with full access to manage your SonicJS installation.</p>
2058
- </div>
2059
- </div>
2060
- </div>
2061
- ` : ""}
2062
2141
  <!-- Alerts -->
2063
- ${data.error ? `<div class="mb-6">${chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
2142
+ ${data.error ? `<div class="mb-6">${chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error })}</div>` : ""}
2064
2143
 
2065
2144
  <!-- Form -->
2066
2145
  <form
@@ -2173,7 +2252,6 @@ function renderRegisterPage(data) {
2173
2252
  </html>
2174
2253
  `;
2175
2254
  }
2176
- var adminExistsCache = null;
2177
2255
  async function isRegistrationEnabled(db) {
2178
2256
  try {
2179
2257
  const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
@@ -2195,21 +2273,6 @@ async function isFirstUserRegistration(db) {
2195
2273
  return false;
2196
2274
  }
2197
2275
  }
2198
- async function checkAdminUserExists(db) {
2199
- if (adminExistsCache !== null) {
2200
- return adminExistsCache;
2201
- }
2202
- try {
2203
- const result = await db.prepare("SELECT id FROM users WHERE role = ?").bind("admin").first();
2204
- adminExistsCache = !!result;
2205
- return adminExistsCache;
2206
- } catch {
2207
- return false;
2208
- }
2209
- }
2210
- function setAdminExists() {
2211
- adminExistsCache = true;
2212
- }
2213
2276
  var baseRegistrationSchema = zod.z.object({
2214
2277
  email: zod.z.string().email("Valid email is required"),
2215
2278
  password: zod.z.string().min(8, "Password must be at least 8 characters"),
@@ -2271,11 +2334,8 @@ authRoutes.get("/register", async (c) => {
2271
2334
  }
2272
2335
  }
2273
2336
  const error = c.req.query("error");
2274
- const isSetup = c.req.query("setup") === "true";
2275
2337
  const pageData = {
2276
- error: error || void 0,
2277
- isSetup: isSetup && isFirstUser
2278
- // Only show setup message if truly first user
2338
+ error: error || void 0
2279
2339
  };
2280
2340
  return c.html(renderRegisterPage(pageData));
2281
2341
  });
@@ -2321,7 +2381,7 @@ authRoutes.post(
2321
2381
  if (existingUser) {
2322
2382
  return c.json({ error: "User with this email or username already exists" }, 400);
2323
2383
  }
2324
- const passwordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(password);
2384
+ const passwordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(password);
2325
2385
  const userId = crypto.randomUUID();
2326
2386
  const now = /* @__PURE__ */ new Date();
2327
2387
  await db.prepare(`
@@ -2341,7 +2401,7 @@ authRoutes.post(
2341
2401
  now.getTime(),
2342
2402
  now.getTime()
2343
2403
  ).run();
2344
- const token = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer");
2404
+ const token = await chunkE2BXLXPW_cjs.AuthManager.generateToken(userId, normalizedEmail, "viewer");
2345
2405
  cookie.setCookie(c, "auth_token", token, {
2346
2406
  httpOnly: true,
2347
2407
  secure: true,
@@ -2394,11 +2454,11 @@ authRoutes.post("/login", async (c) => {
2394
2454
  if (!user) {
2395
2455
  return c.json({ error: "Invalid email or password" }, 401);
2396
2456
  }
2397
- const isValidPassword = await chunkYYV3XQOQ_cjs.AuthManager.verifyPassword(password, user.password_hash);
2457
+ const isValidPassword = await chunkE2BXLXPW_cjs.AuthManager.verifyPassword(password, user.password_hash);
2398
2458
  if (!isValidPassword) {
2399
2459
  return c.json({ error: "Invalid email or password" }, 401);
2400
2460
  }
2401
- const token = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2461
+ const token = await chunkE2BXLXPW_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2402
2462
  cookie.setCookie(c, "auth_token", token, {
2403
2463
  httpOnly: true,
2404
2464
  secure: true,
@@ -2447,7 +2507,7 @@ authRoutes.get("/logout", (c) => {
2447
2507
  });
2448
2508
  return c.redirect("/auth/login?message=You have been logged out successfully");
2449
2509
  });
2450
- authRoutes.get("/me", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
2510
+ authRoutes.get("/me", chunkE2BXLXPW_cjs.requireAuth(), async (c) => {
2451
2511
  try {
2452
2512
  const user = c.get("user");
2453
2513
  if (!user) {
@@ -2464,13 +2524,13 @@ authRoutes.get("/me", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
2464
2524
  return c.json({ error: "Failed to get user" }, 500);
2465
2525
  }
2466
2526
  });
2467
- authRoutes.post("/refresh", chunkYYV3XQOQ_cjs.requireAuth(), async (c) => {
2527
+ authRoutes.post("/refresh", chunkE2BXLXPW_cjs.requireAuth(), async (c) => {
2468
2528
  try {
2469
2529
  const user = c.get("user");
2470
2530
  if (!user) {
2471
2531
  return c.json({ error: "Not authenticated" }, 401);
2472
2532
  }
2473
- const token = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(user.userId, user.email, user.role);
2533
+ const token = await chunkE2BXLXPW_cjs.AuthManager.generateToken(user.userId, user.email, user.role);
2474
2534
  cookie.setCookie(c, "auth_token", token, {
2475
2535
  httpOnly: true,
2476
2536
  secure: true,
@@ -2530,7 +2590,7 @@ authRoutes.post("/register/form", async (c) => {
2530
2590
  </div>
2531
2591
  `);
2532
2592
  }
2533
- const passwordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(password);
2593
+ const passwordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(password);
2534
2594
  const role = isFirstUser ? "admin" : "viewer";
2535
2595
  const userId = crypto.randomUUID();
2536
2596
  const now = /* @__PURE__ */ new Date();
@@ -2550,10 +2610,7 @@ authRoutes.post("/register/form", async (c) => {
2550
2610
  now.getTime(),
2551
2611
  now.getTime()
2552
2612
  ).run();
2553
- if (isFirstUser) {
2554
- setAdminExists();
2555
- }
2556
- const token = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(userId, normalizedEmail, role);
2613
+ const token = await chunkE2BXLXPW_cjs.AuthManager.generateToken(userId, normalizedEmail, role);
2557
2614
  cookie.setCookie(c, "auth_token", token, {
2558
2615
  httpOnly: true,
2559
2616
  secure: false,
@@ -2605,7 +2662,7 @@ authRoutes.post("/login/form", async (c) => {
2605
2662
  </div>
2606
2663
  `);
2607
2664
  }
2608
- const isValidPassword = await chunkYYV3XQOQ_cjs.AuthManager.verifyPassword(password, user.password_hash);
2665
+ const isValidPassword = await chunkE2BXLXPW_cjs.AuthManager.verifyPassword(password, user.password_hash);
2609
2666
  if (!isValidPassword) {
2610
2667
  return c.html(html.html`
2611
2668
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
@@ -2613,7 +2670,7 @@ authRoutes.post("/login/form", async (c) => {
2613
2670
  </div>
2614
2671
  `);
2615
2672
  }
2616
- const token = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2673
+ const token = await chunkE2BXLXPW_cjs.AuthManager.generateToken(user.id, user.email, user.role);
2617
2674
  cookie.setCookie(c, "auth_token", token, {
2618
2675
  httpOnly: true,
2619
2676
  secure: false,
@@ -2672,9 +2729,8 @@ authRoutes.post("/seed-admin", async (c) => {
2672
2729
  `).run();
2673
2730
  const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
2674
2731
  if (existingAdmin) {
2675
- const passwordHash2 = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword("sonicjs!");
2732
+ const passwordHash2 = await chunkE2BXLXPW_cjs.AuthManager.hashPassword("sonicjs!");
2676
2733
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
2677
- setAdminExists();
2678
2734
  return c.json({
2679
2735
  message: "Admin user already exists (password updated)",
2680
2736
  user: {
@@ -2685,7 +2741,7 @@ authRoutes.post("/seed-admin", async (c) => {
2685
2741
  }
2686
2742
  });
2687
2743
  }
2688
- const passwordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword("sonicjs!");
2744
+ const passwordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword("sonicjs!");
2689
2745
  const userId = "admin-user-id";
2690
2746
  const now = Date.now();
2691
2747
  const adminEmail = "admin@sonicjs.com".toLowerCase();
@@ -2705,7 +2761,6 @@ authRoutes.post("/seed-admin", async (c) => {
2705
2761
  now,
2706
2762
  now
2707
2763
  ).run();
2708
- setAdminExists();
2709
2764
  return c.json({
2710
2765
  message: "Admin user created successfully",
2711
2766
  user: {
@@ -2906,7 +2961,7 @@ authRoutes.post("/accept-invitation", async (c) => {
2906
2961
  if (existingUsername) {
2907
2962
  return c.json({ error: "Username is already taken" }, 400);
2908
2963
  }
2909
- const passwordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(password);
2964
+ const passwordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(password);
2910
2965
  const updateStmt = db.prepare(`
2911
2966
  UPDATE users SET
2912
2967
  username = ?,
@@ -2925,7 +2980,7 @@ authRoutes.post("/accept-invitation", async (c) => {
2925
2980
  Date.now(),
2926
2981
  invitedUser.id
2927
2982
  ).run();
2928
- const authToken = await chunkYYV3XQOQ_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role);
2983
+ const authToken = await chunkE2BXLXPW_cjs.AuthManager.generateToken(invitedUser.id, invitedUser.email, invitedUser.role);
2929
2984
  cookie.setCookie(c, "auth_token", authToken, {
2930
2985
  httpOnly: true,
2931
2986
  secure: true,
@@ -3155,7 +3210,7 @@ authRoutes.post("/reset-password", async (c) => {
3155
3210
  if (Date.now() > user.password_reset_expires) {
3156
3211
  return c.json({ error: "Reset token has expired" }, 400);
3157
3212
  }
3158
- const newPasswordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(password);
3213
+ const newPasswordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(password);
3159
3214
  try {
3160
3215
  const historyStmt = db.prepare(`
3161
3216
  INSERT INTO password_history (id, user_id, password_hash, created_at)
@@ -3413,9 +3468,166 @@ app.post("/test-cleanup/content", async (c) => {
3413
3468
  var test_cleanup_default = app;
3414
3469
 
3415
3470
  // src/templates/pages/admin-content-form.template.ts
3416
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
3471
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
3472
+
3473
+ // src/templates/components/drag-sortable.template.ts
3474
+ function getDragSortableScript() {
3475
+ return `
3476
+ <script>
3477
+ if (!window.__sonicDragSortableInit) {
3478
+ window.__sonicDragSortableInit = true;
3479
+
3480
+ window.initializeDragSortable = function(container, options) {
3481
+ if (!container || container.dataset.dragSortableInit === 'true') {
3482
+ return;
3483
+ }
3484
+
3485
+ container.dataset.dragSortableInit = 'true';
3486
+ const itemSelector = options && options.itemSelector ? options.itemSelector : '.sortable-item';
3487
+ const handleSelector = options && options.handleSelector ? options.handleSelector : '[data-action="drag-handle"]';
3488
+ const onUpdate = options && typeof options.onUpdate === 'function' ? options.onUpdate : function() {};
3489
+ let activeDragItem = null;
3490
+
3491
+ const getDragAfterElement = function(list, y) {
3492
+ const items = Array.from(list.querySelectorAll(itemSelector + ':not(.is-dragging)'));
3493
+ let closest = { offset: Number.NEGATIVE_INFINITY, element: null };
3494
+ items.forEach(function(item) {
3495
+ const box = item.getBoundingClientRect();
3496
+ const offset = y - box.top - box.height / 2;
3497
+ if (offset < 0 && offset > closest.offset) {
3498
+ closest = { offset: offset, element: item };
3499
+ }
3500
+ });
3501
+ return closest.element;
3502
+ };
3503
+
3504
+ const activateDragItem = function(event) {
3505
+ const target = event.target;
3506
+ if (!(target instanceof Element)) return;
3507
+ const handle = target.closest(handleSelector);
3508
+ if (!handle) return;
3509
+ const item = handle.closest(itemSelector);
3510
+ if (!item) return;
3511
+ activeDragItem = item;
3512
+ };
3513
+
3514
+ const clearActiveDragItem = function() {
3515
+ activeDragItem = null;
3516
+ };
3517
+
3518
+ container.addEventListener('pointerdown', activateDragItem);
3519
+ container.addEventListener('mousedown', activateDragItem);
3520
+ container.addEventListener('pointerup', clearActiveDragItem);
3521
+ container.addEventListener('mouseup', clearActiveDragItem);
3522
+
3523
+ container.addEventListener('dragstart', function(event) {
3524
+ const target = event.target;
3525
+ if (!(target instanceof Element)) return;
3526
+ const item = target.closest(itemSelector);
3527
+ if (!item || item !== activeDragItem) {
3528
+ event.preventDefault();
3529
+ return;
3530
+ }
3531
+ item.classList.add('is-dragging');
3532
+ if (event.dataTransfer) {
3533
+ event.dataTransfer.setData('text/plain', '');
3534
+ }
3535
+ });
3536
+
3537
+ container.addEventListener('dragend', function(event) {
3538
+ const target = event.target;
3539
+ if (target instanceof Element) {
3540
+ const item = target.closest(itemSelector);
3541
+ if (item) {
3542
+ item.classList.remove('is-dragging');
3543
+ }
3544
+ }
3545
+ activeDragItem = null;
3546
+ onUpdate();
3547
+ });
3548
+
3549
+ container.addEventListener('dragover', function(event) {
3550
+ event.preventDefault();
3551
+ const dragging = container.querySelector(itemSelector + '.is-dragging');
3552
+ if (!dragging) return;
3553
+ const afterElement = getDragAfterElement(container, event.clientY);
3554
+ if (afterElement === null) {
3555
+ container.appendChild(dragging);
3556
+ } else {
3557
+ container.insertBefore(dragging, afterElement);
3558
+ }
3559
+ });
3560
+
3561
+ container.addEventListener('drop', function() {
3562
+ onUpdate();
3563
+ });
3564
+ };
3565
+ }
3566
+ </script>
3567
+ `;
3568
+ }
3417
3569
 
3418
3570
  // src/templates/components/dynamic-field.template.ts
3571
+ function getReadFieldValueScript() {
3572
+ return `
3573
+ <script>
3574
+ if (!window.__sonicReadFieldValueInit) {
3575
+ window.__sonicReadFieldValueInit = true;
3576
+
3577
+ window.sonicReadFieldValue = function(fieldWrapper) {
3578
+ const fieldType = fieldWrapper.dataset.fieldType;
3579
+ const select = fieldWrapper.querySelector('select');
3580
+ const textarea = fieldWrapper.querySelector('textarea');
3581
+ const inputs = Array.from(fieldWrapper.querySelectorAll('input'));
3582
+ const checkbox = inputs.find((input) => input.type === 'checkbox');
3583
+ const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
3584
+ const hiddenInput = inputs.find((input) => input.type === 'hidden');
3585
+
3586
+ if (fieldType === 'object' || fieldType === 'array') {
3587
+ if (!hiddenInput) {
3588
+ return fieldType === 'array' ? [] : {};
3589
+ }
3590
+ const rawValue = hiddenInput.value || '';
3591
+ if (!rawValue.trim()) {
3592
+ return fieldType === 'array' ? [] : {};
3593
+ }
3594
+ try {
3595
+ return JSON.parse(rawValue);
3596
+ } catch {
3597
+ return fieldType === 'array' ? [] : {};
3598
+ }
3599
+ }
3600
+
3601
+ if (fieldType === 'boolean' && checkbox) {
3602
+ return checkbox.checked;
3603
+ }
3604
+
3605
+ if (select) {
3606
+ if (select.multiple) {
3607
+ return Array.from(select.selectedOptions).map((option) => option.value);
3608
+ }
3609
+ return select.value;
3610
+ }
3611
+
3612
+ if (fieldType === 'quill' || fieldType === 'media') {
3613
+ return hiddenInput ? hiddenInput.value : '';
3614
+ }
3615
+
3616
+ const textSource = textarea || nonHiddenInput || hiddenInput;
3617
+ if (!textSource) {
3618
+ return '';
3619
+ }
3620
+
3621
+ if (fieldType === 'number') {
3622
+ return textSource.value === '' ? null : Number(textSource.value);
3623
+ }
3624
+
3625
+ return textSource.value;
3626
+ };
3627
+ }
3628
+ </script>
3629
+ `;
3630
+ }
3419
3631
  function renderDynamicField(field, options = {}) {
3420
3632
  const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {}, collectionId = "", contentId = "" } = options;
3421
3633
  const opts = field.field_options || {};
@@ -3839,11 +4051,11 @@ function renderDynamicField(field, options = {}) {
3839
4051
  `;
3840
4052
  break;
3841
4053
  case "select":
3842
- const options2 = opts.options || [];
4054
+ const selectOptions = opts.options || [];
3843
4055
  const multiple = opts.multiple ? "multiple" : "";
3844
4056
  const selectedValues = Array.isArray(value) ? value : [value];
3845
4057
  fieldHTML = `
3846
- <select
4058
+ <select
3847
4059
  id="${fieldId}"
3848
4060
  name="${fieldName}${opts.multiple ? "[]" : ""}"
3849
4061
  class="${baseClasses} ${errorClasses}"
@@ -3852,7 +4064,7 @@ function renderDynamicField(field, options = {}) {
3852
4064
  ${disabled ? "disabled" : ""}
3853
4065
  >
3854
4066
  ${!required && !opts.multiple ? '<option value="">Choose an option...</option>' : ""}
3855
- ${options2.map((option) => {
4067
+ ${selectOptions.map((option) => {
3856
4068
  const optionValue = typeof option === "string" ? option : option.value;
3857
4069
  const optionLabel = typeof option === "string" ? option : option.label;
3858
4070
  const selected = selectedValues.includes(optionValue) ? "selected" : "";
@@ -3871,43 +4083,124 @@ function renderDynamicField(field, options = {}) {
3871
4083
  ` : ""}
3872
4084
  `;
3873
4085
  break;
4086
+ case "reference":
4087
+ let referenceCollections = [];
4088
+ if (Array.isArray(opts.collection)) {
4089
+ referenceCollections = opts.collection.filter(Boolean);
4090
+ } else if (typeof opts.collection === "string" && opts.collection) {
4091
+ referenceCollections = [opts.collection];
4092
+ }
4093
+ const referenceCollectionsAttr = referenceCollections.join(",");
4094
+ const hasReferenceCollection = referenceCollections.length > 0;
4095
+ const hasReferenceValue = Boolean(value);
4096
+ fieldHTML = `
4097
+ <div class="reference-field-container space-y-3" data-reference-field data-field-name="${escapeHtml2(fieldName)}" data-reference-collection="${escapeHtml2(referenceCollections[0] || "")}" data-reference-collections="${escapeHtml2(referenceCollectionsAttr)}">
4098
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(value)}">
4099
+ <div class="rounded-lg border border-zinc-200 bg-white/60 px-3 py-2 text-sm text-zinc-600 dark:border-white/10 dark:bg-white/5 dark:text-zinc-300" data-reference-display>
4100
+ ${hasReferenceCollection ? hasReferenceValue ? "Loading selection..." : "No reference selected." : "Reference collection not configured."}
4101
+ </div>
4102
+ <div class="flex flex-wrap gap-2">
4103
+ <button
4104
+ type="button"
4105
+ onclick="openReferenceSelector('${fieldId}')"
4106
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4107
+ ${hasReferenceCollection ? "" : "disabled"}
4108
+ >
4109
+ Select reference
4110
+ </button>
4111
+ <button
4112
+ type="button"
4113
+ onclick="clearReferenceField('${fieldId}')"
4114
+ class="inline-flex items-center justify-center rounded-lg border border-zinc-200 px-3 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/10"
4115
+ data-reference-clear
4116
+ ${hasReferenceValue ? "" : "disabled"}
4117
+ >
4118
+ Remove
4119
+ </button>
4120
+ </div>
4121
+ </div>
4122
+ `;
4123
+ break;
3874
4124
  case "media":
4125
+ const isMultiple = opts.multiple === true;
4126
+ const mediaValues = isMultiple && value ? Array.isArray(value) ? value : String(value).split(",").filter(Boolean) : [];
4127
+ const singleValue = !isMultiple ? value : "";
4128
+ const isVideoUrl = (url) => {
4129
+ const videoExtensions = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
4130
+ return videoExtensions.some((ext) => url.toLowerCase().endsWith(ext));
4131
+ };
4132
+ const renderMediaPreview = (url, alt, classes) => {
4133
+ if (isVideoUrl(url)) {
4134
+ return `<video src="${url}" class="${classes}" muted></video>`;
4135
+ }
4136
+ return `<img src="${url}" alt="${alt}" class="${classes}">`;
4137
+ };
3875
4138
  fieldHTML = `
3876
4139
  <div class="media-field-container">
3877
- <input type="hidden" id="${fieldId}" name="${fieldName}" value="${value}">
3878
- <div class="media-preview ${value ? "" : "hidden"}" id="${fieldId}-preview">
3879
- ${value ? `<img src="${value}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
3880
- </div>
4140
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${isMultiple ? mediaValues.join(",") : singleValue}" data-multiple="${isMultiple}">
4141
+
4142
+ ${isMultiple ? `
4143
+ <div class="media-preview-grid grid grid-cols-4 gap-2 mb-2 ${mediaValues.length === 0 ? "hidden" : ""}" id="${fieldId}-preview">
4144
+ ${mediaValues.map((url, idx) => `
4145
+ <div class="relative media-preview-item" data-url="${url}">
4146
+ ${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
4147
+ <button
4148
+ type="button"
4149
+ onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
4150
+ class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
4151
+ ${disabled ? "disabled" : ""}
4152
+ >
4153
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4154
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
4155
+ </svg>
4156
+ </button>
4157
+ </div>
4158
+ `).join("")}
4159
+ </div>
4160
+ ` : `
4161
+ <div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
4162
+ ${singleValue ? renderMediaPreview(singleValue, "Selected media", "w-32 h-32 object-cover rounded-lg border border-white/20") : ""}
4163
+ </div>
4164
+ `}
4165
+
3881
4166
  <div class="media-actions mt-2 space-x-2">
3882
4167
  <button
3883
4168
  type="button"
3884
- onclick="openMediaSelector('${fieldId}')"
4169
+ onclick="openMediaSelector('${fieldId}', ${isMultiple})"
3885
4170
  class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
3886
4171
  ${disabled ? "disabled" : ""}
3887
4172
  >
3888
4173
  <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3889
4174
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
3890
4175
  </svg>
3891
- Select Media
4176
+ ${isMultiple ? "Select Media (Multiple)" : "Select Media"}
3892
4177
  </button>
3893
- ${value ? `
4178
+ ${(isMultiple ? mediaValues.length > 0 : singleValue) ? `
3894
4179
  <button
3895
4180
  type="button"
3896
4181
  onclick="clearMediaField('${fieldId}')"
3897
4182
  class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
3898
4183
  ${disabled ? "disabled" : ""}
3899
4184
  >
3900
- Remove
4185
+ ${isMultiple ? "Clear All" : "Remove"}
3901
4186
  </button>
3902
4187
  ` : ""}
3903
4188
  </div>
3904
4189
  </div>
3905
4190
  `;
3906
4191
  break;
4192
+ case "object":
4193
+ return renderStructuredObjectField(field, options);
4194
+ case "array":
4195
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4196
+ if (itemsConfig.blocks && typeof itemsConfig.blocks === "object") {
4197
+ return renderBlocksField(field, options, baseClasses, errorClasses);
4198
+ }
4199
+ return renderStructuredArrayField(field, options);
3907
4200
  default:
3908
4201
  fieldHTML = `
3909
- <input
3910
- type="text"
4202
+ <input
4203
+ type="text"
3911
4204
  id="${fieldId}"
3912
4205
  name="${fieldName}"
3913
4206
  value="${escapeHtml2(value)}"
@@ -3957,77 +4250,825 @@ function renderFieldGroup(title, fields, collapsible = false) {
3957
4250
  </div>
3958
4251
  `;
3959
4252
  }
3960
- function escapeHtml2(text) {
3961
- if (typeof text !== "string") return String(text || "");
3962
- return text.replace(/[&<>"']/g, (char) => ({
3963
- "&": "&amp;",
3964
- "<": "&lt;",
3965
- ">": "&gt;",
3966
- '"': "&quot;",
3967
- "'": "&#39;"
3968
- })[char] || char);
3969
- }
4253
+ function renderBlocksField(field, options, baseClasses, errorClasses) {
4254
+ const { value = [], pluginStatuses = {} } = options;
4255
+ const opts = field.field_options || {};
4256
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4257
+ const blocks = normalizeBlockDefinitions(itemsConfig.blocks);
4258
+ const discriminator = typeof itemsConfig.discriminator === "string" && itemsConfig.discriminator ? itemsConfig.discriminator : "blockType";
4259
+ const blockValues = normalizeBlocksValue(value, discriminator);
4260
+ const fieldId = `field-${field.field_name}`;
4261
+ const fieldName = field.field_name;
4262
+ const emptyState = blockValues.length === 0 ? `
4263
+ <div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-blocks-empty>
4264
+ No blocks yet. Add your first block to get started.
4265
+ </div>
4266
+ ` : "";
4267
+ const blockOptions = blocks.map((block) => `<option value="${escapeHtml2(block.name)}">${escapeHtml2(block.label)}</option>`).join("");
4268
+ const blockItems = blockValues.map(
4269
+ (blockValue, index) => renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses)
4270
+ ).join("");
4271
+ const templates = blocks.map((block) => renderBlockTemplate(field, block, discriminator, pluginStatuses)).join("");
4272
+ return `
4273
+ <div
4274
+ class="blocks-field space-y-4"
4275
+ data-blocks='${escapeHtml2(JSON.stringify(blocks))}'
4276
+ data-blocks-discriminator="${escapeHtml2(discriminator)}"
4277
+ data-field-name="${escapeHtml2(fieldName)}"
4278
+ >
4279
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(blockValues))}">
3970
4280
 
3971
- // src/plugins/available/tinymce-plugin/index.ts
3972
- var builder = chunkAI2JJIJX_cjs.PluginBuilder.create({
3973
- name: "tinymce-plugin",
3974
- version: "1.0.0",
3975
- description: "Powerful WYSIWYG rich text editor for content creation"
3976
- });
3977
- builder.metadata({
3978
- author: {
3979
- name: "SonicJS Team",
3980
- email: "team@sonicjs.com"
3981
- },
3982
- license: "MIT",
3983
- compatibility: "^2.0.0"
3984
- });
3985
- builder.lifecycle({
3986
- activate: async () => {
3987
- console.info("\u2705 TinyMCE plugin activated");
3988
- },
3989
- deactivate: async () => {
3990
- console.info("\u274C TinyMCE plugin deactivated");
3991
- }
3992
- });
3993
- builder.build();
3994
- function getTinyMCEScript(apiKey = "no-api-key") {
3995
- return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
4281
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4282
+ <div class="flex-1">
4283
+ <select
4284
+ class="${baseClasses} ${errorClasses}"
4285
+ data-role="block-type-select"
4286
+ >
4287
+ <option value="">Choose a block...</option>
4288
+ ${blockOptions}
4289
+ </select>
4290
+ </div>
4291
+ <button
4292
+ type="button"
4293
+ data-action="add-block"
4294
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4295
+ >
4296
+ Add Block
4297
+ </button>
4298
+ </div>
4299
+
4300
+ <div class="space-y-4" data-blocks-list>
4301
+ ${blockItems || emptyState}
4302
+ </div>
4303
+
4304
+ ${templates}
4305
+ </div>
4306
+ ${getDragSortableScript()}
4307
+ ${getBlocksFieldScript()}
4308
+ `;
3996
4309
  }
3997
- function getTinyMCEInitScript(config) {
3998
- const skin = config?.skin || "oxide-dark";
3999
- const contentCss = skin.includes("dark") ? "dark" : "default";
4000
- const defaultHeight = config?.defaultHeight || 300;
4310
+ function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
4311
+ const { value = {}, pluginStatuses = {} } = options;
4312
+ const opts = field.field_options || {};
4313
+ const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
4314
+ const fieldId = `field-${field.field_name}`;
4315
+ const fieldName = field.field_name;
4316
+ const objectValue = normalizeStructuredObjectValue(value);
4317
+ const subfields = Object.entries(properties).map(
4318
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4319
+ field,
4320
+ propertyName,
4321
+ propertyConfig,
4322
+ objectValue,
4323
+ pluginStatuses,
4324
+ field.field_name
4325
+ )
4326
+ ).join("");
4001
4327
  return `
4002
- // Initialize TinyMCE for all richtext fields
4003
- function initializeTinyMCE() {
4004
- if (typeof tinymce !== 'undefined') {
4005
- // Find all textareas that need TinyMCE
4006
- document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
4007
- // Skip if already initialized
4008
- if (tinymce.get(textarea.id)) {
4009
- return;
4010
- }
4328
+ <div class="space-y-4" data-structured-object data-field-name="${escapeHtml2(fieldName)}">
4329
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(objectValue))}">
4330
+ <div class="space-y-4" data-structured-object-fields>
4331
+ ${subfields}
4332
+ </div>
4333
+ </div>
4334
+ ${getStructuredFieldScript()}
4335
+ `;
4336
+ }
4337
+ function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
4338
+ const { value = [], pluginStatuses = {} } = options;
4339
+ const opts = field.field_options || {};
4340
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4341
+ const fieldId = `field-${field.field_name}`;
4342
+ const fieldName = field.field_name;
4343
+ const arrayValue = normalizeStructuredArrayValue(value);
4344
+ const items = arrayValue.map(
4345
+ (itemValue, index) => renderStructuredArrayItem(field, itemsConfig, String(index), itemValue, pluginStatuses)
4346
+ ).join("");
4347
+ const emptyState = arrayValue.length === 0 ? `
4348
+ <div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-structured-empty>
4349
+ No items yet. Add the first item to get started.
4350
+ </div>
4351
+ ` : "";
4352
+ return `
4353
+ <div class="space-y-4" data-structured-array data-field-name="${escapeHtml2(fieldName)}">
4354
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(arrayValue))}">
4011
4355
 
4012
- // Get configuration from data attributes
4013
- const container = textarea.closest('.richtext-container');
4014
- const height = container?.dataset.height || ${defaultHeight};
4015
- const toolbar = container?.dataset.toolbar || 'full';
4356
+ <div class="flex items-center justify-between gap-3">
4357
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">
4358
+ ${escapeHtml2(opts.itemLabel || "Items")}
4359
+ </div>
4360
+ <button
4361
+ type="button"
4362
+ data-action="add-item"
4363
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
4364
+ >
4365
+ Add item
4366
+ </button>
4367
+ </div>
4016
4368
 
4017
- tinymce.init({
4018
- selector: '#' + textarea.id,
4019
- skin: '${skin}',
4020
- content_css: '${contentCss}',
4021
- height: parseInt(height),
4022
- menubar: false,
4023
- plugins: [
4024
- 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
4025
- 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
4026
- 'insertdatetime', 'media', 'table', 'help', 'wordcount'
4027
- ],
4028
- toolbar: toolbar === 'simple'
4029
- ? 'bold italic underline | bullist numlist | link'
4030
- : toolbar === 'minimal'
4369
+ <div class="space-y-4" data-structured-array-list>
4370
+ ${items || emptyState}
4371
+ </div>
4372
+
4373
+ <template data-structured-array-template>
4374
+ ${renderStructuredArrayItem(field, itemsConfig, "__INDEX__", {}, pluginStatuses)}
4375
+ </template>
4376
+ </div>
4377
+ ${getDragSortableScript()}
4378
+ ${getStructuredFieldScript()}
4379
+ `;
4380
+ }
4381
+ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses) {
4382
+ const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
4383
+ return `
4384
+ <div class="structured-array-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-array-index="${escapeHtml2(index)}" draggable="true">
4385
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4386
+ <div class="flex items-center gap-3">
4387
+ <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
4388
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4389
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4390
+ </svg>
4391
+ </div>
4392
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4393
+ Item <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-array-order-label></span>
4394
+ </div>
4395
+ </div>
4396
+ <div class="flex flex-wrap gap-2 text-xs">
4397
+ <button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item up" title="Move up">
4398
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4399
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4400
+ </svg>
4401
+ </button>
4402
+ <button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item down" title="Move down">
4403
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4404
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4405
+ </svg>
4406
+ </button>
4407
+ <button type="button" data-action="remove-item" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
4408
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4409
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 0 00-7.5 0"/>
4410
+ </svg>
4411
+ Delete item
4412
+ </button>
4413
+ </div>
4414
+ </div>
4415
+ <div class="mt-4 space-y-4" data-array-item-fields>
4416
+ ${itemFields}
4417
+ </div>
4418
+ </div>
4419
+ `;
4420
+ }
4421
+ function renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses) {
4422
+ const itemType = itemConfig?.type || "string";
4423
+ if (itemType === "object" && itemConfig?.properties && typeof itemConfig.properties === "object") {
4424
+ const fieldPrefix = `array-${field.field_name}-${index}`;
4425
+ return Object.entries(itemConfig.properties).map(
4426
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4427
+ field,
4428
+ propertyName,
4429
+ propertyConfig,
4430
+ itemValue || {},
4431
+ pluginStatuses,
4432
+ fieldPrefix
4433
+ )
4434
+ ).join("");
4435
+ }
4436
+ const normalizedField = normalizeBlockField(itemConfig, "Item");
4437
+ const fieldValue = itemValue ?? normalizedField.defaultValue ?? "";
4438
+ const fieldDefinition = {
4439
+ id: `array-${field.field_name}-${index}-value`,
4440
+ field_name: `array-${field.field_name}-${index}-value`,
4441
+ field_type: normalizedField.type,
4442
+ field_label: normalizedField.label,
4443
+ field_options: normalizedField.options,
4444
+ is_required: normalizedField.required};
4445
+ return `
4446
+ <div class="structured-subfield" data-structured-field="__value" data-field-type="${escapeHtml2(normalizedField.type)}">
4447
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4448
+ </div>
4449
+ `;
4450
+ }
4451
+ function renderStructuredSubfield(field, propertyName, propertyConfig, objectValue, pluginStatuses, fieldPrefix) {
4452
+ const normalizedField = normalizeBlockField(propertyConfig, propertyName);
4453
+ const fieldValue = objectValue?.[propertyName] ?? normalizedField.defaultValue ?? "";
4454
+ const fieldDefinition = {
4455
+ field_name: `${fieldPrefix}__${propertyName}`,
4456
+ field_type: normalizedField.type,
4457
+ field_label: normalizedField.label,
4458
+ field_options: normalizedField.options,
4459
+ is_required: normalizedField.required};
4460
+ return `
4461
+ <div class="structured-subfield" data-structured-field="${escapeHtml2(propertyName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4462
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4463
+ </div>
4464
+ `;
4465
+ }
4466
+ function normalizeStructuredObjectValue(value) {
4467
+ if (!value) return {};
4468
+ if (typeof value === "string") {
4469
+ try {
4470
+ const parsed = JSON.parse(value);
4471
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4472
+ } catch {
4473
+ return {};
4474
+ }
4475
+ }
4476
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4477
+ return {};
4478
+ }
4479
+ function normalizeStructuredArrayValue(value) {
4480
+ if (!value) return [];
4481
+ if (Array.isArray(value)) return value;
4482
+ if (typeof value === "string") {
4483
+ try {
4484
+ const parsed = JSON.parse(value);
4485
+ return Array.isArray(parsed) ? parsed : [];
4486
+ } catch {
4487
+ return [];
4488
+ }
4489
+ }
4490
+ return [];
4491
+ }
4492
+ function normalizeBlockDefinitions(rawBlocks) {
4493
+ if (!rawBlocks || typeof rawBlocks !== "object") return [];
4494
+ return Object.entries(rawBlocks).filter(([name, block]) => typeof name === "string" && block && typeof block === "object").map(([name, block]) => ({
4495
+ name,
4496
+ label: block.label || name,
4497
+ description: block.description,
4498
+ properties: block.properties && typeof block.properties === "object" ? block.properties : {}
4499
+ }));
4500
+ }
4501
+ function normalizeBlocksValue(value, discriminator) {
4502
+ const normalizeItem = (item) => {
4503
+ if (!item || typeof item !== "object") return null;
4504
+ if (item[discriminator]) return item;
4505
+ if (item.blockType && item.data && typeof item.data === "object") {
4506
+ return { [discriminator]: item.blockType, ...item.data };
4507
+ }
4508
+ return item;
4509
+ };
4510
+ const fromArray = (items) => items.map(normalizeItem).filter((item) => item && typeof item === "object");
4511
+ if (Array.isArray(value)) return fromArray(value);
4512
+ if (typeof value === "string" && value.trim()) {
4513
+ try {
4514
+ const parsed = JSON.parse(value);
4515
+ return Array.isArray(parsed) ? fromArray(parsed) : [];
4516
+ } catch {
4517
+ return [];
4518
+ }
4519
+ }
4520
+ return [];
4521
+ }
4522
+ function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
4523
+ return `
4524
+ <template data-block-template="${escapeHtml2(block.name)}">
4525
+ ${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
4526
+ </template>
4527
+ `;
4528
+ }
4529
+ function renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses) {
4530
+ const blockType = blockValue?.[discriminator] || blockValue?.blockType;
4531
+ const blockDefinition = blocks.find((block) => block.name === blockType);
4532
+ if (!blockDefinition) {
4533
+ return `
4534
+ <div class="rounded-lg border border-amber-200 bg-amber-50/50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200" data-block-raw="${escapeHtml2(JSON.stringify(blockValue || {}))}">
4535
+ Unknown block type: <strong>${escapeHtml2(String(blockType || "unknown"))}</strong>. This block will be preserved as-is.
4536
+ </div>
4537
+ `;
4538
+ }
4539
+ const data = blockValue && typeof blockValue === "object" ? Object.fromEntries(Object.entries(blockValue).filter(([key]) => key !== discriminator)) : {};
4540
+ return renderBlockCard(field, blockDefinition, discriminator, String(index), data, pluginStatuses);
4541
+ }
4542
+ function renderBlockCard(field, block, discriminator, index, data, pluginStatuses) {
4543
+ const blockFields = Object.entries(block.properties).map(([fieldName, fieldConfig]) => {
4544
+ if (fieldConfig?.type === "array" && fieldConfig?.items?.blocks) {
4545
+ return `
4546
+ <div class="rounded-lg border border-dashed border-amber-200 bg-amber-50/50 px-4 py-3 text-xs text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
4547
+ Nested blocks are not supported yet for "${escapeHtml2(fieldName)}".
4548
+ </div>
4549
+ `;
4550
+ }
4551
+ const normalizedField = normalizeBlockField(fieldConfig, fieldName);
4552
+ const fieldValue = data?.[fieldName] ?? normalizedField.defaultValue ?? "";
4553
+ const fieldDefinition = {
4554
+ id: `block-${field.field_name}-${index}-${fieldName}`,
4555
+ field_name: `block-${field.field_name}-${index}-${fieldName}`,
4556
+ field_type: normalizedField.type,
4557
+ field_label: normalizedField.label,
4558
+ field_options: normalizedField.options,
4559
+ is_required: normalizedField.required};
4560
+ return `
4561
+ <div class="blocks-subfield" data-block-field="${escapeHtml2(fieldName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4562
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4563
+ </div>
4564
+ `;
4565
+ }).join("");
4566
+ return `
4567
+ <div class="blocks-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-block-type="${escapeHtml2(block.name)}" data-block-discriminator="${escapeHtml2(discriminator)}" draggable="true">
4568
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4569
+ <div class="flex items-start gap-3">
4570
+ <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
4571
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4572
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4573
+ </svg>
4574
+ </div>
4575
+ <div>
4576
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4577
+ ${escapeHtml2(block.label)}
4578
+ <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
4579
+ </div>
4580
+ ${block.description ? `<p class="text-xs text-zinc-500 dark:text-zinc-400">${escapeHtml2(block.description)}</p>` : ""}
4581
+ </div>
4582
+ </div>
4583
+ <div class="flex flex-wrap gap-2 text-xs">
4584
+ <button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block up" title="Move up">
4585
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4586
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4587
+ </svg>
4588
+ </button>
4589
+ <button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block down" title="Move down">
4590
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4591
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4592
+ </svg>
4593
+ </button>
4594
+ <button type="button" data-action="remove-block" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
4595
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4596
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
4597
+ </svg>
4598
+ Delete block
4599
+ </button>
4600
+ </div>
4601
+ </div>
4602
+ <div class="mt-4 space-y-4">
4603
+ ${blockFields}
4604
+ </div>
4605
+ </div>
4606
+ `;
4607
+ }
4608
+ function normalizeBlockField(fieldConfig, fieldName) {
4609
+ const type = fieldConfig?.type || "text";
4610
+ const label = fieldConfig?.title || fieldName;
4611
+ const required = fieldConfig?.required === true;
4612
+ const options = { ...fieldConfig };
4613
+ if (type === "select" && Array.isArray(fieldConfig?.enum)) {
4614
+ options.options = fieldConfig.enum.map((value, index) => ({
4615
+ value,
4616
+ label: fieldConfig.enumLabels?.[index] || value
4617
+ }));
4618
+ }
4619
+ return {
4620
+ type,
4621
+ label,
4622
+ required,
4623
+ defaultValue: fieldConfig?.default,
4624
+ options
4625
+ };
4626
+ }
4627
+ function getStructuredFieldScript() {
4628
+ return `
4629
+ ${getReadFieldValueScript()}
4630
+ <script>
4631
+ if (!window.__sonicStructuredFieldInit) {
4632
+ window.__sonicStructuredFieldInit = true;
4633
+
4634
+ function initializeStructuredFields() {
4635
+ const readFieldValue = window.sonicReadFieldValue;
4636
+
4637
+ const readStructuredValue = (container) => {
4638
+ const fields = Array.from(container.querySelectorAll('.structured-subfield'));
4639
+ if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
4640
+ return readFieldValue(fields[0]);
4641
+ }
4642
+
4643
+ return fields.reduce((acc, fieldWrapper) => {
4644
+ const fieldName = fieldWrapper.dataset.structuredField;
4645
+ if (!fieldName || fieldName === '__value') return acc;
4646
+ acc[fieldName] = readFieldValue(fieldWrapper);
4647
+ return acc;
4648
+ }, {});
4649
+ };
4650
+
4651
+ document.querySelectorAll('[data-structured-object]').forEach((container) => {
4652
+ if (container.dataset.structuredInitialized === 'true') {
4653
+ return;
4654
+ }
4655
+ container.dataset.structuredInitialized = 'true';
4656
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4657
+
4658
+ const updateHiddenInput = () => {
4659
+ if (!hiddenInput) return;
4660
+ const value = readStructuredValue(container);
4661
+ hiddenInput.value = JSON.stringify(value);
4662
+ };
4663
+
4664
+ container.addEventListener('input', updateHiddenInput);
4665
+ container.addEventListener('change', updateHiddenInput);
4666
+ updateHiddenInput();
4667
+ });
4668
+
4669
+ document.querySelectorAll('[data-structured-array]').forEach((container) => {
4670
+ if (container.dataset.structuredInitialized === 'true') {
4671
+ return;
4672
+ }
4673
+ container.dataset.structuredInitialized = 'true';
4674
+ const list = container.querySelector('[data-structured-array-list]');
4675
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4676
+ const template = container.querySelector('template[data-structured-array-template]');
4677
+
4678
+ const updateOrderLabels = () => {
4679
+ const items = Array.from(container.querySelectorAll('.structured-array-item'));
4680
+ items.forEach((item, index) => {
4681
+ const label = item.querySelector('[data-array-order-label]');
4682
+ if (label) {
4683
+ label.textContent = '#'+ (index + 1);
4684
+ }
4685
+
4686
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4687
+ if (moveUpButton instanceof HTMLButtonElement) {
4688
+ moveUpButton.disabled = index === 0;
4689
+ }
4690
+
4691
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4692
+ if (moveDownButton instanceof HTMLButtonElement) {
4693
+ moveDownButton.disabled = index === items.length - 1;
4694
+ }
4695
+ });
4696
+ };
4697
+
4698
+ const updateHiddenInput = () => {
4699
+ if (!hiddenInput || !list) return;
4700
+ const items = Array.from(list.querySelectorAll('.structured-array-item'));
4701
+ const values = items.map((item) => readStructuredValue(item));
4702
+ hiddenInput.value = JSON.stringify(values);
4703
+
4704
+ const emptyState = list.querySelector('[data-structured-empty]');
4705
+ if (emptyState) {
4706
+ emptyState.style.display = values.length === 0 ? 'block' : 'none';
4707
+ }
4708
+ updateOrderLabels();
4709
+ };
4710
+
4711
+ if (typeof window.initializeDragSortable === 'function' && list) {
4712
+ window.initializeDragSortable(list, {
4713
+ itemSelector: '.structured-array-item',
4714
+ handleSelector: '[data-action="drag-handle"]',
4715
+ onUpdate: updateHiddenInput
4716
+ });
4717
+ }
4718
+
4719
+ container.addEventListener('click', (event) => {
4720
+ const target = event.target;
4721
+ if (!(target instanceof Element)) return;
4722
+ const actionButton = target.closest('[data-action]');
4723
+ if (!actionButton || actionButton.hasAttribute('disabled')) return;
4724
+
4725
+ const action = actionButton.getAttribute('data-action');
4726
+
4727
+ if (action === 'add-item') {
4728
+ if (!list || !template) return;
4729
+ const nextIndex = list.querySelectorAll('.structured-array-item').length;
4730
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4731
+ list.insertAdjacentHTML('beforeend', html);
4732
+ if (typeof initializeTinyMCE === 'function') {
4733
+ initializeTinyMCE();
4734
+ }
4735
+ if (typeof window.initializeQuillEditors === 'function') {
4736
+ window.initializeQuillEditors();
4737
+ }
4738
+ if (typeof initializeMDXEditor === 'function') {
4739
+ initializeMDXEditor();
4740
+ }
4741
+ updateHiddenInput();
4742
+ return;
4743
+ }
4744
+
4745
+ const item = actionButton.closest('.structured-array-item');
4746
+ if (!item || !list) return;
4747
+
4748
+ if (action === 'remove-item') {
4749
+ item.remove();
4750
+ updateHiddenInput();
4751
+ return;
4752
+ }
4753
+
4754
+ if (action === 'move-up') {
4755
+ const previous = item.previousElementSibling;
4756
+ if (previous) {
4757
+ list.insertBefore(item, previous);
4758
+ updateHiddenInput();
4759
+ }
4760
+ return;
4761
+ }
4762
+
4763
+ if (action === 'move-down') {
4764
+ const next = item.nextElementSibling;
4765
+ if (next) {
4766
+ list.insertBefore(next, item);
4767
+ updateHiddenInput();
4768
+ }
4769
+ }
4770
+ });
4771
+
4772
+ container.addEventListener('input', (event) => {
4773
+ const target = event.target;
4774
+ if (!(target instanceof Element)) return;
4775
+ if (target.closest('[data-structured-array-list]')) {
4776
+ updateHiddenInput();
4777
+ }
4778
+ });
4779
+
4780
+ container.addEventListener('change', (event) => {
4781
+ const target = event.target;
4782
+ if (!(target instanceof Element)) return;
4783
+ if (target.closest('[data-structured-array-list]')) {
4784
+ updateHiddenInput();
4785
+ }
4786
+ });
4787
+
4788
+ updateHiddenInput();
4789
+ });
4790
+ }
4791
+
4792
+ window.initializeStructuredFields = initializeStructuredFields;
4793
+
4794
+ if (document.readyState === 'loading') {
4795
+ document.addEventListener('DOMContentLoaded', initializeStructuredFields);
4796
+ } else {
4797
+ initializeStructuredFields();
4798
+ }
4799
+
4800
+ document.addEventListener('htmx:afterSwap', function() {
4801
+ setTimeout(initializeStructuredFields, 50);
4802
+ });
4803
+ } else if (typeof window.initializeStructuredFields === 'function') {
4804
+ window.initializeStructuredFields();
4805
+ }
4806
+ </script>
4807
+ `;
4808
+ }
4809
+ function getBlocksFieldScript() {
4810
+ return `
4811
+ ${getReadFieldValueScript()}
4812
+ <script>
4813
+ if (!window.__sonicBlocksFieldInit) {
4814
+ window.__sonicBlocksFieldInit = true;
4815
+
4816
+ function initializeBlocksFields() {
4817
+ document.querySelectorAll('.blocks-field').forEach((container) => {
4818
+ if (container.dataset.blocksInitialized === 'true') {
4819
+ return;
4820
+ }
4821
+
4822
+ container.dataset.blocksInitialized = 'true';
4823
+ const list = container.querySelector('[data-blocks-list]');
4824
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4825
+ const typeSelect = container.querySelector('[data-role="block-type-select"]');
4826
+ const discriminator = container.dataset.blocksDiscriminator || 'blockType';
4827
+
4828
+ const updateOrderLabels = () => {
4829
+ const items = Array.from(container.querySelectorAll('.blocks-item'));
4830
+ items.forEach((item, index) => {
4831
+ const label = item.querySelector('[data-block-order-label]');
4832
+ if (label) {
4833
+ label.textContent = '#'+ (index + 1);
4834
+ }
4835
+
4836
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4837
+ if (moveUpButton instanceof HTMLButtonElement) {
4838
+ moveUpButton.disabled = index === 0;
4839
+ }
4840
+
4841
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4842
+ if (moveDownButton instanceof HTMLButtonElement) {
4843
+ moveDownButton.disabled = index === items.length - 1;
4844
+ }
4845
+ });
4846
+ };
4847
+
4848
+ const readFieldValue = window.sonicReadFieldValue;
4849
+
4850
+ const readBlockItem = (item) => {
4851
+ if (item.dataset.blockRaw) {
4852
+ try {
4853
+ return JSON.parse(item.dataset.blockRaw);
4854
+ } catch (error) {
4855
+ return {};
4856
+ }
4857
+ }
4858
+
4859
+ const blockType = item.dataset.blockType;
4860
+ const data = {};
4861
+
4862
+ item.querySelectorAll('.blocks-subfield').forEach((fieldWrapper) => {
4863
+ const fieldName = fieldWrapper.dataset.blockField;
4864
+ if (!fieldName) {
4865
+ return;
4866
+ }
4867
+ data[fieldName] = readFieldValue(fieldWrapper);
4868
+ });
4869
+
4870
+ return { [discriminator]: blockType, ...data };
4871
+ };
4872
+
4873
+ const updateHiddenInput = () => {
4874
+ if (!hiddenInput || !list) return;
4875
+ const items = Array.from(list.querySelectorAll('.blocks-item, [data-block-raw]'));
4876
+ const blocksData = items.map((item) => readBlockItem(item));
4877
+ hiddenInput.value = JSON.stringify(blocksData);
4878
+
4879
+ const emptyState = list.querySelector('[data-blocks-empty]');
4880
+ if (emptyState) {
4881
+ emptyState.style.display = blocksData.length === 0 ? 'block' : 'none';
4882
+ }
4883
+ updateOrderLabels();
4884
+ };
4885
+
4886
+ const initializeEditors = () => {
4887
+ if (typeof initializeTinyMCE === 'function') {
4888
+ initializeTinyMCE();
4889
+ }
4890
+ if (typeof window.initializeQuillEditors === 'function') {
4891
+ window.initializeQuillEditors();
4892
+ }
4893
+ if (typeof initializeMDXEditor === 'function') {
4894
+ initializeMDXEditor();
4895
+ }
4896
+ };
4897
+
4898
+ if (typeof window.initializeDragSortable === 'function' && list) {
4899
+ window.initializeDragSortable(list, {
4900
+ itemSelector: '.blocks-item',
4901
+ handleSelector: '[data-action="drag-handle"]',
4902
+ onUpdate: updateHiddenInput
4903
+ });
4904
+ }
4905
+
4906
+ container.addEventListener('click', (event) => {
4907
+ const target = event.target;
4908
+ if (!(target instanceof Element)) return;
4909
+ const actionButton = target.closest('[data-action]');
4910
+ if (!actionButton) return;
4911
+
4912
+ if (actionButton.hasAttribute('disabled')) {
4913
+ return;
4914
+ }
4915
+
4916
+ const action = actionButton.getAttribute('data-action');
4917
+ if (action === 'add-block') {
4918
+ const blockType = typeSelect ? typeSelect.value : '';
4919
+ if (!blockType || !list) return;
4920
+ const template = container.querySelector('template[data-block-template="' + blockType + '"]');
4921
+ if (!template) return;
4922
+
4923
+ const nextIndex = list.querySelectorAll('.blocks-item').length;
4924
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4925
+ list.insertAdjacentHTML('beforeend', html);
4926
+ if (typeSelect) {
4927
+ typeSelect.value = '';
4928
+ }
4929
+ initializeEditors();
4930
+ if (typeof window.initializeStructuredFields === 'function') {
4931
+ window.initializeStructuredFields();
4932
+ }
4933
+ updateHiddenInput();
4934
+ return;
4935
+ }
4936
+
4937
+ const item = actionButton.closest('.blocks-item');
4938
+ if (!item || !list) return;
4939
+
4940
+ if (action === 'remove-block') {
4941
+ item.remove();
4942
+ updateHiddenInput();
4943
+ return;
4944
+ }
4945
+
4946
+ if (action === 'move-up') {
4947
+ const previous = item.previousElementSibling;
4948
+ if (previous) {
4949
+ list.insertBefore(item, previous);
4950
+ updateHiddenInput();
4951
+ }
4952
+ return;
4953
+ }
4954
+
4955
+ if (action === 'move-down') {
4956
+ const next = item.nextElementSibling;
4957
+ if (next) {
4958
+ list.insertBefore(next, item);
4959
+ updateHiddenInput();
4960
+ }
4961
+ }
4962
+ });
4963
+
4964
+ container.addEventListener('input', (event) => {
4965
+ const target = event.target;
4966
+ if (!(target instanceof Element)) return;
4967
+ if (target.closest('[data-blocks-list]')) {
4968
+ updateHiddenInput();
4969
+ }
4970
+ });
4971
+
4972
+ container.addEventListener('change', (event) => {
4973
+ const target = event.target;
4974
+ if (!(target instanceof Element)) return;
4975
+ if (target.closest('[data-blocks-list]')) {
4976
+ updateHiddenInput();
4977
+ }
4978
+ });
4979
+
4980
+ updateHiddenInput();
4981
+ });
4982
+ }
4983
+
4984
+ window.initializeBlocksFields = initializeBlocksFields;
4985
+
4986
+ if (document.readyState === 'loading') {
4987
+ document.addEventListener('DOMContentLoaded', initializeBlocksFields);
4988
+ } else {
4989
+ initializeBlocksFields();
4990
+ }
4991
+
4992
+ document.addEventListener('htmx:afterSwap', function() {
4993
+ setTimeout(initializeBlocksFields, 50);
4994
+ });
4995
+ } else if (typeof window.initializeBlocksFields === 'function') {
4996
+ window.initializeBlocksFields();
4997
+ }
4998
+ </script>
4999
+ `;
5000
+ }
5001
+ function escapeHtml2(text) {
5002
+ if (typeof text !== "string") return String(text || "");
5003
+ return text.replace(/[&<>"']/g, (char) => ({
5004
+ "&": "&amp;",
5005
+ "<": "&lt;",
5006
+ ">": "&gt;",
5007
+ '"': "&quot;",
5008
+ "'": "&#39;"
5009
+ })[char] || char);
5010
+ }
5011
+
5012
+ // src/plugins/available/tinymce-plugin/index.ts
5013
+ var builder = chunkYHW27CBV_cjs.PluginBuilder.create({
5014
+ name: "tinymce-plugin",
5015
+ version: "1.0.0",
5016
+ description: "Powerful WYSIWYG rich text editor for content creation"
5017
+ });
5018
+ builder.metadata({
5019
+ author: {
5020
+ name: "SonicJS Team",
5021
+ email: "team@sonicjs.com"
5022
+ },
5023
+ license: "MIT",
5024
+ compatibility: "^2.0.0"
5025
+ });
5026
+ builder.lifecycle({
5027
+ activate: async () => {
5028
+ console.info("\u2705 TinyMCE plugin activated");
5029
+ },
5030
+ deactivate: async () => {
5031
+ console.info("\u274C TinyMCE plugin deactivated");
5032
+ }
5033
+ });
5034
+ builder.build();
5035
+ function getTinyMCEScript(apiKey = "no-api-key") {
5036
+ return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
5037
+ }
5038
+ function getTinyMCEInitScript(config) {
5039
+ const skin = config?.skin || "oxide-dark";
5040
+ const contentCss = skin.includes("dark") ? "dark" : "default";
5041
+ const defaultHeight = config?.defaultHeight || 300;
5042
+ return `
5043
+ // Initialize TinyMCE for all richtext fields
5044
+ function initializeTinyMCE() {
5045
+ if (typeof tinymce !== 'undefined') {
5046
+ // Find all textareas that need TinyMCE
5047
+ document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
5048
+ // Skip if already initialized
5049
+ if (tinymce.get(textarea.id)) {
5050
+ return;
5051
+ }
5052
+
5053
+ // Get configuration from data attributes
5054
+ const container = textarea.closest('.richtext-container');
5055
+ const height = container?.dataset.height || ${defaultHeight};
5056
+ const toolbar = container?.dataset.toolbar || 'full';
5057
+
5058
+ tinymce.init({
5059
+ selector: '#' + textarea.id,
5060
+ skin: '${skin}',
5061
+ content_css: '${contentCss}',
5062
+ height: parseInt(height),
5063
+ menubar: false,
5064
+ plugins: [
5065
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
5066
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
5067
+ 'insertdatetime', 'media', 'table', 'help', 'wordcount'
5068
+ ],
5069
+ toolbar: toolbar === 'simple'
5070
+ ? 'bold italic underline | bullist numlist | link'
5071
+ : toolbar === 'minimal'
4031
5072
  ? 'bold italic | link'
4032
5073
  : 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
4033
5074
  content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-size: 14px }'
@@ -4252,7 +5293,7 @@ function getQuillCDN(version = "2.0.2") {
4252
5293
  `;
4253
5294
  }
4254
5295
  function createQuillEditorPlugin() {
4255
- const builder3 = chunkAI2JJIJX_cjs.PluginBuilder.create({
5296
+ const builder3 = chunkYHW27CBV_cjs.PluginBuilder.create({
4256
5297
  name: "quill-editor",
4257
5298
  version: "1.0.0",
4258
5299
  description: "Quill rich text editor integration for SonicJS"
@@ -4278,7 +5319,7 @@ function createQuillEditorPlugin() {
4278
5319
  createQuillEditorPlugin();
4279
5320
 
4280
5321
  // src/plugins/available/easy-mdx/index.ts
4281
- var builder2 = chunkAI2JJIJX_cjs.PluginBuilder.create({
5322
+ var builder2 = chunkYHW27CBV_cjs.PluginBuilder.create({
4282
5323
  name: "easy-mdx",
4283
5324
  version: "1.0.0",
4284
5325
  description: "Lightweight markdown editor with live preview"
@@ -4569,8 +5610,8 @@ function renderContentFormPage(data) {
4569
5610
  <!-- Form Content -->
4570
5611
  <div class="px-6 py-6">
4571
5612
  <div id="form-messages">
4572
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
4573
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
5613
+ ${data.error ? chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
5614
+ ${data.success ? chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
4574
5615
  </div>
4575
5616
 
4576
5617
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
@@ -4805,7 +5846,7 @@ function renderContentFormPage(data) {
4805
5846
  </div>
4806
5847
 
4807
5848
  <!-- Confirmation Dialogs -->
4808
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5849
+ ${chunkEHSZ6TAN_cjs.renderConfirmationDialog({
4809
5850
  id: "duplicate-content-confirm",
4810
5851
  title: "Duplicate Content",
4811
5852
  message: "Create a copy of this content?",
@@ -4816,7 +5857,7 @@ function renderContentFormPage(data) {
4816
5857
  onConfirm: "performDuplicateContent()"
4817
5858
  })}
4818
5859
 
4819
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5860
+ ${chunkEHSZ6TAN_cjs.renderConfirmationDialog({
4820
5861
  id: "delete-content-confirm",
4821
5862
  title: "Delete Content",
4822
5863
  message: "Are you sure you want to delete this content? This action cannot be undone.",
@@ -4827,7 +5868,7 @@ function renderContentFormPage(data) {
4827
5868
  onConfirm: `performDeleteContent('${data.id}')`
4828
5869
  })}
4829
5870
 
4830
- ${chunkAZLU3ROK_cjs.getConfirmationDialogScript()}
5871
+ ${chunkEHSZ6TAN_cjs.getConfirmationDialogScript()}
4831
5872
 
4832
5873
  ${data.tinymceEnabled ? getTinyMCEScript(data.tinymceSettings?.apiKey) : "<!-- TinyMCE plugin not active -->"}
4833
5874
 
@@ -4905,79 +5946,379 @@ function renderContentFormPage(data) {
4905
5946
  hiddenInput.value = originalValue;
4906
5947
  }
4907
5948
 
4908
- // If original value was empty, hide the preview and show select button
4909
- if (!originalValue) {
4910
- const preview = document.getElementById(fieldId + '-preview');
4911
- if (preview) {
4912
- preview.classList.add('hidden');
5949
+ // If original value was empty, hide the preview and show select button
5950
+ if (!originalValue) {
5951
+ const preview = document.getElementById(fieldId + '-preview');
5952
+ if (preview) {
5953
+ preview.classList.add('hidden');
5954
+ }
5955
+ }
5956
+
5957
+ // Close modal
5958
+ closeMediaSelector();
5959
+ }
5960
+
5961
+ function clearMediaField(fieldId) {
5962
+ const hiddenInput = document.getElementById(fieldId);
5963
+ const preview = document.getElementById(fieldId + '-preview');
5964
+
5965
+ if (hiddenInput) {
5966
+ hiddenInput.value = '';
5967
+ }
5968
+
5969
+ if (preview) {
5970
+ // Clear all children if it's a grid, or hide it
5971
+ if (preview.classList.contains('media-preview-grid')) {
5972
+ preview.innerHTML = '';
5973
+ }
5974
+ preview.classList.add('hidden');
5975
+ }
5976
+ }
5977
+
5978
+ // Global function to remove a single media from multiple selection
5979
+ window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
5980
+ const hiddenInput = document.getElementById(fieldId);
5981
+ if (!hiddenInput) return;
5982
+
5983
+ const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
5984
+ hiddenInput.value = values.join(',');
5985
+
5986
+ // Remove preview item
5987
+ const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
5988
+ if (previewItem) {
5989
+ previewItem.remove();
5990
+ }
5991
+
5992
+ // Hide preview grid if empty
5993
+ if (values.length === 0) {
5994
+ const preview = document.getElementById(fieldId + '-preview');
5995
+ if (preview) {
5996
+ preview.classList.add('hidden');
5997
+ }
5998
+ }
5999
+ };
6000
+
6001
+ // Global function called by media selector buttons
6002
+ window.selectMediaFile = function(mediaId, mediaUrl, filename) {
6003
+ if (!currentMediaFieldId) {
6004
+ console.error('No field ID set for media selection');
6005
+ return;
6006
+ }
6007
+
6008
+ const fieldId = currentMediaFieldId;
6009
+
6010
+ // Set the hidden input value to the media URL (not ID)
6011
+ const hiddenInput = document.getElementById(fieldId);
6012
+ if (hiddenInput) {
6013
+ hiddenInput.value = mediaUrl;
6014
+ }
6015
+
6016
+ // Update the preview
6017
+ const preview = document.getElementById(fieldId + '-preview');
6018
+ if (preview) {
6019
+ preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
6020
+ preview.classList.remove('hidden');
6021
+ }
6022
+
6023
+ // Show the remove button by finding the media actions container and updating it
6024
+ const mediaField = hiddenInput?.closest('.media-field-container');
6025
+ if (mediaField) {
6026
+ const actionsDiv = mediaField.querySelector('.media-actions');
6027
+ if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
6028
+ const removeBtn = document.createElement('button');
6029
+ removeBtn.type = 'button';
6030
+ removeBtn.onclick = () => clearMediaField(fieldId);
6031
+ removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
6032
+ removeBtn.textContent = 'Remove';
6033
+ actionsDiv.appendChild(removeBtn);
6034
+ }
6035
+ }
6036
+
6037
+ // DON'T close the modal - let user click OK button
6038
+ // Visual feedback: highlight the selected item
6039
+ document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
6040
+ el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6041
+ });
6042
+ const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
6043
+ if (selectedItem) {
6044
+ selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6045
+ }
6046
+ };
6047
+
6048
+ function setMediaField(fieldId, mediaUrl) {
6049
+ document.getElementById(fieldId).value = mediaUrl;
6050
+ const preview = document.getElementById(fieldId + '-preview');
6051
+ preview.innerHTML = \`<img src="\${mediaUrl}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg ring-1 ring-zinc-950/10 dark:ring-white/10">\`;
6052
+ preview.classList.remove('hidden');
6053
+
6054
+ // Close modal
6055
+ document.querySelector('.fixed.inset-0')?.remove();
6056
+ }
6057
+
6058
+ // Reference field functions
6059
+ let currentReferenceFieldId = null;
6060
+ let referenceSearchTimeout = null;
6061
+
6062
+ function getReferenceContainer(fieldId) {
6063
+ const input = document.getElementById(fieldId);
6064
+ return input ? input.closest('[data-reference-field]') : null;
6065
+ }
6066
+
6067
+ function getReferenceCollections(container) {
6068
+ if (!container) return [];
6069
+ const rawCollections = container.dataset.referenceCollections || '';
6070
+ const collections = rawCollections
6071
+ .split(',')
6072
+ .map((value) => value.trim())
6073
+ .filter(Boolean);
6074
+ if (collections.length > 0) {
6075
+ return collections;
6076
+ }
6077
+ const singleCollection = container.dataset.referenceCollection;
6078
+ return singleCollection ? [singleCollection] : [];
6079
+ }
6080
+
6081
+ async function fetchReferenceItems(collections, search = '', limit = 20) {
6082
+ const params = new URLSearchParams({ limit: String(limit) });
6083
+ collections.forEach((collection) => params.append('collection', collection));
6084
+ if (search) {
6085
+ params.set('search', search);
6086
+ }
6087
+ const response = await fetch('/admin/api/references?' + params.toString());
6088
+ if (!response.ok) {
6089
+ throw new Error('Failed to load references');
6090
+ }
6091
+ const data = await response.json();
6092
+ return data?.data || [];
6093
+ }
6094
+
6095
+ async function fetchReferenceById(collections, id) {
6096
+ if (!id) return null;
6097
+ const params = new URLSearchParams({ id });
6098
+ collections.forEach((collection) => params.append('collection', collection));
6099
+ const response = await fetch('/admin/api/references?' + params.toString());
6100
+ if (!response.ok) {
6101
+ return null;
6102
+ }
6103
+ const data = await response.json();
6104
+ return data?.data || null;
6105
+ }
6106
+
6107
+ function renderReferenceDisplay(container, item, fallbackMessage = 'No reference selected.') {
6108
+ const display = container.querySelector('[data-reference-display]');
6109
+ const removeButton = container.querySelector('[data-reference-clear]');
6110
+ if (!display) return;
6111
+
6112
+ display.innerHTML = '';
6113
+
6114
+ if (!item) {
6115
+ display.textContent = fallbackMessage;
6116
+ if (removeButton) {
6117
+ removeButton.disabled = true;
6118
+ }
6119
+ return;
6120
+ }
6121
+
6122
+ const title = item.title || item.slug || item.id || 'Untitled';
6123
+ const titleEl = document.createElement('div');
6124
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6125
+ titleEl.textContent = title;
6126
+
6127
+ display.appendChild(titleEl);
6128
+
6129
+ const metaRow = document.createElement('div');
6130
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
6131
+
6132
+ if (item.collection?.display_name || item.collection?.name) {
6133
+ const collectionLabel = document.createElement('span');
6134
+ collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
6135
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6136
+ metaRow.appendChild(collectionLabel);
6137
+ }
6138
+
6139
+ if (item.slug) {
6140
+ const slugEl = document.createElement('span');
6141
+ slugEl.textContent = item.slug;
6142
+ metaRow.appendChild(slugEl);
6143
+ }
6144
+
6145
+ if (metaRow.childElementCount > 0) {
6146
+ display.appendChild(metaRow);
6147
+ }
6148
+
6149
+ if (removeButton) {
6150
+ removeButton.disabled = false;
6151
+ }
6152
+ }
6153
+
6154
+ function updateReferenceField(fieldId, item) {
6155
+ const input = document.getElementById(fieldId);
6156
+ const container = getReferenceContainer(fieldId);
6157
+ if (!input || !container) return;
6158
+
6159
+ input.value = item?.id || '';
6160
+ renderReferenceDisplay(container, item, 'No reference selected.');
6161
+ input.dispatchEvent(new Event('input', { bubbles: true }));
6162
+ input.dispatchEvent(new Event('change', { bubbles: true }));
6163
+ }
6164
+
6165
+ function clearReferenceField(fieldId) {
6166
+ updateReferenceField(fieldId, null);
6167
+ }
6168
+
6169
+ function closeReferenceSelector() {
6170
+ const modal = document.getElementById('reference-selector-modal');
6171
+ if (modal) {
6172
+ modal.remove();
6173
+ }
6174
+ currentReferenceFieldId = null;
6175
+ }
6176
+
6177
+ function openReferenceSelector(fieldId) {
6178
+ const container = getReferenceContainer(fieldId);
6179
+ const collections = getReferenceCollections(container);
6180
+ if (!container || collections.length === 0) {
6181
+ console.error('Reference collection is missing for field', fieldId);
6182
+ return;
6183
+ }
6184
+
6185
+ currentReferenceFieldId = fieldId;
6186
+
6187
+ const modal = document.createElement('div');
6188
+ modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
6189
+ modal.id = 'reference-selector-modal';
6190
+ modal.innerHTML = \`
6191
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
6192
+ <div class="flex items-center justify-between gap-3">
6193
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">Select Reference</h3>
6194
+ <button
6195
+ type="button"
6196
+ onclick="closeReferenceSelector()"
6197
+ class="rounded-md text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
6198
+ aria-label="Close"
6199
+ >
6200
+ \u2715
6201
+ </button>
6202
+ </div>
6203
+ <div class="mt-4">
6204
+ <input
6205
+ type="search"
6206
+ id="reference-search-input"
6207
+ placeholder="Search by title or slug..."
6208
+ class="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 dark:border-white/10 dark:bg-zinc-900 dark:text-white"
6209
+ >
6210
+ </div>
6211
+ <div id="reference-results" class="mt-4 space-y-2"></div>
6212
+ <div class="mt-4 flex justify-end">
6213
+ <button
6214
+ type="button"
6215
+ onclick="closeReferenceSelector()"
6216
+ class="rounded-lg bg-zinc-950 dark:bg-white px-4 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors"
6217
+ >
6218
+ Close
6219
+ </button>
6220
+ </div>
6221
+ </div>
6222
+ \`;
6223
+
6224
+ document.body.appendChild(modal);
6225
+
6226
+ const resultsContainer = modal.querySelector('#reference-results');
6227
+ const searchInput = modal.querySelector('#reference-search-input');
6228
+
6229
+ const renderResults = (items) => {
6230
+ resultsContainer.innerHTML = '';
6231
+ if (!items || items.length === 0) {
6232
+ resultsContainer.innerHTML = '<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-white/10 dark:text-zinc-400">No items found.</div>';
6233
+ return;
4913
6234
  }
4914
- }
4915
6235
 
4916
- // Close modal
4917
- closeMediaSelector();
4918
- }
6236
+ const selectedId = document.getElementById(fieldId)?.value;
4919
6237
 
4920
- function clearMediaField(fieldId) {
4921
- document.getElementById(fieldId).value = '';
4922
- document.getElementById(fieldId + '-preview').classList.add('hidden');
4923
- }
6238
+ items.forEach((item) => {
6239
+ const button = document.createElement('button');
6240
+ button.type = 'button';
6241
+ button.className = 'w-full text-left rounded-lg border border-zinc-200 px-4 py-3 text-sm text-zinc-700 hover:bg-zinc-50 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/5';
6242
+ if (item.id === selectedId) {
6243
+ button.classList.add('ring-2', 'ring-cyan-500', 'dark:ring-cyan-400');
6244
+ }
4924
6245
 
4925
- // Global function called by media selector buttons
4926
- window.selectMediaFile = function(mediaId, mediaUrl, filename) {
4927
- if (!currentMediaFieldId) {
4928
- console.error('No field ID set for media selection');
4929
- return;
4930
- }
6246
+ const title = item.title || item.slug || item.id || 'Untitled';
6247
+ const titleEl = document.createElement('div');
6248
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6249
+ titleEl.textContent = title;
4931
6250
 
4932
- const fieldId = currentMediaFieldId;
6251
+ button.appendChild(titleEl);
4933
6252
 
4934
- // Set the hidden input value to the media URL (not ID)
4935
- const hiddenInput = document.getElementById(fieldId);
4936
- if (hiddenInput) {
4937
- hiddenInput.value = mediaUrl;
4938
- }
6253
+ const metaRow = document.createElement('div');
6254
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
4939
6255
 
4940
- // Update the preview
4941
- const preview = document.getElementById(fieldId + '-preview');
4942
- if (preview) {
4943
- preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
4944
- preview.classList.remove('hidden');
4945
- }
6256
+ if (item.collection?.display_name || item.collection?.name) {
6257
+ const collectionLabel = document.createElement('span');
6258
+ collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
6259
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6260
+ metaRow.appendChild(collectionLabel);
6261
+ }
4946
6262
 
4947
- // Show the remove button by finding the media actions container and updating it
4948
- const mediaField = hiddenInput?.closest('.media-field-container');
4949
- if (mediaField) {
4950
- const actionsDiv = mediaField.querySelector('.media-actions');
4951
- if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
4952
- const removeBtn = document.createElement('button');
4953
- removeBtn.type = 'button';
4954
- removeBtn.onclick = () => clearMediaField(fieldId);
4955
- removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
4956
- removeBtn.textContent = 'Remove';
4957
- actionsDiv.appendChild(removeBtn);
6263
+ if (item.slug) {
6264
+ const slugEl = document.createElement('span');
6265
+ slugEl.textContent = item.slug;
6266
+ metaRow.appendChild(slugEl);
6267
+ }
6268
+
6269
+ if (metaRow.childElementCount > 0) {
6270
+ button.appendChild(metaRow);
6271
+ }
6272
+
6273
+ button.addEventListener('click', () => {
6274
+ updateReferenceField(fieldId, item);
6275
+ closeReferenceSelector();
6276
+ });
6277
+
6278
+ resultsContainer.appendChild(button);
6279
+ });
6280
+ };
6281
+
6282
+ const loadResults = async (searchValue = '') => {
6283
+ try {
6284
+ const items = await fetchReferenceItems(collections, searchValue);
6285
+ renderResults(items);
6286
+ } catch (error) {
6287
+ resultsContainer.innerHTML = '<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">Failed to load references.</div>';
4958
6288
  }
4959
- }
6289
+ };
4960
6290
 
4961
- // DON'T close the modal - let user click OK button
4962
- // Visual feedback: highlight the selected item
4963
- document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
4964
- el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6291
+ loadResults();
6292
+
6293
+ searchInput.addEventListener('input', () => {
6294
+ if (referenceSearchTimeout) {
6295
+ clearTimeout(referenceSearchTimeout);
6296
+ }
6297
+ referenceSearchTimeout = setTimeout(() => {
6298
+ loadResults(searchInput.value.trim());
6299
+ }, 250);
4965
6300
  });
4966
- const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
4967
- if (selectedItem) {
4968
- selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
4969
- }
4970
- };
6301
+ }
4971
6302
 
4972
- function setMediaField(fieldId, mediaUrl) {
4973
- document.getElementById(fieldId).value = mediaUrl;
4974
- const preview = document.getElementById(fieldId + '-preview');
4975
- preview.innerHTML = \`<img src="\${mediaUrl}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg ring-1 ring-zinc-950/10 dark:ring-white/10">\`;
4976
- preview.classList.remove('hidden');
6303
+ document.addEventListener('DOMContentLoaded', () => {
6304
+ document.querySelectorAll('[data-reference-field]').forEach(async (container) => {
6305
+ const input = container.querySelector('input[type="hidden"]');
6306
+ const collections = getReferenceCollections(container);
6307
+ if (!input || collections.length === 0) return;
4977
6308
 
4978
- // Close modal
4979
- document.querySelector('.fixed.inset-0')?.remove();
4980
- }
6309
+ if (!input.value) {
6310
+ renderReferenceDisplay(container, null, 'No reference selected.');
6311
+ return;
6312
+ }
6313
+
6314
+ const item = await fetchReferenceById(collections, input.value);
6315
+ if (item) {
6316
+ renderReferenceDisplay(container, item);
6317
+ } else {
6318
+ renderReferenceDisplay(container, null, 'Reference not found.');
6319
+ }
6320
+ });
6321
+ });
4981
6322
 
4982
6323
  // Custom select options
4983
6324
  function addCustomOption(input, selectId) {
@@ -5132,11 +6473,11 @@ function renderContentFormPage(data) {
5132
6473
  content: pageContent,
5133
6474
  version: data.version
5134
6475
  };
5135
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
6476
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
5136
6477
  }
5137
6478
 
5138
6479
  // src/templates/pages/admin-content-list.template.ts
5139
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
6480
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
5140
6481
  function renderContentListPage(data) {
5141
6482
  const urlParams = new URLSearchParams();
5142
6483
  if (data.modelName && data.modelName !== "all") urlParams.set("model", data.modelName);
@@ -5144,7 +6485,7 @@ function renderContentListPage(data) {
5144
6485
  if (data.search) urlParams.set("search", data.search);
5145
6486
  if (data.page && data.page !== 1) urlParams.set("page", data.page.toString());
5146
6487
  const currentParams = urlParams.toString();
5147
- const hasActiveFilters = data.modelName !== "all" || data.status !== "all" || !!data.search;
6488
+ data.modelName !== "all" || data.status !== "all" || !!data.search;
5148
6489
  const filterBarData = {
5149
6490
  filters: [
5150
6491
  {
@@ -5174,6 +6515,11 @@ function renderContentListPage(data) {
5174
6515
  }
5175
6516
  ],
5176
6517
  actions: [
6518
+ {
6519
+ label: "Advanced Search",
6520
+ className: "btn-primary",
6521
+ onclick: "openAdvancedSearch()"
6522
+ },
5177
6523
  {
5178
6524
  label: "Refresh",
5179
6525
  className: "btn-secondary",
@@ -5325,12 +6671,57 @@ function renderContentListPage(data) {
5325
6671
  <div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 rounded-xl">
5326
6672
  <div class="px-6 py-5">
5327
6673
  <div class="flex items-center justify-between">
5328
- <div class="flex items-center space-x-4">
5329
- <!-- Search Input -->
6674
+ <div class="flex items-center space-x-4 flex-1">
6675
+ <!-- Model Filter -->
6676
+ <div>
6677
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Model</label>
6678
+ <div class="grid grid-cols-1">
6679
+ <select
6680
+ name="model"
6681
+ onchange="updateContentFilters('model', this.value)"
6682
+ class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm 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 min-w-40"
6683
+ >
6684
+ <option value="all" ${data.modelName === "all" ? "selected" : ""}>All Models</option>
6685
+ ${data.models.map((model) => `
6686
+ <option value="${model.name}" ${data.modelName === model.name ? "selected" : ""}>
6687
+ ${model.displayName}
6688
+ </option>
6689
+ `).join("")}
6690
+ </select>
6691
+ <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">
6692
+ <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" />
6693
+ </svg>
6694
+ </div>
6695
+ </div>
6696
+
6697
+ <!-- Status Filter -->
5330
6698
  <div>
6699
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
6700
+ <div class="grid grid-cols-1">
6701
+ <select
6702
+ name="status"
6703
+ onchange="updateContentFilters('status', this.value)"
6704
+ class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm 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 min-w-40"
6705
+ >
6706
+ <option value="all" ${data.status === "all" ? "selected" : ""}>All Status</option>
6707
+ <option value="draft" ${data.status === "draft" ? "selected" : ""}>Draft</option>
6708
+ <option value="review" ${data.status === "review" ? "selected" : ""}>Under Review</option>
6709
+ <option value="scheduled" ${data.status === "scheduled" ? "selected" : ""}>Scheduled</option>
6710
+ <option value="published" ${data.status === "published" ? "selected" : ""}>Published</option>
6711
+ <option value="archived" ${data.status === "archived" ? "selected" : ""}>Archived</option>
6712
+ <option value="deleted" ${data.status === "deleted" ? "selected" : ""}>Deleted</option>
6713
+ </select>
6714
+ <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">
6715
+ <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" />
6716
+ </svg>
6717
+ </div>
6718
+ </div>
6719
+
6720
+ <!-- Search Input -->
6721
+ <div class="flex-1 max-w-md">
5331
6722
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
5332
6723
  <form onsubmit="performContentSearch(event)" class="flex items-center space-x-2">
5333
- <div class="relative group">
6724
+ <div class="relative group flex-1">
5334
6725
  <input
5335
6726
  type="text"
5336
6727
  name="search"
@@ -5338,7 +6729,7 @@ function renderContentListPage(data) {
5338
6729
  value="${data.search || ""}"
5339
6730
  oninput="toggleContentClearButton()"
5340
6731
  placeholder="Search content..."
5341
- class="rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm w-72 text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
6732
+ class="w-full rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
5342
6733
  >
5343
6734
  <div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
5344
6735
  <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
@@ -5410,57 +6801,6 @@ function renderContentListPage(data) {
5410
6801
  }
5411
6802
  </script>
5412
6803
  </div>
5413
-
5414
- ${filterBarData.filters.map((filter) => {
5415
- const selectedOption = filter.options.find((opt) => opt.selected);
5416
- const selectedColor = selectedOption?.color || "cyan";
5417
- const colorMap = {
5418
- "cyan": "bg-cyan-400 dark:bg-cyan-400",
5419
- "lime": "bg-lime-400 dark:bg-lime-400",
5420
- "pink": "bg-pink-400 dark:bg-pink-400",
5421
- "purple": "bg-purple-400 dark:bg-purple-400",
5422
- "amber": "bg-amber-400 dark:bg-amber-400",
5423
- "zinc": "bg-zinc-400 dark:bg-zinc-400"
5424
- };
5425
- return `
5426
- <div>
5427
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">${filter.label}</label>
5428
- <div class="mt-2 grid grid-cols-1">
5429
- <div class="col-start-1 row-start-1 flex items-center gap-3 pl-3 pr-8 pointer-events-none">
5430
- ${filter.name === "status" ? `<span class="inline-block size-2 shrink-0 rounded-full border border-transparent ${colorMap[selectedColor]}"></span>` : ""}
5431
- </div>
5432
- <select
5433
- name="${filter.name}"
5434
- onchange="updateContentFilters('${filter.name}', this.value)"
5435
- 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"
5436
- >
5437
- ${filter.options.map((opt) => `
5438
- <option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
5439
- `).join("")}
5440
- </select>
5441
- <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">
5442
- <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" />
5443
- </svg>
5444
- </div>
5445
- </div>
5446
- `;
5447
- }).join("")}
5448
-
5449
- <!-- Clear Filters Button -->
5450
- ${hasActiveFilters ? `
5451
- <div>
5452
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">&nbsp;</label>
5453
- <button
5454
- onclick="clearAllFilters()"
5455
- class="inline-flex items-center gap-x-1.5 px-3 py-2 bg-pink-50 dark:bg-pink-500/10 text-pink-700 dark:text-pink-300 text-sm font-medium rounded-md ring-1 ring-inset ring-pink-600/20 dark:ring-pink-500/20 hover:bg-pink-100 dark:hover:bg-pink-500/20 transition-colors"
5456
- >
5457
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
5458
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
5459
- </svg>
5460
- Clear Filters
5461
- </button>
5462
- </div>
5463
- ` : ""}
5464
6804
  </div>
5465
6805
  <div class="flex items-center gap-x-3">
5466
6806
  <span class="text-sm/6 font-medium text-zinc-700 dark:text-zinc-300 px-3 py-1.5 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-sm">${data.totalItems} ${data.totalItems === 1 ? "item" : "items"}</span>
@@ -5541,8 +6881,8 @@ function renderContentListPage(data) {
5541
6881
 
5542
6882
  <!-- Content List -->
5543
6883
  <div id="content-list">
5544
- ${chunkAZLU3ROK_cjs.renderTable(tableData)}
5545
- ${chunkAZLU3ROK_cjs.renderPagination(paginationData)}
6884
+ ${chunkEHSZ6TAN_cjs.renderTable(tableData)}
6885
+ ${chunkEHSZ6TAN_cjs.renderPagination(paginationData)}
5546
6886
  </div>
5547
6887
 
5548
6888
  </div>
@@ -5630,8 +6970,9 @@ function renderContentListPage(data) {
5630
6970
  });
5631
6971
 
5632
6972
  // Store current bulk action context
5633
- let currentBulkAction = null;
5634
- let currentSelectedIds = [];
6973
+ // Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
6974
+ var currentBulkAction = null;
6975
+ var currentSelectedIds = [];
5635
6976
 
5636
6977
  // Perform bulk action
5637
6978
  function performBulkAction(action) {
@@ -5721,49 +7062,336 @@ function renderContentListPage(data) {
5721
7062
  } else {
5722
7063
  alert('Error: ' + (data.error || 'Unknown error'));
5723
7064
  }
5724
- })
5725
- .catch(err => {
5726
- console.error('Bulk action error:', err);
5727
- alert('Failed to perform bulk action');
5728
- })
5729
- .finally(() => {
5730
- // Clear context
5731
- currentBulkAction = null;
5732
- currentSelectedIds = [];
7065
+ })
7066
+ .catch(err => {
7067
+ console.error('Bulk action error:', err);
7068
+ alert('Failed to perform bulk action');
7069
+ })
7070
+ .finally(() => {
7071
+ // Clear context
7072
+ currentBulkAction = null;
7073
+ currentSelectedIds = [];
7074
+ });
7075
+ }
7076
+
7077
+ // Helper to get action text for display
7078
+ function getActionText(action) {
7079
+ const actionCount = currentSelectedIds.length;
7080
+ switch(action) {
7081
+ case 'publish':
7082
+ return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7083
+ case 'draft':
7084
+ return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
7085
+ case 'delete':
7086
+ return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7087
+ default:
7088
+ return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7089
+ }
7090
+ }
7091
+
7092
+ </script>
7093
+
7094
+ <!-- Confirmation Dialog for Bulk Actions -->
7095
+ ${chunkEHSZ6TAN_cjs.renderConfirmationDialog({
7096
+ id: "bulk-action-confirm",
7097
+ title: "Confirm Bulk Action",
7098
+ message: "Are you sure you want to perform this action? This operation will affect multiple items.",
7099
+ confirmText: "Confirm",
7100
+ cancelText: "Cancel",
7101
+ confirmClass: "bg-blue-500 hover:bg-blue-400",
7102
+ iconColor: "blue",
7103
+ onConfirm: "executeBulkAction()"
7104
+ })}
7105
+
7106
+ <!-- Confirmation Dialog Script -->
7107
+ ${chunkEHSZ6TAN_cjs.getConfirmationDialogScript()}
7108
+
7109
+ <!-- Advanced Search Modal -->
7110
+ <div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
7111
+ <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
7112
+ <!-- Background overlay -->
7113
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
7114
+
7115
+ <!-- Modal panel -->
7116
+ <div class="inline-block align-bottom bg-white dark:bg-zinc-900 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
7117
+ <div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
7118
+ <!-- Header -->
7119
+ <div class="flex items-center justify-between mb-4">
7120
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
7121
+ \u{1F50D} Advanced Search
7122
+ </h3>
7123
+ <button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
7124
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7125
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
7126
+ </svg>
7127
+ </button>
7128
+ </div>
7129
+
7130
+ <!-- Search Form -->
7131
+ <form id="advancedSearchForm" class="space-y-4">
7132
+ <!-- Search Input -->
7133
+ <div>
7134
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
7135
+ <div class="relative">
7136
+ <input
7137
+ type="text"
7138
+ id="searchQuery"
7139
+ name="query"
7140
+ placeholder="Enter your search query..."
7141
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-4 py-3 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-indigo-500"
7142
+ autocomplete="off"
7143
+ />
7144
+ <div id="searchSuggestions" class="hidden absolute z-10 w-full mt-1 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 max-h-60 overflow-y-auto"></div>
7145
+ </div>
7146
+ </div>
7147
+
7148
+ <!-- Mode Toggle -->
7149
+ <div>
7150
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
7151
+ <div class="flex gap-4">
7152
+ <label class="flex items-center">
7153
+ <input type="radio" name="mode" value="ai" checked class="mr-2">
7154
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
7155
+ </label>
7156
+ <label class="flex items-center">
7157
+ <input type="radio" name="mode" value="keyword" class="mr-2">
7158
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
7159
+ </label>
7160
+ </div>
7161
+ </div>
7162
+
7163
+ <!-- Filters -->
7164
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7165
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
7166
+
7167
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
7168
+ <!-- Collection Filter -->
7169
+ <div>
7170
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
7171
+ <select
7172
+ id="filterCollections"
7173
+ name="collections"
7174
+ multiple
7175
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
7176
+ size="4"
7177
+ >
7178
+ <option value="">All Collections</option>
7179
+ ${data.models.map(
7180
+ (model) => `
7181
+ <option value="${model.name}">${model.displayName}</option>
7182
+ `
7183
+ ).join("")}
7184
+ </select>
7185
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
7186
+ </div>
7187
+
7188
+ <!-- Status Filter -->
7189
+ <div>
7190
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
7191
+ <select
7192
+ id="filterStatus"
7193
+ name="status"
7194
+ multiple
7195
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
7196
+ size="4"
7197
+ >
7198
+ <option value="published">Published</option>
7199
+ <option value="draft">Draft</option>
7200
+ <option value="review">Under Review</option>
7201
+ <option value="scheduled">Scheduled</option>
7202
+ <option value="archived">Archived</option>
7203
+ </select>
7204
+ </div>
7205
+ </div>
7206
+ </div>
7207
+
7208
+ <!-- Actions -->
7209
+ <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
7210
+ <button
7211
+ type="button"
7212
+ onclick="closeAdvancedSearch()"
7213
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-4 py-2 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"
7214
+ >
7215
+ Cancel
7216
+ </button>
7217
+ <button
7218
+ type="submit"
7219
+ class="inline-flex items-center justify-center rounded-lg bg-indigo-600 text-white px-6 py-2.5 text-sm font-semibold hover:bg-indigo-500 shadow-sm"
7220
+ >
7221
+ Search
7222
+ </button>
7223
+ </div>
7224
+ </form>
7225
+ </div>
7226
+
7227
+ <!-- Results Area -->
7228
+ <div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
7229
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7230
+ <div id="searchResultsContent" class="space-y-3"></div>
7231
+ <div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
7232
+ </div>
7233
+ </div>
7234
+ </div>
7235
+ </div>
7236
+ </div>
7237
+
7238
+ <script>
7239
+ // Open modal
7240
+ function openAdvancedSearch() {
7241
+ document.getElementById('advancedSearchModal').classList.remove('hidden');
7242
+ document.getElementById('searchQuery').focus();
7243
+ }
7244
+
7245
+ // Close modal
7246
+ function closeAdvancedSearch() {
7247
+ document.getElementById('advancedSearchModal').classList.add('hidden');
7248
+ document.getElementById('searchResults').classList.add('hidden');
7249
+ }
7250
+
7251
+ // Autocomplete
7252
+ // Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
7253
+ var autocompleteTimeout;
7254
+ var searchQueryInput = document.getElementById('searchQuery');
7255
+ if (searchQueryInput) {
7256
+ searchQueryInput.addEventListener('input', (e) => {
7257
+ const query = e.target.value.trim();
7258
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7259
+
7260
+ clearTimeout(autocompleteTimeout);
7261
+
7262
+ if (query.length < 2) {
7263
+ suggestionsDiv.classList.add('hidden');
7264
+ return;
7265
+ }
7266
+
7267
+ autocompleteTimeout = setTimeout(async () => {
7268
+ try {
7269
+ const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
7270
+ const { data } = await res.json();
7271
+
7272
+ if (data && data.length > 0) {
7273
+ suggestionsDiv.innerHTML = data.map(s => \`
7274
+ <div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
7275
+ \`).join('');
7276
+ suggestionsDiv.classList.remove('hidden');
7277
+ } else {
7278
+ suggestionsDiv.classList.add('hidden');
7279
+ }
7280
+ } catch (error) {
7281
+ console.error('Autocomplete error:', error);
7282
+ }
7283
+ }, 300);
7284
+ });
7285
+ }
7286
+
7287
+ function selectSuggestion(suggestion) {
7288
+ document.getElementById('searchQuery').value = suggestion;
7289
+ document.getElementById('searchSuggestions').classList.add('hidden');
7290
+ }
7291
+
7292
+ // Hide suggestions when clicking outside
7293
+ document.addEventListener('click', (e) => {
7294
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7295
+ if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
7296
+ suggestionsDiv.classList.add('hidden');
7297
+ }
7298
+ });
7299
+
7300
+ // Form submission
7301
+ var advancedSearchForm = document.getElementById('advancedSearchForm');
7302
+ if (advancedSearchForm) {
7303
+ advancedSearchForm.addEventListener('submit', async (e) => {
7304
+ e.preventDefault();
7305
+
7306
+ const formData = new FormData(e.target);
7307
+ const query = formData.get('query');
7308
+ const mode = formData.get('mode') || 'ai';
7309
+
7310
+ // Build filters
7311
+ const filters = {};
7312
+
7313
+ const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
7314
+ if (collections.length > 0) {
7315
+ // Need to convert collection names to IDs - for now, pass names
7316
+ filters.collections = collections;
7317
+ }
7318
+
7319
+ const status = Array.from(formData.getAll('status'));
7320
+ if (status.length > 0) {
7321
+ filters.status = status;
7322
+ }
7323
+
7324
+ const dateStart = formData.get('date_start');
7325
+ const dateEnd = formData.get('date_end');
7326
+ if (dateStart || dateEnd) {
7327
+ filters.dateRange = {
7328
+ start: dateStart ? new Date(dateStart) : null,
7329
+ end: dateEnd ? new Date(dateEnd) : null,
7330
+ field: 'created_at'
7331
+ };
7332
+ }
7333
+
7334
+ // Execute search
7335
+ try {
7336
+ const res = await fetch('/api/search', {
7337
+ method: 'POST',
7338
+ headers: {'Content-Type': 'application/json'},
7339
+ body: JSON.stringify({
7340
+ query,
7341
+ mode,
7342
+ filters,
7343
+ limit: 20
7344
+ })
7345
+ });
7346
+
7347
+ const { data } = await res.json();
7348
+
7349
+ if (data && data.results) {
7350
+ displaySearchResults(data);
7351
+ }
7352
+ } catch (error) {
7353
+ console.error('Search error:', error);
7354
+ alert('Search failed. Please try again.');
7355
+ }
5733
7356
  });
5734
7357
  }
5735
7358
 
5736
- // Helper to get action text for display
5737
- function getActionText(action) {
5738
- const actionCount = currentSelectedIds.length;
5739
- switch(action) {
5740
- case 'publish':
5741
- return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5742
- case 'draft':
5743
- return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
5744
- case 'delete':
5745
- return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5746
- default:
5747
- return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7359
+ function displaySearchResults(searchData) {
7360
+ const resultsDiv = document.getElementById('searchResultsContent');
7361
+ const resultsSection = document.getElementById('searchResults');
7362
+
7363
+ if (searchData.results.length === 0) {
7364
+ resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
7365
+ } else {
7366
+ resultsDiv.innerHTML = searchData.results.map(result => \`
7367
+ <div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
7368
+ <div class="flex items-start justify-between">
7369
+ <div class="flex-1">
7370
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
7371
+ <a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
7372
+ </h4>
7373
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
7374
+ \${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
7375
+ \${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
7376
+ </p>
7377
+ \${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
7378
+ </div>
7379
+ <div class="ml-4">
7380
+ <span class="px-2 py-1 text-xs rounded-full \${result.status === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'}">\${result.status}</span>
7381
+ </div>
7382
+ </div>
7383
+ </div>
7384
+ \`).join('');
5748
7385
  }
7386
+
7387
+ resultsSection.classList.remove('hidden');
7388
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
5749
7389
  }
5750
7390
 
7391
+ // Make functions globally available
7392
+ window.openAdvancedSearch = openAdvancedSearch;
7393
+ window.closeAdvancedSearch = closeAdvancedSearch;
5751
7394
  </script>
5752
-
5753
- <!-- Confirmation Dialog for Bulk Actions -->
5754
- ${chunkAZLU3ROK_cjs.renderConfirmationDialog({
5755
- id: "bulk-action-confirm",
5756
- title: "Confirm Bulk Action",
5757
- message: "Are you sure you want to perform this action? This operation will affect multiple items.",
5758
- confirmText: "Confirm",
5759
- cancelText: "Cancel",
5760
- confirmClass: "bg-blue-500 hover:bg-blue-400",
5761
- iconColor: "blue",
5762
- onConfirm: "executeBulkAction()"
5763
- })}
5764
-
5765
- <!-- Confirmation Dialog Script -->
5766
- ${chunkAZLU3ROK_cjs.getConfirmationDialogScript()}
5767
7395
  `;
5768
7396
  const layoutData = {
5769
7397
  title: "Content Management",
@@ -5773,7 +7401,7 @@ function renderContentListPage(data) {
5773
7401
  version: data.version,
5774
7402
  content: pageContent
5775
7403
  };
5776
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
7404
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
5777
7405
  }
5778
7406
 
5779
7407
  // src/templates/components/version-history.template.ts
@@ -5967,7 +7595,123 @@ async function isPluginActive2(db, pluginId) {
5967
7595
 
5968
7596
  // src/routes/admin-content.ts
5969
7597
  var adminContentRoutes = new hono.Hono();
5970
- adminContentRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
7598
+ function parseFieldValue(field, formData, options = {}) {
7599
+ const { skipValidation = false } = options;
7600
+ const value = formData.get(field.field_name);
7601
+ const errors = [];
7602
+ const blocksConfig = chunkMYB5RY7H_cjs.getBlocksFieldConfig(field.field_options);
7603
+ if (blocksConfig) {
7604
+ const parsed = chunkMYB5RY7H_cjs.parseBlocksValue(value, blocksConfig);
7605
+ if (!skipValidation && field.is_required && parsed.value.length === 0) {
7606
+ parsed.errors.push(`${field.field_label} is required`);
7607
+ }
7608
+ return { value: parsed.value, errors: parsed.errors };
7609
+ }
7610
+ if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
7611
+ return { value: null, errors: [`${field.field_label} is required`] };
7612
+ }
7613
+ switch (field.field_type) {
7614
+ case "number":
7615
+ if (value && isNaN(Number(value))) {
7616
+ if (!skipValidation) {
7617
+ errors.push(`${field.field_label} must be a valid number`);
7618
+ }
7619
+ return { value: null, errors };
7620
+ }
7621
+ return { value: value ? Number(value) : null, errors: [] };
7622
+ case "boolean":
7623
+ const submitted = formData.get(`${field.field_name}_submitted`);
7624
+ return { value: submitted ? value === "true" : false, errors: [] };
7625
+ case "select":
7626
+ if (field.field_options?.multiple) {
7627
+ return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
7628
+ }
7629
+ return { value, errors: [] };
7630
+ case "array": {
7631
+ if (!value || value.toString().trim() === "") {
7632
+ if (!skipValidation && field.is_required) {
7633
+ errors.push(`${field.field_label} is required`);
7634
+ }
7635
+ return { value: [], errors };
7636
+ }
7637
+ try {
7638
+ const parsed = JSON.parse(value.toString());
7639
+ if (!Array.isArray(parsed)) {
7640
+ if (!skipValidation) {
7641
+ errors.push(`${field.field_label} must be a JSON array`);
7642
+ }
7643
+ return { value: [], errors };
7644
+ }
7645
+ if (!skipValidation && field.is_required && parsed.length === 0) {
7646
+ errors.push(`${field.field_label} is required`);
7647
+ }
7648
+ return { value: parsed, errors };
7649
+ } catch {
7650
+ if (!skipValidation) {
7651
+ errors.push(`${field.field_label} must be valid JSON`);
7652
+ }
7653
+ return { value: [], errors };
7654
+ }
7655
+ }
7656
+ case "object": {
7657
+ if (!value || value.toString().trim() === "") {
7658
+ if (!skipValidation && field.is_required) {
7659
+ errors.push(`${field.field_label} is required`);
7660
+ }
7661
+ return { value: {}, errors };
7662
+ }
7663
+ try {
7664
+ const parsed = JSON.parse(value.toString());
7665
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7666
+ if (!skipValidation) {
7667
+ errors.push(`${field.field_label} must be a JSON object`);
7668
+ }
7669
+ return { value: {}, errors };
7670
+ }
7671
+ if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
7672
+ errors.push(`${field.field_label} is required`);
7673
+ }
7674
+ return { value: parsed, errors };
7675
+ } catch {
7676
+ if (!skipValidation) {
7677
+ errors.push(`${field.field_label} must be valid JSON`);
7678
+ }
7679
+ return { value: {}, errors };
7680
+ }
7681
+ }
7682
+ case "json": {
7683
+ if (!value || value.toString().trim() === "") {
7684
+ if (!skipValidation && field.is_required) {
7685
+ errors.push(`${field.field_label} is required`);
7686
+ }
7687
+ return { value: null, errors };
7688
+ }
7689
+ try {
7690
+ return { value: JSON.parse(value.toString()), errors: [] };
7691
+ } catch {
7692
+ if (!skipValidation) {
7693
+ errors.push(`${field.field_label} must be valid JSON`);
7694
+ }
7695
+ return { value: null, errors };
7696
+ }
7697
+ }
7698
+ default:
7699
+ return { value, errors: [] };
7700
+ }
7701
+ }
7702
+ function extractFieldData(fields, formData, options = {}) {
7703
+ const data = {};
7704
+ const errors = {};
7705
+ for (const field of fields) {
7706
+ const result = parseFieldValue(field, formData, options);
7707
+ data[field.field_name] = result.value;
7708
+ if (result.errors.length > 0) {
7709
+ errors[field.field_name] = result.errors;
7710
+ }
7711
+ }
7712
+ return { data, errors };
7713
+ }
7714
+ adminContentRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
5971
7715
  async function getCollectionFields(db, collectionId) {
5972
7716
  const cache = chunk7FOAMNTI_cjs.getCacheService(chunk7FOAMNTI_cjs.CACHE_CONFIGS.collection);
5973
7717
  return cache.getOrSet(
@@ -6249,21 +7993,21 @@ adminContentRoutes.get("/new", async (c) => {
6249
7993
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
6250
7994
  let tinymceSettings;
6251
7995
  if (tinymceEnabled) {
6252
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
7996
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6253
7997
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
6254
7998
  tinymceSettings = tinymcePlugin2?.settings;
6255
7999
  }
6256
8000
  const quillEnabled = await isPluginActive2(db, "quill-editor");
6257
8001
  let quillSettings;
6258
8002
  if (quillEnabled) {
6259
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8003
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6260
8004
  const quillPlugin = await pluginService.getPlugin("quill-editor");
6261
8005
  quillSettings = quillPlugin?.settings;
6262
8006
  }
6263
8007
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
6264
8008
  let mdxeditorSettings;
6265
8009
  if (mdxeditorEnabled) {
6266
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8010
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6267
8011
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
6268
8012
  mdxeditorSettings = mdxeditorPlugin?.settings;
6269
8013
  }
@@ -6354,21 +8098,21 @@ adminContentRoutes.get("/:id/edit", async (c) => {
6354
8098
  const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
6355
8099
  let tinymceSettings;
6356
8100
  if (tinymceEnabled) {
6357
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8101
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6358
8102
  const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
6359
8103
  tinymceSettings = tinymcePlugin2?.settings;
6360
8104
  }
6361
8105
  const quillEnabled = await isPluginActive2(db, "quill-editor");
6362
8106
  let quillSettings;
6363
8107
  if (quillEnabled) {
6364
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8108
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6365
8109
  const quillPlugin = await pluginService.getPlugin("quill-editor");
6366
8110
  quillSettings = quillPlugin?.settings;
6367
8111
  }
6368
8112
  const mdxeditorEnabled = await isPluginActive2(db, "easy-mdx");
6369
8113
  let mdxeditorSettings;
6370
8114
  if (mdxeditorEnabled) {
6371
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
8115
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
6372
8116
  const mdxeditorPlugin = await pluginService.getPlugin("easy-mdx");
6373
8117
  mdxeditorSettings = mdxeditorPlugin?.settings;
6374
8118
  }
@@ -6440,109 +8184,7 @@ adminContentRoutes.post("/", async (c) => {
6440
8184
  `);
6441
8185
  }
6442
8186
  const fields = await getCollectionFields(db, collectionId);
6443
- const data = {};
6444
- const errors = {};
6445
- for (const field of fields) {
6446
- const value = formData.get(field.field_name);
6447
- const blocksConfig = chunkZWV3EBZ7_cjs.getBlocksFieldConfig(field.field_options);
6448
- if (blocksConfig) {
6449
- const parsed = chunkZWV3EBZ7_cjs.parseBlocksValue(value, blocksConfig);
6450
- if (field.is_required && parsed.value.length === 0) {
6451
- parsed.errors.push(`${field.field_label} is required`);
6452
- }
6453
- if (parsed.errors.length > 0) {
6454
- errors[field.field_name] = parsed.errors;
6455
- }
6456
- data[field.field_name] = parsed.value;
6457
- continue;
6458
- }
6459
- if (field.is_required && (!value || value.toString().trim() === "")) {
6460
- errors[field.field_name] = [`${field.field_label} is required`];
6461
- continue;
6462
- }
6463
- switch (field.field_type) {
6464
- case "number":
6465
- if (value && isNaN(Number(value))) {
6466
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6467
- } else {
6468
- data[field.field_name] = value ? Number(value) : null;
6469
- }
6470
- break;
6471
- case "boolean":
6472
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6473
- break;
6474
- case "select":
6475
- if (field.field_options?.multiple) {
6476
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6477
- } else {
6478
- data[field.field_name] = value;
6479
- }
6480
- break;
6481
- case "array": {
6482
- if (!value || value.toString().trim() === "") {
6483
- data[field.field_name] = [];
6484
- if (field.is_required) {
6485
- errors[field.field_name] = [`${field.field_label} is required`];
6486
- }
6487
- break;
6488
- }
6489
- try {
6490
- const parsed = JSON.parse(value.toString());
6491
- if (!Array.isArray(parsed)) {
6492
- errors[field.field_name] = [`${field.field_label} must be a JSON array`];
6493
- } else {
6494
- if (field.is_required && parsed.length === 0) {
6495
- errors[field.field_name] = [`${field.field_label} is required`];
6496
- }
6497
- data[field.field_name] = parsed;
6498
- }
6499
- } catch {
6500
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6501
- }
6502
- break;
6503
- }
6504
- case "object": {
6505
- if (!value || value.toString().trim() === "") {
6506
- data[field.field_name] = {};
6507
- if (field.is_required) {
6508
- errors[field.field_name] = [`${field.field_label} is required`];
6509
- }
6510
- break;
6511
- }
6512
- try {
6513
- const parsed = JSON.parse(value.toString());
6514
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6515
- errors[field.field_name] = [`${field.field_label} must be a JSON object`];
6516
- } else {
6517
- if (field.is_required && Object.keys(parsed).length === 0) {
6518
- errors[field.field_name] = [`${field.field_label} is required`];
6519
- }
6520
- data[field.field_name] = parsed;
6521
- }
6522
- } catch {
6523
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6524
- }
6525
- break;
6526
- }
6527
- case "json": {
6528
- if (!value || value.toString().trim() === "") {
6529
- data[field.field_name] = null;
6530
- if (field.is_required) {
6531
- errors[field.field_name] = [`${field.field_label} is required`];
6532
- }
6533
- break;
6534
- }
6535
- try {
6536
- data[field.field_name] = JSON.parse(value.toString());
6537
- } catch {
6538
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6539
- }
6540
- break;
6541
- }
6542
- default:
6543
- data[field.field_name] = value;
6544
- }
6545
- }
8187
+ const { data, errors } = extractFieldData(fields, formData);
6546
8188
  if (Object.keys(errors).length > 0) {
6547
8189
  const formDataWithErrors = {
6548
8190
  collection,
@@ -6659,109 +8301,7 @@ adminContentRoutes.put("/:id", async (c) => {
6659
8301
  `);
6660
8302
  }
6661
8303
  const fields = await getCollectionFields(db, existingContent.collection_id);
6662
- const data = {};
6663
- const errors = {};
6664
- for (const field of fields) {
6665
- const value = formData.get(field.field_name);
6666
- const blocksConfig = chunkZWV3EBZ7_cjs.getBlocksFieldConfig(field.field_options);
6667
- if (blocksConfig) {
6668
- const parsed = chunkZWV3EBZ7_cjs.parseBlocksValue(value, blocksConfig);
6669
- if (field.is_required && parsed.value.length === 0) {
6670
- parsed.errors.push(`${field.field_label} is required`);
6671
- }
6672
- if (parsed.errors.length > 0) {
6673
- errors[field.field_name] = parsed.errors;
6674
- }
6675
- data[field.field_name] = parsed.value;
6676
- continue;
6677
- }
6678
- if (field.is_required && (!value || value.toString().trim() === "")) {
6679
- errors[field.field_name] = [`${field.field_label} is required`];
6680
- continue;
6681
- }
6682
- switch (field.field_type) {
6683
- case "number":
6684
- if (value && isNaN(Number(value))) {
6685
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6686
- } else {
6687
- data[field.field_name] = value ? Number(value) : null;
6688
- }
6689
- break;
6690
- case "boolean":
6691
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6692
- break;
6693
- case "select":
6694
- if (field.field_options?.multiple) {
6695
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6696
- } else {
6697
- data[field.field_name] = value;
6698
- }
6699
- break;
6700
- case "array": {
6701
- if (!value || value.toString().trim() === "") {
6702
- data[field.field_name] = [];
6703
- if (field.is_required) {
6704
- errors[field.field_name] = [`${field.field_label} is required`];
6705
- }
6706
- break;
6707
- }
6708
- try {
6709
- const parsed = JSON.parse(value.toString());
6710
- if (!Array.isArray(parsed)) {
6711
- errors[field.field_name] = [`${field.field_label} must be a JSON array`];
6712
- } else {
6713
- if (field.is_required && parsed.length === 0) {
6714
- errors[field.field_name] = [`${field.field_label} is required`];
6715
- }
6716
- data[field.field_name] = parsed;
6717
- }
6718
- } catch {
6719
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6720
- }
6721
- break;
6722
- }
6723
- case "object": {
6724
- if (!value || value.toString().trim() === "") {
6725
- data[field.field_name] = {};
6726
- if (field.is_required) {
6727
- errors[field.field_name] = [`${field.field_label} is required`];
6728
- }
6729
- break;
6730
- }
6731
- try {
6732
- const parsed = JSON.parse(value.toString());
6733
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6734
- errors[field.field_name] = [`${field.field_label} must be a JSON object`];
6735
- } else {
6736
- if (field.is_required && Object.keys(parsed).length === 0) {
6737
- errors[field.field_name] = [`${field.field_label} is required`];
6738
- }
6739
- data[field.field_name] = parsed;
6740
- }
6741
- } catch {
6742
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6743
- }
6744
- break;
6745
- }
6746
- case "json": {
6747
- if (!value || value.toString().trim() === "") {
6748
- data[field.field_name] = null;
6749
- if (field.is_required) {
6750
- errors[field.field_name] = [`${field.field_label} is required`];
6751
- }
6752
- break;
6753
- }
6754
- try {
6755
- data[field.field_name] = JSON.parse(value.toString());
6756
- } catch {
6757
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6758
- }
6759
- break;
6760
- }
6761
- default:
6762
- data[field.field_name] = value;
6763
- }
6764
- }
8304
+ const { data, errors } = extractFieldData(fields, formData);
6765
8305
  if (Object.keys(errors).length > 0) {
6766
8306
  const formDataWithErrors = {
6767
8307
  id,
@@ -6874,33 +8414,7 @@ adminContentRoutes.post("/preview", async (c) => {
6874
8414
  return c.html("<p>Collection not found</p>");
6875
8415
  }
6876
8416
  const fields = await getCollectionFields(db, collectionId);
6877
- const data = {};
6878
- for (const field of fields) {
6879
- const value = formData.get(field.field_name);
6880
- const blocksConfig = chunkZWV3EBZ7_cjs.getBlocksFieldConfig(field.field_options);
6881
- if (blocksConfig) {
6882
- const parsed = chunkZWV3EBZ7_cjs.parseBlocksValue(value, blocksConfig);
6883
- data[field.field_name] = parsed.value;
6884
- continue;
6885
- }
6886
- switch (field.field_type) {
6887
- case "number":
6888
- data[field.field_name] = value ? Number(value) : null;
6889
- break;
6890
- case "boolean":
6891
- data[field.field_name] = value === "true";
6892
- break;
6893
- case "select":
6894
- if (field.field_options?.multiple) {
6895
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6896
- } else {
6897
- data[field.field_name] = value;
6898
- }
6899
- break;
6900
- default:
6901
- data[field.field_name] = value;
6902
- }
6903
- }
8417
+ const { data } = extractFieldData(fields, formData, { skipValidation: true });
6904
8418
  const previewHTML = `
6905
8419
  <!DOCTYPE html>
6906
8420
  <html lang="en">
@@ -7328,7 +8842,7 @@ ${JSON.stringify(data, null, 2)}
7328
8842
  var admin_content_default = adminContentRoutes;
7329
8843
 
7330
8844
  // src/templates/pages/admin-profile.template.ts
7331
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
8845
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
7332
8846
  function renderAvatarImage(avatarUrl, firstName, lastName) {
7333
8847
  return `<div id="avatar-image-container" class="w-24 h-24 rounded-full mx-auto mb-4 overflow-hidden bg-gradient-to-br from-cyan-400 to-purple-400 flex items-center justify-center ring-4 ring-zinc-950/5 dark:ring-white/10">
7334
8848
  ${avatarUrl ? `<img src="${avatarUrl}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${firstName.charAt(0)}${lastName.charAt(0)}</span>`}
@@ -7348,8 +8862,8 @@ function renderProfilePage(data) {
7348
8862
  </div>
7349
8863
 
7350
8864
  <!-- Alert Messages -->
7351
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
7352
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8865
+ ${data.error ? chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8866
+ ${data.success ? chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
7353
8867
 
7354
8868
  <!-- Profile Form -->
7355
8869
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
@@ -7736,7 +9250,7 @@ function renderProfilePage(data) {
7736
9250
  version: data.version,
7737
9251
  content: pageContent
7738
9252
  };
7739
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
9253
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
7740
9254
  }
7741
9255
 
7742
9256
  // src/templates/components/alert.template.ts
@@ -8019,7 +9533,7 @@ function renderActivityLogsPage(data) {
8019
9533
  user: data.user,
8020
9534
  content: pageContent
8021
9535
  };
8022
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
9536
+ return chunkEHSZ6TAN_cjs.renderAdminLayout(layoutData);
8023
9537
  }
8024
9538
  function getActionBadgeClass(action) {
8025
9539
  if (action.includes("login") || action.includes("logout")) {
@@ -8039,7 +9553,7 @@ function formatAction(action) {
8039
9553
  }
8040
9554
 
8041
9555
  // src/templates/pages/admin-user-edit.template.ts
8042
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
9556
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
8043
9557
 
8044
9558
  // src/templates/components/confirmation-dialog.template.ts
8045
9559
  function renderConfirmationDialog2(options) {
@@ -8160,8 +9674,8 @@ function renderUserEditPage(data) {
8160
9674
 
8161
9675
  <!-- Alert Messages -->
8162
9676
  <div id="form-messages">
8163
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8164
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
9677
+ ${data.error ? chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
9678
+ ${data.success ? chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8165
9679
  </div>
8166
9680
 
8167
9681
  <!-- User Edit Form -->
@@ -8180,7 +9694,7 @@ function renderUserEditPage(data) {
8180
9694
  <input
8181
9695
  type="text"
8182
9696
  name="first_name"
8183
- value="${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.firstName || "")}"
9697
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.firstName || "")}"
8184
9698
  required
8185
9699
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8186
9700
  />
@@ -8191,7 +9705,7 @@ function renderUserEditPage(data) {
8191
9705
  <input
8192
9706
  type="text"
8193
9707
  name="last_name"
8194
- value="${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.lastName || "")}"
9708
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.lastName || "")}"
8195
9709
  required
8196
9710
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8197
9711
  />
@@ -8202,7 +9716,7 @@ function renderUserEditPage(data) {
8202
9716
  <input
8203
9717
  type="text"
8204
9718
  name="username"
8205
- value="${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.username || "")}"
9719
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.username || "")}"
8206
9720
  required
8207
9721
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8208
9722
  />
@@ -8213,7 +9727,7 @@ function renderUserEditPage(data) {
8213
9727
  <input
8214
9728
  type="email"
8215
9729
  name="email"
8216
- value="${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.email || "")}"
9730
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.email || "")}"
8217
9731
  required
8218
9732
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8219
9733
  />
@@ -8224,7 +9738,7 @@ function renderUserEditPage(data) {
8224
9738
  <input
8225
9739
  type="tel"
8226
9740
  name="phone"
8227
- value="${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.phone || "")}"
9741
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.phone || "")}"
8228
9742
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8229
9743
  />
8230
9744
  </div>
@@ -8238,7 +9752,7 @@ function renderUserEditPage(data) {
8238
9752
  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-zinc-500/30 dark:outline-zinc-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-zinc-500 dark:focus-visible:outline-zinc-400 sm:text-sm/6"
8239
9753
  >
8240
9754
  ${data.roles.map((role) => `
8241
- <option value="${chunkZWV3EBZ7_cjs.escapeHtml(role.value)}" ${data.userToEdit.role === role.value ? "selected" : ""}>${chunkZWV3EBZ7_cjs.escapeHtml(role.label)}</option>
9755
+ <option value="${chunkMYB5RY7H_cjs.escapeHtml(role.value)}" ${data.userToEdit.role === role.value ? "selected" : ""}>${chunkMYB5RY7H_cjs.escapeHtml(role.label)}</option>
8242
9756
  `).join("")}
8243
9757
  </select>
8244
9758
  <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-zinc-600 dark:text-zinc-400 sm:size-4">
@@ -8247,14 +9761,87 @@ function renderUserEditPage(data) {
8247
9761
  </div>
8248
9762
  </div>
8249
9763
  </div>
9764
+ </div>
9765
+
9766
+ <!-- Profile Information -->
9767
+ <div class="mb-8">
9768
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
9769
+ <p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
9770
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9771
+ <div>
9772
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
9773
+ <input
9774
+ type="text"
9775
+ name="profile_display_name"
9776
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.displayName || "")}"
9777
+ placeholder="Public display name"
9778
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9779
+ />
9780
+ </div>
9781
+
9782
+ <div>
9783
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
9784
+ <input
9785
+ type="text"
9786
+ name="profile_company"
9787
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.company || "")}"
9788
+ placeholder="Company or organization"
9789
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9790
+ />
9791
+ </div>
9792
+
9793
+ <div>
9794
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
9795
+ <input
9796
+ type="text"
9797
+ name="profile_job_title"
9798
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
9799
+ placeholder="Job title or role"
9800
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9801
+ />
9802
+ </div>
9803
+
9804
+ <div>
9805
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
9806
+ <input
9807
+ type="url"
9808
+ name="profile_website"
9809
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.website || "")}"
9810
+ placeholder="https://example.com"
9811
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9812
+ />
9813
+ </div>
9814
+
9815
+ <div>
9816
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
9817
+ <input
9818
+ type="text"
9819
+ name="profile_location"
9820
+ value="${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.location || "")}"
9821
+ placeholder="City, Country"
9822
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9823
+ />
9824
+ </div>
9825
+
9826
+ <div>
9827
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
9828
+ <input
9829
+ type="date"
9830
+ name="profile_date_of_birth"
9831
+ value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
9832
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
9833
+ />
9834
+ </div>
9835
+ </div>
8250
9836
 
8251
9837
  <div class="mt-6">
8252
9838
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
8253
9839
  <textarea
8254
- name="bio"
9840
+ name="profile_bio"
8255
9841
  rows="3"
9842
+ placeholder="Short bio or description"
8256
9843
  class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white 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"
8257
- >${chunkZWV3EBZ7_cjs.escapeHtml(data.userToEdit.bio || "")}</textarea>
9844
+ >${chunkMYB5RY7H_cjs.escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
8258
9845
  </div>
8259
9846
  </div>
8260
9847
 
@@ -8454,11 +10041,11 @@ function renderUserEditPage(data) {
8454
10041
  user: data.user,
8455
10042
  content: pageContent
8456
10043
  };
8457
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10044
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
8458
10045
  }
8459
10046
 
8460
10047
  // src/templates/pages/admin-user-new.template.ts
8461
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
10048
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
8462
10049
  function renderUserNewPage(data) {
8463
10050
  const pageContent = `
8464
10051
  <div>
@@ -8497,8 +10084,8 @@ function renderUserNewPage(data) {
8497
10084
 
8498
10085
  <!-- Alert Messages -->
8499
10086
  <div id="form-messages">
8500
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8501
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
10087
+ ${data.error ? chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
10088
+ ${data.success ? chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8502
10089
  </div>
8503
10090
 
8504
10091
  <!-- User New Form -->
@@ -8742,11 +10329,11 @@ function renderUserNewPage(data) {
8742
10329
  user: data.user,
8743
10330
  content: pageContent
8744
10331
  };
8745
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10332
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
8746
10333
  }
8747
10334
 
8748
10335
  // src/templates/pages/admin-users-list.template.ts
8749
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
10336
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
8750
10337
  function renderUsersListPage(data) {
8751
10338
  const columns = [
8752
10339
  {
@@ -8897,8 +10484,8 @@ function renderUsersListPage(data) {
8897
10484
  </div>
8898
10485
 
8899
10486
  <!-- Alert Messages -->
8900
- ${data.error ? chunkAZLU3ROK_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
8901
- ${data.success ? chunkAZLU3ROK_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
10487
+ ${data.error ? chunkEHSZ6TAN_cjs.renderAlert({ type: "error", message: data.error, dismissible: true }) : ""}
10488
+ ${data.success ? chunkEHSZ6TAN_cjs.renderAlert({ type: "success", message: data.success, dismissible: true }) : ""}
8902
10489
 
8903
10490
  <!-- Stats -->
8904
10491
  <div class="mb-6">
@@ -9075,10 +10662,10 @@ function renderUsersListPage(data) {
9075
10662
  </div>
9076
10663
 
9077
10664
  <!-- Users Table -->
9078
- ${chunkAZLU3ROK_cjs.renderTable(tableData)}
10665
+ ${chunkEHSZ6TAN_cjs.renderTable(tableData)}
9079
10666
 
9080
10667
  <!-- Pagination -->
9081
- ${data.pagination ? chunkAZLU3ROK_cjs.renderPagination(data.pagination) : ""}
10668
+ ${data.pagination ? chunkEHSZ6TAN_cjs.renderPagination(data.pagination) : ""}
9082
10669
  </div>
9083
10670
 
9084
10671
  <script>
@@ -9149,12 +10736,12 @@ function renderUsersListPage(data) {
9149
10736
  version: data.version,
9150
10737
  content: pageContent
9151
10738
  };
9152
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
10739
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
9153
10740
  }
9154
10741
 
9155
10742
  // src/routes/admin-users.ts
9156
10743
  var userRoutes = new hono.Hono();
9157
- userRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
10744
+ userRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
9158
10745
  userRoutes.get("/", (c) => {
9159
10746
  return c.redirect("/admin/dashboard");
9160
10747
  });
@@ -9253,12 +10840,12 @@ userRoutes.put("/profile", async (c) => {
9253
10840
  const db = c.env.DB;
9254
10841
  try {
9255
10842
  const formData = await c.req.formData();
9256
- const firstName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9257
- const lastName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9258
- const username = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("username")?.toString());
10843
+ const firstName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("first_name")?.toString());
10844
+ const lastName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("last_name")?.toString());
10845
+ const username = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("username")?.toString());
9259
10846
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9260
- const phone = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9261
- const bio = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
10847
+ const phone = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
10848
+ const bio = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
9262
10849
  const timezone = formData.get("timezone")?.toString() || "UTC";
9263
10850
  const language = formData.get("language")?.toString() || "en";
9264
10851
  const emailNotifications = formData.get("email_notifications") === "1";
@@ -9309,7 +10896,7 @@ userRoutes.put("/profile", async (c) => {
9309
10896
  Date.now(),
9310
10897
  user.userId
9311
10898
  ).run();
9312
- await chunkYYV3XQOQ_cjs.logActivity(
10899
+ await chunkE2BXLXPW_cjs.logActivity(
9313
10900
  db,
9314
10901
  user.userId,
9315
10902
  "profile.update",
@@ -9372,7 +10959,7 @@ userRoutes.post("/profile/avatar", async (c) => {
9372
10959
  SELECT first_name, last_name FROM users WHERE id = ?
9373
10960
  `);
9374
10961
  const userData = await userStmt.bind(user.userId).first();
9375
- await chunkYYV3XQOQ_cjs.logActivity(
10962
+ await chunkE2BXLXPW_cjs.logActivity(
9376
10963
  db,
9377
10964
  user.userId,
9378
10965
  "profile.avatar_update",
@@ -9443,7 +11030,7 @@ userRoutes.post("/profile/password", async (c) => {
9443
11030
  dismissible: true
9444
11031
  }));
9445
11032
  }
9446
- const validPassword = await chunkYYV3XQOQ_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
11033
+ const validPassword = await chunkE2BXLXPW_cjs.AuthManager.verifyPassword(currentPassword, userData.password_hash);
9447
11034
  if (!validPassword) {
9448
11035
  return c.html(renderAlert2({
9449
11036
  type: "error",
@@ -9451,7 +11038,7 @@ userRoutes.post("/profile/password", async (c) => {
9451
11038
  dismissible: true
9452
11039
  }));
9453
11040
  }
9454
- const newPasswordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(newPassword);
11041
+ const newPasswordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(newPassword);
9455
11042
  const historyStmt = db.prepare(`
9456
11043
  INSERT INTO password_history (id, user_id, password_hash, created_at)
9457
11044
  VALUES (?, ?, ?, ?)
@@ -9467,7 +11054,7 @@ userRoutes.post("/profile/password", async (c) => {
9467
11054
  WHERE id = ?
9468
11055
  `);
9469
11056
  await updateStmt.bind(newPasswordHash, Date.now(), user.userId).run();
9470
- await chunkYYV3XQOQ_cjs.logActivity(
11057
+ await chunkE2BXLXPW_cjs.logActivity(
9471
11058
  db,
9472
11059
  user.userId,
9473
11060
  "profile.password_change",
@@ -9534,7 +11121,7 @@ userRoutes.get("/users", async (c) => {
9534
11121
  `);
9535
11122
  const countResult = await countStmt.bind(...params).first();
9536
11123
  const totalUsers = countResult?.total || 0;
9537
- await chunkYYV3XQOQ_cjs.logActivity(
11124
+ await chunkE2BXLXPW_cjs.logActivity(
9538
11125
  db,
9539
11126
  user.userId,
9540
11127
  "users.list_view",
@@ -9636,12 +11223,12 @@ userRoutes.post("/users/new", async (c) => {
9636
11223
  const user = c.get("user");
9637
11224
  try {
9638
11225
  const formData = await c.req.formData();
9639
- const firstName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9640
- const lastName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9641
- const username = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("username")?.toString());
11226
+ const firstName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("first_name")?.toString());
11227
+ const lastName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("last_name")?.toString());
11228
+ const username = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("username")?.toString());
9642
11229
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9643
- const phone = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9644
- const bio = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11230
+ const phone = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
11231
+ const bio = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
9645
11232
  const role = formData.get("role")?.toString() || "viewer";
9646
11233
  const password = formData.get("password")?.toString() || "";
9647
11234
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
@@ -9688,7 +11275,7 @@ userRoutes.post("/users/new", async (c) => {
9688
11275
  dismissible: true
9689
11276
  }));
9690
11277
  }
9691
- const passwordHash = await chunkYYV3XQOQ_cjs.AuthManager.hashPassword(password);
11278
+ const passwordHash = await chunkE2BXLXPW_cjs.AuthManager.hashPassword(password);
9692
11279
  const userId = crypto.randomUUID();
9693
11280
  const createStmt = db.prepare(`
9694
11281
  INSERT INTO users (
@@ -9711,7 +11298,7 @@ userRoutes.post("/users/new", async (c) => {
9711
11298
  Date.now(),
9712
11299
  Date.now()
9713
11300
  ).run();
9714
- await chunkYYV3XQOQ_cjs.logActivity(
11301
+ await chunkE2BXLXPW_cjs.logActivity(
9715
11302
  db,
9716
11303
  user.userId,
9717
11304
  "user!.create",
@@ -9749,7 +11336,7 @@ userRoutes.get("/users/:id", async (c) => {
9749
11336
  if (!userRecord) {
9750
11337
  return c.json({ error: "User not found" }, 404);
9751
11338
  }
9752
- await chunkYYV3XQOQ_cjs.logActivity(
11339
+ await chunkE2BXLXPW_cjs.logActivity(
9753
11340
  db,
9754
11341
  user.userId,
9755
11342
  "user!.view",
@@ -9788,7 +11375,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9788
11375
  const userId = c.req.param("id");
9789
11376
  try {
9790
11377
  const userStmt = db.prepare(`
9791
- SELECT id, email, username, first_name, last_name, phone, bio, avatar_url,
11378
+ SELECT id, email, username, first_name, last_name, phone, avatar_url,
9792
11379
  role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
9793
11380
  FROM users
9794
11381
  WHERE id = ?
@@ -9801,6 +11388,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
9801
11388
  dismissible: true
9802
11389
  }), 404);
9803
11390
  }
11391
+ const profileStmt = db.prepare(`
11392
+ SELECT display_name, bio, company, job_title, website, location, date_of_birth
11393
+ FROM user_profiles
11394
+ WHERE user_id = ?
11395
+ `);
11396
+ const profileData = await profileStmt.bind(userId).first();
11397
+ const profile = profileData ? {
11398
+ displayName: profileData.display_name,
11399
+ bio: profileData.bio,
11400
+ company: profileData.company,
11401
+ jobTitle: profileData.job_title,
11402
+ website: profileData.website,
11403
+ location: profileData.location,
11404
+ dateOfBirth: profileData.date_of_birth
11405
+ } : void 0;
9804
11406
  const editData = {
9805
11407
  id: userToEdit.id,
9806
11408
  email: userToEdit.email,
@@ -9808,14 +11410,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
9808
11410
  firstName: userToEdit.first_name || "",
9809
11411
  lastName: userToEdit.last_name || "",
9810
11412
  phone: userToEdit.phone,
9811
- bio: userToEdit.bio,
9812
11413
  avatarUrl: userToEdit.avatar_url,
9813
11414
  role: userToEdit.role,
9814
11415
  isActive: Boolean(userToEdit.is_active),
9815
11416
  emailVerified: Boolean(userToEdit.email_verified),
9816
11417
  twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
9817
11418
  createdAt: userToEdit.created_at,
9818
- lastLoginAt: userToEdit.last_login_at
11419
+ lastLoginAt: userToEdit.last_login_at,
11420
+ profile
9819
11421
  };
9820
11422
  const pageData = {
9821
11423
  userToEdit: editData,
@@ -9831,7 +11433,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9831
11433
  console.error("User edit page error:", error);
9832
11434
  return c.html(renderAlert2({
9833
11435
  type: "error",
9834
- message: "Failed to load user!. Please try again.",
11436
+ message: "Failed to load user. Please try again.",
9835
11437
  dismissible: true
9836
11438
  }), 500);
9837
11439
  }
@@ -9842,15 +11444,22 @@ userRoutes.put("/users/:id", async (c) => {
9842
11444
  const userId = c.req.param("id");
9843
11445
  try {
9844
11446
  const formData = await c.req.formData();
9845
- const firstName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("first_name")?.toString());
9846
- const lastName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("last_name")?.toString());
9847
- const username = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("username")?.toString());
11447
+ const firstName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("first_name")?.toString());
11448
+ const lastName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("last_name")?.toString());
11449
+ const username = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("username")?.toString());
9848
11450
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9849
- const phone = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9850
- const bio = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("bio")?.toString()) || null;
11451
+ const phone = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("phone")?.toString()) || null;
9851
11452
  const role = formData.get("role")?.toString() || "viewer";
9852
11453
  const isActive = formData.get("is_active") === "1";
9853
11454
  const emailVerified = formData.get("email_verified") === "1";
11455
+ const profileDisplayName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
11456
+ const profileBio = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("profile_bio")?.toString()) || null;
11457
+ const profileCompany = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("profile_company")?.toString()) || null;
11458
+ const profileJobTitle = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
11459
+ const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
11460
+ const profileLocation = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("profile_location")?.toString()) || null;
11461
+ const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
11462
+ const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
9854
11463
  if (!firstName || !lastName || !username || !email) {
9855
11464
  return c.html(renderAlert2({
9856
11465
  type: "error",
@@ -9866,6 +11475,17 @@ userRoutes.put("/users/:id", async (c) => {
9866
11475
  dismissible: true
9867
11476
  }));
9868
11477
  }
11478
+ if (profileWebsite) {
11479
+ try {
11480
+ new URL(profileWebsite);
11481
+ } catch {
11482
+ return c.html(renderAlert2({
11483
+ type: "error",
11484
+ message: "Please enter a valid website URL.",
11485
+ dismissible: true
11486
+ }));
11487
+ }
11488
+ }
9869
11489
  const checkStmt = db.prepare(`
9870
11490
  SELECT id FROM users
9871
11491
  WHERE (username = ? OR email = ?) AND id != ?
@@ -9874,14 +11494,14 @@ userRoutes.put("/users/:id", async (c) => {
9874
11494
  if (existingUser) {
9875
11495
  return c.html(renderAlert2({
9876
11496
  type: "error",
9877
- message: "Username or email is already taken by another user!.",
11497
+ message: "Username or email is already taken by another user.",
9878
11498
  dismissible: true
9879
11499
  }));
9880
11500
  }
9881
11501
  const updateStmt = db.prepare(`
9882
11502
  UPDATE users SET
9883
11503
  first_name = ?, last_name = ?, username = ?, email = ?,
9884
- phone = ?, bio = ?, role = ?, is_active = ?, email_verified = ?,
11504
+ phone = ?, role = ?, is_active = ?, email_verified = ?,
9885
11505
  updated_at = ?
9886
11506
  WHERE id = ?
9887
11507
  `);
@@ -9891,20 +11511,63 @@ userRoutes.put("/users/:id", async (c) => {
9891
11511
  username,
9892
11512
  email,
9893
11513
  phone,
9894
- bio,
9895
11514
  role,
9896
11515
  isActive ? 1 : 0,
9897
11516
  emailVerified ? 1 : 0,
9898
11517
  Date.now(),
9899
11518
  userId
9900
11519
  ).run();
9901
- await chunkYYV3XQOQ_cjs.logActivity(
11520
+ const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
11521
+ if (hasProfileData) {
11522
+ const now = Date.now();
11523
+ const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
11524
+ const existingProfile = await profileCheckStmt.bind(userId).first();
11525
+ if (existingProfile) {
11526
+ const updateProfileStmt = db.prepare(`
11527
+ UPDATE user_profiles SET
11528
+ display_name = ?, bio = ?, company = ?, job_title = ?,
11529
+ website = ?, location = ?, date_of_birth = ?, updated_at = ?
11530
+ WHERE user_id = ?
11531
+ `);
11532
+ await updateProfileStmt.bind(
11533
+ profileDisplayName,
11534
+ profileBio,
11535
+ profileCompany,
11536
+ profileJobTitle,
11537
+ profileWebsite,
11538
+ profileLocation,
11539
+ profileDateOfBirth,
11540
+ now,
11541
+ userId
11542
+ ).run();
11543
+ } else {
11544
+ const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
11545
+ const insertProfileStmt = db.prepare(`
11546
+ INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
11547
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11548
+ `);
11549
+ await insertProfileStmt.bind(
11550
+ profileId,
11551
+ userId,
11552
+ profileDisplayName,
11553
+ profileBio,
11554
+ profileCompany,
11555
+ profileJobTitle,
11556
+ profileWebsite,
11557
+ profileLocation,
11558
+ profileDateOfBirth,
11559
+ now,
11560
+ now
11561
+ ).run();
11562
+ }
11563
+ }
11564
+ await chunkE2BXLXPW_cjs.logActivity(
9902
11565
  db,
9903
11566
  user.userId,
9904
- "user!.update",
11567
+ "user.update",
9905
11568
  "users",
9906
11569
  userId,
9907
- { fields: ["first_name", "last_name", "username", "email", "phone", "bio", "role", "is_active", "email_verified"] },
11570
+ { fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
9908
11571
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
9909
11572
  c.req.header("user-agent")
9910
11573
  );
@@ -9917,7 +11580,7 @@ userRoutes.put("/users/:id", async (c) => {
9917
11580
  console.error("User update error:", error);
9918
11581
  return c.html(renderAlert2({
9919
11582
  type: "error",
9920
- message: "Failed to update user!. Please try again.",
11583
+ message: "Failed to update user. Please try again.",
9921
11584
  dismissible: true
9922
11585
  }));
9923
11586
  }
@@ -9943,7 +11606,7 @@ userRoutes.post("/users/:id/toggle", async (c) => {
9943
11606
  UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
9944
11607
  `);
9945
11608
  await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
9946
- await chunkYYV3XQOQ_cjs.logActivity(
11609
+ await chunkE2BXLXPW_cjs.logActivity(
9947
11610
  db,
9948
11611
  user.userId,
9949
11612
  active ? "user.activate" : "user.deactivate",
@@ -9984,7 +11647,7 @@ userRoutes.delete("/users/:id", async (c) => {
9984
11647
  DELETE FROM users WHERE id = ?
9985
11648
  `);
9986
11649
  await deleteStmt.bind(userId).run();
9987
- await chunkYYV3XQOQ_cjs.logActivity(
11650
+ await chunkE2BXLXPW_cjs.logActivity(
9988
11651
  db,
9989
11652
  user.userId,
9990
11653
  "user!.hard_delete",
@@ -10003,7 +11666,7 @@ userRoutes.delete("/users/:id", async (c) => {
10003
11666
  UPDATE users SET is_active = 0, updated_at = ? WHERE id = ?
10004
11667
  `);
10005
11668
  await deleteStmt.bind(Date.now(), userId).run();
10006
- await chunkYYV3XQOQ_cjs.logActivity(
11669
+ await chunkE2BXLXPW_cjs.logActivity(
10007
11670
  db,
10008
11671
  user.userId,
10009
11672
  "user!.soft_delete",
@@ -10030,8 +11693,8 @@ userRoutes.post("/invite-user", async (c) => {
10030
11693
  const formData = await c.req.formData();
10031
11694
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
10032
11695
  const role = formData.get("role")?.toString()?.trim() || "viewer";
10033
- const firstName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("first_name")?.toString());
10034
- const lastName = chunkZWV3EBZ7_cjs.sanitizeInput(formData.get("last_name")?.toString());
11696
+ const firstName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("first_name")?.toString());
11697
+ const lastName = chunkMYB5RY7H_cjs.sanitizeInput(formData.get("last_name")?.toString());
10035
11698
  if (!email || !firstName || !lastName) {
10036
11699
  return c.json({ error: "Email, first name, and last name are required" }, 400);
10037
11700
  }
@@ -10069,7 +11732,7 @@ userRoutes.post("/invite-user", async (c) => {
10069
11732
  Date.now(),
10070
11733
  Date.now()
10071
11734
  ).run();
10072
- await chunkYYV3XQOQ_cjs.logActivity(
11735
+ await chunkE2BXLXPW_cjs.logActivity(
10073
11736
  db,
10074
11737
  user.userId,
10075
11738
  "user!.invite_sent",
@@ -10126,7 +11789,7 @@ userRoutes.post("/resend-invitation/:id", async (c) => {
10126
11789
  Date.now(),
10127
11790
  userId
10128
11791
  ).run();
10129
- await chunkYYV3XQOQ_cjs.logActivity(
11792
+ await chunkE2BXLXPW_cjs.logActivity(
10130
11793
  db,
10131
11794
  user.userId,
10132
11795
  "user!.invitation_resent",
@@ -10162,7 +11825,7 @@ userRoutes.delete("/cancel-invitation/:id", async (c) => {
10162
11825
  }
10163
11826
  const deleteStmt = db.prepare(`DELETE FROM users WHERE id = ?`);
10164
11827
  await deleteStmt.bind(userId).run();
10165
- await chunkYYV3XQOQ_cjs.logActivity(
11828
+ await chunkE2BXLXPW_cjs.logActivity(
10166
11829
  db,
10167
11830
  user.userId,
10168
11831
  "user!.invitation_cancelled",
@@ -10245,7 +11908,7 @@ userRoutes.get("/activity-logs", async (c) => {
10245
11908
  ...log,
10246
11909
  details: log.details ? JSON.parse(log.details) : null
10247
11910
  }));
10248
- await chunkYYV3XQOQ_cjs.logActivity(
11911
+ await chunkE2BXLXPW_cjs.logActivity(
10249
11912
  db,
10250
11913
  user.userId,
10251
11914
  "activity.logs_viewed",
@@ -10352,7 +12015,7 @@ userRoutes.get("/activity-logs/export", async (c) => {
10352
12015
  csvRows.push(row.join(","));
10353
12016
  }
10354
12017
  const csvContent = csvRows.join("\n");
10355
- await chunkYYV3XQOQ_cjs.logActivity(
12018
+ await chunkE2BXLXPW_cjs.logActivity(
10356
12019
  db,
10357
12020
  user.userId,
10358
12021
  "activity.logs_exported",
@@ -10570,7 +12233,7 @@ function getFileIcon(mimeType) {
10570
12233
  }
10571
12234
 
10572
12235
  // src/templates/pages/admin-media-library.template.ts
10573
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
12236
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
10574
12237
  function renderMediaLibraryPage(data) {
10575
12238
  const pageContent = `
10576
12239
  <div>
@@ -11505,7 +13168,7 @@ function renderMediaLibraryPage(data) {
11505
13168
  version: data.version,
11506
13169
  content: pageContent
11507
13170
  };
11508
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
13171
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
11509
13172
  }
11510
13173
 
11511
13174
  // src/templates/components/media-file-details.template.ts
@@ -11691,7 +13354,7 @@ var fileValidationSchema2 = zod.z.object({
11691
13354
  // 50MB max
11692
13355
  });
11693
13356
  var adminMediaRoutes = new hono.Hono();
11694
- adminMediaRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
13357
+ adminMediaRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
11695
13358
  adminMediaRoutes.get("/", async (c) => {
11696
13359
  try {
11697
13360
  const user = c.get("user");
@@ -12277,7 +13940,7 @@ adminMediaRoutes.put("/:id", async (c) => {
12277
13940
  `);
12278
13941
  }
12279
13942
  });
12280
- adminMediaRoutes.delete("/cleanup", chunkYYV3XQOQ_cjs.requireRole("admin"), async (c) => {
13943
+ adminMediaRoutes.delete("/cleanup", chunkE2BXLXPW_cjs.requireRole("admin"), async (c) => {
12281
13944
  try {
12282
13945
  const db = c.env.DB;
12283
13946
  const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
@@ -12527,7 +14190,7 @@ function formatFileSize(bytes) {
12527
14190
  }
12528
14191
 
12529
14192
  // src/templates/pages/admin-plugins-list.template.ts
12530
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
14193
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
12531
14194
  function renderPluginsListPage(data) {
12532
14195
  const categories = [
12533
14196
  { value: "content", label: "Content Management" },
@@ -13007,7 +14670,7 @@ function renderPluginsListPage(data) {
13007
14670
  version: data.version,
13008
14671
  content: pageContent
13009
14672
  };
13010
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
14673
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
13011
14674
  }
13012
14675
  function renderPluginCard(plugin) {
13013
14676
  const statusColors = {
@@ -13662,7 +15325,7 @@ function renderPluginSettingsPage(data) {
13662
15325
  user,
13663
15326
  content: pageContent
13664
15327
  };
13665
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
15328
+ return chunkEHSZ6TAN_cjs.renderAdminLayout(layoutData);
13666
15329
  }
13667
15330
  function renderStatusBadge(status) {
13668
15331
  const statusColors = {
@@ -14003,7 +15666,7 @@ function formatTimestamp(timestamp) {
14003
15666
 
14004
15667
  // src/routes/admin-plugins.ts
14005
15668
  var adminPluginRoutes = new hono.Hono();
14006
- adminPluginRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
15669
+ adminPluginRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
14007
15670
  var AVAILABLE_PLUGINS = [
14008
15671
  {
14009
15672
  id: "third-party-faq",
@@ -14108,6 +15771,19 @@ var AVAILABLE_PLUGINS = [
14108
15771
  permissions: [],
14109
15772
  dependencies: [],
14110
15773
  is_core: true
15774
+ },
15775
+ {
15776
+ id: "ai-search",
15777
+ name: "ai-search-plugin",
15778
+ display_name: "AI Search",
15779
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
15780
+ version: "1.0.0",
15781
+ author: "SonicJS Team",
15782
+ category: "search",
15783
+ icon: "\u{1F50D}",
15784
+ permissions: [],
15785
+ dependencies: [],
15786
+ is_core: true
14111
15787
  }
14112
15788
  ];
14113
15789
  adminPluginRoutes.get("/", async (c) => {
@@ -14117,7 +15793,7 @@ adminPluginRoutes.get("/", async (c) => {
14117
15793
  if (user?.role !== "admin") {
14118
15794
  return c.text("Access denied", 403);
14119
15795
  }
14120
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15796
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14121
15797
  let installedPlugins = [];
14122
15798
  let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
14123
15799
  try {
@@ -14186,10 +15862,13 @@ adminPluginRoutes.get("/:id", async (c) => {
14186
15862
  const user = c.get("user");
14187
15863
  const db = c.env.DB;
14188
15864
  const pluginId = c.req.param("id");
15865
+ if (pluginId === "ai-search") {
15866
+ return c.text("", 404);
15867
+ }
14189
15868
  if (user?.role !== "admin") {
14190
15869
  return c.redirect("/admin/plugins");
14191
15870
  }
14192
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15871
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14193
15872
  const plugin = await pluginService.getPlugin(pluginId);
14194
15873
  if (!plugin) {
14195
15874
  return c.text("Plugin not found", 404);
@@ -14243,7 +15922,7 @@ adminPluginRoutes.post("/:id/activate", async (c) => {
14243
15922
  if (user?.role !== "admin") {
14244
15923
  return c.json({ error: "Access denied" }, 403);
14245
15924
  }
14246
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15925
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14247
15926
  await pluginService.activatePlugin(pluginId);
14248
15927
  return c.json({ success: true });
14249
15928
  } catch (error) {
@@ -14260,7 +15939,7 @@ adminPluginRoutes.post("/:id/deactivate", async (c) => {
14260
15939
  if (user?.role !== "admin") {
14261
15940
  return c.json({ error: "Access denied" }, 403);
14262
15941
  }
14263
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15942
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14264
15943
  await pluginService.deactivatePlugin(pluginId);
14265
15944
  return c.json({ success: true });
14266
15945
  } catch (error) {
@@ -14277,7 +15956,7 @@ adminPluginRoutes.post("/install", async (c) => {
14277
15956
  return c.json({ error: "Access denied" }, 403);
14278
15957
  }
14279
15958
  const body = await c.req.json();
14280
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
15959
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14281
15960
  if (body.name === "faq-plugin") {
14282
15961
  const faqPlugin = await pluginService.installPlugin({
14283
15962
  id: "third-party-faq",
@@ -14478,6 +16157,33 @@ adminPluginRoutes.post("/install", async (c) => {
14478
16157
  });
14479
16158
  return c.json({ success: true, plugin: easyMdxPlugin2 });
14480
16159
  }
16160
+ if (body.name === "ai-search-plugin" || body.name === "ai-search") {
16161
+ const defaultSettings = {
16162
+ enabled: true,
16163
+ ai_mode_enabled: true,
16164
+ selected_collections: [],
16165
+ dismissed_collections: [],
16166
+ autocomplete_enabled: true,
16167
+ cache_duration: 1,
16168
+ results_limit: 20,
16169
+ index_media: false
16170
+ };
16171
+ const aiSearchPlugin = await pluginService.installPlugin({
16172
+ id: "ai-search",
16173
+ name: "ai-search-plugin",
16174
+ display_name: "AI Search",
16175
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
16176
+ version: "1.0.0",
16177
+ author: "SonicJS Team",
16178
+ category: "search",
16179
+ icon: "\u{1F50D}",
16180
+ permissions: [],
16181
+ dependencies: [],
16182
+ is_core: true,
16183
+ settings: defaultSettings
16184
+ });
16185
+ return c.json({ success: true, plugin: aiSearchPlugin });
16186
+ }
14481
16187
  if (body.name === "turnstile-plugin") {
14482
16188
  const turnstilePlugin = await pluginService.installPlugin({
14483
16189
  id: "turnstile",
@@ -14520,7 +16226,7 @@ adminPluginRoutes.post("/:id/uninstall", async (c) => {
14520
16226
  if (user?.role !== "admin") {
14521
16227
  return c.json({ error: "Access denied" }, 403);
14522
16228
  }
14523
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
16229
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14524
16230
  await pluginService.uninstallPlugin(pluginId);
14525
16231
  return c.json({ success: true });
14526
16232
  } catch (error) {
@@ -14538,7 +16244,7 @@ adminPluginRoutes.post("/:id/settings", async (c) => {
14538
16244
  return c.json({ error: "Access denied" }, 403);
14539
16245
  }
14540
16246
  const settings = await c.req.json();
14541
- const pluginService = new chunkILZ3DP4I_cjs.PluginService(db);
16247
+ const pluginService = new chunkMPT5PA6U_cjs.PluginService(db);
14542
16248
  await pluginService.updatePluginSettings(pluginId, settings);
14543
16249
  return c.json({ success: true });
14544
16250
  } catch (error) {
@@ -14559,7 +16265,7 @@ function formatLastUpdated(timestamp) {
14559
16265
  }
14560
16266
 
14561
16267
  // src/templates/pages/admin-logs-list.template.ts
14562
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
16268
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
14563
16269
  function renderLogsListPage(data) {
14564
16270
  const { logs, pagination, filters, user } = data;
14565
16271
  const content = `
@@ -14870,7 +16576,7 @@ function renderLogsListPage(data) {
14870
16576
  user,
14871
16577
  content
14872
16578
  };
14873
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
16579
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
14874
16580
  }
14875
16581
  function renderLogDetailsPage(data) {
14876
16582
  const { log, user } = data;
@@ -15082,7 +16788,7 @@ function renderLogDetailsPage(data) {
15082
16788
  </div>
15083
16789
  </div>
15084
16790
  `;
15085
- return chunkAZLU3ROK_cjs.adminLayoutV2({
16791
+ return chunkEHSZ6TAN_cjs.adminLayoutV2({
15086
16792
  title: `Log Details - ${log.id}`,
15087
16793
  user,
15088
16794
  content
@@ -15325,7 +17031,7 @@ function renderLogConfigPage(data) {
15325
17031
 
15326
17032
  <script src="https://unpkg.com/htmx.org@1.9.6"></script>
15327
17033
  `;
15328
- return chunkAZLU3ROK_cjs.adminLayoutV2({
17034
+ return chunkEHSZ6TAN_cjs.adminLayoutV2({
15329
17035
  title: "Log Configuration",
15330
17036
  user,
15331
17037
  content
@@ -15334,7 +17040,7 @@ function renderLogConfigPage(data) {
15334
17040
 
15335
17041
  // src/routes/admin-logs.ts
15336
17042
  var adminLogsRoutes = new hono.Hono();
15337
- adminLogsRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
17043
+ adminLogsRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
15338
17044
  adminLogsRoutes.get("/", async (c) => {
15339
17045
  try {
15340
17046
  const user = c.get("user");
@@ -15706,7 +17412,7 @@ adminDesignRoutes.get("/", (c) => {
15706
17412
  role: user.role
15707
17413
  } : void 0
15708
17414
  };
15709
- return c.html(chunkAZLU3ROK_cjs.renderDesignPage(pageData));
17415
+ return c.html(chunkEHSZ6TAN_cjs.renderDesignPage(pageData));
15710
17416
  });
15711
17417
  var adminCheckboxRoutes = new hono.Hono();
15712
17418
  adminCheckboxRoutes.get("/", (c) => {
@@ -15718,7 +17424,7 @@ adminCheckboxRoutes.get("/", (c) => {
15718
17424
  role: user.role
15719
17425
  } : void 0
15720
17426
  };
15721
- return c.html(chunkAZLU3ROK_cjs.renderCheckboxPage(pageData));
17427
+ return c.html(chunkEHSZ6TAN_cjs.renderCheckboxPage(pageData));
15722
17428
  });
15723
17429
 
15724
17430
  // src/templates/pages/admin-testimonials-form.template.ts
@@ -15746,7 +17452,7 @@ function renderTestimonialsForm(data) {
15746
17452
  </div>
15747
17453
  </div>
15748
17454
 
15749
- ${message ? chunkAZLU3ROK_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
17455
+ ${message ? chunkEHSZ6TAN_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
15750
17456
 
15751
17457
  <!-- Form -->
15752
17458
  <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
@@ -15975,7 +17681,7 @@ function renderTestimonialsForm(data) {
15975
17681
  user: data.user,
15976
17682
  content: pageContent
15977
17683
  };
15978
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
17684
+ return chunkEHSZ6TAN_cjs.renderAdminLayout(layoutData);
15979
17685
  }
15980
17686
  function escapeHtml4(unsafe) {
15981
17687
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -16001,7 +17707,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
16001
17707
  const offset = (currentPage - 1) * limit;
16002
17708
  const db = c.env?.DB;
16003
17709
  if (!db) {
16004
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17710
+ return c.html(chunkEHSZ6TAN_cjs.renderTestimonialsList({
16005
17711
  testimonials: [],
16006
17712
  totalCount: 0,
16007
17713
  currentPage: 1,
@@ -16041,7 +17747,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
16041
17747
  `;
16042
17748
  const { results: testimonials } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
16043
17749
  const totalPages = Math.ceil(totalCount / limit);
16044
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17750
+ return c.html(chunkEHSZ6TAN_cjs.renderTestimonialsList({
16045
17751
  testimonials: testimonials || [],
16046
17752
  totalCount,
16047
17753
  currentPage,
@@ -16055,7 +17761,7 @@ adminTestimonialsRoutes.get("/", async (c) => {
16055
17761
  } catch (error) {
16056
17762
  console.error("Error fetching testimonials:", error);
16057
17763
  const user = c.get("user");
16058
- return c.html(chunkAZLU3ROK_cjs.renderTestimonialsList({
17764
+ return c.html(chunkEHSZ6TAN_cjs.renderTestimonialsList({
16059
17765
  testimonials: [],
16060
17766
  totalCount: 0,
16061
17767
  currentPage: 1,
@@ -16374,7 +18080,7 @@ function renderCodeExamplesForm(data) {
16374
18080
  </div>
16375
18081
  </div>
16376
18082
 
16377
- ${message ? chunkAZLU3ROK_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
18083
+ ${message ? chunkEHSZ6TAN_cjs.renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
16378
18084
 
16379
18085
  <!-- Form -->
16380
18086
  <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
@@ -16644,7 +18350,7 @@ function renderCodeExamplesForm(data) {
16644
18350
  user: data.user,
16645
18351
  content: pageContent
16646
18352
  };
16647
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
18353
+ return chunkEHSZ6TAN_cjs.renderAdminLayout(layoutData);
16648
18354
  }
16649
18355
  function escapeHtml5(unsafe) {
16650
18356
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
@@ -16671,7 +18377,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16671
18377
  const offset = (currentPage - 1) * limit;
16672
18378
  const db = c.env?.DB;
16673
18379
  if (!db) {
16674
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18380
+ return c.html(chunkEHSZ6TAN_cjs.renderCodeExamplesList({
16675
18381
  codeExamples: [],
16676
18382
  totalCount: 0,
16677
18383
  currentPage: 1,
@@ -16711,7 +18417,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16711
18417
  `;
16712
18418
  const { results: codeExamples } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
16713
18419
  const totalPages = Math.ceil(totalCount / limit);
16714
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18420
+ return c.html(chunkEHSZ6TAN_cjs.renderCodeExamplesList({
16715
18421
  codeExamples: codeExamples || [],
16716
18422
  totalCount,
16717
18423
  currentPage,
@@ -16725,7 +18431,7 @@ adminCodeExamplesRoutes.get("/", async (c) => {
16725
18431
  } catch (error) {
16726
18432
  console.error("Error fetching code examples:", error);
16727
18433
  const user = c.get("user");
16728
- return c.html(chunkAZLU3ROK_cjs.renderCodeExamplesList({
18434
+ return c.html(chunkEHSZ6TAN_cjs.renderCodeExamplesList({
16729
18435
  codeExamples: [],
16730
18436
  totalCount: 0,
16731
18437
  currentPage: 1,
@@ -17114,7 +18820,7 @@ function renderDashboardPage(data) {
17114
18820
  version: data.version,
17115
18821
  content: pageContent
17116
18822
  };
17117
- return chunkAZLU3ROK_cjs.renderAdminLayout(layoutData);
18823
+ return chunkEHSZ6TAN_cjs.renderAdminLayout(layoutData);
17118
18824
  }
17119
18825
  function renderStatsCards(stats) {
17120
18826
  const cards = [
@@ -17662,9 +19368,9 @@ function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
17662
19368
  }
17663
19369
 
17664
19370
  // src/routes/admin-dashboard.ts
17665
- var VERSION = chunkZWV3EBZ7_cjs.getCoreVersion();
19371
+ var VERSION = chunkMYB5RY7H_cjs.getCoreVersion();
17666
19372
  var router = new hono.Hono();
17667
- router.use("*", chunkYYV3XQOQ_cjs.requireAuth());
19373
+ router.use("*", chunkE2BXLXPW_cjs.requireAuth());
17668
19374
  router.get("/", async (c) => {
17669
19375
  const user = c.get("user");
17670
19376
  try {
@@ -17889,7 +19595,7 @@ router.get("/system-status", async (c) => {
17889
19595
  });
17890
19596
 
17891
19597
  // src/templates/pages/admin-collections-list.template.ts
17892
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
19598
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
17893
19599
 
17894
19600
  // src/templates/components/table.template.ts
17895
19601
  function renderTable2(data) {
@@ -18363,11 +20069,11 @@ function renderCollectionsListPage(data) {
18363
20069
  version: data.version,
18364
20070
  content: pageContent
18365
20071
  };
18366
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
20072
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
18367
20073
  }
18368
20074
 
18369
20075
  // src/templates/pages/admin-collections-form.template.ts
18370
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
20076
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
18371
20077
  function getFieldTypeBadge(fieldType) {
18372
20078
  const typeLabels = {
18373
20079
  "text": "Text",
@@ -18378,7 +20084,8 @@ function getFieldTypeBadge(fieldType) {
18378
20084
  "boolean": "Boolean",
18379
20085
  "date": "Date",
18380
20086
  "select": "Select",
18381
- "media": "Media"
20087
+ "media": "Media",
20088
+ "reference": "Reference"
18382
20089
  };
18383
20090
  const typeColors = {
18384
20091
  "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",
@@ -18389,7 +20096,8 @@ function getFieldTypeBadge(fieldType) {
18389
20096
  "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",
18390
20097
  "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",
18391
20098
  "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",
18392
- "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"
20099
+ "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",
20100
+ "reference": "bg-teal-500/10 dark:bg-teal-400/10 text-teal-700 dark:text-teal-300 ring-teal-500/20 dark:ring-teal-400/20"
18393
20101
  };
18394
20102
  const label = typeLabels[fieldType] || fieldType;
18395
20103
  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";
@@ -18628,7 +20336,7 @@ function renderCollectionFormPage(data) {
18628
20336
  }
18629
20337
  </style>
18630
20338
 
18631
- ${chunkAZLU3ROK_cjs.renderForm(formData)}
20339
+ ${chunkEHSZ6TAN_cjs.renderForm(formData)}
18632
20340
 
18633
20341
  ${isEdit && data.managed ? `
18634
20342
  <!-- Read-Only Fields Display for Managed Collections -->
@@ -18869,6 +20577,7 @@ function renderCollectionFormPage(data) {
18869
20577
  <option value="date">Date</option>
18870
20578
  <option value="select">Select</option>
18871
20579
  <option value="media">Media</option>
20580
+ <option value="reference">Reference</option>
18872
20581
  </select>
18873
20582
  <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">
18874
20583
  <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" />
@@ -19186,11 +20895,14 @@ function renderCollectionFormPage(data) {
19186
20895
  }
19187
20896
 
19188
20897
  // Show/hide options container based on field type
19189
- const fieldType = field.field_type;
20898
+ // Use the dropdown's actual value (not field.field_type) to ensure consistency
20899
+ const fieldType = fieldTypeSelect?.value || field.field_type;
19190
20900
  const optionsContainer = document.getElementById('field-options-container');
19191
20901
  const helpText = document.getElementById('field-type-help');
19192
20902
 
19193
- if (['select', 'media', 'richtext'].includes(fieldType)) {
20903
+ console.log('[Edit Field] Showing options for field type:', fieldType, '(original:', field.field_type, ')');
20904
+
20905
+ if (['select', 'media', 'richtext', 'reference'].includes(fieldType)) {
19194
20906
  optionsContainer.classList.remove('hidden');
19195
20907
 
19196
20908
  // Set help text based on type
@@ -19204,6 +20916,9 @@ function renderCollectionFormPage(data) {
19204
20916
  case 'richtext':
19205
20917
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
19206
20918
  break;
20919
+ case 'reference':
20920
+ helpText.textContent = 'Link to content from other collections';
20921
+ break;
19207
20922
  }
19208
20923
  } else {
19209
20924
  optionsContainer.classList.add('hidden');
@@ -19338,7 +21053,7 @@ function renderCollectionFormPage(data) {
19338
21053
  const fieldNameInput = document.getElementById('modal-field-name');
19339
21054
 
19340
21055
  // Show/hide options based on field type
19341
- if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
21056
+ if (['select', 'media', 'richtext', 'guid', 'reference'].includes(this.value)) {
19342
21057
  optionsContainer.classList.remove('hidden');
19343
21058
 
19344
21059
  // Set default options and help text based on type
@@ -19355,6 +21070,10 @@ function renderCollectionFormPage(data) {
19355
21070
  fieldOptions.value = '{"toolbar": "full", "height": 400}';
19356
21071
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
19357
21072
  break;
21073
+ case 'reference':
21074
+ fieldOptions.value = '{"collection": ["pages", "posts"]}';
21075
+ helpText.textContent = 'Link to content from other collections';
21076
+ break;
19358
21077
  }
19359
21078
  } else {
19360
21079
  optionsContainer.classList.add('hidden');
@@ -19417,12 +21136,12 @@ function renderCollectionFormPage(data) {
19417
21136
  version: data.version,
19418
21137
  content: pageContent
19419
21138
  };
19420
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
21139
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
19421
21140
  }
19422
21141
 
19423
21142
  // src/routes/admin-collections.ts
19424
21143
  var adminCollectionsRoutes = new hono.Hono();
19425
- adminCollectionsRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
21144
+ adminCollectionsRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
19426
21145
  adminCollectionsRoutes.get("/", async (c) => {
19427
21146
  try {
19428
21147
  const user = c.get("user");
@@ -19923,6 +21642,8 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
19923
21642
  fieldConfig.type = "quill";
19924
21643
  } else if (fieldType === "mdxeditor") {
19925
21644
  fieldConfig.type = "mdxeditor";
21645
+ } else if (fieldType === "reference") {
21646
+ fieldConfig.type = "reference";
19926
21647
  }
19927
21648
  schema.properties[fieldName] = fieldConfig;
19928
21649
  if (isRequired && !schema.required.includes(fieldName)) {
@@ -20011,8 +21732,15 @@ adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
20011
21732
  schema.required = [];
20012
21733
  }
20013
21734
  if (schema.properties[fieldName]) {
21735
+ let parsedFieldOptions = {};
21736
+ try {
21737
+ parsedFieldOptions = JSON.parse(fieldOptions);
21738
+ } catch (e) {
21739
+ console.error("[Field Update] Error parsing field options:", e);
21740
+ }
20014
21741
  const updatedFieldConfig = {
20015
21742
  ...schema.properties[fieldName],
21743
+ ...parsedFieldOptions,
20016
21744
  type: fieldType,
20017
21745
  title: fieldLabel,
20018
21746
  searchable: isSearchable
@@ -20138,7 +21866,7 @@ adminCollectionsRoutes.post("/:collectionId/fields/reorder", async (c) => {
20138
21866
  });
20139
21867
 
20140
21868
  // src/templates/pages/admin-settings.template.ts
20141
- chunkAZLU3ROK_cjs.init_admin_layout_catalyst_template();
21869
+ chunkEHSZ6TAN_cjs.init_admin_layout_catalyst_template();
20142
21870
  function renderSettingsPage(data) {
20143
21871
  const activeTab = data.activeTab || "general";
20144
21872
  const pageContent = `
@@ -20520,7 +22248,7 @@ function renderSettingsPage(data) {
20520
22248
  version: data.version,
20521
22249
  content: pageContent
20522
22250
  };
20523
- return chunkAZLU3ROK_cjs.renderAdminLayoutCatalyst(layoutData);
22251
+ return chunkEHSZ6TAN_cjs.renderAdminLayoutCatalyst(layoutData);
20524
22252
  }
20525
22253
  function renderTabButton(tabId, label, iconPath, activeTab) {
20526
22254
  const isActive = activeTab === tabId;
@@ -21602,7 +23330,7 @@ function renderDatabaseToolsSettings(settings) {
21602
23330
 
21603
23331
  // src/routes/admin-settings.ts
21604
23332
  var adminSettingsRoutes = new hono.Hono();
21605
- adminSettingsRoutes.use("*", chunkYYV3XQOQ_cjs.requireAuth());
23333
+ adminSettingsRoutes.use("*", chunkE2BXLXPW_cjs.requireAuth());
21606
23334
  function getMockSettings(user) {
21607
23335
  return {
21608
23336
  general: {
@@ -21770,7 +23498,7 @@ adminSettingsRoutes.get("/database-tools", (c) => {
21770
23498
  adminSettingsRoutes.get("/api/migrations/status", async (c) => {
21771
23499
  try {
21772
23500
  const db = c.env.DB;
21773
- const migrationService = new chunkI4V3VZWF_cjs.MigrationService(db);
23501
+ const migrationService = new chunkYRFAQ6MI_cjs.MigrationService(db);
21774
23502
  const status = await migrationService.getMigrationStatus();
21775
23503
  return c.json({
21776
23504
  success: true,
@@ -21794,7 +23522,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
21794
23522
  }, 403);
21795
23523
  }
21796
23524
  const db = c.env.DB;
21797
- const migrationService = new chunkI4V3VZWF_cjs.MigrationService(db);
23525
+ const migrationService = new chunkYRFAQ6MI_cjs.MigrationService(db);
21798
23526
  const result = await migrationService.runPendingMigrations();
21799
23527
  return c.json({
21800
23528
  success: result.success,
@@ -21812,7 +23540,7 @@ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
21812
23540
  adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
21813
23541
  try {
21814
23542
  const db = c.env.DB;
21815
- const migrationService = new chunkI4V3VZWF_cjs.MigrationService(db);
23543
+ const migrationService = new chunkYRFAQ6MI_cjs.MigrationService(db);
21816
23544
  const validation = await migrationService.validateSchema();
21817
23545
  return c.json({
21818
23546
  success: true,
@@ -22053,9 +23781,10 @@ exports.api_default = api_default;
22053
23781
  exports.api_media_default = api_media_default;
22054
23782
  exports.api_system_default = api_system_default;
22055
23783
  exports.auth_default = auth_default;
22056
- exports.checkAdminUserExists = checkAdminUserExists;
23784
+ exports.getConfirmationDialogScript = getConfirmationDialogScript2;
23785
+ exports.renderConfirmationDialog = renderConfirmationDialog2;
22057
23786
  exports.router = router;
22058
23787
  exports.test_cleanup_default = test_cleanup_default;
22059
23788
  exports.userRoutes = userRoutes;
22060
- //# sourceMappingURL=chunk-UAQL2VWX.cjs.map
22061
- //# sourceMappingURL=chunk-UAQL2VWX.cjs.map
23789
+ //# sourceMappingURL=chunk-J7F3NPAP.cjs.map
23790
+ //# sourceMappingURL=chunk-J7F3NPAP.cjs.map