@sonicjs-cms/core 2.0.3 → 2.0.5

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 (46) hide show
  1. package/dist/{chunk-LEG4KNFP.cjs → chunk-3JMOWGUU.cjs} +20 -2
  2. package/dist/chunk-3JMOWGUU.cjs.map +1 -0
  3. package/dist/{chunk-LH4Z7QID.js → chunk-6FR25MPC.js} +111 -3
  4. package/dist/chunk-6FR25MPC.js.map +1 -0
  5. package/dist/{chunk-3NVJ6W27.cjs → chunk-DOR2IU73.cjs} +111 -2
  6. package/dist/chunk-DOR2IU73.cjs.map +1 -0
  7. package/dist/{chunk-M6FPVS7E.js → chunk-G5KY3WJV.js} +16 -29
  8. package/dist/chunk-G5KY3WJV.js.map +1 -0
  9. package/dist/{chunk-CDBVZEWR.js → chunk-HSRPDEQQ.js} +20 -2
  10. package/dist/chunk-HSRPDEQQ.js.map +1 -0
  11. package/dist/{chunk-4BJGEGX5.cjs → chunk-IM5SDXOE.cjs} +19 -32
  12. package/dist/chunk-IM5SDXOE.cjs.map +1 -0
  13. package/dist/{chunk-PPUKPNTP.js → chunk-LGC3TNCY.js} +293 -101
  14. package/dist/chunk-LGC3TNCY.js.map +1 -0
  15. package/dist/{chunk-5B3VMVEX.cjs → chunk-NPWWR6RI.cjs} +400 -208
  16. package/dist/chunk-NPWWR6RI.cjs.map +1 -0
  17. package/dist/{chunk-UL32L2KV.cjs → chunk-TRSHFTF6.cjs} +123 -3
  18. package/dist/chunk-TRSHFTF6.cjs.map +1 -0
  19. package/dist/{chunk-XJETEIRU.js → chunk-VSLEA22M.js} +123 -4
  20. package/dist/chunk-VSLEA22M.js.map +1 -0
  21. package/dist/index.cjs +876 -131
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +759 -13
  24. package/dist/index.js.map +1 -1
  25. package/dist/middleware.cjs +23 -23
  26. package/dist/middleware.js +2 -2
  27. package/dist/routes.cjs +25 -25
  28. package/dist/routes.js +5 -5
  29. package/dist/services.cjs +25 -21
  30. package/dist/services.js +2 -2
  31. package/dist/utils.cjs +11 -11
  32. package/dist/utils.js +1 -1
  33. package/migrations/006_plugin_system.sql +2 -2
  34. package/migrations/011_config_managed_collections.sql +1 -0
  35. package/migrations/018_settings_table.sql +23 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-3NVJ6W27.cjs.map +0 -1
  38. package/dist/chunk-4BJGEGX5.cjs.map +0 -1
  39. package/dist/chunk-5B3VMVEX.cjs.map +0 -1
  40. package/dist/chunk-CDBVZEWR.js.map +0 -1
  41. package/dist/chunk-LEG4KNFP.cjs.map +0 -1
  42. package/dist/chunk-LH4Z7QID.js.map +0 -1
  43. package/dist/chunk-M6FPVS7E.js.map +0 -1
  44. package/dist/chunk-PPUKPNTP.js.map +0 -1
  45. package/dist/chunk-UL32L2KV.cjs.map +0 -1
  46. package/dist/chunk-XJETEIRU.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { getCacheService, CACHE_CONFIGS, getLogger } from './chunk-LH4Z7QID.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-M6FPVS7E.js';
3
- import { PluginService, MigrationService } from './chunk-CDBVZEWR.js';
1
+ import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-6FR25MPC.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-G5KY3WJV.js';
3
+ import { PluginService, MigrationService } from './chunk-HSRPDEQQ.js';
4
4
  import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-3LZ6TLPC.js';
5
- import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-XJETEIRU.js';
5
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-VSLEA22M.js';
6
6
  import { metricsTracker } from './chunk-FICTAGD4.js';
7
7
  import { Hono } from 'hono';
8
8
  import { cors } from 'hono/cors';
@@ -78,7 +78,7 @@ apiContentCrudRoutes.post("/", requireAuth(), async (c) => {
78
78
  title,
79
79
  JSON.stringify(data || {}),
80
80
  status || "draft",
81
- user?.userId || "unknown",
81
+ user?.userId || "system",
82
82
  now,
83
83
  now
84
84
  ).run();
@@ -671,6 +671,7 @@ apiMediaRoutes.post("/upload", async (c) => {
671
671
  size: mediaRecord.size,
672
672
  width: mediaRecord.width,
673
673
  height: mediaRecord.height,
674
+ r2_key: mediaRecord.r2_key,
674
675
  publicUrl: mediaRecord.public_url,
675
676
  thumbnailUrl: mediaRecord.thumbnail_url,
676
677
  uploadedAt: new Date(mediaRecord.uploaded_at * 1e3).toISOString()
@@ -797,6 +798,7 @@ apiMediaRoutes.post("/upload-multiple", async (c) => {
797
798
  size: mediaRecord.size,
798
799
  width: mediaRecord.width,
799
800
  height: mediaRecord.height,
801
+ r2_key: mediaRecord.r2_key,
800
802
  publicUrl: mediaRecord.public_url,
801
803
  thumbnailUrl: mediaRecord.thumbnail_url,
802
804
  uploadedAt: new Date(mediaRecord.uploaded_at * 1e3).toISOString()
@@ -916,10 +918,17 @@ apiMediaRoutes.post("/create-folder", async (c) => {
916
918
  }
917
919
  const checkStmt = c.env.DB.prepare("SELECT COUNT(*) as count FROM media WHERE folder = ? AND deleted_at IS NULL");
918
920
  const existingFolder = await checkStmt.bind(folderName).first();
921
+ if (existingFolder && existingFolder.count > 0) {
922
+ return c.json({
923
+ success: false,
924
+ error: `Folder "${folderName}" already exists`
925
+ }, 400);
926
+ }
919
927
  return c.json({
920
928
  success: true,
921
- message: `Folder "${folderName}" created successfully`,
922
- folder: folderName
929
+ message: `Folder "${folderName}" is ready. Upload files to this folder to make it appear in the media library.`,
930
+ folder: folderName,
931
+ note: "Folders appear automatically when you upload files to them"
923
932
  });
924
933
  } catch (error) {
925
934
  console.error("Create folder error:", error);
@@ -1433,8 +1442,12 @@ adminApiRoutes.get("/activity", async (c) => {
1433
1442
  });
1434
1443
  var createCollectionSchema = z.object({
1435
1444
  name: z.string().min(1).max(255).regex(/^[a-z0-9_]+$/, "Must contain only lowercase letters, numbers, and underscores"),
1436
- display_name: z.string().min(1).max(255),
1445
+ displayName: z.string().min(1).max(255).optional(),
1446
+ display_name: z.string().min(1).max(255).optional(),
1437
1447
  description: z.string().optional()
1448
+ }).refine((data) => data.displayName || data.display_name, {
1449
+ message: "Either displayName or display_name is required",
1450
+ path: ["displayName"]
1438
1451
  });
1439
1452
  var updateCollectionSchema = z.object({
1440
1453
  display_name: z.string().min(1).max(255).optional(),
@@ -1521,15 +1534,16 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1521
1534
  updated_at: Number(row.updated_at)
1522
1535
  }));
1523
1536
  return c.json({
1524
- data: {
1525
- ...collection,
1526
- is_active: collection.is_active === 1,
1527
- managed: collection.managed === 1,
1528
- schema: collection.schema ? JSON.parse(collection.schema) : null,
1529
- created_at: Number(collection.created_at),
1530
- updated_at: Number(collection.updated_at),
1531
- fields
1532
- }
1537
+ id: collection.id,
1538
+ name: collection.name,
1539
+ display_name: collection.display_name,
1540
+ description: collection.description,
1541
+ is_active: collection.is_active === 1,
1542
+ managed: collection.managed === 1,
1543
+ schema: collection.schema ? JSON.parse(collection.schema) : null,
1544
+ created_at: Number(collection.created_at),
1545
+ updated_at: Number(collection.updated_at),
1546
+ fields
1533
1547
  });
1534
1548
  } catch (error) {
1535
1549
  console.error("Error fetching collection:", error);
@@ -1538,7 +1552,16 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1538
1552
  });
1539
1553
  adminApiRoutes.post("/collections", async (c) => {
1540
1554
  try {
1541
- const body = await c.req.json();
1555
+ const contentType = c.req.header("Content-Type");
1556
+ if (!contentType || !contentType.includes("application/json")) {
1557
+ return c.json({ error: "Content-Type must be application/json" }, 400);
1558
+ }
1559
+ let body;
1560
+ try {
1561
+ body = await c.req.json();
1562
+ } catch (e) {
1563
+ return c.json({ error: "Invalid JSON in request body" }, 400);
1564
+ }
1542
1565
  const validation = createCollectionSchema.safeParse(body);
1543
1566
  if (!validation.success) {
1544
1567
  return c.json({ error: "Validation failed", details: validation.error.errors }, 400);
@@ -1546,6 +1569,7 @@ adminApiRoutes.post("/collections", async (c) => {
1546
1569
  const validatedData = validation.data;
1547
1570
  const db = c.env.DB;
1548
1571
  const user = c.get("user");
1572
+ const displayName = validatedData.displayName || validatedData.display_name || "";
1549
1573
  const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
1550
1574
  const existing = await existingStmt.bind(validatedData.name).first();
1551
1575
  if (existing) {
@@ -1582,7 +1606,7 @@ adminApiRoutes.post("/collections", async (c) => {
1582
1606
  await insertStmt.bind(
1583
1607
  collectionId,
1584
1608
  validatedData.name,
1585
- validatedData.display_name,
1609
+ displayName,
1586
1610
  validatedData.description || null,
1587
1611
  JSON.stringify(basicSchema),
1588
1612
  1,
@@ -1597,13 +1621,11 @@ adminApiRoutes.post("/collections", async (c) => {
1597
1621
  console.error("Error clearing cache:", e);
1598
1622
  }
1599
1623
  return c.json({
1600
- data: {
1601
- id: collectionId,
1602
- name: validatedData.name,
1603
- display_name: validatedData.display_name,
1604
- description: validatedData.description,
1605
- created_at: now
1606
- }
1624
+ id: collectionId,
1625
+ name: validatedData.name,
1626
+ displayName,
1627
+ description: validatedData.description,
1628
+ created_at: now
1607
1629
  }, 201);
1608
1630
  } catch (error) {
1609
1631
  console.error("Error creating collection:", error);
@@ -1667,6 +1689,11 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1667
1689
  try {
1668
1690
  const id = c.req.param("id");
1669
1691
  const db = c.env.DB;
1692
+ const collectionStmt = db.prepare("SELECT name FROM collections WHERE id = ?");
1693
+ const collection = await collectionStmt.bind(id).first();
1694
+ if (!collection) {
1695
+ return c.json({ error: "Collection not found" }, 404);
1696
+ }
1670
1697
  const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE collection_id = ?");
1671
1698
  const contentResult = await contentStmt.bind(id).first();
1672
1699
  if (contentResult && contentResult.count > 0) {
@@ -1674,17 +1701,13 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1674
1701
  error: `Cannot delete collection: it contains ${contentResult.count} content item(s). Delete all content first.`
1675
1702
  }, 400);
1676
1703
  }
1677
- const collectionStmt = db.prepare("SELECT name FROM collections WHERE id = ?");
1678
- const collection = await collectionStmt.bind(id).first();
1679
1704
  const deleteFieldsStmt = db.prepare("DELETE FROM content_fields WHERE collection_id = ?");
1680
1705
  await deleteFieldsStmt.bind(id).run();
1681
1706
  const deleteStmt = db.prepare("DELETE FROM collections WHERE id = ?");
1682
1707
  await deleteStmt.bind(id).run();
1683
1708
  try {
1684
1709
  await c.env.CACHE_KV.delete("cache:collections:all");
1685
- if (collection) {
1686
- await c.env.CACHE_KV.delete(`cache:collection:${collection.name}`);
1687
- }
1710
+ await c.env.CACHE_KV.delete(`cache:collection:${collection.name}`);
1688
1711
  } catch (e) {
1689
1712
  console.error("Error clearing cache:", e);
1690
1713
  }
@@ -2344,7 +2367,7 @@ authRoutes.post("/register/form", async (c) => {
2344
2367
  Account created successfully! Redirecting to admin dashboard...
2345
2368
  <script>
2346
2369
  setTimeout(() => {
2347
- window.location.href = '/admin/content';
2370
+ window.location.href = '/admin/dashboard';
2348
2371
  }, 2000);
2349
2372
  </script>
2350
2373
  </div>
@@ -2412,7 +2435,7 @@ authRoutes.post("/login/form", async (c) => {
2412
2435
  </div>
2413
2436
  <script>
2414
2437
  setTimeout(() => {
2415
- window.location.href = '/admin/content';
2438
+ window.location.href = '/admin/dashboard';
2416
2439
  }, 2000);
2417
2440
  </script>
2418
2441
  </div>
@@ -2705,7 +2728,7 @@ authRoutes.post("/accept-invitation", async (c) => {
2705
2728
  maxAge: 60 * 60 * 24
2706
2729
  // 24 hours
2707
2730
  });
2708
- return c.redirect("/admin/content?welcome=true");
2731
+ return c.redirect("/admin/dashboard?welcome=true");
2709
2732
  } catch (error) {
2710
2733
  console.error("Accept invitation error:", error);
2711
2734
  return c.json({ error: "Failed to accept invitation" }, 500);
@@ -5183,11 +5206,11 @@ adminContentRoutes.post("/", async (c) => {
5183
5206
  const now = Date.now();
5184
5207
  const insertStmt = db.prepare(`
5185
5208
  INSERT INTO content (
5186
- id, collection_id, slug, title, data, status,
5209
+ id, collection_id, slug, title, data, status,
5187
5210
  scheduled_publish_at, scheduled_unpublish_at,
5188
- meta_title, meta_description, author_id, created_at, updated_at
5211
+ meta_title, meta_description, author_id, created_by, created_at, updated_at
5189
5212
  )
5190
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5213
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5191
5214
  `);
5192
5215
  await insertStmt.bind(
5193
5216
  contentId,
@@ -5201,6 +5224,7 @@ adminContentRoutes.post("/", async (c) => {
5201
5224
  data.meta_title || null,
5202
5225
  data.meta_description || null,
5203
5226
  user?.userId || "unknown",
5227
+ user?.userId || "unknown",
5204
5228
  now,
5205
5229
  now
5206
5230
  ).run();
@@ -5502,10 +5526,10 @@ adminContentRoutes.post("/duplicate", async (c) => {
5502
5526
  originalData.title = `${originalData.title || "Untitled"} (Copy)`;
5503
5527
  const insertStmt = db.prepare(`
5504
5528
  INSERT INTO content (
5505
- id, collection_id, slug, title, data, status,
5506
- author_id, created_at, updated_at
5529
+ id, collection_id, slug, title, data, status,
5530
+ author_id, created_by, created_at, updated_at
5507
5531
  )
5508
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5532
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5509
5533
  `);
5510
5534
  await insertStmt.bind(
5511
5535
  newId,
@@ -5516,6 +5540,7 @@ adminContentRoutes.post("/duplicate", async (c) => {
5516
5540
  "draft",
5517
5541
  // Always start as draft
5518
5542
  user?.userId || "unknown",
5543
+ user?.userId || "unknown",
5519
5544
  now,
5520
5545
  now
5521
5546
  ).run();
@@ -5866,6 +5891,11 @@ var admin_content_default = adminContentRoutes;
5866
5891
 
5867
5892
  // src/templates/pages/admin-profile.template.ts
5868
5893
  init_admin_layout_catalyst_template();
5894
+ function renderAvatarImage(avatarUrl, firstName, lastName) {
5895
+ return `<div id="avatar-image-container" class="w-24 h-24 rounded-full mx-auto mb-4 overflow-hidden bg-gradient-to-br from-cyan-400 to-purple-400 flex items-center justify-center ring-4 ring-zinc-950/5 dark:ring-white/10">
5896
+ ${avatarUrl ? `<img src="${avatarUrl}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${firstName.charAt(0)}${lastName.charAt(0)}</span>`}
5897
+ </div>`;
5898
+ }
5869
5899
  function renderProfilePage(data) {
5870
5900
  const pageContent = `
5871
5901
  <div class="space-y-8">
@@ -6064,9 +6094,7 @@ function renderProfilePage(data) {
6064
6094
  <h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Picture</h3>
6065
6095
 
6066
6096
  <div class="text-center">
6067
- <div class="w-24 h-24 rounded-full mx-auto mb-4 overflow-hidden bg-gradient-to-br from-cyan-400 to-purple-400 flex items-center justify-center ring-4 ring-zinc-950/5 dark:ring-white/10">
6068
- ${data.profile.avatar_url ? `<img src="${data.profile.avatar_url}" alt="Profile picture" class="w-full h-full object-cover">` : `<span class="text-2xl font-bold text-white">${data.profile.first_name.charAt(0)}${data.profile.last_name.charAt(0)}</span>`}
6069
- </div>
6097
+ ${renderAvatarImage(data.profile.avatar_url, data.profile.first_name, data.profile.last_name)}
6070
6098
 
6071
6099
  <form id="avatar-form" hx-post="/admin/profile/avatar" hx-target="#avatar-messages" hx-encoding="multipart/form-data">
6072
6100
  <input
@@ -7902,6 +7930,10 @@ userRoutes.post("/profile/avatar", async (c) => {
7902
7930
  WHERE id = ?
7903
7931
  `);
7904
7932
  await updateStmt.bind(avatarUrl, Date.now(), user.userId).run();
7933
+ const userStmt = db.prepare(`
7934
+ SELECT first_name, last_name FROM users WHERE id = ?
7935
+ `);
7936
+ const userData = await userStmt.bind(user.userId).first();
7905
7937
  await logActivity(
7906
7938
  db,
7907
7939
  user.userId,
@@ -7912,11 +7944,18 @@ userRoutes.post("/profile/avatar", async (c) => {
7912
7944
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
7913
7945
  c.req.header("user-agent")
7914
7946
  );
7915
- return c.html(renderAlert2({
7947
+ const alertHtml = renderAlert2({
7916
7948
  type: "success",
7917
7949
  message: "Profile picture updated successfully!",
7918
7950
  dismissible: true
7919
- }));
7951
+ });
7952
+ const avatarUrlWithCache = `${avatarUrl}?t=${Date.now()}`;
7953
+ const avatarImageHtml = renderAvatarImage(avatarUrlWithCache, userData.first_name, userData.last_name);
7954
+ const avatarImageWithOob = avatarImageHtml.replace(
7955
+ 'id="avatar-image-container"',
7956
+ 'id="avatar-image-container" hx-swap-oob="true"'
7957
+ );
7958
+ return c.html(alertHtml + avatarImageWithOob);
7920
7959
  } catch (error) {
7921
7960
  console.error("Avatar upload error:", error);
7922
7961
  return c.html(renderAlert2({
@@ -9148,8 +9187,10 @@ function renderMediaLibraryPage(data) {
9148
9187
  </button>
9149
9188
  <button
9150
9189
  class="w-full text-left px-3 py-2 text-sm text-zinc-700 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-white hover:bg-zinc-50 dark:hover:bg-zinc-800/50 rounded-lg transition-colors"
9151
- hx-delete="/media/cleanup"
9190
+ hx-delete="/admin/media/cleanup"
9152
9191
  hx-confirm="Delete unused files?"
9192
+ hx-target="body"
9193
+ hx-swap="beforeend"
9153
9194
  >
9154
9195
  Cleanup Unused
9155
9196
  </button>
@@ -9329,11 +9370,12 @@ function renderMediaLibraryPage(data) {
9329
9370
  </div>
9330
9371
 
9331
9372
  <!-- Upload Form -->
9332
- <form
9373
+ <form
9333
9374
  id="upload-form"
9334
9375
  hx-post="/admin/media/upload"
9335
9376
  hx-encoding="multipart/form-data"
9336
9377
  hx-target="#upload-results"
9378
+ hx-on::after-request="if(event.detail.successful) { setTimeout(() => { window.location.href = '/admin/media?t=' + Date.now(); }, 1500); }"
9337
9379
  class="space-y-4"
9338
9380
  >
9339
9381
  <!-- Drag and Drop Zone -->
@@ -10216,21 +10258,23 @@ adminMediaRoutes.get("/", async (c) => {
10216
10258
  const { results } = await stmt.bind(...params).all();
10217
10259
  const foldersStmt = db.prepare(`
10218
10260
  SELECT folder, COUNT(*) as count, SUM(size) as totalSize
10219
- FROM media
10220
- GROUP BY folder
10261
+ FROM media
10262
+ WHERE deleted_at IS NULL
10263
+ GROUP BY folder
10221
10264
  ORDER BY folder
10222
10265
  `);
10223
10266
  const { results: folders } = await foldersStmt.all();
10224
10267
  const typesStmt = db.prepare(`
10225
- SELECT
10226
- CASE
10268
+ SELECT
10269
+ CASE
10227
10270
  WHEN mime_type LIKE 'image/%' THEN 'images'
10228
10271
  WHEN mime_type LIKE 'video/%' THEN 'videos'
10229
10272
  WHEN mime_type IN ('application/pdf', 'text/plain') THEN 'documents'
10230
10273
  ELSE 'other'
10231
10274
  END as type,
10232
10275
  COUNT(*) as count
10233
- FROM media
10276
+ FROM media
10277
+ WHERE deleted_at IS NULL
10234
10278
  GROUP BY type
10235
10279
  `);
10236
10280
  const { results: types } = await typesStmt.all();
@@ -10506,6 +10550,18 @@ adminMediaRoutes.post("/upload", async (c) => {
10506
10550
  }
10507
10551
  const uploadResults = [];
10508
10552
  const errors = [];
10553
+ console.log("[MEDIA UPLOAD] c.env keys:", Object.keys(c.env));
10554
+ console.log("[MEDIA UPLOAD] MEDIA_BUCKET defined?", !!c.env.MEDIA_BUCKET);
10555
+ console.log("[MEDIA UPLOAD] MEDIA_BUCKET type:", typeof c.env.MEDIA_BUCKET);
10556
+ if (!c.env.MEDIA_BUCKET) {
10557
+ console.error("[MEDIA UPLOAD] MEDIA_BUCKET is not available! Available env keys:", Object.keys(c.env));
10558
+ return c.html(html`
10559
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
10560
+ Media storage (R2) is not configured. Please check your wrangler.toml configuration.
10561
+ <br><small>Debug: Available bindings: ${Object.keys(c.env).join(", ")}</small>
10562
+ </div>
10563
+ `);
10564
+ }
10509
10565
  for (const file of files) {
10510
10566
  try {
10511
10567
  const validation = fileValidationSchema2.safeParse({
@@ -10737,6 +10793,87 @@ adminMediaRoutes.put("/:id", async (c) => {
10737
10793
  `);
10738
10794
  }
10739
10795
  });
10796
+ adminMediaRoutes.delete("/cleanup", requireRole("admin"), async (c) => {
10797
+ try {
10798
+ const db = c.env.DB;
10799
+ const allMediaStmt = db.prepare("SELECT id, r2_key, filename FROM media WHERE deleted_at IS NULL");
10800
+ const { results: allMedia } = await allMediaStmt.all();
10801
+ const contentStmt = db.prepare("SELECT data FROM content");
10802
+ const { results: contentRecords } = await contentStmt.all();
10803
+ const referencedUrls = /* @__PURE__ */ new Set();
10804
+ for (const record of contentRecords) {
10805
+ if (record.data) {
10806
+ const dataStr = typeof record.data === "string" ? record.data : JSON.stringify(record.data);
10807
+ const urlMatches = dataStr.matchAll(/\/files\/([^\s"',]+)/g);
10808
+ for (const match of urlMatches) {
10809
+ referencedUrls.add(match[1]);
10810
+ }
10811
+ }
10812
+ }
10813
+ const unusedFiles = allMedia.filter((file) => !referencedUrls.has(file.r2_key));
10814
+ if (unusedFiles.length === 0) {
10815
+ return c.html(html`
10816
+ <div class="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded">
10817
+ No unused media files found. All files are referenced in content.
10818
+ </div>
10819
+ <script>
10820
+ setTimeout(() => {
10821
+ window.location.href = '/admin/media?t=' + Date.now();
10822
+ }, 2000);
10823
+ </script>
10824
+ `);
10825
+ }
10826
+ let deletedCount = 0;
10827
+ const errors = [];
10828
+ for (const file of unusedFiles) {
10829
+ try {
10830
+ await c.env.MEDIA_BUCKET.delete(file.r2_key);
10831
+ const deleteStmt = db.prepare("UPDATE media SET deleted_at = ? WHERE id = ?");
10832
+ await deleteStmt.bind(Math.floor(Date.now() / 1e3), file.id).run();
10833
+ deletedCount++;
10834
+ } catch (error) {
10835
+ console.error(`Failed to delete ${file.filename}:`, error);
10836
+ errors.push({
10837
+ filename: file.filename,
10838
+ error: error instanceof Error ? error.message : "Unknown error"
10839
+ });
10840
+ }
10841
+ }
10842
+ return c.html(html`
10843
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
10844
+ Successfully cleaned up ${deletedCount} unused media file${deletedCount !== 1 ? "s" : ""}.
10845
+ ${errors.length > 0 ? html`
10846
+ <br><span class="text-sm">Failed to delete ${errors.length} file${errors.length !== 1 ? "s" : ""}.</span>
10847
+ ` : ""}
10848
+ </div>
10849
+
10850
+ ${errors.length > 0 ? html`
10851
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
10852
+ <p class="font-medium">Cleanup errors:</p>
10853
+ <ul class="list-disc list-inside mt-2 text-sm">
10854
+ ${errors.map((error) => html`
10855
+ <li>${error.filename}: ${error.error}</li>
10856
+ `)}
10857
+ </ul>
10858
+ </div>
10859
+ ` : ""}
10860
+
10861
+ <script>
10862
+ // Refresh media library after cleanup
10863
+ setTimeout(() => {
10864
+ window.location.href = '/admin/media?t=' + Date.now();
10865
+ }, 2500);
10866
+ </script>
10867
+ `);
10868
+ } catch (error) {
10869
+ console.error("Cleanup error:", error);
10870
+ return c.html(html`
10871
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
10872
+ Cleanup failed: ${error instanceof Error ? error.message : "Unknown error"}
10873
+ </div>
10874
+ `);
10875
+ }
10876
+ });
10740
10877
  adminMediaRoutes.delete("/:id", async (c) => {
10741
10878
  try {
10742
10879
  const user = c.get("user");
@@ -16276,7 +16413,7 @@ router.get("/stats", async (c) => {
16276
16413
  }
16277
16414
  let contentCount = 0;
16278
16415
  try {
16279
- const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE deleted_at IS NULL");
16416
+ const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content");
16280
16417
  const contentResult = await contentStmt.first();
16281
16418
  contentCount = contentResult?.count || 0;
16282
16419
  } catch (error) {
@@ -16300,14 +16437,14 @@ router.get("/stats", async (c) => {
16300
16437
  } catch (error) {
16301
16438
  console.error("Error fetching users count:", error);
16302
16439
  }
16303
- const html9 = renderStatsCards({
16440
+ const html8 = renderStatsCards({
16304
16441
  collections: collectionsCount,
16305
16442
  contentItems: contentCount,
16306
16443
  mediaFiles: mediaCount,
16307
16444
  users: usersCount,
16308
16445
  mediaSize
16309
16446
  });
16310
- return c.html(html9);
16447
+ return c.html(html8);
16311
16448
  } catch (error) {
16312
16449
  console.error("Error fetching stats:", error);
16313
16450
  return c.html('<div class="text-red-500">Failed to load statistics</div>');
@@ -16331,8 +16468,8 @@ router.get("/storage", async (c) => {
16331
16468
  } catch (error) {
16332
16469
  console.error("Error fetching media size:", error);
16333
16470
  }
16334
- const html9 = renderStorageUsage(databaseSize, mediaSize);
16335
- return c.html(html9);
16471
+ const html8 = renderStorageUsage(databaseSize, mediaSize);
16472
+ return c.html(html8);
16336
16473
  } catch (error) {
16337
16474
  console.error("Error fetching storage usage:", error);
16338
16475
  return c.html('<div class="text-red-500">Failed to load storage information</div>');
@@ -16381,12 +16518,12 @@ router.get("/recent-activity", async (c) => {
16381
16518
  user: userName
16382
16519
  };
16383
16520
  });
16384
- const html9 = renderRecentActivity(activities);
16385
- return c.html(html9);
16521
+ const html8 = renderRecentActivity(activities);
16522
+ return c.html(html8);
16386
16523
  } catch (error) {
16387
16524
  console.error("Error fetching recent activity:", error);
16388
- const html9 = renderRecentActivity([]);
16389
- return c.html(html9);
16525
+ const html8 = renderRecentActivity([]);
16526
+ return c.html(html8);
16390
16527
  }
16391
16528
  });
16392
16529
  router.get("/api/metrics", async (c) => {
@@ -16399,7 +16536,7 @@ router.get("/api/metrics", async (c) => {
16399
16536
  });
16400
16537
  router.get("/system-status", async (c) => {
16401
16538
  try {
16402
- const html9 = `
16539
+ const html8 = `
16403
16540
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
16404
16541
  <div class="relative group">
16405
16542
  <div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 dark:from-blue-500/10 dark:to-cyan-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
@@ -16454,7 +16591,7 @@ router.get("/system-status", async (c) => {
16454
16591
  </div>
16455
16592
  </div>
16456
16593
  `;
16457
- return c.html(html9);
16594
+ return c.html(html8);
16458
16595
  } catch (error) {
16459
16596
  console.error("Error fetching system status:", error);
16460
16597
  return c.html('<div class="text-red-500">Failed to load system status</div>');
@@ -17872,11 +18009,13 @@ adminCollectionsRoutes.post("/", async (c) => {
17872
18009
  now,
17873
18010
  now
17874
18011
  ).run();
17875
- try {
17876
- await c.env.CACHE_KV.delete("cache:collections:all");
17877
- await c.env.CACHE_KV.delete(`cache:collection:${name}`);
17878
- } catch (e) {
17879
- console.error("Error clearing cache:", e);
18012
+ if (c.env.CACHE_KV) {
18013
+ try {
18014
+ await c.env.CACHE_KV.delete("cache:collections:all");
18015
+ await c.env.CACHE_KV.delete(`cache:collection:${name}`);
18016
+ } catch (e) {
18017
+ console.error("Error clearing cache:", e);
18018
+ }
17880
18019
  }
17881
18020
  if (isHtmx) {
17882
18021
  return c.html(html`
@@ -18208,31 +18347,52 @@ function renderSettingsPage(data) {
18208
18347
  // Initialize tab-specific features on page load
18209
18348
  const currentTab = '${activeTab}';
18210
18349
 
18211
- function saveAllSettings() {
18350
+ async function saveAllSettings() {
18212
18351
  // Collect all form data
18213
18352
  const formData = new FormData();
18214
-
18215
- // Get all form inputs
18216
- document.querySelectorAll('input, select, textarea').forEach(input => {
18353
+
18354
+ // Get all form inputs in the settings content area
18355
+ document.querySelectorAll('#settings-content input, #settings-content select, #settings-content textarea').forEach(input => {
18217
18356
  if (input.type === 'checkbox') {
18218
- formData.append(input.name, input.checked);
18357
+ formData.append(input.name, input.checked ? 'true' : 'false');
18219
18358
  } else if (input.name) {
18220
18359
  formData.append(input.name, input.value);
18221
18360
  }
18222
18361
  });
18223
-
18362
+
18224
18363
  // Show loading state
18225
18364
  const saveBtn = document.querySelector('button[onclick="saveAllSettings()"]');
18226
18365
  const originalText = saveBtn.innerHTML;
18227
- saveBtn.innerHTML = 'Saving...';
18366
+ saveBtn.innerHTML = '<svg class="animate-spin -ml-0.5 mr-1.5 h-5 w-5 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Saving...';
18228
18367
  saveBtn.disabled = true;
18229
-
18230
- // Simulate save (replace with actual API call)
18231
- setTimeout(() => {
18368
+
18369
+ try {
18370
+ // Determine which endpoint to call based on current tab
18371
+ let endpoint = '/admin/settings/general';
18372
+ if (currentTab === 'general') {
18373
+ endpoint = '/admin/settings/general';
18374
+ }
18375
+ // Add more endpoints for other tabs when implemented
18376
+
18377
+ const response = await fetch(endpoint, {
18378
+ method: 'POST',
18379
+ body: formData
18380
+ });
18381
+
18382
+ const result = await response.json();
18383
+
18384
+ if (result.success) {
18385
+ showNotification(result.message || 'Settings saved successfully!', 'success');
18386
+ } else {
18387
+ showNotification(result.error || 'Failed to save settings', 'error');
18388
+ }
18389
+ } catch (error) {
18390
+ console.error('Error saving settings:', error);
18391
+ showNotification('Failed to save settings. Please try again.', 'error');
18392
+ } finally {
18232
18393
  saveBtn.innerHTML = originalText;
18233
18394
  saveBtn.disabled = false;
18234
- showNotification('Settings saved successfully!', 'success');
18235
- }, 1000);
18395
+ }
18236
18396
  }
18237
18397
 
18238
18398
  function resetSettings() {
@@ -19637,15 +19797,20 @@ function getMockSettings(user) {
19637
19797
  adminSettingsRoutes.get("/", (c) => {
19638
19798
  return c.redirect("/admin/settings/general");
19639
19799
  });
19640
- adminSettingsRoutes.get("/general", (c) => {
19800
+ adminSettingsRoutes.get("/general", async (c) => {
19641
19801
  const user = c.get("user");
19802
+ const db = c.env.DB;
19803
+ const settingsService = new SettingsService(db);
19804
+ const generalSettings = await settingsService.getGeneralSettings(user?.email);
19805
+ const mockSettings = getMockSettings(user);
19806
+ mockSettings.general = generalSettings;
19642
19807
  const pageData = {
19643
19808
  user: user ? {
19644
19809
  name: user.email,
19645
19810
  email: user.email,
19646
19811
  role: user.role
19647
19812
  } : void 0,
19648
- settings: getMockSettings(user),
19813
+ settings: mockSettings,
19649
19814
  activeTab: "general",
19650
19815
  version: c.get("appVersion")
19651
19816
  };
@@ -19926,28 +20091,55 @@ adminSettingsRoutes.post("/api/database-tools/truncate", async (c) => {
19926
20091
  }, 500);
19927
20092
  }
19928
20093
  });
19929
- adminSettingsRoutes.post("/", async (c) => {
20094
+ adminSettingsRoutes.post("/general", async (c) => {
19930
20095
  try {
20096
+ const user = c.get("user");
20097
+ if (!user || user.role !== "admin") {
20098
+ return c.json({
20099
+ success: false,
20100
+ error: "Unauthorized. Admin access required."
20101
+ }, 403);
20102
+ }
19931
20103
  const formData = await c.req.formData();
19932
- return c.html(html`
19933
- <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
19934
- Settings saved successfully!
19935
- <script>
19936
- setTimeout(() => {
19937
- showNotification('Settings saved successfully!', 'success');
19938
- }, 100);
19939
- </script>
19940
- </div>
19941
- `);
20104
+ const db = c.env.DB;
20105
+ const settingsService = new SettingsService(db);
20106
+ const settings = {
20107
+ siteName: formData.get("siteName"),
20108
+ siteDescription: formData.get("siteDescription"),
20109
+ adminEmail: formData.get("adminEmail"),
20110
+ timezone: formData.get("timezone"),
20111
+ language: formData.get("language"),
20112
+ maintenanceMode: formData.get("maintenanceMode") === "true"
20113
+ };
20114
+ if (!settings.siteName || !settings.siteDescription) {
20115
+ return c.json({
20116
+ success: false,
20117
+ error: "Site name and description are required"
20118
+ }, 400);
20119
+ }
20120
+ const success = await settingsService.saveGeneralSettings(settings);
20121
+ if (success) {
20122
+ return c.json({
20123
+ success: true,
20124
+ message: "General settings saved successfully!"
20125
+ });
20126
+ } else {
20127
+ return c.json({
20128
+ success: false,
20129
+ error: "Failed to save settings"
20130
+ }, 500);
20131
+ }
19942
20132
  } catch (error) {
19943
- console.error("Error saving settings:", error);
19944
- return c.html(html`
19945
- <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
19946
- Failed to save settings. Please try again.
19947
- </div>
19948
- `);
20133
+ console.error("Error saving general settings:", error);
20134
+ return c.json({
20135
+ success: false,
20136
+ error: "Failed to save settings. Please try again."
20137
+ }, 500);
19949
20138
  }
19950
20139
  });
20140
+ adminSettingsRoutes.post("/", async (c) => {
20141
+ return c.redirect("/admin/settings/general");
20142
+ });
19951
20143
 
19952
20144
  // src/routes/index.ts
19953
20145
  var ROUTES_INFO = {
@@ -19978,5 +20170,5 @@ var ROUTES_INFO = {
19978
20170
  };
19979
20171
 
19980
20172
  export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
19981
- //# sourceMappingURL=chunk-PPUKPNTP.js.map
19982
- //# sourceMappingURL=chunk-PPUKPNTP.js.map
20173
+ //# sourceMappingURL=chunk-LGC3TNCY.js.map
20174
+ //# sourceMappingURL=chunk-LGC3TNCY.js.map