@sonicjs-cms/core 2.5.0 → 2.6.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-63K7XXRX.cjs} +5 -5
  4. package/dist/{chunk-YIXSSJWD.cjs.map → chunk-63K7XXRX.cjs.map} +1 -1
  5. package/dist/{chunk-BHNDALCA.js → chunk-7DL5SPPX.js} +6 -4
  6. package/dist/chunk-7DL5SPPX.js.map +1 -0
  7. package/dist/{chunk-AZLU3ROK.cjs → chunk-BZC4FYW7.cjs} +4 -4
  8. package/dist/chunk-BZC4FYW7.cjs.map +1 -0
  9. package/dist/chunk-CLIH2T74.js +403 -0
  10. package/dist/chunk-CLIH2T74.js.map +1 -0
  11. package/dist/{chunk-VEL7QRYI.js → chunk-EVZOVYLO.js} +9 -2
  12. package/dist/chunk-EVZOVYLO.js.map +1 -0
  13. package/dist/{chunk-TJTWRO4G.js → chunk-EYWR6UA2.js} +4 -4
  14. package/dist/{chunk-TJTWRO4G.js.map → chunk-EYWR6UA2.js.map} +1 -1
  15. package/dist/{chunk-3YUHXWSG.js → chunk-F332TENF.js} +3 -3
  16. package/dist/{chunk-3YUHXWSG.js.map → chunk-F332TENF.js.map} +1 -1
  17. package/dist/{chunk-OJZ45OJD.js → chunk-F6GZURXJ.js} +2243 -539
  18. package/dist/chunk-F6GZURXJ.js.map +1 -0
  19. package/dist/{chunk-I4V3VZWF.cjs → chunk-IIRVZSP2.cjs} +9 -2
  20. package/dist/chunk-IIRVZSP2.cjs.map +1 -0
  21. package/dist/{chunk-V5LBQN3I.js → chunk-KA2PDJNB.js} +4 -4
  22. package/dist/chunk-KA2PDJNB.js.map +1 -0
  23. package/dist/{chunk-AVPUX57O.js → chunk-KAOWRIFD.js} +3 -3
  24. package/dist/{chunk-AVPUX57O.js.map → chunk-KAOWRIFD.js.map} +1 -1
  25. package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
  26. package/dist/chunk-MPT5PA6U.cjs.map +1 -0
  27. package/dist/{chunk-UAQL2VWX.cjs → chunk-N7TDLOUE.cjs} +2406 -703
  28. package/dist/chunk-N7TDLOUE.cjs.map +1 -0
  29. package/dist/{chunk-YYV3XQOQ.cjs → chunk-T3YIKW2A.cjs} +7 -7
  30. package/dist/{chunk-YYV3XQOQ.cjs.map → chunk-T3YIKW2A.cjs.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-ZWV3EBZ7.cjs → chunk-YMTTGHEK.cjs} +6 -4
  38. package/dist/chunk-YMTTGHEK.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 +2156 -300
  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 +1893 -44
  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-QNYAWQLB.cjs +13 -0
  52. package/dist/{migrations-NIEUFG44.cjs.map → migrations-QNYAWQLB.cjs.map} +1 -1
  53. package/dist/migrations-R6NQBKQV.js +4 -0
  54. package/dist/{migrations-TGZKJKV4.js.map → migrations-R6NQBKQV.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-EYWR6UA2.js';
3
+ import { PluginService } from './chunk-YFJJU26H.js';
4
+ import { MigrationService } from './chunk-EVZOVYLO.js';
5
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-KA2PDJNB.js';
6
+ import { PluginBuilder } from './chunk-CLIH2T74.js';
7
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml, getBlocksFieldConfig, parseBlocksValue } from './chunk-7DL5SPPX.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-R6NQBKQV.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-R6NQBKQV.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-R6NQBKQV.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 || {};
@@ -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, options2);
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, options2, baseClasses, errorClasses);
4196
+ }
4197
+ return renderStructuredArrayField(field, options2);
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',
@@ -4903,79 +5944,379 @@ function renderContentFormPage(data) {
4903
5944
  hiddenInput.value = originalValue;
4904
5945
  }
4905
5946
 
4906
- // If original value was empty, hide the preview and show select button
4907
- if (!originalValue) {
4908
- const preview = document.getElementById(fieldId + '-preview');
4909
- if (preview) {
4910
- preview.classList.add('hidden');
5947
+ // If original value was empty, hide the preview and show select button
5948
+ if (!originalValue) {
5949
+ const preview = document.getElementById(fieldId + '-preview');
5950
+ if (preview) {
5951
+ preview.classList.add('hidden');
5952
+ }
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;
4911
6232
  }
4912
- }
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>
@@ -5719,49 +7059,335 @@ function renderContentListPage(data) {
5719
7059
  } else {
5720
7060
  alert('Error: ' + (data.error || 'Unknown error'));
5721
7061
  }
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 = [];
7062
+ })
7063
+ .catch(err => {
7064
+ console.error('Bulk action error:', err);
7065
+ alert('Failed to perform bulk action');
7066
+ })
7067
+ .finally(() => {
7068
+ // Clear context
7069
+ currentBulkAction = null;
7070
+ currentSelectedIds = [];
7071
+ });
7072
+ }
7073
+
7074
+ // Helper to get action text for display
7075
+ function getActionText(action) {
7076
+ const actionCount = currentSelectedIds.length;
7077
+ switch(action) {
7078
+ case 'publish':
7079
+ return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7080
+ case 'draft':
7081
+ return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
7082
+ case 'delete':
7083
+ return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7084
+ default:
7085
+ return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
7086
+ }
7087
+ }
7088
+
7089
+ </script>
7090
+
7091
+ <!-- Confirmation Dialog for Bulk Actions -->
7092
+ ${renderConfirmationDialog({
7093
+ id: "bulk-action-confirm",
7094
+ title: "Confirm Bulk Action",
7095
+ message: "Are you sure you want to perform this action? This operation will affect multiple items.",
7096
+ confirmText: "Confirm",
7097
+ cancelText: "Cancel",
7098
+ confirmClass: "bg-blue-500 hover:bg-blue-400",
7099
+ iconColor: "blue",
7100
+ onConfirm: "executeBulkAction()"
7101
+ })}
7102
+
7103
+ <!-- Confirmation Dialog Script -->
7104
+ ${getConfirmationDialogScript()}
7105
+
7106
+ <!-- Advanced Search Modal -->
7107
+ <div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
7108
+ <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
7109
+ <!-- Background overlay -->
7110
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
7111
+
7112
+ <!-- Modal panel -->
7113
+ <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">
7114
+ <div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
7115
+ <!-- Header -->
7116
+ <div class="flex items-center justify-between mb-4">
7117
+ <h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
7118
+ \u{1F50D} Advanced Search
7119
+ </h3>
7120
+ <button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
7121
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7122
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
7123
+ </svg>
7124
+ </button>
7125
+ </div>
7126
+
7127
+ <!-- Search Form -->
7128
+ <form id="advancedSearchForm" class="space-y-4">
7129
+ <!-- Search Input -->
7130
+ <div>
7131
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
7132
+ <div class="relative">
7133
+ <input
7134
+ type="text"
7135
+ id="searchQuery"
7136
+ name="query"
7137
+ placeholder="Enter your search query..."
7138
+ 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"
7139
+ autocomplete="off"
7140
+ />
7141
+ <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>
7142
+ </div>
7143
+ </div>
7144
+
7145
+ <!-- Mode Toggle -->
7146
+ <div>
7147
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
7148
+ <div class="flex gap-4">
7149
+ <label class="flex items-center">
7150
+ <input type="radio" name="mode" value="ai" checked class="mr-2">
7151
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
7152
+ </label>
7153
+ <label class="flex items-center">
7154
+ <input type="radio" name="mode" value="keyword" class="mr-2">
7155
+ <span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
7156
+ </label>
7157
+ </div>
7158
+ </div>
7159
+
7160
+ <!-- Filters -->
7161
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7162
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
7163
+
7164
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
7165
+ <!-- Collection Filter -->
7166
+ <div>
7167
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
7168
+ <select
7169
+ id="filterCollections"
7170
+ name="collections"
7171
+ multiple
7172
+ 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"
7173
+ size="4"
7174
+ >
7175
+ <option value="">All Collections</option>
7176
+ ${data.models.map(
7177
+ (model) => `
7178
+ <option value="${model.name}">${model.displayName}</option>
7179
+ `
7180
+ ).join("")}
7181
+ </select>
7182
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
7183
+ </div>
7184
+
7185
+ <!-- Status Filter -->
7186
+ <div>
7187
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
7188
+ <select
7189
+ id="filterStatus"
7190
+ name="status"
7191
+ multiple
7192
+ 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"
7193
+ size="4"
7194
+ >
7195
+ <option value="published">Published</option>
7196
+ <option value="draft">Draft</option>
7197
+ <option value="review">Under Review</option>
7198
+ <option value="scheduled">Scheduled</option>
7199
+ <option value="archived">Archived</option>
7200
+ </select>
7201
+ </div>
7202
+ </div>
7203
+ </div>
7204
+
7205
+ <!-- Actions -->
7206
+ <div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
7207
+ <button
7208
+ type="button"
7209
+ onclick="closeAdvancedSearch()"
7210
+ 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"
7211
+ >
7212
+ Cancel
7213
+ </button>
7214
+ <button
7215
+ type="submit"
7216
+ 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"
7217
+ >
7218
+ Search
7219
+ </button>
7220
+ </div>
7221
+ </form>
7222
+ </div>
7223
+
7224
+ <!-- Results Area -->
7225
+ <div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
7226
+ <div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
7227
+ <div id="searchResultsContent" class="space-y-3"></div>
7228
+ <div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
7229
+ </div>
7230
+ </div>
7231
+ </div>
7232
+ </div>
7233
+ </div>
7234
+
7235
+ <script>
7236
+ // Open modal
7237
+ function openAdvancedSearch() {
7238
+ document.getElementById('advancedSearchModal').classList.remove('hidden');
7239
+ document.getElementById('searchQuery').focus();
7240
+ }
7241
+
7242
+ // Close modal
7243
+ function closeAdvancedSearch() {
7244
+ document.getElementById('advancedSearchModal').classList.add('hidden');
7245
+ document.getElementById('searchResults').classList.add('hidden');
7246
+ }
7247
+
7248
+ // Autocomplete
7249
+ let autocompleteTimeout;
7250
+ const searchQueryInput = document.getElementById('searchQuery');
7251
+ if (searchQueryInput) {
7252
+ searchQueryInput.addEventListener('input', (e) => {
7253
+ const query = e.target.value.trim();
7254
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7255
+
7256
+ clearTimeout(autocompleteTimeout);
7257
+
7258
+ if (query.length < 2) {
7259
+ suggestionsDiv.classList.add('hidden');
7260
+ return;
7261
+ }
7262
+
7263
+ autocompleteTimeout = setTimeout(async () => {
7264
+ try {
7265
+ const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
7266
+ const { data } = await res.json();
7267
+
7268
+ if (data && data.length > 0) {
7269
+ suggestionsDiv.innerHTML = data.map(s => \`
7270
+ <div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
7271
+ \`).join('');
7272
+ suggestionsDiv.classList.remove('hidden');
7273
+ } else {
7274
+ suggestionsDiv.classList.add('hidden');
7275
+ }
7276
+ } catch (error) {
7277
+ console.error('Autocomplete error:', error);
7278
+ }
7279
+ }, 300);
7280
+ });
7281
+ }
7282
+
7283
+ function selectSuggestion(suggestion) {
7284
+ document.getElementById('searchQuery').value = suggestion;
7285
+ document.getElementById('searchSuggestions').classList.add('hidden');
7286
+ }
7287
+
7288
+ // Hide suggestions when clicking outside
7289
+ document.addEventListener('click', (e) => {
7290
+ const suggestionsDiv = document.getElementById('searchSuggestions');
7291
+ if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
7292
+ suggestionsDiv.classList.add('hidden');
7293
+ }
7294
+ });
7295
+
7296
+ // Form submission
7297
+ const advancedSearchForm = document.getElementById('advancedSearchForm');
7298
+ if (advancedSearchForm) {
7299
+ advancedSearchForm.addEventListener('submit', async (e) => {
7300
+ e.preventDefault();
7301
+
7302
+ const formData = new FormData(e.target);
7303
+ const query = formData.get('query');
7304
+ const mode = formData.get('mode') || 'ai';
7305
+
7306
+ // Build filters
7307
+ const filters = {};
7308
+
7309
+ const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
7310
+ if (collections.length > 0) {
7311
+ // Need to convert collection names to IDs - for now, pass names
7312
+ filters.collections = collections;
7313
+ }
7314
+
7315
+ const status = Array.from(formData.getAll('status'));
7316
+ if (status.length > 0) {
7317
+ filters.status = status;
7318
+ }
7319
+
7320
+ const dateStart = formData.get('date_start');
7321
+ const dateEnd = formData.get('date_end');
7322
+ if (dateStart || dateEnd) {
7323
+ filters.dateRange = {
7324
+ start: dateStart ? new Date(dateStart) : null,
7325
+ end: dateEnd ? new Date(dateEnd) : null,
7326
+ field: 'created_at'
7327
+ };
7328
+ }
7329
+
7330
+ // Execute search
7331
+ try {
7332
+ const res = await fetch('/api/search', {
7333
+ method: 'POST',
7334
+ headers: {'Content-Type': 'application/json'},
7335
+ body: JSON.stringify({
7336
+ query,
7337
+ mode,
7338
+ filters,
7339
+ limit: 20
7340
+ })
7341
+ });
7342
+
7343
+ const { data } = await res.json();
7344
+
7345
+ if (data && data.results) {
7346
+ displaySearchResults(data);
7347
+ }
7348
+ } catch (error) {
7349
+ console.error('Search error:', error);
7350
+ alert('Search failed. Please try again.');
7351
+ }
5731
7352
  });
5732
7353
  }
5733
7354
 
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' : ''}\`;
7355
+ function displaySearchResults(searchData) {
7356
+ const resultsDiv = document.getElementById('searchResultsContent');
7357
+ const resultsSection = document.getElementById('searchResults');
7358
+
7359
+ if (searchData.results.length === 0) {
7360
+ resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
7361
+ } else {
7362
+ resultsDiv.innerHTML = searchData.results.map(result => \`
7363
+ <div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
7364
+ <div class="flex items-start justify-between">
7365
+ <div class="flex-1">
7366
+ <h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
7367
+ <a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
7368
+ </h4>
7369
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
7370
+ \${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
7371
+ \${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
7372
+ </p>
7373
+ \${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
7374
+ </div>
7375
+ <div class="ml-4">
7376
+ <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>
7377
+ </div>
7378
+ </div>
7379
+ </div>
7380
+ \`).join('');
5746
7381
  }
7382
+
7383
+ resultsSection.classList.remove('hidden');
7384
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
5747
7385
  }
5748
7386
 
7387
+ // Make functions globally available
7388
+ window.openAdvancedSearch = openAdvancedSearch;
7389
+ window.closeAdvancedSearch = closeAdvancedSearch;
5749
7390
  </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
7391
  `;
5766
7392
  const layoutData = {
5767
7393
  title: "Content Management",
@@ -5965,6 +7591,122 @@ async function isPluginActive2(db, pluginId) {
5965
7591
 
5966
7592
  // src/routes/admin-content.ts
5967
7593
  var adminContentRoutes = new Hono();
7594
+ function parseFieldValue(field, formData, options = {}) {
7595
+ const { skipValidation = false } = options;
7596
+ const value = formData.get(field.field_name);
7597
+ const errors = [];
7598
+ const blocksConfig = getBlocksFieldConfig(field.field_options);
7599
+ if (blocksConfig) {
7600
+ const parsed = parseBlocksValue(value, blocksConfig);
7601
+ if (!skipValidation && field.is_required && parsed.value.length === 0) {
7602
+ parsed.errors.push(`${field.field_label} is required`);
7603
+ }
7604
+ return { value: parsed.value, errors: parsed.errors };
7605
+ }
7606
+ if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
7607
+ return { value: null, errors: [`${field.field_label} is required`] };
7608
+ }
7609
+ switch (field.field_type) {
7610
+ case "number":
7611
+ if (value && isNaN(Number(value))) {
7612
+ if (!skipValidation) {
7613
+ errors.push(`${field.field_label} must be a valid number`);
7614
+ }
7615
+ return { value: null, errors };
7616
+ }
7617
+ return { value: value ? Number(value) : null, errors: [] };
7618
+ case "boolean":
7619
+ const submitted = formData.get(`${field.field_name}_submitted`);
7620
+ return { value: submitted ? value === "true" : false, errors: [] };
7621
+ case "select":
7622
+ if (field.field_options?.multiple) {
7623
+ return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
7624
+ }
7625
+ return { value, errors: [] };
7626
+ case "array": {
7627
+ if (!value || value.toString().trim() === "") {
7628
+ if (!skipValidation && field.is_required) {
7629
+ errors.push(`${field.field_label} is required`);
7630
+ }
7631
+ return { value: [], errors };
7632
+ }
7633
+ try {
7634
+ const parsed = JSON.parse(value.toString());
7635
+ if (!Array.isArray(parsed)) {
7636
+ if (!skipValidation) {
7637
+ errors.push(`${field.field_label} must be a JSON array`);
7638
+ }
7639
+ return { value: [], errors };
7640
+ }
7641
+ if (!skipValidation && field.is_required && parsed.length === 0) {
7642
+ errors.push(`${field.field_label} is required`);
7643
+ }
7644
+ return { value: parsed, errors };
7645
+ } catch {
7646
+ if (!skipValidation) {
7647
+ errors.push(`${field.field_label} must be valid JSON`);
7648
+ }
7649
+ return { value: [], errors };
7650
+ }
7651
+ }
7652
+ case "object": {
7653
+ if (!value || value.toString().trim() === "") {
7654
+ if (!skipValidation && field.is_required) {
7655
+ errors.push(`${field.field_label} is required`);
7656
+ }
7657
+ return { value: {}, errors };
7658
+ }
7659
+ try {
7660
+ const parsed = JSON.parse(value.toString());
7661
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7662
+ if (!skipValidation) {
7663
+ errors.push(`${field.field_label} must be a JSON object`);
7664
+ }
7665
+ return { value: {}, errors };
7666
+ }
7667
+ if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
7668
+ errors.push(`${field.field_label} is required`);
7669
+ }
7670
+ return { value: parsed, errors };
7671
+ } catch {
7672
+ if (!skipValidation) {
7673
+ errors.push(`${field.field_label} must be valid JSON`);
7674
+ }
7675
+ return { value: {}, errors };
7676
+ }
7677
+ }
7678
+ case "json": {
7679
+ if (!value || value.toString().trim() === "") {
7680
+ if (!skipValidation && field.is_required) {
7681
+ errors.push(`${field.field_label} is required`);
7682
+ }
7683
+ return { value: null, errors };
7684
+ }
7685
+ try {
7686
+ return { value: JSON.parse(value.toString()), errors: [] };
7687
+ } catch {
7688
+ if (!skipValidation) {
7689
+ errors.push(`${field.field_label} must be valid JSON`);
7690
+ }
7691
+ return { value: null, errors };
7692
+ }
7693
+ }
7694
+ default:
7695
+ return { value, errors: [] };
7696
+ }
7697
+ }
7698
+ function extractFieldData(fields, formData, options = {}) {
7699
+ const data = {};
7700
+ const errors = {};
7701
+ for (const field of fields) {
7702
+ const result = parseFieldValue(field, formData, options);
7703
+ data[field.field_name] = result.value;
7704
+ if (result.errors.length > 0) {
7705
+ errors[field.field_name] = result.errors;
7706
+ }
7707
+ }
7708
+ return { data, errors };
7709
+ }
5968
7710
  adminContentRoutes.use("*", requireAuth());
5969
7711
  async function getCollectionFields(db, collectionId) {
5970
7712
  const cache = getCacheService(CACHE_CONFIGS.collection);
@@ -6438,109 +8180,7 @@ adminContentRoutes.post("/", async (c) => {
6438
8180
  `);
6439
8181
  }
6440
8182
  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
- }
8183
+ const { data, errors } = extractFieldData(fields, formData);
6544
8184
  if (Object.keys(errors).length > 0) {
6545
8185
  const formDataWithErrors = {
6546
8186
  collection,
@@ -6657,109 +8297,7 @@ adminContentRoutes.put("/:id", async (c) => {
6657
8297
  `);
6658
8298
  }
6659
8299
  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
- }
8300
+ const { data, errors } = extractFieldData(fields, formData);
6763
8301
  if (Object.keys(errors).length > 0) {
6764
8302
  const formDataWithErrors = {
6765
8303
  id,
@@ -6872,33 +8410,7 @@ adminContentRoutes.post("/preview", async (c) => {
6872
8410
  return c.html("<p>Collection not found</p>");
6873
8411
  }
6874
8412
  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
- }
8413
+ const { data } = extractFieldData(fields, formData, { skipValidation: true });
6902
8414
  const previewHTML = `
6903
8415
  <!DOCTYPE html>
6904
8416
  <html lang="en">
@@ -8245,14 +9757,87 @@ function renderUserEditPage(data) {
8245
9757
  </div>
8246
9758
  </div>
8247
9759
  </div>
9760
+ </div>
9761
+
9762
+ <!-- Profile Information -->
9763
+ <div class="mb-8">
9764
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
9765
+ <p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
9766
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9767
+ <div>
9768
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
9769
+ <input
9770
+ type="text"
9771
+ name="profile_display_name"
9772
+ value="${escapeHtml(data.userToEdit.profile?.displayName || "")}"
9773
+ placeholder="Public display name"
9774
+ 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"
9775
+ />
9776
+ </div>
9777
+
9778
+ <div>
9779
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
9780
+ <input
9781
+ type="text"
9782
+ name="profile_company"
9783
+ value="${escapeHtml(data.userToEdit.profile?.company || "")}"
9784
+ placeholder="Company or organization"
9785
+ 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"
9786
+ />
9787
+ </div>
9788
+
9789
+ <div>
9790
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
9791
+ <input
9792
+ type="text"
9793
+ name="profile_job_title"
9794
+ value="${escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
9795
+ placeholder="Job title or role"
9796
+ 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"
9797
+ />
9798
+ </div>
9799
+
9800
+ <div>
9801
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
9802
+ <input
9803
+ type="url"
9804
+ name="profile_website"
9805
+ value="${escapeHtml(data.userToEdit.profile?.website || "")}"
9806
+ placeholder="https://example.com"
9807
+ 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"
9808
+ />
9809
+ </div>
9810
+
9811
+ <div>
9812
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
9813
+ <input
9814
+ type="text"
9815
+ name="profile_location"
9816
+ value="${escapeHtml(data.userToEdit.profile?.location || "")}"
9817
+ placeholder="City, Country"
9818
+ 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"
9819
+ />
9820
+ </div>
9821
+
9822
+ <div>
9823
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
9824
+ <input
9825
+ type="date"
9826
+ name="profile_date_of_birth"
9827
+ value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
9828
+ 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"
9829
+ />
9830
+ </div>
9831
+ </div>
8248
9832
 
8249
9833
  <div class="mt-6">
8250
9834
  <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
8251
9835
  <textarea
8252
- name="bio"
9836
+ name="profile_bio"
8253
9837
  rows="3"
9838
+ placeholder="Short bio or description"
8254
9839
  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>
9840
+ >${escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
8256
9841
  </div>
8257
9842
  </div>
8258
9843
 
@@ -9786,7 +11371,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9786
11371
  const userId = c.req.param("id");
9787
11372
  try {
9788
11373
  const userStmt = db.prepare(`
9789
- SELECT id, email, username, first_name, last_name, phone, bio, avatar_url,
11374
+ SELECT id, email, username, first_name, last_name, phone, avatar_url,
9790
11375
  role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
9791
11376
  FROM users
9792
11377
  WHERE id = ?
@@ -9799,6 +11384,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
9799
11384
  dismissible: true
9800
11385
  }), 404);
9801
11386
  }
11387
+ const profileStmt = db.prepare(`
11388
+ SELECT display_name, bio, company, job_title, website, location, date_of_birth
11389
+ FROM user_profiles
11390
+ WHERE user_id = ?
11391
+ `);
11392
+ const profileData = await profileStmt.bind(userId).first();
11393
+ const profile = profileData ? {
11394
+ displayName: profileData.display_name,
11395
+ bio: profileData.bio,
11396
+ company: profileData.company,
11397
+ jobTitle: profileData.job_title,
11398
+ website: profileData.website,
11399
+ location: profileData.location,
11400
+ dateOfBirth: profileData.date_of_birth
11401
+ } : void 0;
9802
11402
  const editData = {
9803
11403
  id: userToEdit.id,
9804
11404
  email: userToEdit.email,
@@ -9806,14 +11406,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
9806
11406
  firstName: userToEdit.first_name || "",
9807
11407
  lastName: userToEdit.last_name || "",
9808
11408
  phone: userToEdit.phone,
9809
- bio: userToEdit.bio,
9810
11409
  avatarUrl: userToEdit.avatar_url,
9811
11410
  role: userToEdit.role,
9812
11411
  isActive: Boolean(userToEdit.is_active),
9813
11412
  emailVerified: Boolean(userToEdit.email_verified),
9814
11413
  twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
9815
11414
  createdAt: userToEdit.created_at,
9816
- lastLoginAt: userToEdit.last_login_at
11415
+ lastLoginAt: userToEdit.last_login_at,
11416
+ profile
9817
11417
  };
9818
11418
  const pageData = {
9819
11419
  userToEdit: editData,
@@ -9829,7 +11429,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
9829
11429
  console.error("User edit page error:", error);
9830
11430
  return c.html(renderAlert2({
9831
11431
  type: "error",
9832
- message: "Failed to load user!. Please try again.",
11432
+ message: "Failed to load user. Please try again.",
9833
11433
  dismissible: true
9834
11434
  }), 500);
9835
11435
  }
@@ -9845,10 +11445,17 @@ userRoutes.put("/users/:id", async (c) => {
9845
11445
  const username = sanitizeInput(formData.get("username")?.toString());
9846
11446
  const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
9847
11447
  const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
9848
- const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
9849
11448
  const role = formData.get("role")?.toString() || "viewer";
9850
11449
  const isActive = formData.get("is_active") === "1";
9851
11450
  const emailVerified = formData.get("email_verified") === "1";
11451
+ const profileDisplayName = sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
11452
+ const profileBio = sanitizeInput(formData.get("profile_bio")?.toString()) || null;
11453
+ const profileCompany = sanitizeInput(formData.get("profile_company")?.toString()) || null;
11454
+ const profileJobTitle = sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
11455
+ const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
11456
+ const profileLocation = sanitizeInput(formData.get("profile_location")?.toString()) || null;
11457
+ const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
11458
+ const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
9852
11459
  if (!firstName || !lastName || !username || !email) {
9853
11460
  return c.html(renderAlert2({
9854
11461
  type: "error",
@@ -9864,6 +11471,17 @@ userRoutes.put("/users/:id", async (c) => {
9864
11471
  dismissible: true
9865
11472
  }));
9866
11473
  }
11474
+ if (profileWebsite) {
11475
+ try {
11476
+ new URL(profileWebsite);
11477
+ } catch {
11478
+ return c.html(renderAlert2({
11479
+ type: "error",
11480
+ message: "Please enter a valid website URL.",
11481
+ dismissible: true
11482
+ }));
11483
+ }
11484
+ }
9867
11485
  const checkStmt = db.prepare(`
9868
11486
  SELECT id FROM users
9869
11487
  WHERE (username = ? OR email = ?) AND id != ?
@@ -9872,14 +11490,14 @@ userRoutes.put("/users/:id", async (c) => {
9872
11490
  if (existingUser) {
9873
11491
  return c.html(renderAlert2({
9874
11492
  type: "error",
9875
- message: "Username or email is already taken by another user!.",
11493
+ message: "Username or email is already taken by another user.",
9876
11494
  dismissible: true
9877
11495
  }));
9878
11496
  }
9879
11497
  const updateStmt = db.prepare(`
9880
11498
  UPDATE users SET
9881
11499
  first_name = ?, last_name = ?, username = ?, email = ?,
9882
- phone = ?, bio = ?, role = ?, is_active = ?, email_verified = ?,
11500
+ phone = ?, role = ?, is_active = ?, email_verified = ?,
9883
11501
  updated_at = ?
9884
11502
  WHERE id = ?
9885
11503
  `);
@@ -9889,20 +11507,63 @@ userRoutes.put("/users/:id", async (c) => {
9889
11507
  username,
9890
11508
  email,
9891
11509
  phone,
9892
- bio,
9893
11510
  role,
9894
11511
  isActive ? 1 : 0,
9895
11512
  emailVerified ? 1 : 0,
9896
11513
  Date.now(),
9897
11514
  userId
9898
11515
  ).run();
11516
+ const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
11517
+ if (hasProfileData) {
11518
+ const now = Date.now();
11519
+ const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
11520
+ const existingProfile = await profileCheckStmt.bind(userId).first();
11521
+ if (existingProfile) {
11522
+ const updateProfileStmt = db.prepare(`
11523
+ UPDATE user_profiles SET
11524
+ display_name = ?, bio = ?, company = ?, job_title = ?,
11525
+ website = ?, location = ?, date_of_birth = ?, updated_at = ?
11526
+ WHERE user_id = ?
11527
+ `);
11528
+ await updateProfileStmt.bind(
11529
+ profileDisplayName,
11530
+ profileBio,
11531
+ profileCompany,
11532
+ profileJobTitle,
11533
+ profileWebsite,
11534
+ profileLocation,
11535
+ profileDateOfBirth,
11536
+ now,
11537
+ userId
11538
+ ).run();
11539
+ } else {
11540
+ const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
11541
+ const insertProfileStmt = db.prepare(`
11542
+ INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
11543
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11544
+ `);
11545
+ await insertProfileStmt.bind(
11546
+ profileId,
11547
+ userId,
11548
+ profileDisplayName,
11549
+ profileBio,
11550
+ profileCompany,
11551
+ profileJobTitle,
11552
+ profileWebsite,
11553
+ profileLocation,
11554
+ profileDateOfBirth,
11555
+ now,
11556
+ now
11557
+ ).run();
11558
+ }
11559
+ }
9899
11560
  await logActivity(
9900
11561
  db,
9901
11562
  user.userId,
9902
- "user!.update",
11563
+ "user.update",
9903
11564
  "users",
9904
11565
  userId,
9905
- { fields: ["first_name", "last_name", "username", "email", "phone", "bio", "role", "is_active", "email_verified"] },
11566
+ { fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
9906
11567
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
9907
11568
  c.req.header("user-agent")
9908
11569
  );
@@ -9915,7 +11576,7 @@ userRoutes.put("/users/:id", async (c) => {
9915
11576
  console.error("User update error:", error);
9916
11577
  return c.html(renderAlert2({
9917
11578
  type: "error",
9918
- message: "Failed to update user!. Please try again.",
11579
+ message: "Failed to update user. Please try again.",
9919
11580
  dismissible: true
9920
11581
  }));
9921
11582
  }
@@ -14106,6 +15767,19 @@ var AVAILABLE_PLUGINS = [
14106
15767
  permissions: [],
14107
15768
  dependencies: [],
14108
15769
  is_core: true
15770
+ },
15771
+ {
15772
+ id: "ai-search",
15773
+ name: "ai-search-plugin",
15774
+ display_name: "AI Search",
15775
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
15776
+ version: "1.0.0",
15777
+ author: "SonicJS Team",
15778
+ category: "search",
15779
+ icon: "\u{1F50D}",
15780
+ permissions: [],
15781
+ dependencies: [],
15782
+ is_core: true
14109
15783
  }
14110
15784
  ];
14111
15785
  adminPluginRoutes.get("/", async (c) => {
@@ -14184,6 +15858,9 @@ adminPluginRoutes.get("/:id", async (c) => {
14184
15858
  const user = c.get("user");
14185
15859
  const db = c.env.DB;
14186
15860
  const pluginId = c.req.param("id");
15861
+ if (pluginId === "ai-search") {
15862
+ return c.text("", 404);
15863
+ }
14187
15864
  if (user?.role !== "admin") {
14188
15865
  return c.redirect("/admin/plugins");
14189
15866
  }
@@ -14476,6 +16153,33 @@ adminPluginRoutes.post("/install", async (c) => {
14476
16153
  });
14477
16154
  return c.json({ success: true, plugin: easyMdxPlugin2 });
14478
16155
  }
16156
+ if (body.name === "ai-search-plugin" || body.name === "ai-search") {
16157
+ const defaultSettings = {
16158
+ enabled: true,
16159
+ ai_mode_enabled: true,
16160
+ selected_collections: [],
16161
+ dismissed_collections: [],
16162
+ autocomplete_enabled: true,
16163
+ cache_duration: 1,
16164
+ results_limit: 20,
16165
+ index_media: false
16166
+ };
16167
+ const aiSearchPlugin = await pluginService.installPlugin({
16168
+ id: "ai-search",
16169
+ name: "ai-search-plugin",
16170
+ display_name: "AI Search",
16171
+ description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
16172
+ version: "1.0.0",
16173
+ author: "SonicJS Team",
16174
+ category: "search",
16175
+ icon: "\u{1F50D}",
16176
+ permissions: [],
16177
+ dependencies: [],
16178
+ is_core: true,
16179
+ settings: defaultSettings
16180
+ });
16181
+ return c.json({ success: true, plugin: aiSearchPlugin });
16182
+ }
14479
16183
  if (body.name === "turnstile-plugin") {
14480
16184
  const turnstilePlugin = await pluginService.installPlugin({
14481
16185
  id: "turnstile",
@@ -22034,6 +23738,6 @@ var ROUTES_INFO = {
22034
23738
  reference: "https://github.com/sonicjs/sonicjs"
22035
23739
  };
22036
23740
 
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
23741
+ 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, router, test_cleanup_default, userRoutes };
23742
+ //# sourceMappingURL=chunk-F6GZURXJ.js.map
23743
+ //# sourceMappingURL=chunk-F6GZURXJ.js.map