@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,10 +1,10 @@
1
1
  import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-3YNNVSMC.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-TJTWRO4G.js';
3
- import { PluginService } from './chunk-SGAG6FD3.js';
4
- import { MigrationService } from './chunk-VEL7QRYI.js';
5
- import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-V5LBQN3I.js';
6
- import { PluginBuilder } from './chunk-QDBNW7KQ.js';
7
- import { QueryFilterBuilder, getBlocksFieldConfig, parseBlocksValue, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-BHNDALCA.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-Y3EWJQ4D.js';
3
+ import { PluginService } from './chunk-YFJJU26H.js';
4
+ import { MigrationService } from './chunk-L2IDZI7F.js';
5
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-GRN3GHUG.js';
6
+ import { PluginBuilder } from './chunk-CLIH2T74.js';
7
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml, getBlocksFieldConfig, parseBlocksValue } from './chunk-DNHJS6RN.js';
8
8
  import { metricsTracker } from './chunk-FICTAGD4.js';
9
9
  import { Hono } from 'hono';
10
10
  import { cors } from 'hono/cors';
@@ -1583,6 +1583,107 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1583
1583
  return c.json({ error: "Failed to fetch collection" }, 500);
1584
1584
  }
1585
1585
  });
1586
+ adminApiRoutes.get("/references", async (c) => {
1587
+ try {
1588
+ const db = c.env.DB;
1589
+ const url = new URL(c.req.url);
1590
+ const collectionParams = url.searchParams.getAll("collection").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
1591
+ const search = c.req.query("search") || "";
1592
+ const id = c.req.query("id") || "";
1593
+ const limit = Math.min(Number.parseInt(c.req.query("limit") || "20", 10) || 20, 100);
1594
+ if (collectionParams.length === 0) {
1595
+ return c.json({ error: "Collection is required" }, 400);
1596
+ }
1597
+ const placeholders = collectionParams.map(() => "?").join(", ");
1598
+ const collectionStmt = db.prepare(`
1599
+ SELECT id, name, display_name
1600
+ FROM collections
1601
+ WHERE id IN (${placeholders}) OR name IN (${placeholders})
1602
+ `);
1603
+ const collectionResults = await collectionStmt.bind(...collectionParams, ...collectionParams).all();
1604
+ const collections = collectionResults.results || [];
1605
+ if (collections.length === 0) {
1606
+ return c.json({ error: "Collection not found" }, 404);
1607
+ }
1608
+ const collectionById = Object.fromEntries(
1609
+ collections.map((entry) => [
1610
+ entry.id,
1611
+ {
1612
+ id: entry.id,
1613
+ name: entry.name,
1614
+ display_name: entry.display_name
1615
+ }
1616
+ ])
1617
+ );
1618
+ const collectionIds = collections.map((entry) => entry.id);
1619
+ if (id) {
1620
+ const idPlaceholders = collectionIds.map(() => "?").join(", ");
1621
+ const itemStmt = db.prepare(`
1622
+ SELECT id, title, slug, collection_id
1623
+ FROM content
1624
+ WHERE id = ? AND collection_id IN (${idPlaceholders})
1625
+ LIMIT 1
1626
+ `);
1627
+ const item = await itemStmt.bind(id, ...collectionIds).first();
1628
+ if (!item) {
1629
+ return c.json({ error: "Reference not found" }, 404);
1630
+ }
1631
+ return c.json({
1632
+ data: {
1633
+ id: item.id,
1634
+ title: item.title,
1635
+ slug: item.slug,
1636
+ collection: collectionById[item.collection_id]
1637
+ }
1638
+ });
1639
+ }
1640
+ let stmt;
1641
+ let results;
1642
+ const listPlaceholders = collectionIds.map(() => "?").join(", ");
1643
+ const statusFilterValues = ["published"];
1644
+ const statusClause = ` AND status IN (${statusFilterValues.map(() => "?").join(", ")})`;
1645
+ if (search) {
1646
+ const searchParam = `%${search}%`;
1647
+ stmt = db.prepare(`
1648
+ SELECT id, title, slug, status, updated_at, collection_id
1649
+ FROM content
1650
+ WHERE collection_id IN (${listPlaceholders})
1651
+ AND (title LIKE ? OR slug LIKE ?)
1652
+ ${statusClause}
1653
+ ORDER BY updated_at DESC
1654
+ LIMIT ?
1655
+ `);
1656
+ const queryResults = await stmt.bind(...collectionIds, searchParam, searchParam, ...statusFilterValues, limit).all();
1657
+ results = queryResults.results;
1658
+ } else {
1659
+ stmt = db.prepare(`
1660
+ SELECT id, title, slug, status, updated_at, collection_id
1661
+ FROM content
1662
+ WHERE collection_id IN (${listPlaceholders})
1663
+ ${statusClause}
1664
+ ORDER BY updated_at DESC
1665
+ LIMIT ?
1666
+ `);
1667
+ const queryResults = await stmt.bind(...collectionIds, ...statusFilterValues, limit).all();
1668
+ results = queryResults.results;
1669
+ }
1670
+ const items = (results || []).map((row) => ({
1671
+ id: row.id,
1672
+ title: row.title,
1673
+ slug: row.slug,
1674
+ status: row.status,
1675
+ updated_at: row.updated_at ? Number(row.updated_at) : null,
1676
+ collection: collectionById[row.collection_id]
1677
+ }));
1678
+ return c.json({
1679
+ data: items,
1680
+ count: items.length
1681
+ });
1682
+ } catch (error) {
1683
+ console.error("Error fetching reference options:", error);
1684
+ return c.json({ error: "Failed to fetch references" }, 500);
1685
+ }
1686
+ });
1586
1687
  adminApiRoutes.post("/collections", async (c) => {
1587
1688
  try {
1588
1689
  const contentType = c.req.header("Content-Type");
@@ -1752,7 +1853,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1752
1853
  });
1753
1854
  adminApiRoutes.get("/migrations/status", async (c) => {
1754
1855
  try {
1755
- const { MigrationService: MigrationService2 } = await import('./migrations-TGZKJKV4.js');
1856
+ const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
1756
1857
  const db = c.env.DB;
1757
1858
  const migrationService = new MigrationService2(db);
1758
1859
  const status = await migrationService.getMigrationStatus();
@@ -1777,7 +1878,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1777
1878
  error: "Unauthorized. Admin access required."
1778
1879
  }, 403);
1779
1880
  }
1780
- const { MigrationService: MigrationService2 } = await import('./migrations-TGZKJKV4.js');
1881
+ const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
1781
1882
  const db = c.env.DB;
1782
1883
  const migrationService = new MigrationService2(db);
1783
1884
  const result = await migrationService.runPendingMigrations();
@@ -1796,7 +1897,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
1796
1897
  });
1797
1898
  adminApiRoutes.get("/migrations/validate", async (c) => {
1798
1899
  try {
1799
- const { MigrationService: MigrationService2 } = await import('./migrations-TGZKJKV4.js');
1900
+ const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
1800
1901
  const db = c.env.DB;
1801
1902
  const migrationService = new MigrationService2(db);
1802
1903
  const validation = await migrationService.validateSchema();
@@ -1823,7 +1924,7 @@ function renderLoginPage(data, demoLoginActive = false) {
1823
1924
  <meta charset="UTF-8">
1824
1925
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1825
1926
  <title>Login - SonicJS AI</title>
1826
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
1927
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
1827
1928
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
1828
1929
  <script src="https://cdn.tailwindcss.com"></script>
1829
1930
  <script>
@@ -2000,7 +2101,7 @@ function renderRegisterPage(data) {
2000
2101
  <meta charset="UTF-8">
2001
2102
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2002
2103
  <title>Register - SonicJS AI</title>
2003
- <link rel="icon" type="image/x-icon" href="https://demo.sonicjs.com/images/favicon.ico">
2104
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
2004
2105
  <script src="https://unpkg.com/htmx.org@2.0.3"></script>
2005
2106
  <script src="https://cdn.tailwindcss.com"></script>
2006
2107
  <script>
@@ -2023,40 +2124,18 @@ function renderRegisterPage(data) {
2023
2124
  <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
2024
2125
  <!-- Logo Section -->
2025
2126
  <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
2026
- <div class="mx-auto w-64 mb-8">
2027
- <svg class="w-full h-auto" viewBox="380 1300 2250 400" aria-hidden="true">
2028
- <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>
2029
- <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>
2030
- <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>
2031
- <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>
2032
- <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>
2033
- <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>
2034
- <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>
2035
- <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>
2036
- <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>
2127
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-white">
2128
+ <svg class="h-7 w-7 text-zinc-950" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2129
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
2037
2130
  </svg>
2038
2131
  </div>
2039
- <h2 class="mt-6 text-xl font-medium text-white">${data.isSetup ? "Welcome to SonicJS" : "Create Account"}</h2>
2040
- ${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>`}
2132
+ <h1 class="mt-6 text-3xl font-semibold tracking-tight text-white">SonicJS AI</h1>
2133
+ <p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>
2041
2134
  </div>
2042
2135
 
2043
2136
  <!-- Form Container -->
2044
2137
  <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
2045
2138
  <div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
2046
- <!-- Setup Banner -->
2047
- ${data.isSetup ? `
2048
- <div class="mb-6 rounded-lg bg-blue-500/10 p-4 ring-1 ring-blue-500/20">
2049
- <div class="flex items-start gap-x-3">
2050
- <svg class="h-5 w-5 text-blue-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2051
- <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"/>
2052
- </svg>
2053
- <div class="flex-1">
2054
- <p class="text-sm font-medium text-blue-300">First-time Setup</p>
2055
- <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>
2056
- </div>
2057
- </div>
2058
- </div>
2059
- ` : ""}
2060
2139
  <!-- Alerts -->
2061
2140
  ${data.error ? `<div class="mb-6">${renderAlert({ type: "error", message: data.error })}</div>` : ""}
2062
2141
 
@@ -2171,7 +2250,6 @@ function renderRegisterPage(data) {
2171
2250
  </html>
2172
2251
  `;
2173
2252
  }
2174
- var adminExistsCache = null;
2175
2253
  async function isRegistrationEnabled(db) {
2176
2254
  try {
2177
2255
  const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
@@ -2193,21 +2271,6 @@ async function isFirstUserRegistration(db) {
2193
2271
  return false;
2194
2272
  }
2195
2273
  }
2196
- async function checkAdminUserExists(db) {
2197
- if (adminExistsCache !== null) {
2198
- return adminExistsCache;
2199
- }
2200
- try {
2201
- const result = await db.prepare("SELECT id FROM users WHERE role = ?").bind("admin").first();
2202
- adminExistsCache = !!result;
2203
- return adminExistsCache;
2204
- } catch {
2205
- return false;
2206
- }
2207
- }
2208
- function setAdminExists() {
2209
- adminExistsCache = true;
2210
- }
2211
2274
  var baseRegistrationSchema = z.object({
2212
2275
  email: z.string().email("Valid email is required"),
2213
2276
  password: z.string().min(8, "Password must be at least 8 characters"),
@@ -2269,11 +2332,8 @@ authRoutes.get("/register", async (c) => {
2269
2332
  }
2270
2333
  }
2271
2334
  const error = c.req.query("error");
2272
- const isSetup = c.req.query("setup") === "true";
2273
2335
  const pageData = {
2274
- error: error || void 0,
2275
- isSetup: isSetup && isFirstUser
2276
- // Only show setup message if truly first user
2336
+ error: error || void 0
2277
2337
  };
2278
2338
  return c.html(renderRegisterPage(pageData));
2279
2339
  });
@@ -2548,9 +2608,6 @@ authRoutes.post("/register/form", async (c) => {
2548
2608
  now.getTime(),
2549
2609
  now.getTime()
2550
2610
  ).run();
2551
- if (isFirstUser) {
2552
- setAdminExists();
2553
- }
2554
2611
  const token = await AuthManager.generateToken(userId, normalizedEmail, role);
2555
2612
  setCookie(c, "auth_token", token, {
2556
2613
  httpOnly: true,
@@ -2672,7 +2729,6 @@ authRoutes.post("/seed-admin", async (c) => {
2672
2729
  if (existingAdmin) {
2673
2730
  const passwordHash2 = await AuthManager.hashPassword("sonicjs!");
2674
2731
  await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
2675
- setAdminExists();
2676
2732
  return c.json({
2677
2733
  message: "Admin user already exists (password updated)",
2678
2734
  user: {
@@ -2703,7 +2759,6 @@ authRoutes.post("/seed-admin", async (c) => {
2703
2759
  now,
2704
2760
  now
2705
2761
  ).run();
2706
- setAdminExists();
2707
2762
  return c.json({
2708
2763
  message: "Admin user created successfully",
2709
2764
  user: {
@@ -3413,7 +3468,164 @@ var test_cleanup_default = app;
3413
3468
  // src/templates/pages/admin-content-form.template.ts
3414
3469
  init_admin_layout_catalyst_template();
3415
3470
 
3471
+ // src/templates/components/drag-sortable.template.ts
3472
+ function getDragSortableScript() {
3473
+ return `
3474
+ <script>
3475
+ if (!window.__sonicDragSortableInit) {
3476
+ window.__sonicDragSortableInit = true;
3477
+
3478
+ window.initializeDragSortable = function(container, options) {
3479
+ if (!container || container.dataset.dragSortableInit === 'true') {
3480
+ return;
3481
+ }
3482
+
3483
+ container.dataset.dragSortableInit = 'true';
3484
+ const itemSelector = options && options.itemSelector ? options.itemSelector : '.sortable-item';
3485
+ const handleSelector = options && options.handleSelector ? options.handleSelector : '[data-action="drag-handle"]';
3486
+ const onUpdate = options && typeof options.onUpdate === 'function' ? options.onUpdate : function() {};
3487
+ let activeDragItem = null;
3488
+
3489
+ const getDragAfterElement = function(list, y) {
3490
+ const items = Array.from(list.querySelectorAll(itemSelector + ':not(.is-dragging)'));
3491
+ let closest = { offset: Number.NEGATIVE_INFINITY, element: null };
3492
+ items.forEach(function(item) {
3493
+ const box = item.getBoundingClientRect();
3494
+ const offset = y - box.top - box.height / 2;
3495
+ if (offset < 0 && offset > closest.offset) {
3496
+ closest = { offset: offset, element: item };
3497
+ }
3498
+ });
3499
+ return closest.element;
3500
+ };
3501
+
3502
+ const activateDragItem = function(event) {
3503
+ const target = event.target;
3504
+ if (!(target instanceof Element)) return;
3505
+ const handle = target.closest(handleSelector);
3506
+ if (!handle) return;
3507
+ const item = handle.closest(itemSelector);
3508
+ if (!item) return;
3509
+ activeDragItem = item;
3510
+ };
3511
+
3512
+ const clearActiveDragItem = function() {
3513
+ activeDragItem = null;
3514
+ };
3515
+
3516
+ container.addEventListener('pointerdown', activateDragItem);
3517
+ container.addEventListener('mousedown', activateDragItem);
3518
+ container.addEventListener('pointerup', clearActiveDragItem);
3519
+ container.addEventListener('mouseup', clearActiveDragItem);
3520
+
3521
+ container.addEventListener('dragstart', function(event) {
3522
+ const target = event.target;
3523
+ if (!(target instanceof Element)) return;
3524
+ const item = target.closest(itemSelector);
3525
+ if (!item || item !== activeDragItem) {
3526
+ event.preventDefault();
3527
+ return;
3528
+ }
3529
+ item.classList.add('is-dragging');
3530
+ if (event.dataTransfer) {
3531
+ event.dataTransfer.setData('text/plain', '');
3532
+ }
3533
+ });
3534
+
3535
+ container.addEventListener('dragend', function(event) {
3536
+ const target = event.target;
3537
+ if (target instanceof Element) {
3538
+ const item = target.closest(itemSelector);
3539
+ if (item) {
3540
+ item.classList.remove('is-dragging');
3541
+ }
3542
+ }
3543
+ activeDragItem = null;
3544
+ onUpdate();
3545
+ });
3546
+
3547
+ container.addEventListener('dragover', function(event) {
3548
+ event.preventDefault();
3549
+ const dragging = container.querySelector(itemSelector + '.is-dragging');
3550
+ if (!dragging) return;
3551
+ const afterElement = getDragAfterElement(container, event.clientY);
3552
+ if (afterElement === null) {
3553
+ container.appendChild(dragging);
3554
+ } else {
3555
+ container.insertBefore(dragging, afterElement);
3556
+ }
3557
+ });
3558
+
3559
+ container.addEventListener('drop', function() {
3560
+ onUpdate();
3561
+ });
3562
+ };
3563
+ }
3564
+ </script>
3565
+ `;
3566
+ }
3567
+
3416
3568
  // src/templates/components/dynamic-field.template.ts
3569
+ function getReadFieldValueScript() {
3570
+ return `
3571
+ <script>
3572
+ if (!window.__sonicReadFieldValueInit) {
3573
+ window.__sonicReadFieldValueInit = true;
3574
+
3575
+ window.sonicReadFieldValue = function(fieldWrapper) {
3576
+ const fieldType = fieldWrapper.dataset.fieldType;
3577
+ const select = fieldWrapper.querySelector('select');
3578
+ const textarea = fieldWrapper.querySelector('textarea');
3579
+ const inputs = Array.from(fieldWrapper.querySelectorAll('input'));
3580
+ const checkbox = inputs.find((input) => input.type === 'checkbox');
3581
+ const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
3582
+ const hiddenInput = inputs.find((input) => input.type === 'hidden');
3583
+
3584
+ if (fieldType === 'object' || fieldType === 'array') {
3585
+ if (!hiddenInput) {
3586
+ return fieldType === 'array' ? [] : {};
3587
+ }
3588
+ const rawValue = hiddenInput.value || '';
3589
+ if (!rawValue.trim()) {
3590
+ return fieldType === 'array' ? [] : {};
3591
+ }
3592
+ try {
3593
+ return JSON.parse(rawValue);
3594
+ } catch {
3595
+ return fieldType === 'array' ? [] : {};
3596
+ }
3597
+ }
3598
+
3599
+ if (fieldType === 'boolean' && checkbox) {
3600
+ return checkbox.checked;
3601
+ }
3602
+
3603
+ if (select) {
3604
+ if (select.multiple) {
3605
+ return Array.from(select.selectedOptions).map((option) => option.value);
3606
+ }
3607
+ return select.value;
3608
+ }
3609
+
3610
+ if (fieldType === 'quill' || fieldType === 'media') {
3611
+ return hiddenInput ? hiddenInput.value : '';
3612
+ }
3613
+
3614
+ const textSource = textarea || nonHiddenInput || hiddenInput;
3615
+ if (!textSource) {
3616
+ return '';
3617
+ }
3618
+
3619
+ if (fieldType === 'number') {
3620
+ return textSource.value === '' ? null : Number(textSource.value);
3621
+ }
3622
+
3623
+ return textSource.value;
3624
+ };
3625
+ }
3626
+ </script>
3627
+ `;
3628
+ }
3417
3629
  function renderDynamicField(field, options = {}) {
3418
3630
  const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {}, collectionId = "", contentId = "" } = options;
3419
3631
  const opts = field.field_options || {};
@@ -3837,11 +4049,11 @@ function renderDynamicField(field, options = {}) {
3837
4049
  `;
3838
4050
  break;
3839
4051
  case "select":
3840
- const options2 = opts.options || [];
4052
+ const selectOptions = opts.options || [];
3841
4053
  const multiple = opts.multiple ? "multiple" : "";
3842
4054
  const selectedValues = Array.isArray(value) ? value : [value];
3843
4055
  fieldHTML = `
3844
- <select
4056
+ <select
3845
4057
  id="${fieldId}"
3846
4058
  name="${fieldName}${opts.multiple ? "[]" : ""}"
3847
4059
  class="${baseClasses} ${errorClasses}"
@@ -3850,7 +4062,7 @@ function renderDynamicField(field, options = {}) {
3850
4062
  ${disabled ? "disabled" : ""}
3851
4063
  >
3852
4064
  ${!required && !opts.multiple ? '<option value="">Choose an option...</option>' : ""}
3853
- ${options2.map((option) => {
4065
+ ${selectOptions.map((option) => {
3854
4066
  const optionValue = typeof option === "string" ? option : option.value;
3855
4067
  const optionLabel = typeof option === "string" ? option : option.label;
3856
4068
  const selected = selectedValues.includes(optionValue) ? "selected" : "";
@@ -3869,43 +4081,124 @@ function renderDynamicField(field, options = {}) {
3869
4081
  ` : ""}
3870
4082
  `;
3871
4083
  break;
4084
+ case "reference":
4085
+ let referenceCollections = [];
4086
+ if (Array.isArray(opts.collection)) {
4087
+ referenceCollections = opts.collection.filter(Boolean);
4088
+ } else if (typeof opts.collection === "string" && opts.collection) {
4089
+ referenceCollections = [opts.collection];
4090
+ }
4091
+ const referenceCollectionsAttr = referenceCollections.join(",");
4092
+ const hasReferenceCollection = referenceCollections.length > 0;
4093
+ const hasReferenceValue = Boolean(value);
4094
+ fieldHTML = `
4095
+ <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)}">
4096
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(value)}">
4097
+ <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>
4098
+ ${hasReferenceCollection ? hasReferenceValue ? "Loading selection..." : "No reference selected." : "Reference collection not configured."}
4099
+ </div>
4100
+ <div class="flex flex-wrap gap-2">
4101
+ <button
4102
+ type="button"
4103
+ onclick="openReferenceSelector('${fieldId}')"
4104
+ 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"
4105
+ ${hasReferenceCollection ? "" : "disabled"}
4106
+ >
4107
+ Select reference
4108
+ </button>
4109
+ <button
4110
+ type="button"
4111
+ onclick="clearReferenceField('${fieldId}')"
4112
+ 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"
4113
+ data-reference-clear
4114
+ ${hasReferenceValue ? "" : "disabled"}
4115
+ >
4116
+ Remove
4117
+ </button>
4118
+ </div>
4119
+ </div>
4120
+ `;
4121
+ break;
3872
4122
  case "media":
4123
+ const isMultiple = opts.multiple === true;
4124
+ const mediaValues = isMultiple && value ? Array.isArray(value) ? value : String(value).split(",").filter(Boolean) : [];
4125
+ const singleValue = !isMultiple ? value : "";
4126
+ const isVideoUrl = (url) => {
4127
+ const videoExtensions = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
4128
+ return videoExtensions.some((ext) => url.toLowerCase().endsWith(ext));
4129
+ };
4130
+ const renderMediaPreview = (url, alt, classes) => {
4131
+ if (isVideoUrl(url)) {
4132
+ return `<video src="${url}" class="${classes}" muted></video>`;
4133
+ }
4134
+ return `<img src="${url}" alt="${alt}" class="${classes}">`;
4135
+ };
3873
4136
  fieldHTML = `
3874
4137
  <div class="media-field-container">
3875
- <input type="hidden" id="${fieldId}" name="${fieldName}" value="${value}">
3876
- <div class="media-preview ${value ? "" : "hidden"}" id="${fieldId}-preview">
3877
- ${value ? `<img src="${value}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
3878
- </div>
4138
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${isMultiple ? mediaValues.join(",") : singleValue}" data-multiple="${isMultiple}">
4139
+
4140
+ ${isMultiple ? `
4141
+ <div class="media-preview-grid grid grid-cols-4 gap-2 mb-2 ${mediaValues.length === 0 ? "hidden" : ""}" id="${fieldId}-preview">
4142
+ ${mediaValues.map((url, idx) => `
4143
+ <div class="relative media-preview-item" data-url="${url}">
4144
+ ${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
4145
+ <button
4146
+ type="button"
4147
+ onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
4148
+ class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
4149
+ ${disabled ? "disabled" : ""}
4150
+ >
4151
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4152
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
4153
+ </svg>
4154
+ </button>
4155
+ </div>
4156
+ `).join("")}
4157
+ </div>
4158
+ ` : `
4159
+ <div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
4160
+ ${singleValue ? renderMediaPreview(singleValue, "Selected media", "w-32 h-32 object-cover rounded-lg border border-white/20") : ""}
4161
+ </div>
4162
+ `}
4163
+
3879
4164
  <div class="media-actions mt-2 space-x-2">
3880
4165
  <button
3881
4166
  type="button"
3882
- onclick="openMediaSelector('${fieldId}')"
4167
+ onclick="openMediaSelector('${fieldId}', ${isMultiple})"
3883
4168
  class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
3884
4169
  ${disabled ? "disabled" : ""}
3885
4170
  >
3886
4171
  <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3887
4172
  <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>
3888
4173
  </svg>
3889
- Select Media
4174
+ ${isMultiple ? "Select Media (Multiple)" : "Select Media"}
3890
4175
  </button>
3891
- ${value ? `
4176
+ ${(isMultiple ? mediaValues.length > 0 : singleValue) ? `
3892
4177
  <button
3893
4178
  type="button"
3894
4179
  onclick="clearMediaField('${fieldId}')"
3895
4180
  class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
3896
4181
  ${disabled ? "disabled" : ""}
3897
4182
  >
3898
- Remove
4183
+ ${isMultiple ? "Clear All" : "Remove"}
3899
4184
  </button>
3900
4185
  ` : ""}
3901
4186
  </div>
3902
4187
  </div>
3903
4188
  `;
3904
4189
  break;
4190
+ case "object":
4191
+ return renderStructuredObjectField(field, options);
4192
+ case "array":
4193
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4194
+ if (itemsConfig.blocks && typeof itemsConfig.blocks === "object") {
4195
+ return renderBlocksField(field, options, baseClasses, errorClasses);
4196
+ }
4197
+ return renderStructuredArrayField(field, options);
3905
4198
  default:
3906
4199
  fieldHTML = `
3907
- <input
3908
- type="text"
4200
+ <input
4201
+ type="text"
3909
4202
  id="${fieldId}"
3910
4203
  name="${fieldName}"
3911
4204
  value="${escapeHtml2(value)}"
@@ -3955,76 +4248,824 @@ function renderFieldGroup(title, fields, collapsible = false) {
3955
4248
  </div>
3956
4249
  `;
3957
4250
  }
3958
- function escapeHtml2(text) {
3959
- if (typeof text !== "string") return String(text || "");
3960
- return text.replace(/[&<>"']/g, (char) => ({
3961
- "&": "&amp;",
3962
- "<": "&lt;",
3963
- ">": "&gt;",
3964
- '"': "&quot;",
3965
- "'": "&#39;"
3966
- })[char] || char);
3967
- }
4251
+ function renderBlocksField(field, options, baseClasses, errorClasses) {
4252
+ const { value = [], pluginStatuses = {} } = options;
4253
+ const opts = field.field_options || {};
4254
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4255
+ const blocks = normalizeBlockDefinitions(itemsConfig.blocks);
4256
+ const discriminator = typeof itemsConfig.discriminator === "string" && itemsConfig.discriminator ? itemsConfig.discriminator : "blockType";
4257
+ const blockValues = normalizeBlocksValue(value, discriminator);
4258
+ const fieldId = `field-${field.field_name}`;
4259
+ const fieldName = field.field_name;
4260
+ const emptyState = blockValues.length === 0 ? `
4261
+ <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>
4262
+ No blocks yet. Add your first block to get started.
4263
+ </div>
4264
+ ` : "";
4265
+ const blockOptions = blocks.map((block) => `<option value="${escapeHtml2(block.name)}">${escapeHtml2(block.label)}</option>`).join("");
4266
+ const blockItems = blockValues.map(
4267
+ (blockValue, index) => renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses)
4268
+ ).join("");
4269
+ const templates = blocks.map((block) => renderBlockTemplate(field, block, discriminator, pluginStatuses)).join("");
4270
+ return `
4271
+ <div
4272
+ class="blocks-field space-y-4"
4273
+ data-blocks='${escapeHtml2(JSON.stringify(blocks))}'
4274
+ data-blocks-discriminator="${escapeHtml2(discriminator)}"
4275
+ data-field-name="${escapeHtml2(fieldName)}"
4276
+ >
4277
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(blockValues))}">
3968
4278
 
3969
- // src/plugins/available/tinymce-plugin/index.ts
3970
- var builder = PluginBuilder.create({
3971
- name: "tinymce-plugin",
3972
- version: "1.0.0",
3973
- description: "Powerful WYSIWYG rich text editor for content creation"
3974
- });
3975
- builder.metadata({
3976
- author: {
3977
- name: "SonicJS Team",
3978
- email: "team@sonicjs.com"
3979
- },
3980
- license: "MIT",
3981
- compatibility: "^2.0.0"
3982
- });
3983
- builder.lifecycle({
3984
- activate: async () => {
3985
- console.info("\u2705 TinyMCE plugin activated");
3986
- },
3987
- deactivate: async () => {
3988
- console.info("\u274C TinyMCE plugin deactivated");
3989
- }
3990
- });
3991
- builder.build();
3992
- function getTinyMCEScript(apiKey = "no-api-key") {
3993
- return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
4279
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4280
+ <div class="flex-1">
4281
+ <select
4282
+ class="${baseClasses} ${errorClasses}"
4283
+ data-role="block-type-select"
4284
+ >
4285
+ <option value="">Choose a block...</option>
4286
+ ${blockOptions}
4287
+ </select>
4288
+ </div>
4289
+ <button
4290
+ type="button"
4291
+ data-action="add-block"
4292
+ 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"
4293
+ >
4294
+ Add Block
4295
+ </button>
4296
+ </div>
4297
+
4298
+ <div class="space-y-4" data-blocks-list>
4299
+ ${blockItems || emptyState}
4300
+ </div>
4301
+
4302
+ ${templates}
4303
+ </div>
4304
+ ${getDragSortableScript()}
4305
+ ${getBlocksFieldScript()}
4306
+ `;
3994
4307
  }
3995
- function getTinyMCEInitScript(config) {
3996
- const skin = config?.skin || "oxide-dark";
3997
- const contentCss = skin.includes("dark") ? "dark" : "default";
3998
- const defaultHeight = config?.defaultHeight || 300;
4308
+ function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
4309
+ const { value = {}, pluginStatuses = {} } = options;
4310
+ const opts = field.field_options || {};
4311
+ const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
4312
+ const fieldId = `field-${field.field_name}`;
4313
+ const fieldName = field.field_name;
4314
+ const objectValue = normalizeStructuredObjectValue(value);
4315
+ const subfields = Object.entries(properties).map(
4316
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4317
+ field,
4318
+ propertyName,
4319
+ propertyConfig,
4320
+ objectValue,
4321
+ pluginStatuses,
4322
+ field.field_name
4323
+ )
4324
+ ).join("");
3999
4325
  return `
4000
- // Initialize TinyMCE for all richtext fields
4001
- function initializeTinyMCE() {
4002
- if (typeof tinymce !== 'undefined') {
4003
- // Find all textareas that need TinyMCE
4004
- document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
4005
- // Skip if already initialized
4006
- if (tinymce.get(textarea.id)) {
4007
- return;
4008
- }
4326
+ <div class="space-y-4" data-structured-object data-field-name="${escapeHtml2(fieldName)}">
4327
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(objectValue))}">
4328
+ <div class="space-y-4" data-structured-object-fields>
4329
+ ${subfields}
4330
+ </div>
4331
+ </div>
4332
+ ${getStructuredFieldScript()}
4333
+ `;
4334
+ }
4335
+ function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
4336
+ const { value = [], pluginStatuses = {} } = options;
4337
+ const opts = field.field_options || {};
4338
+ const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
4339
+ const fieldId = `field-${field.field_name}`;
4340
+ const fieldName = field.field_name;
4341
+ const arrayValue = normalizeStructuredArrayValue(value);
4342
+ const items = arrayValue.map(
4343
+ (itemValue, index) => renderStructuredArrayItem(field, itemsConfig, String(index), itemValue, pluginStatuses)
4344
+ ).join("");
4345
+ const emptyState = arrayValue.length === 0 ? `
4346
+ <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>
4347
+ No items yet. Add the first item to get started.
4348
+ </div>
4349
+ ` : "";
4350
+ return `
4351
+ <div class="space-y-4" data-structured-array data-field-name="${escapeHtml2(fieldName)}">
4352
+ <input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(arrayValue))}">
4009
4353
 
4010
- // Get configuration from data attributes
4011
- const container = textarea.closest('.richtext-container');
4012
- const height = container?.dataset.height || ${defaultHeight};
4013
- const toolbar = container?.dataset.toolbar || 'full';
4354
+ <div class="flex items-center justify-between gap-3">
4355
+ <div class="text-sm text-zinc-500 dark:text-zinc-400">
4356
+ ${escapeHtml2(opts.itemLabel || "Items")}
4357
+ </div>
4358
+ <button
4359
+ type="button"
4360
+ data-action="add-item"
4361
+ 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"
4362
+ >
4363
+ Add item
4364
+ </button>
4365
+ </div>
4014
4366
 
4015
- tinymce.init({
4016
- selector: '#' + textarea.id,
4017
- skin: '${skin}',
4018
- content_css: '${contentCss}',
4019
- height: parseInt(height),
4020
- menubar: false,
4021
- plugins: [
4022
- 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
4023
- 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
4024
- 'insertdatetime', 'media', 'table', 'help', 'wordcount'
4025
- ],
4026
- toolbar: toolbar === 'simple'
4027
- ? 'bold italic underline | bullist numlist | link'
4367
+ <div class="space-y-4" data-structured-array-list>
4368
+ ${items || emptyState}
4369
+ </div>
4370
+
4371
+ <template data-structured-array-template>
4372
+ ${renderStructuredArrayItem(field, itemsConfig, "__INDEX__", {}, pluginStatuses)}
4373
+ </template>
4374
+ </div>
4375
+ ${getDragSortableScript()}
4376
+ ${getStructuredFieldScript()}
4377
+ `;
4378
+ }
4379
+ function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses) {
4380
+ const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
4381
+ return `
4382
+ <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">
4383
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4384
+ <div class="flex items-center gap-3">
4385
+ <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">
4386
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4387
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4388
+ </svg>
4389
+ </div>
4390
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4391
+ Item <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-array-order-label></span>
4392
+ </div>
4393
+ </div>
4394
+ <div class="flex flex-wrap gap-2 text-xs">
4395
+ <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">
4396
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4397
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4398
+ </svg>
4399
+ </button>
4400
+ <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">
4401
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4402
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4403
+ </svg>
4404
+ </button>
4405
+ <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">
4406
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4407
+ <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"/>
4408
+ </svg>
4409
+ Delete item
4410
+ </button>
4411
+ </div>
4412
+ </div>
4413
+ <div class="mt-4 space-y-4" data-array-item-fields>
4414
+ ${itemFields}
4415
+ </div>
4416
+ </div>
4417
+ `;
4418
+ }
4419
+ function renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses) {
4420
+ const itemType = itemConfig?.type || "string";
4421
+ if (itemType === "object" && itemConfig?.properties && typeof itemConfig.properties === "object") {
4422
+ const fieldPrefix = `array-${field.field_name}-${index}`;
4423
+ return Object.entries(itemConfig.properties).map(
4424
+ ([propertyName, propertyConfig]) => renderStructuredSubfield(
4425
+ field,
4426
+ propertyName,
4427
+ propertyConfig,
4428
+ itemValue || {},
4429
+ pluginStatuses,
4430
+ fieldPrefix
4431
+ )
4432
+ ).join("");
4433
+ }
4434
+ const normalizedField = normalizeBlockField(itemConfig, "Item");
4435
+ const fieldValue = itemValue ?? normalizedField.defaultValue ?? "";
4436
+ const fieldDefinition = {
4437
+ id: `array-${field.field_name}-${index}-value`,
4438
+ field_name: `array-${field.field_name}-${index}-value`,
4439
+ field_type: normalizedField.type,
4440
+ field_label: normalizedField.label,
4441
+ field_options: normalizedField.options,
4442
+ is_required: normalizedField.required};
4443
+ return `
4444
+ <div class="structured-subfield" data-structured-field="__value" data-field-type="${escapeHtml2(normalizedField.type)}">
4445
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4446
+ </div>
4447
+ `;
4448
+ }
4449
+ function renderStructuredSubfield(field, propertyName, propertyConfig, objectValue, pluginStatuses, fieldPrefix) {
4450
+ const normalizedField = normalizeBlockField(propertyConfig, propertyName);
4451
+ const fieldValue = objectValue?.[propertyName] ?? normalizedField.defaultValue ?? "";
4452
+ const fieldDefinition = {
4453
+ field_name: `${fieldPrefix}__${propertyName}`,
4454
+ field_type: normalizedField.type,
4455
+ field_label: normalizedField.label,
4456
+ field_options: normalizedField.options,
4457
+ is_required: normalizedField.required};
4458
+ return `
4459
+ <div class="structured-subfield" data-structured-field="${escapeHtml2(propertyName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4460
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4461
+ </div>
4462
+ `;
4463
+ }
4464
+ function normalizeStructuredObjectValue(value) {
4465
+ if (!value) return {};
4466
+ if (typeof value === "string") {
4467
+ try {
4468
+ const parsed = JSON.parse(value);
4469
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4470
+ } catch {
4471
+ return {};
4472
+ }
4473
+ }
4474
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4475
+ return {};
4476
+ }
4477
+ function normalizeStructuredArrayValue(value) {
4478
+ if (!value) return [];
4479
+ if (Array.isArray(value)) return value;
4480
+ if (typeof value === "string") {
4481
+ try {
4482
+ const parsed = JSON.parse(value);
4483
+ return Array.isArray(parsed) ? parsed : [];
4484
+ } catch {
4485
+ return [];
4486
+ }
4487
+ }
4488
+ return [];
4489
+ }
4490
+ function normalizeBlockDefinitions(rawBlocks) {
4491
+ if (!rawBlocks || typeof rawBlocks !== "object") return [];
4492
+ return Object.entries(rawBlocks).filter(([name, block]) => typeof name === "string" && block && typeof block === "object").map(([name, block]) => ({
4493
+ name,
4494
+ label: block.label || name,
4495
+ description: block.description,
4496
+ properties: block.properties && typeof block.properties === "object" ? block.properties : {}
4497
+ }));
4498
+ }
4499
+ function normalizeBlocksValue(value, discriminator) {
4500
+ const normalizeItem = (item) => {
4501
+ if (!item || typeof item !== "object") return null;
4502
+ if (item[discriminator]) return item;
4503
+ if (item.blockType && item.data && typeof item.data === "object") {
4504
+ return { [discriminator]: item.blockType, ...item.data };
4505
+ }
4506
+ return item;
4507
+ };
4508
+ const fromArray = (items) => items.map(normalizeItem).filter((item) => item && typeof item === "object");
4509
+ if (Array.isArray(value)) return fromArray(value);
4510
+ if (typeof value === "string" && value.trim()) {
4511
+ try {
4512
+ const parsed = JSON.parse(value);
4513
+ return Array.isArray(parsed) ? fromArray(parsed) : [];
4514
+ } catch {
4515
+ return [];
4516
+ }
4517
+ }
4518
+ return [];
4519
+ }
4520
+ function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
4521
+ return `
4522
+ <template data-block-template="${escapeHtml2(block.name)}">
4523
+ ${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
4524
+ </template>
4525
+ `;
4526
+ }
4527
+ function renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses) {
4528
+ const blockType = blockValue?.[discriminator] || blockValue?.blockType;
4529
+ const blockDefinition = blocks.find((block) => block.name === blockType);
4530
+ if (!blockDefinition) {
4531
+ return `
4532
+ <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 || {}))}">
4533
+ Unknown block type: <strong>${escapeHtml2(String(blockType || "unknown"))}</strong>. This block will be preserved as-is.
4534
+ </div>
4535
+ `;
4536
+ }
4537
+ const data = blockValue && typeof blockValue === "object" ? Object.fromEntries(Object.entries(blockValue).filter(([key]) => key !== discriminator)) : {};
4538
+ return renderBlockCard(field, blockDefinition, discriminator, String(index), data, pluginStatuses);
4539
+ }
4540
+ function renderBlockCard(field, block, discriminator, index, data, pluginStatuses) {
4541
+ const blockFields = Object.entries(block.properties).map(([fieldName, fieldConfig]) => {
4542
+ if (fieldConfig?.type === "array" && fieldConfig?.items?.blocks) {
4543
+ return `
4544
+ <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">
4545
+ Nested blocks are not supported yet for "${escapeHtml2(fieldName)}".
4546
+ </div>
4547
+ `;
4548
+ }
4549
+ const normalizedField = normalizeBlockField(fieldConfig, fieldName);
4550
+ const fieldValue = data?.[fieldName] ?? normalizedField.defaultValue ?? "";
4551
+ const fieldDefinition = {
4552
+ id: `block-${field.field_name}-${index}-${fieldName}`,
4553
+ field_name: `block-${field.field_name}-${index}-${fieldName}`,
4554
+ field_type: normalizedField.type,
4555
+ field_label: normalizedField.label,
4556
+ field_options: normalizedField.options,
4557
+ is_required: normalizedField.required};
4558
+ return `
4559
+ <div class="blocks-subfield" data-block-field="${escapeHtml2(fieldName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
4560
+ ${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
4561
+ </div>
4562
+ `;
4563
+ }).join("");
4564
+ return `
4565
+ <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">
4566
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4567
+ <div class="flex items-start gap-3">
4568
+ <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">
4569
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
4570
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
4571
+ </svg>
4572
+ </div>
4573
+ <div>
4574
+ <div class="text-sm font-semibold text-zinc-900 dark:text-white">
4575
+ ${escapeHtml2(block.label)}
4576
+ <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
4577
+ </div>
4578
+ ${block.description ? `<p class="text-xs text-zinc-500 dark:text-zinc-400">${escapeHtml2(block.description)}</p>` : ""}
4579
+ </div>
4580
+ </div>
4581
+ <div class="flex flex-wrap gap-2 text-xs">
4582
+ <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">
4583
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4584
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
4585
+ </svg>
4586
+ </button>
4587
+ <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">
4588
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
4589
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
4590
+ </svg>
4591
+ </button>
4592
+ <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">
4593
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
4594
+ <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"/>
4595
+ </svg>
4596
+ Delete block
4597
+ </button>
4598
+ </div>
4599
+ </div>
4600
+ <div class="mt-4 space-y-4">
4601
+ ${blockFields}
4602
+ </div>
4603
+ </div>
4604
+ `;
4605
+ }
4606
+ function normalizeBlockField(fieldConfig, fieldName) {
4607
+ const type = fieldConfig?.type || "text";
4608
+ const label = fieldConfig?.title || fieldName;
4609
+ const required = fieldConfig?.required === true;
4610
+ const options = { ...fieldConfig };
4611
+ if (type === "select" && Array.isArray(fieldConfig?.enum)) {
4612
+ options.options = fieldConfig.enum.map((value, index) => ({
4613
+ value,
4614
+ label: fieldConfig.enumLabels?.[index] || value
4615
+ }));
4616
+ }
4617
+ return {
4618
+ type,
4619
+ label,
4620
+ required,
4621
+ defaultValue: fieldConfig?.default,
4622
+ options
4623
+ };
4624
+ }
4625
+ function getStructuredFieldScript() {
4626
+ return `
4627
+ ${getReadFieldValueScript()}
4628
+ <script>
4629
+ if (!window.__sonicStructuredFieldInit) {
4630
+ window.__sonicStructuredFieldInit = true;
4631
+
4632
+ function initializeStructuredFields() {
4633
+ const readFieldValue = window.sonicReadFieldValue;
4634
+
4635
+ const readStructuredValue = (container) => {
4636
+ const fields = Array.from(container.querySelectorAll('.structured-subfield'));
4637
+ if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
4638
+ return readFieldValue(fields[0]);
4639
+ }
4640
+
4641
+ return fields.reduce((acc, fieldWrapper) => {
4642
+ const fieldName = fieldWrapper.dataset.structuredField;
4643
+ if (!fieldName || fieldName === '__value') return acc;
4644
+ acc[fieldName] = readFieldValue(fieldWrapper);
4645
+ return acc;
4646
+ }, {});
4647
+ };
4648
+
4649
+ document.querySelectorAll('[data-structured-object]').forEach((container) => {
4650
+ if (container.dataset.structuredInitialized === 'true') {
4651
+ return;
4652
+ }
4653
+ container.dataset.structuredInitialized = 'true';
4654
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4655
+
4656
+ const updateHiddenInput = () => {
4657
+ if (!hiddenInput) return;
4658
+ const value = readStructuredValue(container);
4659
+ hiddenInput.value = JSON.stringify(value);
4660
+ };
4661
+
4662
+ container.addEventListener('input', updateHiddenInput);
4663
+ container.addEventListener('change', updateHiddenInput);
4664
+ updateHiddenInput();
4665
+ });
4666
+
4667
+ document.querySelectorAll('[data-structured-array]').forEach((container) => {
4668
+ if (container.dataset.structuredInitialized === 'true') {
4669
+ return;
4670
+ }
4671
+ container.dataset.structuredInitialized = 'true';
4672
+ const list = container.querySelector('[data-structured-array-list]');
4673
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4674
+ const template = container.querySelector('template[data-structured-array-template]');
4675
+
4676
+ const updateOrderLabels = () => {
4677
+ const items = Array.from(container.querySelectorAll('.structured-array-item'));
4678
+ items.forEach((item, index) => {
4679
+ const label = item.querySelector('[data-array-order-label]');
4680
+ if (label) {
4681
+ label.textContent = '#'+ (index + 1);
4682
+ }
4683
+
4684
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4685
+ if (moveUpButton instanceof HTMLButtonElement) {
4686
+ moveUpButton.disabled = index === 0;
4687
+ }
4688
+
4689
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4690
+ if (moveDownButton instanceof HTMLButtonElement) {
4691
+ moveDownButton.disabled = index === items.length - 1;
4692
+ }
4693
+ });
4694
+ };
4695
+
4696
+ const updateHiddenInput = () => {
4697
+ if (!hiddenInput || !list) return;
4698
+ const items = Array.from(list.querySelectorAll('.structured-array-item'));
4699
+ const values = items.map((item) => readStructuredValue(item));
4700
+ hiddenInput.value = JSON.stringify(values);
4701
+
4702
+ const emptyState = list.querySelector('[data-structured-empty]');
4703
+ if (emptyState) {
4704
+ emptyState.style.display = values.length === 0 ? 'block' : 'none';
4705
+ }
4706
+ updateOrderLabels();
4707
+ };
4708
+
4709
+ if (typeof window.initializeDragSortable === 'function' && list) {
4710
+ window.initializeDragSortable(list, {
4711
+ itemSelector: '.structured-array-item',
4712
+ handleSelector: '[data-action="drag-handle"]',
4713
+ onUpdate: updateHiddenInput
4714
+ });
4715
+ }
4716
+
4717
+ container.addEventListener('click', (event) => {
4718
+ const target = event.target;
4719
+ if (!(target instanceof Element)) return;
4720
+ const actionButton = target.closest('[data-action]');
4721
+ if (!actionButton || actionButton.hasAttribute('disabled')) return;
4722
+
4723
+ const action = actionButton.getAttribute('data-action');
4724
+
4725
+ if (action === 'add-item') {
4726
+ if (!list || !template) return;
4727
+ const nextIndex = list.querySelectorAll('.structured-array-item').length;
4728
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4729
+ list.insertAdjacentHTML('beforeend', html);
4730
+ if (typeof initializeTinyMCE === 'function') {
4731
+ initializeTinyMCE();
4732
+ }
4733
+ if (typeof window.initializeQuillEditors === 'function') {
4734
+ window.initializeQuillEditors();
4735
+ }
4736
+ if (typeof initializeMDXEditor === 'function') {
4737
+ initializeMDXEditor();
4738
+ }
4739
+ updateHiddenInput();
4740
+ return;
4741
+ }
4742
+
4743
+ const item = actionButton.closest('.structured-array-item');
4744
+ if (!item || !list) return;
4745
+
4746
+ if (action === 'remove-item') {
4747
+ item.remove();
4748
+ updateHiddenInput();
4749
+ return;
4750
+ }
4751
+
4752
+ if (action === 'move-up') {
4753
+ const previous = item.previousElementSibling;
4754
+ if (previous) {
4755
+ list.insertBefore(item, previous);
4756
+ updateHiddenInput();
4757
+ }
4758
+ return;
4759
+ }
4760
+
4761
+ if (action === 'move-down') {
4762
+ const next = item.nextElementSibling;
4763
+ if (next) {
4764
+ list.insertBefore(next, item);
4765
+ updateHiddenInput();
4766
+ }
4767
+ }
4768
+ });
4769
+
4770
+ container.addEventListener('input', (event) => {
4771
+ const target = event.target;
4772
+ if (!(target instanceof Element)) return;
4773
+ if (target.closest('[data-structured-array-list]')) {
4774
+ updateHiddenInput();
4775
+ }
4776
+ });
4777
+
4778
+ container.addEventListener('change', (event) => {
4779
+ const target = event.target;
4780
+ if (!(target instanceof Element)) return;
4781
+ if (target.closest('[data-structured-array-list]')) {
4782
+ updateHiddenInput();
4783
+ }
4784
+ });
4785
+
4786
+ updateHiddenInput();
4787
+ });
4788
+ }
4789
+
4790
+ window.initializeStructuredFields = initializeStructuredFields;
4791
+
4792
+ if (document.readyState === 'loading') {
4793
+ document.addEventListener('DOMContentLoaded', initializeStructuredFields);
4794
+ } else {
4795
+ initializeStructuredFields();
4796
+ }
4797
+
4798
+ document.addEventListener('htmx:afterSwap', function() {
4799
+ setTimeout(initializeStructuredFields, 50);
4800
+ });
4801
+ } else if (typeof window.initializeStructuredFields === 'function') {
4802
+ window.initializeStructuredFields();
4803
+ }
4804
+ </script>
4805
+ `;
4806
+ }
4807
+ function getBlocksFieldScript() {
4808
+ return `
4809
+ ${getReadFieldValueScript()}
4810
+ <script>
4811
+ if (!window.__sonicBlocksFieldInit) {
4812
+ window.__sonicBlocksFieldInit = true;
4813
+
4814
+ function initializeBlocksFields() {
4815
+ document.querySelectorAll('.blocks-field').forEach((container) => {
4816
+ if (container.dataset.blocksInitialized === 'true') {
4817
+ return;
4818
+ }
4819
+
4820
+ container.dataset.blocksInitialized = 'true';
4821
+ const list = container.querySelector('[data-blocks-list]');
4822
+ const hiddenInput = container.querySelector('input[type="hidden"]');
4823
+ const typeSelect = container.querySelector('[data-role="block-type-select"]');
4824
+ const discriminator = container.dataset.blocksDiscriminator || 'blockType';
4825
+
4826
+ const updateOrderLabels = () => {
4827
+ const items = Array.from(container.querySelectorAll('.blocks-item'));
4828
+ items.forEach((item, index) => {
4829
+ const label = item.querySelector('[data-block-order-label]');
4830
+ if (label) {
4831
+ label.textContent = '#'+ (index + 1);
4832
+ }
4833
+
4834
+ const moveUpButton = item.querySelector('[data-action="move-up"]');
4835
+ if (moveUpButton instanceof HTMLButtonElement) {
4836
+ moveUpButton.disabled = index === 0;
4837
+ }
4838
+
4839
+ const moveDownButton = item.querySelector('[data-action="move-down"]');
4840
+ if (moveDownButton instanceof HTMLButtonElement) {
4841
+ moveDownButton.disabled = index === items.length - 1;
4842
+ }
4843
+ });
4844
+ };
4845
+
4846
+ const readFieldValue = window.sonicReadFieldValue;
4847
+
4848
+ const readBlockItem = (item) => {
4849
+ if (item.dataset.blockRaw) {
4850
+ try {
4851
+ return JSON.parse(item.dataset.blockRaw);
4852
+ } catch (error) {
4853
+ return {};
4854
+ }
4855
+ }
4856
+
4857
+ const blockType = item.dataset.blockType;
4858
+ const data = {};
4859
+
4860
+ item.querySelectorAll('.blocks-subfield').forEach((fieldWrapper) => {
4861
+ const fieldName = fieldWrapper.dataset.blockField;
4862
+ if (!fieldName) {
4863
+ return;
4864
+ }
4865
+ data[fieldName] = readFieldValue(fieldWrapper);
4866
+ });
4867
+
4868
+ return { [discriminator]: blockType, ...data };
4869
+ };
4870
+
4871
+ const updateHiddenInput = () => {
4872
+ if (!hiddenInput || !list) return;
4873
+ const items = Array.from(list.querySelectorAll('.blocks-item, [data-block-raw]'));
4874
+ const blocksData = items.map((item) => readBlockItem(item));
4875
+ hiddenInput.value = JSON.stringify(blocksData);
4876
+
4877
+ const emptyState = list.querySelector('[data-blocks-empty]');
4878
+ if (emptyState) {
4879
+ emptyState.style.display = blocksData.length === 0 ? 'block' : 'none';
4880
+ }
4881
+ updateOrderLabels();
4882
+ };
4883
+
4884
+ const initializeEditors = () => {
4885
+ if (typeof initializeTinyMCE === 'function') {
4886
+ initializeTinyMCE();
4887
+ }
4888
+ if (typeof window.initializeQuillEditors === 'function') {
4889
+ window.initializeQuillEditors();
4890
+ }
4891
+ if (typeof initializeMDXEditor === 'function') {
4892
+ initializeMDXEditor();
4893
+ }
4894
+ };
4895
+
4896
+ if (typeof window.initializeDragSortable === 'function' && list) {
4897
+ window.initializeDragSortable(list, {
4898
+ itemSelector: '.blocks-item',
4899
+ handleSelector: '[data-action="drag-handle"]',
4900
+ onUpdate: updateHiddenInput
4901
+ });
4902
+ }
4903
+
4904
+ container.addEventListener('click', (event) => {
4905
+ const target = event.target;
4906
+ if (!(target instanceof Element)) return;
4907
+ const actionButton = target.closest('[data-action]');
4908
+ if (!actionButton) return;
4909
+
4910
+ if (actionButton.hasAttribute('disabled')) {
4911
+ return;
4912
+ }
4913
+
4914
+ const action = actionButton.getAttribute('data-action');
4915
+ if (action === 'add-block') {
4916
+ const blockType = typeSelect ? typeSelect.value : '';
4917
+ if (!blockType || !list) return;
4918
+ const template = container.querySelector('template[data-block-template="' + blockType + '"]');
4919
+ if (!template) return;
4920
+
4921
+ const nextIndex = list.querySelectorAll('.blocks-item').length;
4922
+ const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
4923
+ list.insertAdjacentHTML('beforeend', html);
4924
+ if (typeSelect) {
4925
+ typeSelect.value = '';
4926
+ }
4927
+ initializeEditors();
4928
+ if (typeof window.initializeStructuredFields === 'function') {
4929
+ window.initializeStructuredFields();
4930
+ }
4931
+ updateHiddenInput();
4932
+ return;
4933
+ }
4934
+
4935
+ const item = actionButton.closest('.blocks-item');
4936
+ if (!item || !list) return;
4937
+
4938
+ if (action === 'remove-block') {
4939
+ item.remove();
4940
+ updateHiddenInput();
4941
+ return;
4942
+ }
4943
+
4944
+ if (action === 'move-up') {
4945
+ const previous = item.previousElementSibling;
4946
+ if (previous) {
4947
+ list.insertBefore(item, previous);
4948
+ updateHiddenInput();
4949
+ }
4950
+ return;
4951
+ }
4952
+
4953
+ if (action === 'move-down') {
4954
+ const next = item.nextElementSibling;
4955
+ if (next) {
4956
+ list.insertBefore(next, item);
4957
+ updateHiddenInput();
4958
+ }
4959
+ }
4960
+ });
4961
+
4962
+ container.addEventListener('input', (event) => {
4963
+ const target = event.target;
4964
+ if (!(target instanceof Element)) return;
4965
+ if (target.closest('[data-blocks-list]')) {
4966
+ updateHiddenInput();
4967
+ }
4968
+ });
4969
+
4970
+ container.addEventListener('change', (event) => {
4971
+ const target = event.target;
4972
+ if (!(target instanceof Element)) return;
4973
+ if (target.closest('[data-blocks-list]')) {
4974
+ updateHiddenInput();
4975
+ }
4976
+ });
4977
+
4978
+ updateHiddenInput();
4979
+ });
4980
+ }
4981
+
4982
+ window.initializeBlocksFields = initializeBlocksFields;
4983
+
4984
+ if (document.readyState === 'loading') {
4985
+ document.addEventListener('DOMContentLoaded', initializeBlocksFields);
4986
+ } else {
4987
+ initializeBlocksFields();
4988
+ }
4989
+
4990
+ document.addEventListener('htmx:afterSwap', function() {
4991
+ setTimeout(initializeBlocksFields, 50);
4992
+ });
4993
+ } else if (typeof window.initializeBlocksFields === 'function') {
4994
+ window.initializeBlocksFields();
4995
+ }
4996
+ </script>
4997
+ `;
4998
+ }
4999
+ function escapeHtml2(text) {
5000
+ if (typeof text !== "string") return String(text || "");
5001
+ return text.replace(/[&<>"']/g, (char) => ({
5002
+ "&": "&amp;",
5003
+ "<": "&lt;",
5004
+ ">": "&gt;",
5005
+ '"': "&quot;",
5006
+ "'": "&#39;"
5007
+ })[char] || char);
5008
+ }
5009
+
5010
+ // src/plugins/available/tinymce-plugin/index.ts
5011
+ var builder = PluginBuilder.create({
5012
+ name: "tinymce-plugin",
5013
+ version: "1.0.0",
5014
+ description: "Powerful WYSIWYG rich text editor for content creation"
5015
+ });
5016
+ builder.metadata({
5017
+ author: {
5018
+ name: "SonicJS Team",
5019
+ email: "team@sonicjs.com"
5020
+ },
5021
+ license: "MIT",
5022
+ compatibility: "^2.0.0"
5023
+ });
5024
+ builder.lifecycle({
5025
+ activate: async () => {
5026
+ console.info("\u2705 TinyMCE plugin activated");
5027
+ },
5028
+ deactivate: async () => {
5029
+ console.info("\u274C TinyMCE plugin deactivated");
5030
+ }
5031
+ });
5032
+ builder.build();
5033
+ function getTinyMCEScript(apiKey = "no-api-key") {
5034
+ return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
5035
+ }
5036
+ function getTinyMCEInitScript(config) {
5037
+ const skin = config?.skin || "oxide-dark";
5038
+ const contentCss = skin.includes("dark") ? "dark" : "default";
5039
+ const defaultHeight = config?.defaultHeight || 300;
5040
+ return `
5041
+ // Initialize TinyMCE for all richtext fields
5042
+ function initializeTinyMCE() {
5043
+ if (typeof tinymce !== 'undefined') {
5044
+ // Find all textareas that need TinyMCE
5045
+ document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
5046
+ // Skip if already initialized
5047
+ if (tinymce.get(textarea.id)) {
5048
+ return;
5049
+ }
5050
+
5051
+ // Get configuration from data attributes
5052
+ const container = textarea.closest('.richtext-container');
5053
+ const height = container?.dataset.height || ${defaultHeight};
5054
+ const toolbar = container?.dataset.toolbar || 'full';
5055
+
5056
+ tinymce.init({
5057
+ selector: '#' + textarea.id,
5058
+ skin: '${skin}',
5059
+ content_css: '${contentCss}',
5060
+ height: parseInt(height),
5061
+ menubar: false,
5062
+ plugins: [
5063
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
5064
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
5065
+ 'insertdatetime', 'media', 'table', 'help', 'wordcount'
5066
+ ],
5067
+ toolbar: toolbar === 'simple'
5068
+ ? 'bold italic underline | bullist numlist | link'
4028
5069
  : toolbar === 'minimal'
4029
5070
  ? 'bold italic | link'
4030
5071
  : 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
@@ -4909,73 +5950,373 @@ function renderContentFormPage(data) {
4909
5950
  if (preview) {
4910
5951
  preview.classList.add('hidden');
4911
5952
  }
4912
- }
5953
+ }
5954
+
5955
+ // Close modal
5956
+ closeMediaSelector();
5957
+ }
5958
+
5959
+ function clearMediaField(fieldId) {
5960
+ const hiddenInput = document.getElementById(fieldId);
5961
+ const preview = document.getElementById(fieldId + '-preview');
5962
+
5963
+ if (hiddenInput) {
5964
+ hiddenInput.value = '';
5965
+ }
5966
+
5967
+ if (preview) {
5968
+ // Clear all children if it's a grid, or hide it
5969
+ if (preview.classList.contains('media-preview-grid')) {
5970
+ preview.innerHTML = '';
5971
+ }
5972
+ preview.classList.add('hidden');
5973
+ }
5974
+ }
5975
+
5976
+ // Global function to remove a single media from multiple selection
5977
+ window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
5978
+ const hiddenInput = document.getElementById(fieldId);
5979
+ if (!hiddenInput) return;
5980
+
5981
+ const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
5982
+ hiddenInput.value = values.join(',');
5983
+
5984
+ // Remove preview item
5985
+ const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
5986
+ if (previewItem) {
5987
+ previewItem.remove();
5988
+ }
5989
+
5990
+ // Hide preview grid if empty
5991
+ if (values.length === 0) {
5992
+ const preview = document.getElementById(fieldId + '-preview');
5993
+ if (preview) {
5994
+ preview.classList.add('hidden');
5995
+ }
5996
+ }
5997
+ };
5998
+
5999
+ // Global function called by media selector buttons
6000
+ window.selectMediaFile = function(mediaId, mediaUrl, filename) {
6001
+ if (!currentMediaFieldId) {
6002
+ console.error('No field ID set for media selection');
6003
+ return;
6004
+ }
6005
+
6006
+ const fieldId = currentMediaFieldId;
6007
+
6008
+ // Set the hidden input value to the media URL (not ID)
6009
+ const hiddenInput = document.getElementById(fieldId);
6010
+ if (hiddenInput) {
6011
+ hiddenInput.value = mediaUrl;
6012
+ }
6013
+
6014
+ // Update the preview
6015
+ const preview = document.getElementById(fieldId + '-preview');
6016
+ if (preview) {
6017
+ preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
6018
+ preview.classList.remove('hidden');
6019
+ }
6020
+
6021
+ // Show the remove button by finding the media actions container and updating it
6022
+ const mediaField = hiddenInput?.closest('.media-field-container');
6023
+ if (mediaField) {
6024
+ const actionsDiv = mediaField.querySelector('.media-actions');
6025
+ if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
6026
+ const removeBtn = document.createElement('button');
6027
+ removeBtn.type = 'button';
6028
+ removeBtn.onclick = () => clearMediaField(fieldId);
6029
+ removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
6030
+ removeBtn.textContent = 'Remove';
6031
+ actionsDiv.appendChild(removeBtn);
6032
+ }
6033
+ }
6034
+
6035
+ // DON'T close the modal - let user click OK button
6036
+ // Visual feedback: highlight the selected item
6037
+ document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
6038
+ el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6039
+ });
6040
+ const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
6041
+ if (selectedItem) {
6042
+ selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6043
+ }
6044
+ };
6045
+
6046
+ function setMediaField(fieldId, mediaUrl) {
6047
+ document.getElementById(fieldId).value = mediaUrl;
6048
+ const preview = document.getElementById(fieldId + '-preview');
6049
+ 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">\`;
6050
+ preview.classList.remove('hidden');
6051
+
6052
+ // Close modal
6053
+ document.querySelector('.fixed.inset-0')?.remove();
6054
+ }
6055
+
6056
+ // Reference field functions
6057
+ let currentReferenceFieldId = null;
6058
+ let referenceSearchTimeout = null;
6059
+
6060
+ function getReferenceContainer(fieldId) {
6061
+ const input = document.getElementById(fieldId);
6062
+ return input ? input.closest('[data-reference-field]') : null;
6063
+ }
6064
+
6065
+ function getReferenceCollections(container) {
6066
+ if (!container) return [];
6067
+ const rawCollections = container.dataset.referenceCollections || '';
6068
+ const collections = rawCollections
6069
+ .split(',')
6070
+ .map((value) => value.trim())
6071
+ .filter(Boolean);
6072
+ if (collections.length > 0) {
6073
+ return collections;
6074
+ }
6075
+ const singleCollection = container.dataset.referenceCollection;
6076
+ return singleCollection ? [singleCollection] : [];
6077
+ }
6078
+
6079
+ async function fetchReferenceItems(collections, search = '', limit = 20) {
6080
+ const params = new URLSearchParams({ limit: String(limit) });
6081
+ collections.forEach((collection) => params.append('collection', collection));
6082
+ if (search) {
6083
+ params.set('search', search);
6084
+ }
6085
+ const response = await fetch('/admin/api/references?' + params.toString());
6086
+ if (!response.ok) {
6087
+ throw new Error('Failed to load references');
6088
+ }
6089
+ const data = await response.json();
6090
+ return data?.data || [];
6091
+ }
6092
+
6093
+ async function fetchReferenceById(collections, id) {
6094
+ if (!id) return null;
6095
+ const params = new URLSearchParams({ id });
6096
+ collections.forEach((collection) => params.append('collection', collection));
6097
+ const response = await fetch('/admin/api/references?' + params.toString());
6098
+ if (!response.ok) {
6099
+ return null;
6100
+ }
6101
+ const data = await response.json();
6102
+ return data?.data || null;
6103
+ }
6104
+
6105
+ function renderReferenceDisplay(container, item, fallbackMessage = 'No reference selected.') {
6106
+ const display = container.querySelector('[data-reference-display]');
6107
+ const removeButton = container.querySelector('[data-reference-clear]');
6108
+ if (!display) return;
6109
+
6110
+ display.innerHTML = '';
6111
+
6112
+ if (!item) {
6113
+ display.textContent = fallbackMessage;
6114
+ if (removeButton) {
6115
+ removeButton.disabled = true;
6116
+ }
6117
+ return;
6118
+ }
6119
+
6120
+ const title = item.title || item.slug || item.id || 'Untitled';
6121
+ const titleEl = document.createElement('div');
6122
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6123
+ titleEl.textContent = title;
6124
+
6125
+ display.appendChild(titleEl);
6126
+
6127
+ const metaRow = document.createElement('div');
6128
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
6129
+
6130
+ if (item.collection?.display_name || item.collection?.name) {
6131
+ const collectionLabel = document.createElement('span');
6132
+ 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';
6133
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6134
+ metaRow.appendChild(collectionLabel);
6135
+ }
6136
+
6137
+ if (item.slug) {
6138
+ const slugEl = document.createElement('span');
6139
+ slugEl.textContent = item.slug;
6140
+ metaRow.appendChild(slugEl);
6141
+ }
6142
+
6143
+ if (metaRow.childElementCount > 0) {
6144
+ display.appendChild(metaRow);
6145
+ }
6146
+
6147
+ if (removeButton) {
6148
+ removeButton.disabled = false;
6149
+ }
6150
+ }
6151
+
6152
+ function updateReferenceField(fieldId, item) {
6153
+ const input = document.getElementById(fieldId);
6154
+ const container = getReferenceContainer(fieldId);
6155
+ if (!input || !container) return;
6156
+
6157
+ input.value = item?.id || '';
6158
+ renderReferenceDisplay(container, item, 'No reference selected.');
6159
+ input.dispatchEvent(new Event('input', { bubbles: true }));
6160
+ input.dispatchEvent(new Event('change', { bubbles: true }));
6161
+ }
6162
+
6163
+ function clearReferenceField(fieldId) {
6164
+ updateReferenceField(fieldId, null);
6165
+ }
6166
+
6167
+ function closeReferenceSelector() {
6168
+ const modal = document.getElementById('reference-selector-modal');
6169
+ if (modal) {
6170
+ modal.remove();
6171
+ }
6172
+ currentReferenceFieldId = null;
6173
+ }
6174
+
6175
+ function openReferenceSelector(fieldId) {
6176
+ const container = getReferenceContainer(fieldId);
6177
+ const collections = getReferenceCollections(container);
6178
+ if (!container || collections.length === 0) {
6179
+ console.error('Reference collection is missing for field', fieldId);
6180
+ return;
6181
+ }
6182
+
6183
+ currentReferenceFieldId = fieldId;
6184
+
6185
+ const modal = document.createElement('div');
6186
+ modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
6187
+ modal.id = 'reference-selector-modal';
6188
+ modal.innerHTML = \`
6189
+ <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">
6190
+ <div class="flex items-center justify-between gap-3">
6191
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white">Select Reference</h3>
6192
+ <button
6193
+ type="button"
6194
+ onclick="closeReferenceSelector()"
6195
+ class="rounded-md text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
6196
+ aria-label="Close"
6197
+ >
6198
+ \u2715
6199
+ </button>
6200
+ </div>
6201
+ <div class="mt-4">
6202
+ <input
6203
+ type="search"
6204
+ id="reference-search-input"
6205
+ placeholder="Search by title or slug..."
6206
+ 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"
6207
+ >
6208
+ </div>
6209
+ <div id="reference-results" class="mt-4 space-y-2"></div>
6210
+ <div class="mt-4 flex justify-end">
6211
+ <button
6212
+ type="button"
6213
+ onclick="closeReferenceSelector()"
6214
+ 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"
6215
+ >
6216
+ Close
6217
+ </button>
6218
+ </div>
6219
+ </div>
6220
+ \`;
6221
+
6222
+ document.body.appendChild(modal);
6223
+
6224
+ const resultsContainer = modal.querySelector('#reference-results');
6225
+ const searchInput = modal.querySelector('#reference-search-input');
6226
+
6227
+ const renderResults = (items) => {
6228
+ resultsContainer.innerHTML = '';
6229
+ if (!items || items.length === 0) {
6230
+ 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>';
6231
+ return;
6232
+ }
4913
6233
 
4914
- // Close modal
4915
- closeMediaSelector();
4916
- }
6234
+ const selectedId = document.getElementById(fieldId)?.value;
4917
6235
 
4918
- function clearMediaField(fieldId) {
4919
- document.getElementById(fieldId).value = '';
4920
- document.getElementById(fieldId + '-preview').classList.add('hidden');
4921
- }
6236
+ items.forEach((item) => {
6237
+ const button = document.createElement('button');
6238
+ button.type = 'button';
6239
+ 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';
6240
+ if (item.id === selectedId) {
6241
+ button.classList.add('ring-2', 'ring-cyan-500', 'dark:ring-cyan-400');
6242
+ }
4922
6243
 
4923
- // Global function called by media selector buttons
4924
- window.selectMediaFile = function(mediaId, mediaUrl, filename) {
4925
- if (!currentMediaFieldId) {
4926
- console.error('No field ID set for media selection');
4927
- return;
4928
- }
6244
+ const title = item.title || item.slug || item.id || 'Untitled';
6245
+ const titleEl = document.createElement('div');
6246
+ titleEl.className = 'font-medium text-zinc-900 dark:text-white';
6247
+ titleEl.textContent = title;
4929
6248
 
4930
- const fieldId = currentMediaFieldId;
6249
+ button.appendChild(titleEl);
4931
6250
 
4932
- // Set the hidden input value to the media URL (not ID)
4933
- const hiddenInput = document.getElementById(fieldId);
4934
- if (hiddenInput) {
4935
- hiddenInput.value = mediaUrl;
4936
- }
6251
+ const metaRow = document.createElement('div');
6252
+ metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
4937
6253
 
4938
- // Update the preview
4939
- const preview = document.getElementById(fieldId + '-preview');
4940
- if (preview) {
4941
- preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
4942
- preview.classList.remove('hidden');
4943
- }
6254
+ if (item.collection?.display_name || item.collection?.name) {
6255
+ const collectionLabel = document.createElement('span');
6256
+ 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';
6257
+ collectionLabel.textContent = item.collection.display_name || item.collection.name;
6258
+ metaRow.appendChild(collectionLabel);
6259
+ }
4944
6260
 
4945
- // Show the remove button by finding the media actions container and updating it
4946
- const mediaField = hiddenInput?.closest('.media-field-container');
4947
- if (mediaField) {
4948
- const actionsDiv = mediaField.querySelector('.media-actions');
4949
- if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
4950
- const removeBtn = document.createElement('button');
4951
- removeBtn.type = 'button';
4952
- removeBtn.onclick = () => clearMediaField(fieldId);
4953
- removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
4954
- removeBtn.textContent = 'Remove';
4955
- actionsDiv.appendChild(removeBtn);
6261
+ if (item.slug) {
6262
+ const slugEl = document.createElement('span');
6263
+ slugEl.textContent = item.slug;
6264
+ metaRow.appendChild(slugEl);
6265
+ }
6266
+
6267
+ if (metaRow.childElementCount > 0) {
6268
+ button.appendChild(metaRow);
6269
+ }
6270
+
6271
+ button.addEventListener('click', () => {
6272
+ updateReferenceField(fieldId, item);
6273
+ closeReferenceSelector();
6274
+ });
6275
+
6276
+ resultsContainer.appendChild(button);
6277
+ });
6278
+ };
6279
+
6280
+ const loadResults = async (searchValue = '') => {
6281
+ try {
6282
+ const items = await fetchReferenceItems(collections, searchValue);
6283
+ renderResults(items);
6284
+ } catch (error) {
6285
+ 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>';
4956
6286
  }
4957
- }
6287
+ };
4958
6288
 
4959
- // DON'T close the modal - let user click OK button
4960
- // Visual feedback: highlight the selected item
4961
- document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
4962
- el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
6289
+ loadResults();
6290
+
6291
+ searchInput.addEventListener('input', () => {
6292
+ if (referenceSearchTimeout) {
6293
+ clearTimeout(referenceSearchTimeout);
6294
+ }
6295
+ referenceSearchTimeout = setTimeout(() => {
6296
+ loadResults(searchInput.value.trim());
6297
+ }, 250);
4963
6298
  });
4964
- const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
4965
- if (selectedItem) {
4966
- selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
4967
- }
4968
- };
6299
+ }
4969
6300
 
4970
- function setMediaField(fieldId, mediaUrl) {
4971
- document.getElementById(fieldId).value = mediaUrl;
4972
- const preview = document.getElementById(fieldId + '-preview');
4973
- 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">\`;
4974
- preview.classList.remove('hidden');
6301
+ document.addEventListener('DOMContentLoaded', () => {
6302
+ document.querySelectorAll('[data-reference-field]').forEach(async (container) => {
6303
+ const input = container.querySelector('input[type="hidden"]');
6304
+ const collections = getReferenceCollections(container);
6305
+ if (!input || collections.length === 0) return;
4975
6306
 
4976
- // Close modal
4977
- document.querySelector('.fixed.inset-0')?.remove();
4978
- }
6307
+ if (!input.value) {
6308
+ renderReferenceDisplay(container, null, 'No reference selected.');
6309
+ return;
6310
+ }
6311
+
6312
+ const item = await fetchReferenceById(collections, input.value);
6313
+ if (item) {
6314
+ renderReferenceDisplay(container, item);
6315
+ } else {
6316
+ renderReferenceDisplay(container, null, 'Reference not found.');
6317
+ }
6318
+ });
6319
+ });
4979
6320
 
4980
6321
  // Custom select options
4981
6322
  function addCustomOption(input, selectId) {
@@ -5142,7 +6483,7 @@ function renderContentListPage(data) {
5142
6483
  if (data.search) urlParams.set("search", data.search);
5143
6484
  if (data.page && data.page !== 1) urlParams.set("page", data.page.toString());
5144
6485
  const currentParams = urlParams.toString();
5145
- const hasActiveFilters = data.modelName !== "all" || data.status !== "all" || !!data.search;
6486
+ data.modelName !== "all" || data.status !== "all" || !!data.search;
5146
6487
  const filterBarData = {
5147
6488
  filters: [
5148
6489
  {
@@ -5172,6 +6513,11 @@ function renderContentListPage(data) {
5172
6513
  }
5173
6514
  ],
5174
6515
  actions: [
6516
+ {
6517
+ label: "Advanced Search",
6518
+ className: "btn-primary",
6519
+ onclick: "openAdvancedSearch()"
6520
+ },
5175
6521
  {
5176
6522
  label: "Refresh",
5177
6523
  className: "btn-secondary",
@@ -5323,12 +6669,57 @@ function renderContentListPage(data) {
5323
6669
  <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">
5324
6670
  <div class="px-6 py-5">
5325
6671
  <div class="flex items-center justify-between">
5326
- <div class="flex items-center space-x-4">
5327
- <!-- Search Input -->
6672
+ <div class="flex items-center space-x-4 flex-1">
6673
+ <!-- Model Filter -->
6674
+ <div>
6675
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Model</label>
6676
+ <div class="grid grid-cols-1">
6677
+ <select
6678
+ name="model"
6679
+ onchange="updateContentFilters('model', this.value)"
6680
+ 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"
6681
+ >
6682
+ <option value="all" ${data.modelName === "all" ? "selected" : ""}>All Models</option>
6683
+ ${data.models.map((model) => `
6684
+ <option value="${model.name}" ${data.modelName === model.name ? "selected" : ""}>
6685
+ ${model.displayName}
6686
+ </option>
6687
+ `).join("")}
6688
+ </select>
6689
+ <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">
6690
+ <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" />
6691
+ </svg>
6692
+ </div>
6693
+ </div>
6694
+
6695
+ <!-- Status Filter -->
5328
6696
  <div>
6697
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
6698
+ <div class="grid grid-cols-1">
6699
+ <select
6700
+ name="status"
6701
+ onchange="updateContentFilters('status', this.value)"
6702
+ 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"
6703
+ >
6704
+ <option value="all" ${data.status === "all" ? "selected" : ""}>All Status</option>
6705
+ <option value="draft" ${data.status === "draft" ? "selected" : ""}>Draft</option>
6706
+ <option value="review" ${data.status === "review" ? "selected" : ""}>Under Review</option>
6707
+ <option value="scheduled" ${data.status === "scheduled" ? "selected" : ""}>Scheduled</option>
6708
+ <option value="published" ${data.status === "published" ? "selected" : ""}>Published</option>
6709
+ <option value="archived" ${data.status === "archived" ? "selected" : ""}>Archived</option>
6710
+ <option value="deleted" ${data.status === "deleted" ? "selected" : ""}>Deleted</option>
6711
+ </select>
6712
+ <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">
6713
+ <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" />
6714
+ </svg>
6715
+ </div>
6716
+ </div>
6717
+
6718
+ <!-- Search Input -->
6719
+ <div class="flex-1 max-w-md">
5329
6720
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
5330
6721
  <form onsubmit="performContentSearch(event)" class="flex items-center space-x-2">
5331
- <div class="relative group">
6722
+ <div class="relative group flex-1">
5332
6723
  <input
5333
6724
  type="text"
5334
6725
  name="search"
@@ -5336,7 +6727,7 @@ function renderContentListPage(data) {
5336
6727
  value="${data.search || ""}"
5337
6728
  oninput="toggleContentClearButton()"
5338
6729
  placeholder="Search content..."
5339
- 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"
6730
+ 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"
5340
6731
  >
5341
6732
  <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">
5342
6733
  <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
@@ -5408,57 +6799,6 @@ function renderContentListPage(data) {
5408
6799
  }
5409
6800
  </script>
5410
6801
  </div>
5411
-
5412
- ${filterBarData.filters.map((filter) => {
5413
- const selectedOption = filter.options.find((opt) => opt.selected);
5414
- const selectedColor = selectedOption?.color || "cyan";
5415
- const colorMap = {
5416
- "cyan": "bg-cyan-400 dark:bg-cyan-400",
5417
- "lime": "bg-lime-400 dark:bg-lime-400",
5418
- "pink": "bg-pink-400 dark:bg-pink-400",
5419
- "purple": "bg-purple-400 dark:bg-purple-400",
5420
- "amber": "bg-amber-400 dark:bg-amber-400",
5421
- "zinc": "bg-zinc-400 dark:bg-zinc-400"
5422
- };
5423
- return `
5424
- <div>
5425
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">${filter.label}</label>
5426
- <div class="mt-2 grid grid-cols-1">
5427
- <div class="col-start-1 row-start-1 flex items-center gap-3 pl-3 pr-8 pointer-events-none">
5428
- ${filter.name === "status" ? `<span class="inline-block size-2 shrink-0 rounded-full border border-transparent ${colorMap[selectedColor]}"></span>` : ""}
5429
- </div>
5430
- <select
5431
- name="${filter.name}"
5432
- onchange="updateContentFilters('${filter.name}', this.value)"
5433
- 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"
5434
- >
5435
- ${filter.options.map((opt) => `
5436
- <option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
5437
- `).join("")}
5438
- </select>
5439
- <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">
5440
- <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" />
5441
- </svg>
5442
- </div>
5443
- </div>
5444
- `;
5445
- }).join("")}
5446
-
5447
- <!-- Clear Filters Button -->
5448
- ${hasActiveFilters ? `
5449
- <div>
5450
- <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">&nbsp;</label>
5451
- <button
5452
- onclick="clearAllFilters()"
5453
- 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"
5454
- >
5455
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
5456
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
5457
- </svg>
5458
- Clear Filters
5459
- </button>
5460
- </div>
5461
- ` : ""}
5462
6802
  </div>
5463
6803
  <div class="flex items-center gap-x-3">
5464
6804
  <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>
@@ -5628,8 +6968,9 @@ function renderContentListPage(data) {
5628
6968
  });
5629
6969
 
5630
6970
  // Store current bulk action context
5631
- let currentBulkAction = null;
5632
- let currentSelectedIds = [];
6971
+ // Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
6972
+ var currentBulkAction = null;
6973
+ var currentSelectedIds = [];
5633
6974
 
5634
6975
  // Perform bulk action
5635
6976
  function performBulkAction(action) {
@@ -5719,49 +7060,336 @@ function renderContentListPage(data) {
5719
7060
  } else {
5720
7061
  alert('Error: ' + (data.error || 'Unknown error'));
5721
7062
  }
5722
- })
5723
- .catch(err => {
5724
- console.error('Bulk action error:', err);
5725
- alert('Failed to perform bulk action');
5726
- })
5727
- .finally(() => {
5728
- // Clear context
5729
- currentBulkAction = null;
5730
- currentSelectedIds = [];
7063
+ })
7064
+ .catch(err => {
7065
+ console.error('Bulk action error:', err);
7066
+ alert('Failed to perform bulk action');
7067
+ })
7068
+ .finally(() => {
7069
+ // Clear context
7070
+ currentBulkAction = null;
7071
+ currentSelectedIds = [];
7072
+ });
7073
+ }
7074
+
7075
+ // Helper to get action text for display
7076
+ function getActionText(action) {
7077
+ const actionCount = currentSelectedIds.length;
7078
+ switch(action) {
7079
+ case 'publish':
7080
+ return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7081
+ case 'draft':
7082
+ return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
7083
+ case 'delete':
7084
+ return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7085
+ default:
7086
+ return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7087
+ }
7088
+ }
7089
+
7090
+ </script>
7091
+
7092
+ <!-- Confirmation Dialog for Bulk Actions -->
7093
+ ${renderConfirmationDialog({
7094
+ id: "bulk-action-confirm",
7095
+ title: "Confirm Bulk Action",
7096
+ message: "Are you sure you want to perform this action? This operation will affect multiple items.",
7097
+ confirmText: "Confirm",
7098
+ cancelText: "Cancel",
7099
+ confirmClass: "bg-blue-500 hover:bg-blue-400",
7100
+ iconColor: "blue",
7101
+ onConfirm: "executeBulkAction()"
7102
+ })}
7103
+
7104
+ <!-- Confirmation Dialog Script -->
7105
+ ${getConfirmationDialogScript()}
7106
+
7107
+ <!-- Advanced Search Modal -->
7108
+ <div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
7109
+ <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
7110
+ <!-- Background overlay -->
7111
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
7112
+
7113
+ <!-- Modal panel -->
7114
+ <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">
7115
+ <div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
7116
+ <!-- Header -->
7117
+ <div class="flex items-center justify-between mb-4">
7118
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
7119
+ \u{1F50D} Advanced Search
7120
+ </h3>
7121
+ <button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
7122
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7123
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
7124
+ </svg>
7125
+ </button>
7126
+ </div>
7127
+
7128
+ <!-- Search Form -->
7129
+ <form id="advancedSearchForm" class="space-y-4">
7130
+ <!-- Search Input -->
7131
+ <div>
7132
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
7133
+ <div class="relative">
7134
+ <input
7135
+ type="text"
7136
+ id="searchQuery"
7137
+ name="query"
7138
+ placeholder="Enter your search query..."
7139
+ 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"
7140
+ autocomplete="off"
7141
+ />
7142
+ <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>
7143
+ </div>
7144
+ </div>
7145
+
7146
+ <!-- Mode Toggle -->
7147
+ <div>
7148
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
7149
+ <div class="flex gap-4">
7150
+ <label class="flex items-center">
7151
+ <input type="radio" name="mode" value="ai" checked class="mr-2">
7152
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
7153
+ </label>
7154
+ <label class="flex items-center">
7155
+ <input type="radio" name="mode" value="keyword" class="mr-2">
7156
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
7157
+ </label>
7158
+ </div>
7159
+ </div>
7160
+
7161
+ <!-- Filters -->
7162
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7163
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
7164
+
7165
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
7166
+ <!-- Collection Filter -->
7167
+ <div>
7168
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
7169
+ <select
7170
+ id="filterCollections"
7171
+ name="collections"
7172
+ multiple
7173
+ 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"
7174
+ size="4"
7175
+ >
7176
+ <option value="">All Collections</option>
7177
+ ${data.models.map(
7178
+ (model) => `
7179
+ <option value="${model.name}">${model.displayName}</option>
7180
+ `
7181
+ ).join("")}
7182
+ </select>
7183
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
7184
+ </div>
7185
+
7186
+ <!-- Status Filter -->
7187
+ <div>
7188
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
7189
+ <select
7190
+ id="filterStatus"
7191
+ name="status"
7192
+ multiple
7193
+ 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"
7194
+ size="4"
7195
+ >
7196
+ <option value="published">Published</option>
7197
+ <option value="draft">Draft</option>
7198
+ <option value="review">Under Review</option>
7199
+ <option value="scheduled">Scheduled</option>
7200
+ <option value="archived">Archived</option>
7201
+ </select>
7202
+ </div>
7203
+ </div>
7204
+ </div>
7205
+
7206
+ <!-- Actions -->
7207
+ <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
7208
+ <button
7209
+ type="button"
7210
+ onclick="closeAdvancedSearch()"
7211
+ 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"
7212
+ >
7213
+ Cancel
7214
+ </button>
7215
+ <button
7216
+ type="submit"
7217
+ 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"
7218
+ >
7219
+ Search
7220
+ </button>
7221
+ </div>
7222
+ </form>
7223
+ </div>
7224
+
7225
+ <!-- Results Area -->
7226
+ <div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
7227
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7228
+ <div id="searchResultsContent" class="space-y-3"></div>
7229
+ <div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
7230
+ </div>
7231
+ </div>
7232
+ </div>
7233
+ </div>
7234
+ </div>
7235
+
7236
+ <script>
7237
+ // Open modal
7238
+ function openAdvancedSearch() {
7239
+ document.getElementById('advancedSearchModal').classList.remove('hidden');
7240
+ document.getElementById('searchQuery').focus();
7241
+ }
7242
+
7243
+ // Close modal
7244
+ function closeAdvancedSearch() {
7245
+ document.getElementById('advancedSearchModal').classList.add('hidden');
7246
+ document.getElementById('searchResults').classList.add('hidden');
7247
+ }
7248
+
7249
+ // Autocomplete
7250
+ // Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
7251
+ var autocompleteTimeout;
7252
+ var searchQueryInput = document.getElementById('searchQuery');
7253
+ if (searchQueryInput) {
7254
+ searchQueryInput.addEventListener('input', (e) => {
7255
+ const query = e.target.value.trim();
7256
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7257
+
7258
+ clearTimeout(autocompleteTimeout);
7259
+
7260
+ if (query.length < 2) {
7261
+ suggestionsDiv.classList.add('hidden');
7262
+ return;
7263
+ }
7264
+
7265
+ autocompleteTimeout = setTimeout(async () => {
7266
+ try {
7267
+ const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
7268
+ const { data } = await res.json();
7269
+
7270
+ if (data && data.length > 0) {
7271
+ suggestionsDiv.innerHTML = data.map(s => \`
7272
+ <div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
7273
+ \`).join('');
7274
+ suggestionsDiv.classList.remove('hidden');
7275
+ } else {
7276
+ suggestionsDiv.classList.add('hidden');
7277
+ }
7278
+ } catch (error) {
7279
+ console.error('Autocomplete error:', error);
7280
+ }
7281
+ }, 300);
7282
+ });
7283
+ }
7284
+
7285
+ function selectSuggestion(suggestion) {
7286
+ document.getElementById('searchQuery').value = suggestion;
7287
+ document.getElementById('searchSuggestions').classList.add('hidden');
7288
+ }
7289
+
7290
+ // Hide suggestions when clicking outside
7291
+ document.addEventListener('click', (e) => {
7292
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7293
+ if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
7294
+ suggestionsDiv.classList.add('hidden');
7295
+ }
7296
+ });
7297
+
7298
+ // Form submission
7299
+ var advancedSearchForm = document.getElementById('advancedSearchForm');
7300
+ if (advancedSearchForm) {
7301
+ advancedSearchForm.addEventListener('submit', async (e) => {
7302
+ e.preventDefault();
7303
+
7304
+ const formData = new FormData(e.target);
7305
+ const query = formData.get('query');
7306
+ const mode = formData.get('mode') || 'ai';
7307
+
7308
+ // Build filters
7309
+ const filters = {};
7310
+
7311
+ const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
7312
+ if (collections.length > 0) {
7313
+ // Need to convert collection names to IDs - for now, pass names
7314
+ filters.collections = collections;
7315
+ }
7316
+
7317
+ const status = Array.from(formData.getAll('status'));
7318
+ if (status.length > 0) {
7319
+ filters.status = status;
7320
+ }
7321
+
7322
+ const dateStart = formData.get('date_start');
7323
+ const dateEnd = formData.get('date_end');
7324
+ if (dateStart || dateEnd) {
7325
+ filters.dateRange = {
7326
+ start: dateStart ? new Date(dateStart) : null,
7327
+ end: dateEnd ? new Date(dateEnd) : null,
7328
+ field: 'created_at'
7329
+ };
7330
+ }
7331
+
7332
+ // Execute search
7333
+ try {
7334
+ const res = await fetch('/api/search', {
7335
+ method: 'POST',
7336
+ headers: {'Content-Type': 'application/json'},
7337
+ body: JSON.stringify({
7338
+ query,
7339
+ mode,
7340
+ filters,
7341
+ limit: 20
7342
+ })
7343
+ });
7344
+
7345
+ const { data } = await res.json();
7346
+
7347
+ if (data && data.results) {
7348
+ displaySearchResults(data);
7349
+ }
7350
+ } catch (error) {
7351
+ console.error('Search error:', error);
7352
+ alert('Search failed. Please try again.');
7353
+ }
5731
7354
  });
5732
7355
  }
5733
7356
 
5734
- // Helper to get action text for display
5735
- function getActionText(action) {
5736
- const actionCount = currentSelectedIds.length;
5737
- switch(action) {
5738
- case 'publish':
5739
- return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5740
- case 'draft':
5741
- return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
5742
- case 'delete':
5743
- return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
5744
- default:
5745
- return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7357
+ function displaySearchResults(searchData) {
7358
+ const resultsDiv = document.getElementById('searchResultsContent');
7359
+ const resultsSection = document.getElementById('searchResults');
7360
+
7361
+ if (searchData.results.length === 0) {
7362
+ resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
7363
+ } else {
7364
+ resultsDiv.innerHTML = searchData.results.map(result => \`
7365
+ <div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
7366
+ <div class="flex items-start justify-between">
7367
+ <div class="flex-1">
7368
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
7369
+ <a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
7370
+ </h4>
7371
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
7372
+ \${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
7373
+ \${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
7374
+ </p>
7375
+ \${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
7376
+ </div>
7377
+ <div class="ml-4">
7378
+ <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>
7379
+ </div>
7380
+ </div>
7381
+ </div>
7382
+ \`).join('');
5746
7383
  }
7384
+
7385
+ resultsSection.classList.remove('hidden');
7386
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
5747
7387
  }
5748
7388
 
7389
+ // Make functions globally available
7390
+ window.openAdvancedSearch = openAdvancedSearch;
7391
+ window.closeAdvancedSearch = closeAdvancedSearch;
5749
7392
  </script>
5750
-
5751
- <!-- Confirmation Dialog for Bulk Actions -->
5752
- ${renderConfirmationDialog({
5753
- id: "bulk-action-confirm",
5754
- title: "Confirm Bulk Action",
5755
- message: "Are you sure you want to perform this action? This operation will affect multiple items.",
5756
- confirmText: "Confirm",
5757
- cancelText: "Cancel",
5758
- confirmClass: "bg-blue-500 hover:bg-blue-400",
5759
- iconColor: "blue",
5760
- onConfirm: "executeBulkAction()"
5761
- })}
5762
-
5763
- <!-- Confirmation Dialog Script -->
5764
- ${getConfirmationDialogScript()}
5765
7393
  `;
5766
7394
  const layoutData = {
5767
7395
  title: "Content Management",
@@ -5965,6 +7593,122 @@ async function isPluginActive2(db, pluginId) {
5965
7593
 
5966
7594
  // src/routes/admin-content.ts
5967
7595
  var adminContentRoutes = new Hono();
7596
+ function parseFieldValue(field, formData, options = {}) {
7597
+ const { skipValidation = false } = options;
7598
+ const value = formData.get(field.field_name);
7599
+ const errors = [];
7600
+ const blocksConfig = getBlocksFieldConfig(field.field_options);
7601
+ if (blocksConfig) {
7602
+ const parsed = parseBlocksValue(value, blocksConfig);
7603
+ if (!skipValidation && field.is_required && parsed.value.length === 0) {
7604
+ parsed.errors.push(`${field.field_label} is required`);
7605
+ }
7606
+ return { value: parsed.value, errors: parsed.errors };
7607
+ }
7608
+ if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
7609
+ return { value: null, errors: [`${field.field_label} is required`] };
7610
+ }
7611
+ switch (field.field_type) {
7612
+ case "number":
7613
+ if (value && isNaN(Number(value))) {
7614
+ if (!skipValidation) {
7615
+ errors.push(`${field.field_label} must be a valid number`);
7616
+ }
7617
+ return { value: null, errors };
7618
+ }
7619
+ return { value: value ? Number(value) : null, errors: [] };
7620
+ case "boolean":
7621
+ const submitted = formData.get(`${field.field_name}_submitted`);
7622
+ return { value: submitted ? value === "true" : false, errors: [] };
7623
+ case "select":
7624
+ if (field.field_options?.multiple) {
7625
+ return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
7626
+ }
7627
+ return { value, errors: [] };
7628
+ case "array": {
7629
+ if (!value || value.toString().trim() === "") {
7630
+ if (!skipValidation && field.is_required) {
7631
+ errors.push(`${field.field_label} is required`);
7632
+ }
7633
+ return { value: [], errors };
7634
+ }
7635
+ try {
7636
+ const parsed = JSON.parse(value.toString());
7637
+ if (!Array.isArray(parsed)) {
7638
+ if (!skipValidation) {
7639
+ errors.push(`${field.field_label} must be a JSON array`);
7640
+ }
7641
+ return { value: [], errors };
7642
+ }
7643
+ if (!skipValidation && field.is_required && parsed.length === 0) {
7644
+ errors.push(`${field.field_label} is required`);
7645
+ }
7646
+ return { value: parsed, errors };
7647
+ } catch {
7648
+ if (!skipValidation) {
7649
+ errors.push(`${field.field_label} must be valid JSON`);
7650
+ }
7651
+ return { value: [], errors };
7652
+ }
7653
+ }
7654
+ case "object": {
7655
+ if (!value || value.toString().trim() === "") {
7656
+ if (!skipValidation && field.is_required) {
7657
+ errors.push(`${field.field_label} is required`);
7658
+ }
7659
+ return { value: {}, errors };
7660
+ }
7661
+ try {
7662
+ const parsed = JSON.parse(value.toString());
7663
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7664
+ if (!skipValidation) {
7665
+ errors.push(`${field.field_label} must be a JSON object`);
7666
+ }
7667
+ return { value: {}, errors };
7668
+ }
7669
+ if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
7670
+ errors.push(`${field.field_label} is required`);
7671
+ }
7672
+ return { value: parsed, errors };
7673
+ } catch {
7674
+ if (!skipValidation) {
7675
+ errors.push(`${field.field_label} must be valid JSON`);
7676
+ }
7677
+ return { value: {}, errors };
7678
+ }
7679
+ }
7680
+ case "json": {
7681
+ if (!value || value.toString().trim() === "") {
7682
+ if (!skipValidation && field.is_required) {
7683
+ errors.push(`${field.field_label} is required`);
7684
+ }
7685
+ return { value: null, errors };
7686
+ }
7687
+ try {
7688
+ return { value: JSON.parse(value.toString()), errors: [] };
7689
+ } catch {
7690
+ if (!skipValidation) {
7691
+ errors.push(`${field.field_label} must be valid JSON`);
7692
+ }
7693
+ return { value: null, errors };
7694
+ }
7695
+ }
7696
+ default:
7697
+ return { value, errors: [] };
7698
+ }
7699
+ }
7700
+ function extractFieldData(fields, formData, options = {}) {
7701
+ const data = {};
7702
+ const errors = {};
7703
+ for (const field of fields) {
7704
+ const result = parseFieldValue(field, formData, options);
7705
+ data[field.field_name] = result.value;
7706
+ if (result.errors.length > 0) {
7707
+ errors[field.field_name] = result.errors;
7708
+ }
7709
+ }
7710
+ return { data, errors };
7711
+ }
5968
7712
  adminContentRoutes.use("*", requireAuth());
5969
7713
  async function getCollectionFields(db, collectionId) {
5970
7714
  const cache = getCacheService(CACHE_CONFIGS.collection);
@@ -6438,109 +8182,7 @@ adminContentRoutes.post("/", async (c) => {
6438
8182
  `);
6439
8183
  }
6440
8184
  const fields = await getCollectionFields(db, collectionId);
6441
- const data = {};
6442
- const errors = {};
6443
- for (const field of fields) {
6444
- const value = formData.get(field.field_name);
6445
- const blocksConfig = getBlocksFieldConfig(field.field_options);
6446
- if (blocksConfig) {
6447
- const parsed = parseBlocksValue(value, blocksConfig);
6448
- if (field.is_required && parsed.value.length === 0) {
6449
- parsed.errors.push(`${field.field_label} is required`);
6450
- }
6451
- if (parsed.errors.length > 0) {
6452
- errors[field.field_name] = parsed.errors;
6453
- }
6454
- data[field.field_name] = parsed.value;
6455
- continue;
6456
- }
6457
- if (field.is_required && (!value || value.toString().trim() === "")) {
6458
- errors[field.field_name] = [`${field.field_label} is required`];
6459
- continue;
6460
- }
6461
- switch (field.field_type) {
6462
- case "number":
6463
- if (value && isNaN(Number(value))) {
6464
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6465
- } else {
6466
- data[field.field_name] = value ? Number(value) : null;
6467
- }
6468
- break;
6469
- case "boolean":
6470
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6471
- break;
6472
- case "select":
6473
- if (field.field_options?.multiple) {
6474
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6475
- } else {
6476
- data[field.field_name] = value;
6477
- }
6478
- break;
6479
- case "array": {
6480
- if (!value || value.toString().trim() === "") {
6481
- data[field.field_name] = [];
6482
- if (field.is_required) {
6483
- errors[field.field_name] = [`${field.field_label} is required`];
6484
- }
6485
- break;
6486
- }
6487
- try {
6488
- const parsed = JSON.parse(value.toString());
6489
- if (!Array.isArray(parsed)) {
6490
- errors[field.field_name] = [`${field.field_label} must be a JSON array`];
6491
- } else {
6492
- if (field.is_required && parsed.length === 0) {
6493
- errors[field.field_name] = [`${field.field_label} is required`];
6494
- }
6495
- data[field.field_name] = parsed;
6496
- }
6497
- } catch {
6498
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6499
- }
6500
- break;
6501
- }
6502
- case "object": {
6503
- if (!value || value.toString().trim() === "") {
6504
- data[field.field_name] = {};
6505
- if (field.is_required) {
6506
- errors[field.field_name] = [`${field.field_label} is required`];
6507
- }
6508
- break;
6509
- }
6510
- try {
6511
- const parsed = JSON.parse(value.toString());
6512
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6513
- errors[field.field_name] = [`${field.field_label} must be a JSON object`];
6514
- } else {
6515
- if (field.is_required && Object.keys(parsed).length === 0) {
6516
- errors[field.field_name] = [`${field.field_label} is required`];
6517
- }
6518
- data[field.field_name] = parsed;
6519
- }
6520
- } catch {
6521
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6522
- }
6523
- break;
6524
- }
6525
- case "json": {
6526
- if (!value || value.toString().trim() === "") {
6527
- data[field.field_name] = null;
6528
- if (field.is_required) {
6529
- errors[field.field_name] = [`${field.field_label} is required`];
6530
- }
6531
- break;
6532
- }
6533
- try {
6534
- data[field.field_name] = JSON.parse(value.toString());
6535
- } catch {
6536
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6537
- }
6538
- break;
6539
- }
6540
- default:
6541
- data[field.field_name] = value;
6542
- }
6543
- }
8185
+ const { data, errors } = extractFieldData(fields, formData);
6544
8186
  if (Object.keys(errors).length > 0) {
6545
8187
  const formDataWithErrors = {
6546
8188
  collection,
@@ -6657,109 +8299,7 @@ adminContentRoutes.put("/:id", async (c) => {
6657
8299
  `);
6658
8300
  }
6659
8301
  const fields = await getCollectionFields(db, existingContent.collection_id);
6660
- const data = {};
6661
- const errors = {};
6662
- for (const field of fields) {
6663
- const value = formData.get(field.field_name);
6664
- const blocksConfig = getBlocksFieldConfig(field.field_options);
6665
- if (blocksConfig) {
6666
- const parsed = parseBlocksValue(value, blocksConfig);
6667
- if (field.is_required && parsed.value.length === 0) {
6668
- parsed.errors.push(`${field.field_label} is required`);
6669
- }
6670
- if (parsed.errors.length > 0) {
6671
- errors[field.field_name] = parsed.errors;
6672
- }
6673
- data[field.field_name] = parsed.value;
6674
- continue;
6675
- }
6676
- if (field.is_required && (!value || value.toString().trim() === "")) {
6677
- errors[field.field_name] = [`${field.field_label} is required`];
6678
- continue;
6679
- }
6680
- switch (field.field_type) {
6681
- case "number":
6682
- if (value && isNaN(Number(value))) {
6683
- errors[field.field_name] = [`${field.field_label} must be a valid number`];
6684
- } else {
6685
- data[field.field_name] = value ? Number(value) : null;
6686
- }
6687
- break;
6688
- case "boolean":
6689
- data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
6690
- break;
6691
- case "select":
6692
- if (field.field_options?.multiple) {
6693
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6694
- } else {
6695
- data[field.field_name] = value;
6696
- }
6697
- break;
6698
- case "array": {
6699
- if (!value || value.toString().trim() === "") {
6700
- data[field.field_name] = [];
6701
- if (field.is_required) {
6702
- errors[field.field_name] = [`${field.field_label} is required`];
6703
- }
6704
- break;
6705
- }
6706
- try {
6707
- const parsed = JSON.parse(value.toString());
6708
- if (!Array.isArray(parsed)) {
6709
- errors[field.field_name] = [`${field.field_label} must be a JSON array`];
6710
- } else {
6711
- if (field.is_required && parsed.length === 0) {
6712
- errors[field.field_name] = [`${field.field_label} is required`];
6713
- }
6714
- data[field.field_name] = parsed;
6715
- }
6716
- } catch {
6717
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6718
- }
6719
- break;
6720
- }
6721
- case "object": {
6722
- if (!value || value.toString().trim() === "") {
6723
- data[field.field_name] = {};
6724
- if (field.is_required) {
6725
- errors[field.field_name] = [`${field.field_label} is required`];
6726
- }
6727
- break;
6728
- }
6729
- try {
6730
- const parsed = JSON.parse(value.toString());
6731
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6732
- errors[field.field_name] = [`${field.field_label} must be a JSON object`];
6733
- } else {
6734
- if (field.is_required && Object.keys(parsed).length === 0) {
6735
- errors[field.field_name] = [`${field.field_label} is required`];
6736
- }
6737
- data[field.field_name] = parsed;
6738
- }
6739
- } catch {
6740
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6741
- }
6742
- break;
6743
- }
6744
- case "json": {
6745
- if (!value || value.toString().trim() === "") {
6746
- data[field.field_name] = null;
6747
- if (field.is_required) {
6748
- errors[field.field_name] = [`${field.field_label} is required`];
6749
- }
6750
- break;
6751
- }
6752
- try {
6753
- data[field.field_name] = JSON.parse(value.toString());
6754
- } catch {
6755
- errors[field.field_name] = [`${field.field_label} must be valid JSON`];
6756
- }
6757
- break;
6758
- }
6759
- default:
6760
- data[field.field_name] = value;
6761
- }
6762
- }
8302
+ const { data, errors } = extractFieldData(fields, formData);
6763
8303
  if (Object.keys(errors).length > 0) {
6764
8304
  const formDataWithErrors = {
6765
8305
  id,
@@ -6872,33 +8412,7 @@ adminContentRoutes.post("/preview", async (c) => {
6872
8412
  return c.html("<p>Collection not found</p>");
6873
8413
  }
6874
8414
  const fields = await getCollectionFields(db, collectionId);
6875
- const data = {};
6876
- for (const field of fields) {
6877
- const value = formData.get(field.field_name);
6878
- const blocksConfig = getBlocksFieldConfig(field.field_options);
6879
- if (blocksConfig) {
6880
- const parsed = parseBlocksValue(value, blocksConfig);
6881
- data[field.field_name] = parsed.value;
6882
- continue;
6883
- }
6884
- switch (field.field_type) {
6885
- case "number":
6886
- data[field.field_name] = value ? Number(value) : null;
6887
- break;
6888
- case "boolean":
6889
- data[field.field_name] = value === "true";
6890
- break;
6891
- case "select":
6892
- if (field.field_options?.multiple) {
6893
- data[field.field_name] = formData.getAll(`${field.field_name}[]`);
6894
- } else {
6895
- data[field.field_name] = value;
6896
- }
6897
- break;
6898
- default:
6899
- data[field.field_name] = value;
6900
- }
6901
- }
8415
+ const { data } = extractFieldData(fields, formData, { skipValidation: true });
6902
8416
  const previewHTML = `
6903
8417
  <!DOCTYPE html>
6904
8418
  <html lang="en">
@@ -8245,14 +9759,87 @@ function renderUserEditPage(data) {
8245
9759
  </div>
8246
9760
  </div>
8247
9761
  </div>
9762
+ </div>
9763
+
9764
+ <!-- Profile Information -->
9765
+ <div class="mb-8">
9766
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
9767
+ <p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
9768
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9769
+ <div>
9770
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
9771
+ <input
9772
+ type="text"
9773
+ name="profile_display_name"
9774
+ value="${escapeHtml(data.userToEdit.profile?.displayName || "")}"
9775
+ placeholder="Public display name"
9776
+ 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"
9777
+ />
9778
+ </div>
9779
+
9780
+ <div>
9781
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
9782
+ <input
9783
+ type="text"
9784
+ name="profile_company"
9785
+ value="${escapeHtml(data.userToEdit.profile?.company || "")}"
9786
+ placeholder="Company or organization"
9787
+ 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"
9788
+ />
9789
+ </div>
9790
+
9791
+ <div>
9792
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
9793
+ <input
9794
+ type="text"
9795
+ name="profile_job_title"
9796
+ value="${escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
9797
+ placeholder="Job title or role"
9798
+ 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"
9799
+ />
9800
+ </div>
9801
+
9802
+ <div>
9803
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
9804
+ <input
9805
+ type="url"
9806
+ name="profile_website"
9807
+ value="${escapeHtml(data.userToEdit.profile?.website || "")}"
9808
+ placeholder="https://example.com"
9809
+ 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"
9810
+ />
9811
+ </div>
9812
+
9813
+ <div>
9814
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
9815
+ <input
9816
+ type="text"
9817
+ name="profile_location"
9818
+ value="${escapeHtml(data.userToEdit.profile?.location || "")}"
9819
+ placeholder="City, Country"
9820
+ 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"
9821
+ />
9822
+ </div>
9823
+
9824
+ <div>
9825
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
9826
+ <input
9827
+ type="date"
9828
+ name="profile_date_of_birth"
9829
+ value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
9830
+ 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"
9831
+ />
9832
+ </div>
9833
+ </div>
8248
9834
 
8249
9835
  <div class="mt-6">
8250
9836
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
8251
9837
  <textarea
8252
- name="bio"
9838
+ name="profile_bio"
8253
9839
  rows="3"
9840
+ placeholder="Short bio or description"
8254
9841
  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"
8255
- >${escapeHtml(data.userToEdit.bio || "")}</textarea>
9842
+ >${escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
8256
9843
  </div>
8257
9844
  </div>
8258
9845
 
@@ -9786,7 +11373,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9786
11373
  const userId = c.req.param("id");
9787
11374
  try {
9788
11375
  const userStmt = db.prepare(`
9789
- SELECT id, email, username, first_name, last_name, phone, bio, avatar_url,
11376
+ SELECT id, email, username, first_name, last_name, phone, avatar_url,
9790
11377
  role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
9791
11378
  FROM users
9792
11379
  WHERE id = ?
@@ -9799,6 +11386,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
9799
11386
  dismissible: true
9800
11387
  }), 404);
9801
11388
  }
11389
+ const profileStmt = db.prepare(`
11390
+ SELECT display_name, bio, company, job_title, website, location, date_of_birth
11391
+ FROM user_profiles
11392
+ WHERE user_id = ?
11393
+ `);
11394
+ const profileData = await profileStmt.bind(userId).first();
11395
+ const profile = profileData ? {
11396
+ displayName: profileData.display_name,
11397
+ bio: profileData.bio,
11398
+ company: profileData.company,
11399
+ jobTitle: profileData.job_title,
11400
+ website: profileData.website,
11401
+ location: profileData.location,
11402
+ dateOfBirth: profileData.date_of_birth
11403
+ } : void 0;
9802
11404
  const editData = {
9803
11405
  id: userToEdit.id,
9804
11406
  email: userToEdit.email,
@@ -9806,14 +11408,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
9806
11408
  firstName: userToEdit.first_name || "",
9807
11409
  lastName: userToEdit.last_name || "",
9808
11410
  phone: userToEdit.phone,
9809
- bio: userToEdit.bio,
9810
11411
  avatarUrl: userToEdit.avatar_url,
9811
11412
  role: userToEdit.role,
9812
11413
  isActive: Boolean(userToEdit.is_active),
9813
11414
  emailVerified: Boolean(userToEdit.email_verified),
9814
11415
  twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
9815
11416
  createdAt: userToEdit.created_at,
9816
- lastLoginAt: userToEdit.last_login_at
11417
+ lastLoginAt: userToEdit.last_login_at,
11418
+ profile
9817
11419
  };
9818
11420
  const pageData = {
9819
11421
  userToEdit: editData,
@@ -9829,7 +11431,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9829
11431
  console.error("User edit page error:", error);
9830
11432
  return c.html(renderAlert2({
9831
11433
  type: "error",
9832
- message: "Failed to load user!. Please try again.",
11434
+ message: "Failed to load user. Please try again.",
9833
11435
  dismissible: true
9834
11436
  }), 500);
9835
11437
  }
@@ -9845,10 +11447,17 @@ userRoutes.put("/users/:id", async (c) => {
9845
11447
  const username = sanitizeInput(formData.get("username")?.toString());
9846
11448
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9847
11449
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
9848
- const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
9849
11450
  const role = formData.get("role")?.toString() || "viewer";
9850
11451
  const isActive = formData.get("is_active") === "1";
9851
11452
  const emailVerified = formData.get("email_verified") === "1";
11453
+ const profileDisplayName = sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
11454
+ const profileBio = sanitizeInput(formData.get("profile_bio")?.toString()) || null;
11455
+ const profileCompany = sanitizeInput(formData.get("profile_company")?.toString()) || null;
11456
+ const profileJobTitle = sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
11457
+ const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
11458
+ const profileLocation = sanitizeInput(formData.get("profile_location")?.toString()) || null;
11459
+ const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
11460
+ const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
9852
11461
  if (!firstName || !lastName || !username || !email) {
9853
11462
  return c.html(renderAlert2({
9854
11463
  type: "error",
@@ -9864,6 +11473,17 @@ userRoutes.put("/users/:id", async (c) => {
9864
11473
  dismissible: true
9865
11474
  }));
9866
11475
  }
11476
+ if (profileWebsite) {
11477
+ try {
11478
+ new URL(profileWebsite);
11479
+ } catch {
11480
+ return c.html(renderAlert2({
11481
+ type: "error",
11482
+ message: "Please enter a valid website URL.",
11483
+ dismissible: true
11484
+ }));
11485
+ }
11486
+ }
9867
11487
  const checkStmt = db.prepare(`
9868
11488
  SELECT id FROM users
9869
11489
  WHERE (username = ? OR email = ?) AND id != ?
@@ -9872,14 +11492,14 @@ userRoutes.put("/users/:id", async (c) => {
9872
11492
  if (existingUser) {
9873
11493
  return c.html(renderAlert2({
9874
11494
  type: "error",
9875
- message: "Username or email is already taken by another user!.",
11495
+ message: "Username or email is already taken by another user.",
9876
11496
  dismissible: true
9877
11497
  }));
9878
11498
  }
9879
11499
  const updateStmt = db.prepare(`
9880
11500
  UPDATE users SET
9881
11501
  first_name = ?, last_name = ?, username = ?, email = ?,
9882
- phone = ?, bio = ?, role = ?, is_active = ?, email_verified = ?,
11502
+ phone = ?, role = ?, is_active = ?, email_verified = ?,
9883
11503
  updated_at = ?
9884
11504
  WHERE id = ?
9885
11505
  `);
@@ -9889,20 +11509,63 @@ userRoutes.put("/users/:id", async (c) => {
9889
11509
  username,
9890
11510
  email,
9891
11511
  phone,
9892
- bio,
9893
11512
  role,
9894
11513
  isActive ? 1 : 0,
9895
11514
  emailVerified ? 1 : 0,
9896
11515
  Date.now(),
9897
11516
  userId
9898
11517
  ).run();
11518
+ const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
11519
+ if (hasProfileData) {
11520
+ const now = Date.now();
11521
+ const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
11522
+ const existingProfile = await profileCheckStmt.bind(userId).first();
11523
+ if (existingProfile) {
11524
+ const updateProfileStmt = db.prepare(`
11525
+ UPDATE user_profiles SET
11526
+ display_name = ?, bio = ?, company = ?, job_title = ?,
11527
+ website = ?, location = ?, date_of_birth = ?, updated_at = ?
11528
+ WHERE user_id = ?
11529
+ `);
11530
+ await updateProfileStmt.bind(
11531
+ profileDisplayName,
11532
+ profileBio,
11533
+ profileCompany,
11534
+ profileJobTitle,
11535
+ profileWebsite,
11536
+ profileLocation,
11537
+ profileDateOfBirth,
11538
+ now,
11539
+ userId
11540
+ ).run();
11541
+ } else {
11542
+ const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
11543
+ const insertProfileStmt = db.prepare(`
11544
+ INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
11545
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11546
+ `);
11547
+ await insertProfileStmt.bind(
11548
+ profileId,
11549
+ userId,
11550
+ profileDisplayName,
11551
+ profileBio,
11552
+ profileCompany,
11553
+ profileJobTitle,
11554
+ profileWebsite,
11555
+ profileLocation,
11556
+ profileDateOfBirth,
11557
+ now,
11558
+ now
11559
+ ).run();
11560
+ }
11561
+ }
9899
11562
  await logActivity(
9900
11563
  db,
9901
11564
  user.userId,
9902
- "user!.update",
11565
+ "user.update",
9903
11566
  "users",
9904
11567
  userId,
9905
- { fields: ["first_name", "last_name", "username", "email", "phone", "bio", "role", "is_active", "email_verified"] },
11568
+ { fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
9906
11569
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
9907
11570
  c.req.header("user-agent")
9908
11571
  );
@@ -9915,7 +11578,7 @@ userRoutes.put("/users/:id", async (c) => {
9915
11578
  console.error("User update error:", error);
9916
11579
  return c.html(renderAlert2({
9917
11580
  type: "error",
9918
- message: "Failed to update user!. Please try again.",
11581
+ message: "Failed to update user. Please try again.",
9919
11582
  dismissible: true
9920
11583
  }));
9921
11584
  }
@@ -14106,6 +15769,19 @@ var AVAILABLE_PLUGINS = [
14106
15769
  permissions: [],
14107
15770
  dependencies: [],
14108
15771
  is_core: true
15772
+ },
15773
+ {
15774
+ id: "ai-search",
15775
+ name: "ai-search-plugin",
15776
+ display_name: "AI Search",
15777
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
15778
+ version: "1.0.0",
15779
+ author: "SonicJS Team",
15780
+ category: "search",
15781
+ icon: "\u{1F50D}",
15782
+ permissions: [],
15783
+ dependencies: [],
15784
+ is_core: true
14109
15785
  }
14110
15786
  ];
14111
15787
  adminPluginRoutes.get("/", async (c) => {
@@ -14184,6 +15860,9 @@ adminPluginRoutes.get("/:id", async (c) => {
14184
15860
  const user = c.get("user");
14185
15861
  const db = c.env.DB;
14186
15862
  const pluginId = c.req.param("id");
15863
+ if (pluginId === "ai-search") {
15864
+ return c.text("", 404);
15865
+ }
14187
15866
  if (user?.role !== "admin") {
14188
15867
  return c.redirect("/admin/plugins");
14189
15868
  }
@@ -14476,6 +16155,33 @@ adminPluginRoutes.post("/install", async (c) => {
14476
16155
  });
14477
16156
  return c.json({ success: true, plugin: easyMdxPlugin2 });
14478
16157
  }
16158
+ if (body.name === "ai-search-plugin" || body.name === "ai-search") {
16159
+ const defaultSettings = {
16160
+ enabled: true,
16161
+ ai_mode_enabled: true,
16162
+ selected_collections: [],
16163
+ dismissed_collections: [],
16164
+ autocomplete_enabled: true,
16165
+ cache_duration: 1,
16166
+ results_limit: 20,
16167
+ index_media: false
16168
+ };
16169
+ const aiSearchPlugin = await pluginService.installPlugin({
16170
+ id: "ai-search",
16171
+ name: "ai-search-plugin",
16172
+ display_name: "AI Search",
16173
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
16174
+ version: "1.0.0",
16175
+ author: "SonicJS Team",
16176
+ category: "search",
16177
+ icon: "\u{1F50D}",
16178
+ permissions: [],
16179
+ dependencies: [],
16180
+ is_core: true,
16181
+ settings: defaultSettings
16182
+ });
16183
+ return c.json({ success: true, plugin: aiSearchPlugin });
16184
+ }
14479
16185
  if (body.name === "turnstile-plugin") {
14480
16186
  const turnstilePlugin = await pluginService.installPlugin({
14481
16187
  id: "turnstile",
@@ -18376,7 +20082,8 @@ function getFieldTypeBadge(fieldType) {
18376
20082
  "boolean": "Boolean",
18377
20083
  "date": "Date",
18378
20084
  "select": "Select",
18379
- "media": "Media"
20085
+ "media": "Media",
20086
+ "reference": "Reference"
18380
20087
  };
18381
20088
  const typeColors = {
18382
20089
  "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",
@@ -18387,7 +20094,8 @@ function getFieldTypeBadge(fieldType) {
18387
20094
  "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",
18388
20095
  "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",
18389
20096
  "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",
18390
- "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"
20097
+ "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",
20098
+ "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"
18391
20099
  };
18392
20100
  const label = typeLabels[fieldType] || fieldType;
18393
20101
  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";
@@ -18867,6 +20575,7 @@ function renderCollectionFormPage(data) {
18867
20575
  <option value="date">Date</option>
18868
20576
  <option value="select">Select</option>
18869
20577
  <option value="media">Media</option>
20578
+ <option value="reference">Reference</option>
18870
20579
  </select>
18871
20580
  <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">
18872
20581
  <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" />
@@ -19184,11 +20893,14 @@ function renderCollectionFormPage(data) {
19184
20893
  }
19185
20894
 
19186
20895
  // Show/hide options container based on field type
19187
- const fieldType = field.field_type;
20896
+ // Use the dropdown's actual value (not field.field_type) to ensure consistency
20897
+ const fieldType = fieldTypeSelect?.value || field.field_type;
19188
20898
  const optionsContainer = document.getElementById('field-options-container');
19189
20899
  const helpText = document.getElementById('field-type-help');
19190
20900
 
19191
- if (['select', 'media', 'richtext'].includes(fieldType)) {
20901
+ console.log('[Edit Field] Showing options for field type:', fieldType, '(original:', field.field_type, ')');
20902
+
20903
+ if (['select', 'media', 'richtext', 'reference'].includes(fieldType)) {
19192
20904
  optionsContainer.classList.remove('hidden');
19193
20905
 
19194
20906
  // Set help text based on type
@@ -19202,6 +20914,9 @@ function renderCollectionFormPage(data) {
19202
20914
  case 'richtext':
19203
20915
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
19204
20916
  break;
20917
+ case 'reference':
20918
+ helpText.textContent = 'Link to content from other collections';
20919
+ break;
19205
20920
  }
19206
20921
  } else {
19207
20922
  optionsContainer.classList.add('hidden');
@@ -19336,7 +21051,7 @@ function renderCollectionFormPage(data) {
19336
21051
  const fieldNameInput = document.getElementById('modal-field-name');
19337
21052
 
19338
21053
  // Show/hide options based on field type
19339
- if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
21054
+ if (['select', 'media', 'richtext', 'guid', 'reference'].includes(this.value)) {
19340
21055
  optionsContainer.classList.remove('hidden');
19341
21056
 
19342
21057
  // Set default options and help text based on type
@@ -19353,6 +21068,10 @@ function renderCollectionFormPage(data) {
19353
21068
  fieldOptions.value = '{"toolbar": "full", "height": 400}';
19354
21069
  helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
19355
21070
  break;
21071
+ case 'reference':
21072
+ fieldOptions.value = '{"collection": ["pages", "posts"]}';
21073
+ helpText.textContent = 'Link to content from other collections';
21074
+ break;
19356
21075
  }
19357
21076
  } else {
19358
21077
  optionsContainer.classList.add('hidden');
@@ -19921,6 +21640,8 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
19921
21640
  fieldConfig.type = "quill";
19922
21641
  } else if (fieldType === "mdxeditor") {
19923
21642
  fieldConfig.type = "mdxeditor";
21643
+ } else if (fieldType === "reference") {
21644
+ fieldConfig.type = "reference";
19924
21645
  }
19925
21646
  schema.properties[fieldName] = fieldConfig;
19926
21647
  if (isRequired && !schema.required.includes(fieldName)) {
@@ -20009,8 +21730,15 @@ adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
20009
21730
  schema.required = [];
20010
21731
  }
20011
21732
  if (schema.properties[fieldName]) {
21733
+ let parsedFieldOptions = {};
21734
+ try {
21735
+ parsedFieldOptions = JSON.parse(fieldOptions);
21736
+ } catch (e) {
21737
+ console.error("[Field Update] Error parsing field options:", e);
21738
+ }
20012
21739
  const updatedFieldConfig = {
20013
21740
  ...schema.properties[fieldName],
21741
+ ...parsedFieldOptions,
20014
21742
  type: fieldType,
20015
21743
  title: fieldLabel,
20016
21744
  searchable: isSearchable
@@ -22034,6 +23762,6 @@ var ROUTES_INFO = {
22034
23762
  reference: "https://github.com/sonicjs/sonicjs"
22035
23763
  };
22036
23764
 
22037
- export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, checkAdminUserExists, router, test_cleanup_default, userRoutes };
22038
- //# sourceMappingURL=chunk-OJZ45OJD.js.map
22039
- //# sourceMappingURL=chunk-OJZ45OJD.js.map
23765
+ export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, getConfirmationDialogScript2 as getConfirmationDialogScript, renderConfirmationDialog2 as renderConfirmationDialog, router, test_cleanup_default, userRoutes };
23766
+ //# sourceMappingURL=chunk-UISZ2MBW.js.map
23767
+ //# sourceMappingURL=chunk-UISZ2MBW.js.map