@sonicjs-cms/core 2.0.0-alpha.11 → 2.0.0-alpha.12

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.
@@ -1,12 +1,11 @@
1
1
  import { getCacheService, CACHE_CONFIGS } from './chunk-3MNMOLSA.js';
2
2
  import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-WESS2U3K.js';
3
3
  import { PluginService, getLogger } from './chunk-7N3HK7ZK.js';
4
- import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2 } from './chunk-G4JGVZAF.js';
4
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2 } from './chunk-P3VS4DV3.js';
5
5
  import { QueryFilterBuilder, sanitizeInput, escapeHtml } from './chunk-JIINOD2W.js';
6
6
  import { Hono } from 'hono';
7
7
  import { cors } from 'hono/cors';
8
8
  import { z } from 'zod';
9
- import { validator } from 'hono/validator';
10
9
  import { setCookie } from 'hono/cookie';
11
10
  import { html, raw } from 'hono/html';
12
11
 
@@ -1299,22 +1298,6 @@ apiSystemRoutes.get("/env", (c) => {
1299
1298
  });
1300
1299
  });
1301
1300
  var api_system_default = apiSystemRoutes;
1302
- var zValidator = (target, schema, hook, options) => (
1303
- // @ts-expect-error not typed well
1304
- validator(target, async (value, c) => {
1305
- let validatorValue = value;
1306
- const result = (
1307
- // @ts-expect-error z4.$ZodType has safeParseAsync
1308
- await schema.safeParseAsync(validatorValue)
1309
- );
1310
- if (!result.success) {
1311
- return c.json(result, 400);
1312
- }
1313
- return result.data;
1314
- })
1315
- );
1316
-
1317
- // src/routes/admin-api.ts
1318
1301
  var adminApiRoutes = new Hono();
1319
1302
  adminApiRoutes.use("*", requireAuth());
1320
1303
  adminApiRoutes.use("*", requireRole(["admin", "editor"]));
@@ -1552,131 +1535,133 @@ adminApiRoutes.get("/collections/:id", async (c) => {
1552
1535
  return c.json({ error: "Failed to fetch collection" }, 500);
1553
1536
  }
1554
1537
  });
1555
- adminApiRoutes.post(
1556
- "/collections",
1557
- zValidator("json", createCollectionSchema),
1558
- async (c) => {
1559
- try {
1560
- const validatedData = c.req.valid("json");
1561
- const db = c.env.DB;
1562
- const user = c.get("user");
1563
- const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
1564
- const existing = await existingStmt.bind(validatedData.name).first();
1565
- if (existing) {
1566
- return c.json({ error: "A collection with this name already exists" }, 400);
1567
- }
1568
- const basicSchema = {
1569
- type: "object",
1570
- properties: {
1571
- title: {
1572
- type: "string",
1573
- title: "Title",
1574
- required: true
1575
- },
1576
- content: {
1577
- type: "string",
1578
- title: "Content",
1579
- format: "richtext"
1580
- },
1581
- status: {
1582
- type: "string",
1583
- title: "Status",
1584
- enum: ["draft", "published", "archived"],
1585
- default: "draft"
1586
- }
1538
+ adminApiRoutes.post("/collections", async (c) => {
1539
+ try {
1540
+ const body = await c.req.json();
1541
+ const validation = createCollectionSchema.safeParse(body);
1542
+ if (!validation.success) {
1543
+ return c.json({ error: "Validation failed", details: validation.error.errors }, 400);
1544
+ }
1545
+ const validatedData = validation.data;
1546
+ const db = c.env.DB;
1547
+ const user = c.get("user");
1548
+ const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
1549
+ const existing = await existingStmt.bind(validatedData.name).first();
1550
+ if (existing) {
1551
+ return c.json({ error: "A collection with this name already exists" }, 400);
1552
+ }
1553
+ const basicSchema = {
1554
+ type: "object",
1555
+ properties: {
1556
+ title: {
1557
+ type: "string",
1558
+ title: "Title",
1559
+ required: true
1587
1560
  },
1588
- required: ["title"]
1589
- };
1590
- const collectionId = crypto.randomUUID();
1591
- const now = Date.now();
1592
- const insertStmt = db.prepare(`
1561
+ content: {
1562
+ type: "string",
1563
+ title: "Content",
1564
+ format: "richtext"
1565
+ },
1566
+ status: {
1567
+ type: "string",
1568
+ title: "Status",
1569
+ enum: ["draft", "published", "archived"],
1570
+ default: "draft"
1571
+ }
1572
+ },
1573
+ required: ["title"]
1574
+ };
1575
+ const collectionId = crypto.randomUUID();
1576
+ const now = Date.now();
1577
+ const insertStmt = db.prepare(`
1593
1578
  INSERT INTO collections (id, name, display_name, description, schema, is_active, created_at, updated_at)
1594
1579
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1595
1580
  `);
1596
- await insertStmt.bind(
1597
- collectionId,
1598
- validatedData.name,
1599
- validatedData.display_name,
1600
- validatedData.description || null,
1601
- JSON.stringify(basicSchema),
1602
- 1,
1603
- // is_active
1604
- now,
1605
- now
1606
- ).run();
1607
- try {
1608
- await c.env.CACHE_KV.delete("cache:collections:all");
1609
- await c.env.CACHE_KV.delete(`cache:collection:${validatedData.name}`);
1610
- } catch (e) {
1611
- console.error("Error clearing cache:", e);
1612
- }
1613
- return c.json({
1614
- data: {
1615
- id: collectionId,
1616
- name: validatedData.name,
1617
- display_name: validatedData.display_name,
1618
- description: validatedData.description,
1619
- created_at: now
1620
- }
1621
- }, 201);
1622
- } catch (error) {
1623
- console.error("Error creating collection:", error);
1624
- return c.json({ error: "Failed to create collection" }, 500);
1581
+ await insertStmt.bind(
1582
+ collectionId,
1583
+ validatedData.name,
1584
+ validatedData.display_name,
1585
+ validatedData.description || null,
1586
+ JSON.stringify(basicSchema),
1587
+ 1,
1588
+ // is_active
1589
+ now,
1590
+ now
1591
+ ).run();
1592
+ try {
1593
+ await c.env.CACHE_KV.delete("cache:collections:all");
1594
+ await c.env.CACHE_KV.delete(`cache:collection:${validatedData.name}`);
1595
+ } catch (e) {
1596
+ console.error("Error clearing cache:", e);
1625
1597
  }
1598
+ return c.json({
1599
+ data: {
1600
+ id: collectionId,
1601
+ name: validatedData.name,
1602
+ display_name: validatedData.display_name,
1603
+ description: validatedData.description,
1604
+ created_at: now
1605
+ }
1606
+ }, 201);
1607
+ } catch (error) {
1608
+ console.error("Error creating collection:", error);
1609
+ return c.json({ error: "Failed to create collection" }, 500);
1626
1610
  }
1627
- );
1628
- adminApiRoutes.patch(
1629
- "/collections/:id",
1630
- zValidator("json", updateCollectionSchema),
1631
- async (c) => {
1632
- try {
1633
- const id = c.req.param("id");
1634
- const validatedData = c.req.valid("json");
1635
- const db = c.env.DB;
1636
- const checkStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
1637
- const existing = await checkStmt.bind(id).first();
1638
- if (!existing) {
1639
- return c.json({ error: "Collection not found" }, 404);
1640
- }
1641
- const updateFields = [];
1642
- const updateParams = [];
1643
- if (validatedData.display_name !== void 0) {
1644
- updateFields.push("display_name = ?");
1645
- updateParams.push(validatedData.display_name);
1646
- }
1647
- if (validatedData.description !== void 0) {
1648
- updateFields.push("description = ?");
1649
- updateParams.push(validatedData.description);
1650
- }
1651
- if (validatedData.is_active !== void 0) {
1652
- updateFields.push("is_active = ?");
1653
- updateParams.push(validatedData.is_active ? 1 : 0);
1654
- }
1655
- if (updateFields.length === 0) {
1656
- return c.json({ error: "No fields to update" }, 400);
1657
- }
1658
- updateFields.push("updated_at = ?");
1659
- updateParams.push(Date.now());
1660
- updateParams.push(id);
1661
- const updateStmt = db.prepare(`
1611
+ });
1612
+ adminApiRoutes.patch("/collections/:id", async (c) => {
1613
+ try {
1614
+ const id = c.req.param("id");
1615
+ const body = await c.req.json();
1616
+ const validation = updateCollectionSchema.safeParse(body);
1617
+ if (!validation.success) {
1618
+ return c.json({ error: "Validation failed", details: validation.error.errors }, 400);
1619
+ }
1620
+ const validatedData = validation.data;
1621
+ const db = c.env.DB;
1622
+ const checkStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
1623
+ const existing = await checkStmt.bind(id).first();
1624
+ if (!existing) {
1625
+ return c.json({ error: "Collection not found" }, 404);
1626
+ }
1627
+ const updateFields = [];
1628
+ const updateParams = [];
1629
+ if (validatedData.display_name !== void 0) {
1630
+ updateFields.push("display_name = ?");
1631
+ updateParams.push(validatedData.display_name);
1632
+ }
1633
+ if (validatedData.description !== void 0) {
1634
+ updateFields.push("description = ?");
1635
+ updateParams.push(validatedData.description);
1636
+ }
1637
+ if (validatedData.is_active !== void 0) {
1638
+ updateFields.push("is_active = ?");
1639
+ updateParams.push(validatedData.is_active ? 1 : 0);
1640
+ }
1641
+ if (updateFields.length === 0) {
1642
+ return c.json({ error: "No fields to update" }, 400);
1643
+ }
1644
+ updateFields.push("updated_at = ?");
1645
+ updateParams.push(Date.now());
1646
+ updateParams.push(id);
1647
+ const updateStmt = db.prepare(`
1662
1648
  UPDATE collections
1663
1649
  SET ${updateFields.join(", ")}
1664
1650
  WHERE id = ?
1665
1651
  `);
1666
- await updateStmt.bind(...updateParams).run();
1667
- try {
1668
- await c.env.CACHE_KV.delete("cache:collections:all");
1669
- await c.env.CACHE_KV.delete(`cache:collection:${existing.name}`);
1670
- } catch (e) {
1671
- console.error("Error clearing cache:", e);
1672
- }
1673
- return c.json({ message: "Collection updated successfully" });
1674
- } catch (error) {
1675
- console.error("Error updating collection:", error);
1676
- return c.json({ error: "Failed to update collection" }, 500);
1652
+ await updateStmt.bind(...updateParams).run();
1653
+ try {
1654
+ await c.env.CACHE_KV.delete("cache:collections:all");
1655
+ await c.env.CACHE_KV.delete(`cache:collection:${existing.name}`);
1656
+ } catch (e) {
1657
+ console.error("Error clearing cache:", e);
1677
1658
  }
1659
+ return c.json({ message: "Collection updated successfully" });
1660
+ } catch (error) {
1661
+ console.error("Error updating collection:", error);
1662
+ return c.json({ error: "Failed to update collection" }, 500);
1678
1663
  }
1679
- );
1664
+ });
1680
1665
  adminApiRoutes.delete("/collections/:id", async (c) => {
1681
1666
  try {
1682
1667
  const id = c.req.param("id");
@@ -1710,79 +1695,6 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
1710
1695
  });
1711
1696
  var admin_api_default = adminApiRoutes;
1712
1697
 
1713
- // src/templates/components/alert.template.ts
1714
- function renderAlert(data) {
1715
- const typeClasses = {
1716
- success: "bg-green-50 dark:bg-green-500/10 border border-green-600/20 dark:border-green-500/20",
1717
- error: "bg-error/10 border border-red-600/20 dark:border-red-500/20",
1718
- warning: "bg-amber-50 dark:bg-amber-500/10 border border-amber-600/20 dark:border-amber-500/20",
1719
- info: "bg-blue-50 dark:bg-blue-500/10 border border-blue-600/20 dark:border-blue-500/20"
1720
- };
1721
- const iconClasses = {
1722
- success: "text-green-600 dark:text-green-400",
1723
- error: "text-red-600 dark:text-red-400",
1724
- warning: "text-amber-600 dark:text-amber-400",
1725
- info: "text-blue-600 dark:text-blue-400"
1726
- };
1727
- const textClasses = {
1728
- success: "text-green-900 dark:text-green-300",
1729
- error: "text-red-900 dark:text-red-300",
1730
- warning: "text-amber-900 dark:text-amber-300",
1731
- info: "text-blue-900 dark:text-blue-300"
1732
- };
1733
- const messageTextClasses = {
1734
- success: "text-green-700 dark:text-green-400",
1735
- error: "text-red-700 dark:text-red-400",
1736
- warning: "text-amber-700 dark:text-amber-400",
1737
- info: "text-blue-700 dark:text-blue-400"
1738
- };
1739
- const icons = {
1740
- success: `<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />`,
1741
- error: `<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />`,
1742
- warning: `<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />`,
1743
- info: `<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />`
1744
- };
1745
- return `
1746
- <div class="rounded-lg p-4 ${typeClasses[data.type]} ${data.className || ""}" ${data.dismissible ? 'id="dismissible-alert"' : ""}>
1747
- <div class="flex">
1748
- ${data.icon !== false ? `
1749
- <div class="flex-shrink-0">
1750
- <svg class="h-5 w-5 ${iconClasses[data.type]}" viewBox="0 0 20 20" fill="currentColor">
1751
- ${icons[data.type]}
1752
- </svg>
1753
- </div>
1754
- ` : ""}
1755
- <div class="${data.icon !== false ? "ml-3" : ""}">
1756
- ${data.title ? `
1757
- <h3 class="text-sm font-semibold ${textClasses[data.type]}">
1758
- ${data.title}
1759
- </h3>
1760
- ` : ""}
1761
- <div class="${data.title ? "mt-1 text-sm" : "text-sm"} ${messageTextClasses[data.type]}">
1762
- <p>${data.message}</p>
1763
- </div>
1764
- </div>
1765
- ${data.dismissible ? `
1766
- <div class="ml-auto pl-3">
1767
- <div class="-mx-1.5 -my-1.5">
1768
- <button
1769
- type="button"
1770
- class="inline-flex rounded-md p-1.5 ${iconClasses[data.type]} hover:bg-opacity-20 focus:outline-none focus:ring-2 focus:ring-offset-2"
1771
- onclick="document.getElementById('dismissible-alert').remove()"
1772
- >
1773
- <span class="sr-only">Dismiss</span>
1774
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
1775
- <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
1776
- </svg>
1777
- </button>
1778
- </div>
1779
- </div>
1780
- ` : ""}
1781
- </div>
1782
- </div>
1783
- `;
1784
- }
1785
-
1786
1698
  // src/templates/pages/auth-login.template.ts
1787
1699
  function renderLoginPage(data, demoLoginActive = false) {
1788
1700
  return `
@@ -2422,58 +2334,59 @@ authRoutes.post(
2422
2334
  }
2423
2335
  }
2424
2336
  );
2425
- authRoutes.post(
2426
- "/login",
2427
- zValidator("json", loginSchema),
2428
- async (c) => {
2429
- try {
2430
- const { email, password } = c.req.valid("json");
2431
- const db = c.env.DB;
2432
- const normalizedEmail = email.toLowerCase();
2433
- const cache = getCacheService(CACHE_CONFIGS.user);
2434
- let user = await cache.get(cache.generateKey("user", `email:${normalizedEmail}`));
2435
- if (!user) {
2436
- user = await db.prepare("SELECT * FROM users WHERE email = ? AND is_active = 1").bind(normalizedEmail).first();
2437
- if (user) {
2438
- await cache.set(cache.generateKey("user", `email:${normalizedEmail}`), user);
2439
- await cache.set(cache.generateKey("user", user.id), user);
2440
- }
2441
- }
2442
- if (!user) {
2443
- return c.json({ error: "Invalid email or password" }, 401);
2444
- }
2445
- const isValidPassword = await AuthManager.verifyPassword(password, user.password_hash);
2446
- if (!isValidPassword) {
2447
- return c.json({ error: "Invalid email or password" }, 401);
2337
+ authRoutes.post("/login", async (c) => {
2338
+ try {
2339
+ const body = await c.req.json();
2340
+ const validation = loginSchema.safeParse(body);
2341
+ if (!validation.success) {
2342
+ return c.json({ error: "Validation failed", details: validation.error.errors }, 400);
2343
+ }
2344
+ const { email, password } = validation.data;
2345
+ const db = c.env.DB;
2346
+ const normalizedEmail = email.toLowerCase();
2347
+ const cache = getCacheService(CACHE_CONFIGS.user);
2348
+ let user = await cache.get(cache.generateKey("user", `email:${normalizedEmail}`));
2349
+ if (!user) {
2350
+ user = await db.prepare("SELECT * FROM users WHERE email = ? AND is_active = 1").bind(normalizedEmail).first();
2351
+ if (user) {
2352
+ await cache.set(cache.generateKey("user", `email:${normalizedEmail}`), user);
2353
+ await cache.set(cache.generateKey("user", user.id), user);
2448
2354
  }
2449
- const token = await AuthManager.generateToken(user.id, user.email, user.role);
2450
- setCookie(c, "auth_token", token, {
2451
- httpOnly: true,
2452
- secure: true,
2453
- sameSite: "Strict",
2454
- maxAge: 60 * 60 * 24
2455
- // 24 hours
2456
- });
2457
- await db.prepare("UPDATE users SET last_login_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).getTime(), user.id).run();
2458
- await cache.delete(cache.generateKey("user", user.id));
2459
- await cache.delete(cache.generateKey("user", `email:${normalizedEmail}`));
2460
- return c.json({
2461
- user: {
2462
- id: user.id,
2463
- email: user.email,
2464
- username: user.username,
2465
- firstName: user.firstName,
2466
- lastName: user.lastName,
2467
- role: user.role
2468
- },
2469
- token
2470
- });
2471
- } catch (error) {
2472
- console.error("Login error:", error);
2473
- return c.json({ error: "Login failed" }, 500);
2474
2355
  }
2356
+ if (!user) {
2357
+ return c.json({ error: "Invalid email or password" }, 401);
2358
+ }
2359
+ const isValidPassword = await AuthManager.verifyPassword(password, user.password_hash);
2360
+ if (!isValidPassword) {
2361
+ return c.json({ error: "Invalid email or password" }, 401);
2362
+ }
2363
+ const token = await AuthManager.generateToken(user.id, user.email, user.role);
2364
+ setCookie(c, "auth_token", token, {
2365
+ httpOnly: true,
2366
+ secure: true,
2367
+ sameSite: "Strict",
2368
+ maxAge: 60 * 60 * 24
2369
+ // 24 hours
2370
+ });
2371
+ await db.prepare("UPDATE users SET last_login_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).getTime(), user.id).run();
2372
+ await cache.delete(cache.generateKey("user", user.id));
2373
+ await cache.delete(cache.generateKey("user", `email:${normalizedEmail}`));
2374
+ return c.json({
2375
+ user: {
2376
+ id: user.id,
2377
+ email: user.email,
2378
+ username: user.username,
2379
+ firstName: user.firstName,
2380
+ lastName: user.lastName,
2381
+ role: user.role
2382
+ },
2383
+ token
2384
+ });
2385
+ } catch (error) {
2386
+ console.error("Login error:", error);
2387
+ return c.json({ error: "Login failed" }, 500);
2475
2388
  }
2476
- );
2389
+ });
2477
2390
  authRoutes.post("/logout", (c) => {
2478
2391
  setCookie(c, "auth_token", "", {
2479
2392
  httpOnly: true,
@@ -3562,98 +3475,18 @@ function escapeHtml2(text) {
3562
3475
  })[char] || char);
3563
3476
  }
3564
3477
 
3565
- // src/templates/components/confirmation-dialog.template.ts
3566
- function renderConfirmationDialog(options) {
3567
- const {
3568
- id,
3569
- title,
3570
- message,
3571
- confirmText = "Confirm",
3572
- cancelText = "Cancel",
3573
- confirmClass = "bg-red-500 hover:bg-red-400",
3574
- iconColor = "red",
3575
- onConfirm = ""
3576
- } = options;
3577
- const iconColorClasses = {
3578
- red: "bg-red-500/10 text-red-400",
3579
- yellow: "bg-yellow-500/10 text-yellow-400",
3580
- blue: "bg-blue-500/10 text-blue-400"
3581
- };
3582
- return `
3583
- <el-dialog>
3584
- <dialog
3585
- id="${id}"
3586
- aria-labelledby="${id}-title"
3587
- class="fixed inset-0 m-0 size-auto max-h-none max-w-none overflow-y-auto bg-transparent p-0 backdrop:bg-transparent"
3588
- >
3589
- <el-dialog-backdrop class="fixed inset-0 bg-gray-900/50 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"></el-dialog-backdrop>
3590
-
3591
- <div tabindex="0" class="flex min-h-full items-end justify-center p-4 text-center focus:outline focus:outline-0 sm:items-center sm:p-0">
3592
- <el-dialog-panel class="relative transform overflow-hidden rounded-lg bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl outline outline-1 -outline-offset-1 outline-white/10 transition-all data-[closed]:translate-y-4 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in sm:my-8 sm:w-full sm:max-w-lg sm:p-6 data-[closed]:sm:translate-y-0 data-[closed]:sm:scale-95">
3593
- <div class="sm:flex sm:items-start">
3594
- <div class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full ${iconColorClasses[iconColor]} sm:mx-0 sm:size-10">
3595
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" data-slot="icon" aria-hidden="true" class="size-6">
3596
- <path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" stroke-linecap="round" stroke-linejoin="round" />
3597
- </svg>
3598
- </div>
3599
- <div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
3600
- <h3 id="${id}-title" class="text-base font-semibold text-white">${title}</h3>
3601
- <div class="mt-2">
3602
- <p class="text-sm text-gray-400">${message}</p>
3603
- </div>
3604
- </div>
3605
- </div>
3606
- <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
3607
- <button
3608
- type="button"
3609
- onclick="${onConfirm}; document.getElementById('${id}').close()"
3610
- command="close"
3611
- commandfor="${id}"
3612
- class="confirm-button inline-flex w-full justify-center rounded-md ${confirmClass} px-3 py-2 text-sm font-semibold text-white sm:ml-3 sm:w-auto"
3613
- >
3614
- ${confirmText}
3615
- </button>
3616
- <button
3617
- type="button"
3618
- command="close"
3619
- commandfor="${id}"
3620
- class="mt-3 inline-flex w-full justify-center rounded-md bg-white/10 px-3 py-2 text-sm font-semibold text-white ring-1 ring-inset ring-white/5 hover:bg-white/20 sm:mt-0 sm:w-auto"
3621
- >
3622
- ${cancelText}
3623
- </button>
3624
- </div>
3625
- </el-dialog-panel>
3626
- </div>
3627
- </dialog>
3628
- </el-dialog>
3629
- `;
3630
- }
3631
- function getConfirmationDialogScript() {
3632
- return `
3633
- <script src="https://cdn.jsdelivr.net/npm/@tailwindplus/elements@1" type="module"></script>
3634
- <script>
3635
- function showConfirmDialog(dialogId) {
3636
- const dialog = document.getElementById(dialogId);
3637
- if (dialog) {
3638
- dialog.showModal();
3639
- }
3640
- }
3641
- </script>
3642
- `;
3643
- }
3644
-
3645
- // src/templates/pages/admin-content-form.template.ts
3646
- function renderContentFormPage(data) {
3647
- const isEdit = data.isEdit || !!data.id;
3648
- const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
3649
- const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
3650
- const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
3651
- const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
3652
- const metaFields = data.fields.filter((f) => f.field_name.startsWith("meta_"));
3653
- const getFieldValue = (fieldName) => {
3654
- if (fieldName === "title") return data.title || data.data?.[fieldName] || "";
3655
- if (fieldName === "slug") return data.slug || data.data?.[fieldName] || "";
3656
- return data.data?.[fieldName] || "";
3478
+ // src/templates/pages/admin-content-form.template.ts
3479
+ function renderContentFormPage(data) {
3480
+ const isEdit = data.isEdit || !!data.id;
3481
+ const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
3482
+ const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
3483
+ const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
3484
+ const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
3485
+ const metaFields = data.fields.filter((f) => f.field_name.startsWith("meta_"));
3486
+ const getFieldValue = (fieldName) => {
3487
+ if (fieldName === "title") return data.title || data.data?.[fieldName] || "";
3488
+ if (fieldName === "slug") return data.slug || data.data?.[fieldName] || "";
3489
+ return data.data?.[fieldName] || "";
3657
3490
  };
3658
3491
  const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
3659
3492
  value: getFieldValue(field.field_name),
@@ -4258,382 +4091,6 @@ function renderContentFormPage(data) {
4258
4091
 
4259
4092
  // src/templates/pages/admin-content-list.template.ts
4260
4093
  init_admin_layout_catalyst_template();
4261
-
4262
- // src/templates/components/table.template.ts
4263
- function renderTable(data) {
4264
- const tableId = data.tableId || `table-${Math.random().toString(36).substr(2, 9)}`;
4265
- if (data.rows.length === 0) {
4266
- return `
4267
- <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-8 text-center">
4268
- <div class="text-zinc-500 dark:text-zinc-400">
4269
- <svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4270
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
4271
- </svg>
4272
- <p class="mt-2 text-sm text-zinc-500 dark:text-zinc-400">${data.emptyMessage || "No data available"}</p>
4273
- </div>
4274
- </div>
4275
- `;
4276
- }
4277
- return `
4278
- <div class="${data.className || ""}" id="${tableId}">
4279
- ${data.title ? `
4280
- <div class="px-4 sm:px-0 mb-4">
4281
- <h3 class="text-base font-semibold text-zinc-950 dark:text-white">${data.title}</h3>
4282
- </div>
4283
- ` : ""}
4284
- <div class="overflow-x-auto">
4285
- <table class="min-w-full sortable-table">
4286
- <thead>
4287
- <tr>
4288
- ${data.selectable ? `
4289
- <th class="px-4 py-3.5 text-center sm:pl-0">
4290
- <div class="flex items-center justify-center">
4291
- <div class="group grid size-4 grid-cols-1">
4292
- <input type="checkbox" id="select-all-${tableId}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
4293
- <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
4294
- <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
4295
- <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
4296
- </svg>
4297
- </div>
4298
- </div>
4299
- </th>
4300
- ` : ""}
4301
- ${data.columns.map((column, index) => {
4302
- const isFirst = index === 0 && !data.selectable;
4303
- const isLast = index === data.columns.length - 1;
4304
- return `
4305
- <th class="px-4 py-3.5 text-left text-sm font-semibold text-zinc-950 dark:text-white ${isFirst ? "sm:pl-0" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}">
4306
- ${column.sortable ? `
4307
- <button
4308
- class="flex items-center gap-x-2 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors sort-btn text-left"
4309
- data-column="${column.key}"
4310
- data-sort-type="${column.sortType || "string"}"
4311
- data-sort-direction="none"
4312
- onclick="sortTable('${tableId}', '${column.key}', '${column.sortType || "string"}')"
4313
- >
4314
- <span>${column.label}</span>
4315
- <div class="sort-icons flex flex-col">
4316
- <svg class="w-3 h-3 sort-up opacity-30" fill="currentColor" viewBox="0 0 20 20">
4317
- <path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" />
4318
- </svg>
4319
- <svg class="w-3 h-3 sort-down opacity-30 -mt-1" fill="currentColor" viewBox="0 0 20 20">
4320
- <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
4321
- </svg>
4322
- </div>
4323
- </button>
4324
- ` : column.label}
4325
- </th>
4326
- `;
4327
- }).join("")}
4328
- </tr>
4329
- </thead>
4330
- <tbody>
4331
- ${data.rows.map((row, rowIndex) => {
4332
- if (!row) return "";
4333
- const clickableClass = data.rowClickable ? "cursor-pointer" : "";
4334
- const clickHandler = data.rowClickable && data.rowClickUrl ? `onclick="window.location.href='${data.rowClickUrl(row)}'"` : "";
4335
- return `
4336
- <tr class="group border-t border-zinc-950/5 dark:border-white/5 hover:bg-gradient-to-r hover:from-cyan-50/50 hover:via-blue-50/30 hover:to-purple-50/50 dark:hover:from-cyan-900/20 dark:hover:via-blue-900/10 dark:hover:to-purple-900/20 hover:shadow-sm hover:shadow-cyan-500/5 dark:hover:shadow-cyan-400/5 transition-all duration-300 ${clickableClass}" ${clickHandler}>
4337
- ${data.selectable ? `
4338
- <td class="px-4 py-4 sm:pl-0" onclick="event.stopPropagation()">
4339
- <div class="flex items-center justify-center">
4340
- <div class="group grid size-4 grid-cols-1">
4341
- <input type="checkbox" value="${row.id || ""}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
4342
- <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
4343
- <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
4344
- <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
4345
- </svg>
4346
- </div>
4347
- </div>
4348
- </td>
4349
- ` : ""}
4350
- ${data.columns.map((column, colIndex) => {
4351
- const value = row[column.key];
4352
- const displayValue = column.render ? column.render(value, row) : value;
4353
- const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
4354
- const isFirst = colIndex === 0 && !data.selectable;
4355
- const isLast = colIndex === data.columns.length - 1;
4356
- return `
4357
- <td class="px-4 py-4 text-sm text-zinc-500 dark:text-zinc-400 ${isFirst ? "sm:pl-0 font-medium text-zinc-950 dark:text-white" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}" ${stopPropagation}>
4358
- ${displayValue || ""}
4359
- </td>
4360
- `;
4361
- }).join("")}
4362
- </tr>
4363
- `;
4364
- }).join("")}
4365
- </tbody>
4366
- </table>
4367
- </div>
4368
-
4369
- <script>
4370
- // Table sorting functionality
4371
- window.sortTable = function(tableId, column, sortType) {
4372
- const tableContainer = document.getElementById(tableId);
4373
- const table = tableContainer.querySelector('.sortable-table');
4374
- const tbody = table.querySelector('tbody');
4375
- const rows = Array.from(tbody.querySelectorAll('tr'));
4376
- const headerBtn = table.querySelector(\`[data-column="\${column}"]\`);
4377
-
4378
- // Get current sort direction
4379
- let direction = headerBtn.getAttribute('data-sort-direction');
4380
-
4381
- // Reset all sort indicators
4382
- table.querySelectorAll('.sort-btn').forEach(btn => {
4383
- btn.setAttribute('data-sort-direction', 'none');
4384
- btn.querySelectorAll('.sort-up, .sort-down').forEach(icon => {
4385
- icon.classList.add('opacity-30');
4386
- icon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
4387
- });
4388
- });
4389
-
4390
- // Determine new direction
4391
- if (direction === 'none' || direction === 'desc') {
4392
- direction = 'asc';
4393
- } else {
4394
- direction = 'desc';
4395
- }
4396
-
4397
- // Update current header
4398
- headerBtn.setAttribute('data-sort-direction', direction);
4399
- const upIcon = headerBtn.querySelector('.sort-up');
4400
- const downIcon = headerBtn.querySelector('.sort-down');
4401
-
4402
- if (direction === 'asc') {
4403
- upIcon.classList.remove('opacity-30');
4404
- upIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
4405
- downIcon.classList.add('opacity-30');
4406
- downIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
4407
- } else {
4408
- downIcon.classList.remove('opacity-30');
4409
- downIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
4410
- upIcon.classList.add('opacity-30');
4411
- upIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
4412
- }
4413
-
4414
- // Find column index (accounting for potential select column)
4415
- const headers = Array.from(table.querySelectorAll('th'));
4416
- const selectableOffset = table.querySelector('input[id^="select-all"]') ? 1 : 0;
4417
- const columnIndex = headers.findIndex(th => th.querySelector(\`[data-column="\${column}"]\`)) - selectableOffset;
4418
-
4419
- // Sort rows
4420
- rows.sort((a, b) => {
4421
- const aCell = a.children[columnIndex + selectableOffset];
4422
- const bCell = b.children[columnIndex + selectableOffset];
4423
-
4424
- if (!aCell || !bCell) return 0;
4425
-
4426
- let aValue = aCell.textContent.trim();
4427
- let bValue = bCell.textContent.trim();
4428
-
4429
- // Handle different sort types
4430
- switch (sortType) {
4431
- case 'number':
4432
- aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
4433
- bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
4434
- break;
4435
- case 'date':
4436
- aValue = new Date(aValue).getTime() || 0;
4437
- bValue = new Date(bValue).getTime() || 0;
4438
- break;
4439
- case 'boolean':
4440
- aValue = aValue.toLowerCase() === 'true' || aValue.toLowerCase() === 'published' || aValue.toLowerCase() === 'active';
4441
- bValue = bValue.toLowerCase() === 'true' || bValue.toLowerCase() === 'published' || bValue.toLowerCase() === 'active';
4442
- break;
4443
- default: // string
4444
- aValue = aValue.toLowerCase();
4445
- bValue = bValue.toLowerCase();
4446
- }
4447
-
4448
- if (aValue < bValue) return direction === 'asc' ? -1 : 1;
4449
- if (aValue > bValue) return direction === 'asc' ? 1 : -1;
4450
- return 0;
4451
- });
4452
-
4453
- // Re-append sorted rows
4454
- rows.forEach(row => tbody.appendChild(row));
4455
- };
4456
-
4457
- // Select all functionality
4458
- document.addEventListener('DOMContentLoaded', function() {
4459
- document.querySelectorAll('[id^="select-all"]').forEach(selectAll => {
4460
- selectAll.addEventListener('change', function() {
4461
- const tableId = this.id.replace('select-all-', '');
4462
- const table = document.getElementById(tableId);
4463
- if (table) {
4464
- const checkboxes = table.querySelectorAll('.row-checkbox');
4465
- checkboxes.forEach(checkbox => {
4466
- checkbox.checked = this.checked;
4467
- });
4468
- }
4469
- });
4470
- });
4471
- });
4472
- </script>
4473
- </div>
4474
- `;
4475
- }
4476
-
4477
- // src/templates/components/pagination.template.ts
4478
- function renderPagination(data) {
4479
- const shouldShowPagination = data.totalPages > 1 || data.showPageSizeSelector !== false && data.totalItems > 0;
4480
- if (!shouldShowPagination) {
4481
- return "";
4482
- }
4483
- const buildUrl = (page, limit) => {
4484
- const params = new URLSearchParams(data.queryParams || {});
4485
- params.set("page", page.toString());
4486
- if (data.itemsPerPage !== 20) {
4487
- params.set("limit", data.itemsPerPage.toString());
4488
- }
4489
- return `${data.baseUrl}?${params.toString()}`;
4490
- };
4491
- const buildPageSizeUrl = (limit) => {
4492
- const params = new URLSearchParams(data.queryParams || {});
4493
- params.set("page", "1");
4494
- params.set("limit", limit.toString());
4495
- return `${data.baseUrl}?${params.toString()}`;
4496
- };
4497
- const generatePageNumbers = () => {
4498
- const maxNumbers = data.maxPageNumbers || 5;
4499
- const half = Math.floor(maxNumbers / 2);
4500
- let start = Math.max(1, data.currentPage - half);
4501
- let end = Math.min(data.totalPages, start + maxNumbers - 1);
4502
- if (end - start + 1 < maxNumbers) {
4503
- start = Math.max(1, end - maxNumbers + 1);
4504
- }
4505
- const pages = [];
4506
- for (let i = start; i <= end; i++) {
4507
- pages.push(i);
4508
- }
4509
- return pages;
4510
- };
4511
- return `
4512
- <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 px-4 py-3 flex items-center justify-between mt-4">
4513
- ${data.totalPages > 1 ? `
4514
- <!-- Mobile Pagination -->
4515
- <div class="flex-1 flex justify-between sm:hidden">
4516
- ${data.currentPage > 1 ? `
4517
- <a href="${buildUrl(data.currentPage - 1)}" class="inline-flex items-center rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4518
- Previous
4519
- </a>
4520
- ` : `
4521
- <span class="inline-flex items-center rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-400 dark:text-zinc-600 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 opacity-50 cursor-not-allowed">Previous</span>
4522
- `}
4523
-
4524
- ${data.currentPage < data.totalPages ? `
4525
- <a href="${buildUrl(data.currentPage + 1)}" class="inline-flex items-center rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4526
- Next
4527
- </a>
4528
- ` : `
4529
- <span class="inline-flex items-center rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-400 dark:text-zinc-600 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 opacity-50 cursor-not-allowed">Next</span>
4530
- `}
4531
- </div>
4532
- ` : ""}
4533
-
4534
- <!-- Desktop Pagination -->
4535
- <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
4536
- <div class="flex items-center gap-4">
4537
- <p class="text-sm text-zinc-500 dark:text-zinc-400">
4538
- Showing <span class="font-medium text-zinc-950 dark:text-white">${data.startItem}</span> to
4539
- <span class="font-medium text-zinc-950 dark:text-white">${data.endItem}</span> of
4540
- <span class="font-medium text-zinc-950 dark:text-white">${data.totalItems}</span> results
4541
- </p>
4542
- ${data.showPageSizeSelector !== false ? `
4543
- <div class="flex items-center gap-2">
4544
- <label for="page-size" class="text-sm text-zinc-500 dark:text-zinc-400">Per page:</label>
4545
- <div class="grid grid-cols-1">
4546
- <select
4547
- id="page-size"
4548
- onchange="window.location.href = this.value"
4549
- class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-sm text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-zinc-500/30 dark:outline-zinc-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-zinc-500 dark:focus-visible:outline-zinc-400"
4550
- >
4551
- ${(data.pageSizeOptions || [10, 20, 50, 100]).map((size) => `
4552
- <option value="${buildPageSizeUrl(size)}" ${size === data.itemsPerPage ? "selected" : ""}>
4553
- ${size}
4554
- </option>
4555
- `).join("")}
4556
- </select>
4557
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-4 self-center justify-self-end text-zinc-600 dark:text-zinc-400">
4558
- <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" />
4559
- </svg>
4560
- </div>
4561
- </div>
4562
- ` : ""}
4563
- </div>
4564
-
4565
- ${data.totalPages > 1 ? `
4566
- <div class="flex items-center gap-x-1">
4567
- <!-- Previous Button -->
4568
- ${data.currentPage > 1 ? `
4569
- <a href="${buildUrl(data.currentPage - 1)}"
4570
- class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4571
- Previous
4572
- </a>
4573
- ` : ""}
4574
-
4575
- <!-- Page Numbers -->
4576
- ${data.showPageNumbers !== false ? `
4577
- <!-- First page if not in range -->
4578
- ${(() => {
4579
- const pageNumbers = generatePageNumbers();
4580
- const firstPage = pageNumbers.length > 0 ? pageNumbers[0] : null;
4581
- return firstPage && firstPage > 1 ? `
4582
- <a href="${buildUrl(1)}"
4583
- class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4584
- 1
4585
- </a>
4586
- ${firstPage > 2 ? `
4587
- <span class="px-2 text-sm text-zinc-500 dark:text-zinc-400">...</span>
4588
- ` : ""}
4589
- ` : "";
4590
- })()}
4591
-
4592
- <!-- Page number buttons -->
4593
- ${generatePageNumbers().map((pageNum) => `
4594
- ${pageNum === data.currentPage ? `
4595
- <span class="rounded-lg bg-zinc-950 dark:bg-white px-3 py-2 text-sm font-semibold text-white dark:text-zinc-950">
4596
- ${pageNum}
4597
- </span>
4598
- ` : `
4599
- <a href="${buildUrl(pageNum)}"
4600
- class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4601
- ${pageNum}
4602
- </a>
4603
- `}
4604
- `).join("")}
4605
-
4606
- <!-- Last page if not in range -->
4607
- ${(() => {
4608
- const pageNumbers = generatePageNumbers();
4609
- const lastPageNum = pageNumbers.length > 0 ? pageNumbers.slice(-1)[0] : null;
4610
- return lastPageNum && lastPageNum < data.totalPages ? `
4611
- ${lastPageNum < data.totalPages - 1 ? `
4612
- <span class="px-2 text-sm text-zinc-500 dark:text-zinc-400">...</span>
4613
- ` : ""}
4614
- <a href="${buildUrl(data.totalPages)}"
4615
- class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4616
- ${data.totalPages}
4617
- </a>
4618
- ` : "";
4619
- })()}
4620
- ` : ""}
4621
-
4622
- <!-- Next Button -->
4623
- ${data.currentPage < data.totalPages ? `
4624
- <a href="${buildUrl(data.currentPage + 1)}"
4625
- class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors">
4626
- Next
4627
- </a>
4628
- ` : ""}
4629
- </div>
4630
- ` : ""}
4631
- </div>
4632
- </div>
4633
- `;
4634
- }
4635
-
4636
- // src/templates/pages/admin-content-list.template.ts
4637
4094
  function renderContentListPage(data) {
4638
4095
  const urlParams = new URLSearchParams();
4639
4096
  if (data.modelName && data.modelName !== "all") urlParams.set("model", data.modelName);
@@ -6977,6 +6434,79 @@ function renderProfilePage(data) {
6977
6434
  return renderAdminLayout(layoutData);
6978
6435
  }
6979
6436
 
6437
+ // src/templates/components/alert.template.ts
6438
+ function renderAlert2(data) {
6439
+ const typeClasses = {
6440
+ success: "bg-green-50 dark:bg-green-500/10 border border-green-600/20 dark:border-green-500/20",
6441
+ error: "bg-error/10 border border-red-600/20 dark:border-red-500/20",
6442
+ warning: "bg-amber-50 dark:bg-amber-500/10 border border-amber-600/20 dark:border-amber-500/20",
6443
+ info: "bg-blue-50 dark:bg-blue-500/10 border border-blue-600/20 dark:border-blue-500/20"
6444
+ };
6445
+ const iconClasses = {
6446
+ success: "text-green-600 dark:text-green-400",
6447
+ error: "text-red-600 dark:text-red-400",
6448
+ warning: "text-amber-600 dark:text-amber-400",
6449
+ info: "text-blue-600 dark:text-blue-400"
6450
+ };
6451
+ const textClasses = {
6452
+ success: "text-green-900 dark:text-green-300",
6453
+ error: "text-red-900 dark:text-red-300",
6454
+ warning: "text-amber-900 dark:text-amber-300",
6455
+ info: "text-blue-900 dark:text-blue-300"
6456
+ };
6457
+ const messageTextClasses = {
6458
+ success: "text-green-700 dark:text-green-400",
6459
+ error: "text-red-700 dark:text-red-400",
6460
+ warning: "text-amber-700 dark:text-amber-400",
6461
+ info: "text-blue-700 dark:text-blue-400"
6462
+ };
6463
+ const icons = {
6464
+ success: `<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />`,
6465
+ error: `<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />`,
6466
+ warning: `<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />`,
6467
+ info: `<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />`
6468
+ };
6469
+ return `
6470
+ <div class="rounded-lg p-4 ${typeClasses[data.type]} ${data.className || ""}" ${data.dismissible ? 'id="dismissible-alert"' : ""}>
6471
+ <div class="flex">
6472
+ ${data.icon !== false ? `
6473
+ <div class="flex-shrink-0">
6474
+ <svg class="h-5 w-5 ${iconClasses[data.type]}" viewBox="0 0 20 20" fill="currentColor">
6475
+ ${icons[data.type]}
6476
+ </svg>
6477
+ </div>
6478
+ ` : ""}
6479
+ <div class="${data.icon !== false ? "ml-3" : ""}">
6480
+ ${data.title ? `
6481
+ <h3 class="text-sm font-semibold ${textClasses[data.type]}">
6482
+ ${data.title}
6483
+ </h3>
6484
+ ` : ""}
6485
+ <div class="${data.title ? "mt-1 text-sm" : "text-sm"} ${messageTextClasses[data.type]}">
6486
+ <p>${data.message}</p>
6487
+ </div>
6488
+ </div>
6489
+ ${data.dismissible ? `
6490
+ <div class="ml-auto pl-3">
6491
+ <div class="-mx-1.5 -my-1.5">
6492
+ <button
6493
+ type="button"
6494
+ class="inline-flex rounded-md p-1.5 ${iconClasses[data.type]} hover:bg-opacity-20 focus:outline-none focus:ring-2 focus:ring-offset-2"
6495
+ onclick="document.getElementById('dismissible-alert').remove()"
6496
+ >
6497
+ <span class="sr-only">Dismiss</span>
6498
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
6499
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
6500
+ </svg>
6501
+ </button>
6502
+ </div>
6503
+ </div>
6504
+ ` : ""}
6505
+ </div>
6506
+ </div>
6507
+ `;
6508
+ }
6509
+
6980
6510
  // src/templates/pages/admin-activity-logs.template.ts
6981
6511
  function renderActivityLogsPage(data) {
6982
6512
  const pageContent = `
@@ -7205,16 +6735,98 @@ function formatAction(action) {
7205
6735
 
7206
6736
  // src/templates/pages/admin-user-edit.template.ts
7207
6737
  init_admin_layout_catalyst_template();
7208
- function renderUserEditPage(data) {
7209
- const pageContent = `
7210
- <div>
7211
- <!-- Header -->
7212
- <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
7213
- <div>
7214
- <div class="flex items-center gap-3 mb-2">
7215
- <a href="/admin/users" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-950 dark:hover:text-white transition-colors">
7216
- <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7217
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
6738
+
6739
+ // src/templates/components/confirmation-dialog.template.ts
6740
+ function renderConfirmationDialog2(options) {
6741
+ const {
6742
+ id,
6743
+ title,
6744
+ message,
6745
+ confirmText = "Confirm",
6746
+ cancelText = "Cancel",
6747
+ confirmClass = "bg-red-500 hover:bg-red-400",
6748
+ iconColor = "red",
6749
+ onConfirm = ""
6750
+ } = options;
6751
+ const iconColorClasses = {
6752
+ red: "bg-red-500/10 text-red-400",
6753
+ yellow: "bg-yellow-500/10 text-yellow-400",
6754
+ blue: "bg-blue-500/10 text-blue-400"
6755
+ };
6756
+ return `
6757
+ <el-dialog>
6758
+ <dialog
6759
+ id="${id}"
6760
+ aria-labelledby="${id}-title"
6761
+ class="fixed inset-0 m-0 size-auto max-h-none max-w-none overflow-y-auto bg-transparent p-0 backdrop:bg-transparent"
6762
+ >
6763
+ <el-dialog-backdrop class="fixed inset-0 bg-gray-900/50 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in"></el-dialog-backdrop>
6764
+
6765
+ <div tabindex="0" class="flex min-h-full items-end justify-center p-4 text-center focus:outline focus:outline-0 sm:items-center sm:p-0">
6766
+ <el-dialog-panel class="relative transform overflow-hidden rounded-lg bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl outline outline-1 -outline-offset-1 outline-white/10 transition-all data-[closed]:translate-y-4 data-[closed]:opacity-0 data-[enter]:duration-300 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in sm:my-8 sm:w-full sm:max-w-lg sm:p-6 data-[closed]:sm:translate-y-0 data-[closed]:sm:scale-95">
6767
+ <div class="sm:flex sm:items-start">
6768
+ <div class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full ${iconColorClasses[iconColor]} sm:mx-0 sm:size-10">
6769
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" data-slot="icon" aria-hidden="true" class="size-6">
6770
+ <path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" stroke-linecap="round" stroke-linejoin="round" />
6771
+ </svg>
6772
+ </div>
6773
+ <div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
6774
+ <h3 id="${id}-title" class="text-base font-semibold text-white">${title}</h3>
6775
+ <div class="mt-2">
6776
+ <p class="text-sm text-gray-400">${message}</p>
6777
+ </div>
6778
+ </div>
6779
+ </div>
6780
+ <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
6781
+ <button
6782
+ type="button"
6783
+ onclick="${onConfirm}; document.getElementById('${id}').close()"
6784
+ command="close"
6785
+ commandfor="${id}"
6786
+ class="confirm-button inline-flex w-full justify-center rounded-md ${confirmClass} px-3 py-2 text-sm font-semibold text-white sm:ml-3 sm:w-auto"
6787
+ >
6788
+ ${confirmText}
6789
+ </button>
6790
+ <button
6791
+ type="button"
6792
+ command="close"
6793
+ commandfor="${id}"
6794
+ class="mt-3 inline-flex w-full justify-center rounded-md bg-white/10 px-3 py-2 text-sm font-semibold text-white ring-1 ring-inset ring-white/5 hover:bg-white/20 sm:mt-0 sm:w-auto"
6795
+ >
6796
+ ${cancelText}
6797
+ </button>
6798
+ </div>
6799
+ </el-dialog-panel>
6800
+ </div>
6801
+ </dialog>
6802
+ </el-dialog>
6803
+ `;
6804
+ }
6805
+ function getConfirmationDialogScript2() {
6806
+ return `
6807
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindplus/elements@1" type="module"></script>
6808
+ <script>
6809
+ function showConfirmDialog(dialogId) {
6810
+ const dialog = document.getElementById(dialogId);
6811
+ if (dialog) {
6812
+ dialog.showModal();
6813
+ }
6814
+ }
6815
+ </script>
6816
+ `;
6817
+ }
6818
+
6819
+ // src/templates/pages/admin-user-edit.template.ts
6820
+ function renderUserEditPage(data) {
6821
+ const pageContent = `
6822
+ <div>
6823
+ <!-- Header -->
6824
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
6825
+ <div>
6826
+ <div class="flex items-center gap-3 mb-2">
6827
+ <a href="/admin/users" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-950 dark:hover:text-white transition-colors">
6828
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6829
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
7218
6830
  </svg>
7219
6831
  </a>
7220
6832
  <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Edit User</h1>
@@ -7517,7 +7129,7 @@ function renderUserEditPage(data) {
7517
7129
  </script>
7518
7130
 
7519
7131
  <!-- Confirmation Dialogs -->
7520
- ${renderConfirmationDialog({
7132
+ ${renderConfirmationDialog2({
7521
7133
  id: "delete-user-confirm",
7522
7134
  title: "Delete User",
7523
7135
  message: 'Are you sure you want to delete this user? Check the "Hard Delete" option to permanently remove all data from the database. This action cannot be undone!',
@@ -7528,7 +7140,7 @@ function renderUserEditPage(data) {
7528
7140
  onConfirm: "performDeleteUser()"
7529
7141
  })}
7530
7142
 
7531
- ${getConfirmationDialogScript()}
7143
+ ${getConfirmationDialogScript2()}
7532
7144
  `;
7533
7145
  const layoutData = {
7534
7146
  title: "Edit User",
@@ -7855,7 +7467,7 @@ function renderUsersListPage(data) {
7855
7467
  sortable: true,
7856
7468
  sortType: "string",
7857
7469
  render: (value, row) => {
7858
- const escapeHtml4 = (text) => text.replace(/[&<>"']/g, (char) => ({
7470
+ const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
7859
7471
  "&": "&amp;",
7860
7472
  "<": "&lt;",
7861
7473
  ">": "&gt;",
@@ -7864,9 +7476,9 @@ function renderUsersListPage(data) {
7864
7476
  })[char] || char);
7865
7477
  const truncatedFirstName = row.firstName.length > 25 ? row.firstName.substring(0, 25) + "..." : row.firstName;
7866
7478
  const truncatedLastName = row.lastName.length > 25 ? row.lastName.substring(0, 25) + "..." : row.lastName;
7867
- const fullName = escapeHtml4(`${truncatedFirstName} ${truncatedLastName}`);
7479
+ const fullName = escapeHtml7(`${truncatedFirstName} ${truncatedLastName}`);
7868
7480
  const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
7869
- const username = escapeHtml4(truncatedUsername);
7481
+ const username = escapeHtml7(truncatedUsername);
7870
7482
  const statusBadge = row.isActive ? '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-1 ring-inset ring-lime-700/10 dark:ring-lime-400/20 ml-2">Active</span>' : '<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-700/10 dark:ring-red-500/20 ml-2">Inactive</span>';
7871
7483
  return `
7872
7484
  <div>
@@ -7882,14 +7494,14 @@ function renderUsersListPage(data) {
7882
7494
  sortable: true,
7883
7495
  sortType: "string",
7884
7496
  render: (value) => {
7885
- const escapeHtml4 = (text) => text.replace(/[&<>"']/g, (char) => ({
7497
+ const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
7886
7498
  "&": "&amp;",
7887
7499
  "<": "&lt;",
7888
7500
  ">": "&gt;",
7889
7501
  '"': "&quot;",
7890
7502
  "'": "&#39;"
7891
7503
  })[char] || char);
7892
- const escapedEmail = escapeHtml4(value);
7504
+ const escapedEmail = escapeHtml7(value);
7893
7505
  return `<a href="mailto:${escapedEmail}" class="text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 transition-colors">${escapedEmail}</a>`;
7894
7506
  }
7895
7507
  },
@@ -8211,7 +7823,7 @@ function renderUsersListPage(data) {
8211
7823
  </script>
8212
7824
 
8213
7825
  <!-- Confirmation Dialogs -->
8214
- ${renderConfirmationDialog({
7826
+ ${renderConfirmationDialog2({
8215
7827
  id: "toggle-user-status-confirm",
8216
7828
  title: "Toggle User Status",
8217
7829
  message: "Are you sure you want to activate/deactivate this user?",
@@ -8222,7 +7834,7 @@ function renderUsersListPage(data) {
8222
7834
  onConfirm: "performToggleUserStatus()"
8223
7835
  })}
8224
7836
 
8225
- ${getConfirmationDialogScript()}
7837
+ ${getConfirmationDialogScript2()}
8226
7838
  `;
8227
7839
  const layoutData = {
8228
7840
  title: "Users",
@@ -8343,7 +7955,7 @@ userRoutes.put("/profile", async (c) => {
8343
7955
  const language = formData.get("language")?.toString() || "en";
8344
7956
  const emailNotifications = formData.get("email_notifications") === "1";
8345
7957
  if (!firstName || !lastName || !username || !email) {
8346
- return c.html(renderAlert({
7958
+ return c.html(renderAlert2({
8347
7959
  type: "error",
8348
7960
  message: "First name, last name, username, and email are required.",
8349
7961
  dismissible: true
@@ -8351,7 +7963,7 @@ userRoutes.put("/profile", async (c) => {
8351
7963
  }
8352
7964
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8353
7965
  if (!emailRegex.test(email)) {
8354
- return c.html(renderAlert({
7966
+ return c.html(renderAlert2({
8355
7967
  type: "error",
8356
7968
  message: "Please enter a valid email address.",
8357
7969
  dismissible: true
@@ -8363,9 +7975,9 @@ userRoutes.put("/profile", async (c) => {
8363
7975
  `);
8364
7976
  const existingUser = await checkStmt.bind(username, email, user.userId).first();
8365
7977
  if (existingUser) {
8366
- return c.html(renderAlert({
7978
+ return c.html(renderAlert2({
8367
7979
  type: "error",
8368
- message: "Username or email is already taken by another user.",
7980
+ message: "Username or email is already taken by another user!.",
8369
7981
  dismissible: true
8370
7982
  }));
8371
7983
  }
@@ -8399,14 +8011,14 @@ userRoutes.put("/profile", async (c) => {
8399
8011
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
8400
8012
  c.req.header("user-agent")
8401
8013
  );
8402
- return c.html(renderAlert({
8014
+ return c.html(renderAlert2({
8403
8015
  type: "success",
8404
8016
  message: "Profile updated successfully!",
8405
8017
  dismissible: true
8406
8018
  }));
8407
8019
  } catch (error) {
8408
8020
  console.error("Profile update error:", error);
8409
- return c.html(renderAlert({
8021
+ return c.html(renderAlert2({
8410
8022
  type: "error",
8411
8023
  message: "Failed to update profile. Please try again.",
8412
8024
  dismissible: true
@@ -8420,7 +8032,7 @@ userRoutes.post("/profile/avatar", async (c) => {
8420
8032
  const formData = await c.req.formData();
8421
8033
  const avatarFile = formData.get("avatar");
8422
8034
  if (!avatarFile || !avatarFile.name) {
8423
- return c.html(renderAlert({
8035
+ return c.html(renderAlert2({
8424
8036
  type: "error",
8425
8037
  message: "Please select an image file.",
8426
8038
  dismissible: true
@@ -8428,7 +8040,7 @@ userRoutes.post("/profile/avatar", async (c) => {
8428
8040
  }
8429
8041
  const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
8430
8042
  if (!allowedTypes.includes(avatarFile.type)) {
8431
- return c.html(renderAlert({
8043
+ return c.html(renderAlert2({
8432
8044
  type: "error",
8433
8045
  message: "Please upload a valid image file (JPEG, PNG, GIF, or WebP).",
8434
8046
  dismissible: true
@@ -8436,7 +8048,7 @@ userRoutes.post("/profile/avatar", async (c) => {
8436
8048
  }
8437
8049
  const maxSize = 5 * 1024 * 1024;
8438
8050
  if (avatarFile.size > maxSize) {
8439
- return c.html(renderAlert({
8051
+ return c.html(renderAlert2({
8440
8052
  type: "error",
8441
8053
  message: "Image file must be smaller than 5MB.",
8442
8054
  dismissible: true
@@ -8458,14 +8070,14 @@ userRoutes.post("/profile/avatar", async (c) => {
8458
8070
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
8459
8071
  c.req.header("user-agent")
8460
8072
  );
8461
- return c.html(renderAlert({
8073
+ return c.html(renderAlert2({
8462
8074
  type: "success",
8463
8075
  message: "Profile picture updated successfully!",
8464
8076
  dismissible: true
8465
8077
  }));
8466
8078
  } catch (error) {
8467
8079
  console.error("Avatar upload error:", error);
8468
- return c.html(renderAlert({
8080
+ return c.html(renderAlert2({
8469
8081
  type: "error",
8470
8082
  message: "Failed to upload profile picture. Please try again.",
8471
8083
  dismissible: true
@@ -8481,21 +8093,21 @@ userRoutes.post("/profile/password", async (c) => {
8481
8093
  const newPassword = formData.get("new_password")?.toString() || "";
8482
8094
  const confirmPassword = formData.get("confirm_password")?.toString() || "";
8483
8095
  if (!currentPassword || !newPassword || !confirmPassword) {
8484
- return c.html(renderAlert({
8096
+ return c.html(renderAlert2({
8485
8097
  type: "error",
8486
8098
  message: "All password fields are required.",
8487
8099
  dismissible: true
8488
8100
  }));
8489
8101
  }
8490
8102
  if (newPassword !== confirmPassword) {
8491
- return c.html(renderAlert({
8103
+ return c.html(renderAlert2({
8492
8104
  type: "error",
8493
8105
  message: "New passwords do not match.",
8494
8106
  dismissible: true
8495
8107
  }));
8496
8108
  }
8497
8109
  if (newPassword.length < 8) {
8498
- return c.html(renderAlert({
8110
+ return c.html(renderAlert2({
8499
8111
  type: "error",
8500
8112
  message: "New password must be at least 8 characters long.",
8501
8113
  dismissible: true
@@ -8506,7 +8118,7 @@ userRoutes.post("/profile/password", async (c) => {
8506
8118
  `);
8507
8119
  const userData = await userStmt.bind(user.userId).first();
8508
8120
  if (!userData) {
8509
- return c.html(renderAlert({
8121
+ return c.html(renderAlert2({
8510
8122
  type: "error",
8511
8123
  message: "User not found.",
8512
8124
  dismissible: true
@@ -8514,7 +8126,7 @@ userRoutes.post("/profile/password", async (c) => {
8514
8126
  }
8515
8127
  const validPassword = await AuthManager.verifyPassword(currentPassword, userData.password_hash);
8516
8128
  if (!validPassword) {
8517
- return c.html(renderAlert({
8129
+ return c.html(renderAlert2({
8518
8130
  type: "error",
8519
8131
  message: "Current password is incorrect.",
8520
8132
  dismissible: true
@@ -8546,14 +8158,14 @@ userRoutes.post("/profile/password", async (c) => {
8546
8158
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
8547
8159
  c.req.header("user-agent")
8548
8160
  );
8549
- return c.html(renderAlert({
8161
+ return c.html(renderAlert2({
8550
8162
  type: "success",
8551
8163
  message: "Password updated successfully!",
8552
8164
  dismissible: true
8553
8165
  }));
8554
8166
  } catch (error) {
8555
8167
  console.error("Password change error:", error);
8556
- return c.html(renderAlert({
8168
+ return c.html(renderAlert2({
8557
8169
  type: "error",
8558
8170
  message: "Failed to update password. Please try again.",
8559
8171
  dismissible: true
@@ -8672,7 +8284,7 @@ userRoutes.get("/users", requirePermission("users.read"), async (c) => {
8672
8284
  if (isApiRequest) {
8673
8285
  return c.json({ error: "Failed to load users" }, 500);
8674
8286
  }
8675
- return c.html(renderAlert({
8287
+ return c.html(renderAlert2({
8676
8288
  type: "error",
8677
8289
  message: "Failed to load users. Please try again.",
8678
8290
  dismissible: true
@@ -8693,7 +8305,7 @@ userRoutes.get("/users/new", requirePermission("users.create"), async (c) => {
8693
8305
  return c.html(renderUserNewPage(pageData));
8694
8306
  } catch (error) {
8695
8307
  console.error("User new page error:", error);
8696
- return c.html(renderAlert({
8308
+ return c.html(renderAlert2({
8697
8309
  type: "error",
8698
8310
  message: "Failed to load user creation page. Please try again.",
8699
8311
  dismissible: true
@@ -8717,7 +8329,7 @@ userRoutes.post("/users/new", requirePermission("users.create"), async (c) => {
8717
8329
  const isActive = formData.get("is_active") === "1";
8718
8330
  const emailVerified = formData.get("email_verified") === "1";
8719
8331
  if (!firstName || !lastName || !username || !email || !password) {
8720
- return c.html(renderAlert({
8332
+ return c.html(renderAlert2({
8721
8333
  type: "error",
8722
8334
  message: "First name, last name, username, email, and password are required.",
8723
8335
  dismissible: true
@@ -8725,21 +8337,21 @@ userRoutes.post("/users/new", requirePermission("users.create"), async (c) => {
8725
8337
  }
8726
8338
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8727
8339
  if (!emailRegex.test(email)) {
8728
- return c.html(renderAlert({
8340
+ return c.html(renderAlert2({
8729
8341
  type: "error",
8730
8342
  message: "Please enter a valid email address.",
8731
8343
  dismissible: true
8732
8344
  }));
8733
8345
  }
8734
8346
  if (password.length < 8) {
8735
- return c.html(renderAlert({
8347
+ return c.html(renderAlert2({
8736
8348
  type: "error",
8737
8349
  message: "Password must be at least 8 characters long.",
8738
8350
  dismissible: true
8739
8351
  }));
8740
8352
  }
8741
8353
  if (password !== confirmPassword) {
8742
- return c.html(renderAlert({
8354
+ return c.html(renderAlert2({
8743
8355
  type: "error",
8744
8356
  message: "Passwords do not match.",
8745
8357
  dismissible: true
@@ -8751,7 +8363,7 @@ userRoutes.post("/users/new", requirePermission("users.create"), async (c) => {
8751
8363
  `);
8752
8364
  const existingUser = await checkStmt.bind(username, email).first();
8753
8365
  if (existingUser) {
8754
- return c.html(renderAlert({
8366
+ return c.html(renderAlert2({
8755
8367
  type: "error",
8756
8368
  message: "Username or email is already taken.",
8757
8369
  dismissible: true
@@ -8783,7 +8395,7 @@ userRoutes.post("/users/new", requirePermission("users.create"), async (c) => {
8783
8395
  await logActivity(
8784
8396
  db,
8785
8397
  user.userId,
8786
- "user.create",
8398
+ "user!.create",
8787
8399
  "users",
8788
8400
  userId,
8789
8401
  { email, username, role },
@@ -8793,9 +8405,9 @@ userRoutes.post("/users/new", requirePermission("users.create"), async (c) => {
8793
8405
  return c.redirect(`/admin/users/${userId}/edit?success=User created successfully`);
8794
8406
  } catch (error) {
8795
8407
  console.error("User creation error:", error);
8796
- return c.html(renderAlert({
8408
+ return c.html(renderAlert2({
8797
8409
  type: "error",
8798
- message: "Failed to create user. Please try again.",
8410
+ message: "Failed to create user!. Please try again.",
8799
8411
  dismissible: true
8800
8412
  }));
8801
8413
  }
@@ -8821,7 +8433,7 @@ userRoutes.get("/users/:id", requirePermission("users.read"), async (c) => {
8821
8433
  await logActivity(
8822
8434
  db,
8823
8435
  user.userId,
8824
- "user.view",
8436
+ "user!.view",
8825
8437
  "users",
8826
8438
  userId,
8827
8439
  null,
@@ -8864,7 +8476,7 @@ userRoutes.get("/users/:id/edit", requirePermission("users.update"), async (c) =
8864
8476
  `);
8865
8477
  const userToEdit = await userStmt.bind(userId).first();
8866
8478
  if (!userToEdit) {
8867
- return c.html(renderAlert({
8479
+ return c.html(renderAlert2({
8868
8480
  type: "error",
8869
8481
  message: "User not found",
8870
8482
  dismissible: true
@@ -8898,9 +8510,9 @@ userRoutes.get("/users/:id/edit", requirePermission("users.update"), async (c) =
8898
8510
  return c.html(renderUserEditPage(pageData));
8899
8511
  } catch (error) {
8900
8512
  console.error("User edit page error:", error);
8901
- return c.html(renderAlert({
8513
+ return c.html(renderAlert2({
8902
8514
  type: "error",
8903
- message: "Failed to load user. Please try again.",
8515
+ message: "Failed to load user!. Please try again.",
8904
8516
  dismissible: true
8905
8517
  }), 500);
8906
8518
  }
@@ -8921,7 +8533,7 @@ userRoutes.put("/users/:id", requirePermission("users.update"), async (c) => {
8921
8533
  const isActive = formData.get("is_active") === "1";
8922
8534
  const emailVerified = formData.get("email_verified") === "1";
8923
8535
  if (!firstName || !lastName || !username || !email) {
8924
- return c.html(renderAlert({
8536
+ return c.html(renderAlert2({
8925
8537
  type: "error",
8926
8538
  message: "First name, last name, username, and email are required.",
8927
8539
  dismissible: true
@@ -8929,7 +8541,7 @@ userRoutes.put("/users/:id", requirePermission("users.update"), async (c) => {
8929
8541
  }
8930
8542
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8931
8543
  if (!emailRegex.test(email)) {
8932
- return c.html(renderAlert({
8544
+ return c.html(renderAlert2({
8933
8545
  type: "error",
8934
8546
  message: "Please enter a valid email address.",
8935
8547
  dismissible: true
@@ -8941,9 +8553,9 @@ userRoutes.put("/users/:id", requirePermission("users.update"), async (c) => {
8941
8553
  `);
8942
8554
  const existingUser = await checkStmt.bind(username, email, userId).first();
8943
8555
  if (existingUser) {
8944
- return c.html(renderAlert({
8556
+ return c.html(renderAlert2({
8945
8557
  type: "error",
8946
- message: "Username or email is already taken by another user.",
8558
+ message: "Username or email is already taken by another user!.",
8947
8559
  dismissible: true
8948
8560
  }));
8949
8561
  }
@@ -8970,23 +8582,23 @@ userRoutes.put("/users/:id", requirePermission("users.update"), async (c) => {
8970
8582
  await logActivity(
8971
8583
  db,
8972
8584
  user.userId,
8973
- "user.update",
8585
+ "user!.update",
8974
8586
  "users",
8975
8587
  userId,
8976
8588
  { fields: ["first_name", "last_name", "username", "email", "phone", "bio", "role", "is_active", "email_verified"] },
8977
8589
  c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
8978
8590
  c.req.header("user-agent")
8979
8591
  );
8980
- return c.html(renderAlert({
8592
+ return c.html(renderAlert2({
8981
8593
  type: "success",
8982
8594
  message: "User updated successfully!",
8983
8595
  dismissible: true
8984
8596
  }));
8985
8597
  } catch (error) {
8986
8598
  console.error("User update error:", error);
8987
- return c.html(renderAlert({
8599
+ return c.html(renderAlert2({
8988
8600
  type: "error",
8989
- message: "Failed to update user. Please try again.",
8601
+ message: "Failed to update user!. Please try again.",
8990
8602
  dismissible: true
8991
8603
  }));
8992
8604
  }
@@ -9016,7 +8628,7 @@ userRoutes.delete("/users/:id", requirePermission("users.delete"), async (c) =>
9016
8628
  await logActivity(
9017
8629
  db,
9018
8630
  user.userId,
9019
- "user.hard_delete",
8631
+ "user!.hard_delete",
9020
8632
  "users",
9021
8633
  userId,
9022
8634
  { email: userToDelete.email, permanent: true },
@@ -9035,7 +8647,7 @@ userRoutes.delete("/users/:id", requirePermission("users.delete"), async (c) =>
9035
8647
  await logActivity(
9036
8648
  db,
9037
8649
  user.userId,
9038
- "user.soft_delete",
8650
+ "user!.soft_delete",
9039
8651
  "users",
9040
8652
  userId,
9041
8653
  { email: userToDelete.email },
@@ -9102,7 +8714,7 @@ userRoutes.post("/invite-user", requirePermission("users.create"), async (c) =>
9102
8714
  await logActivity(
9103
8715
  db,
9104
8716
  user.userId,
9105
- "user.invite_sent",
8717
+ "user!.invite_sent",
9106
8718
  "users",
9107
8719
  userId,
9108
8720
  { email, role, invited_user_id: userId },
@@ -9159,7 +8771,7 @@ userRoutes.post("/resend-invitation/:id", requirePermission("users.create"), asy
9159
8771
  await logActivity(
9160
8772
  db,
9161
8773
  user.userId,
9162
- "user.invitation_resent",
8774
+ "user!.invitation_resent",
9163
8775
  "users",
9164
8776
  userId,
9165
8777
  { email: invitedUser.email },
@@ -9195,7 +8807,7 @@ userRoutes.delete("/cancel-invitation/:id", requirePermission("users.delete"), a
9195
8807
  await logActivity(
9196
8808
  db,
9197
8809
  user.userId,
9198
- "user.invitation_cancelled",
8810
+ "user!.invitation_cancelled",
9199
8811
  "users",
9200
8812
  userId,
9201
8813
  { email: invitedUser.email },
@@ -10503,7 +10115,7 @@ function renderMediaLibraryPage(data) {
10503
10115
  </script>
10504
10116
 
10505
10117
  <!-- Confirmation Dialog for Bulk Delete -->
10506
- ${renderConfirmationDialog({
10118
+ ${renderConfirmationDialog2({
10507
10119
  id: "media-bulk-delete-confirm",
10508
10120
  title: "Delete Selected Files",
10509
10121
  message: `Are you sure you want to delete ${data.files.length > 0 ? "the selected files" : "these files"}? This action cannot be undone and the files will be permanently removed.`,
@@ -10515,7 +10127,7 @@ function renderMediaLibraryPage(data) {
10515
10127
  })}
10516
10128
 
10517
10129
  <!-- Confirmation Dialog Script -->
10518
- ${getConfirmationDialogScript()}
10130
+ ${getConfirmationDialogScript2()}
10519
10131
  `;
10520
10132
  function buildPageUrl(page, folder, type) {
10521
10133
  const params = new URLSearchParams();
@@ -11803,7 +11415,7 @@ function renderPluginsListPage(data) {
11803
11415
  </script>
11804
11416
 
11805
11417
  <!-- Confirmation Dialogs -->
11806
- ${renderConfirmationDialog({
11418
+ ${renderConfirmationDialog2({
11807
11419
  id: "uninstall-plugin-confirm",
11808
11420
  title: "Uninstall Plugin",
11809
11421
  message: "Are you sure you want to uninstall this plugin? This action cannot be undone.",
@@ -11814,7 +11426,7 @@ function renderPluginsListPage(data) {
11814
11426
  onConfirm: "performUninstallPlugin()"
11815
11427
  })}
11816
11428
 
11817
- ${getConfirmationDialogScript()}
11429
+ ${getConfirmationDialogScript2()}
11818
11430
  `;
11819
11431
  const layoutData = {
11820
11432
  title: "Plugins",
@@ -14207,10 +13819,1943 @@ function getCategoryClass(category) {
14207
13819
  return "bg-gray-100 text-gray-800";
14208
13820
  }
14209
13821
  }
13822
+ var adminDesignRoutes = new Hono();
13823
+ adminDesignRoutes.get("/", (c) => {
13824
+ const user = c.get("user");
13825
+ const pageData = {
13826
+ user: user ? {
13827
+ name: user.email,
13828
+ email: user.email,
13829
+ role: user.role
13830
+ } : void 0
13831
+ };
13832
+ return c.html(renderDesignPage(pageData));
13833
+ });
13834
+ var adminCheckboxRoutes = new Hono();
13835
+ adminCheckboxRoutes.get("/", (c) => {
13836
+ const user = c.get("user");
13837
+ const pageData = {
13838
+ user: user ? {
13839
+ name: user.email,
13840
+ email: user.email,
13841
+ role: user.role
13842
+ } : void 0
13843
+ };
13844
+ return c.html(renderCheckboxPage(pageData));
13845
+ });
13846
+
13847
+ // src/templates/pages/admin-faq-form.template.ts
13848
+ function renderFAQForm(data) {
13849
+ const { faq, isEdit, errors, message, messageType } = data;
13850
+ const pageTitle = isEdit ? "Edit FAQ" : "New FAQ";
13851
+ const pageContent = `
13852
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
13853
+ <!-- Header -->
13854
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
13855
+ <div>
13856
+ <h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
13857
+ <p class="mt-2 text-sm text-gray-300">
13858
+ ${isEdit ? "Update the FAQ details below" : "Create a new frequently asked question"}
13859
+ </p>
13860
+ </div>
13861
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
13862
+ <a href="/admin/faq"
13863
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
13864
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13865
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
13866
+ </svg>
13867
+ Back to List
13868
+ </a>
13869
+ </div>
13870
+ </div>
13871
+
13872
+ ${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
13873
+
13874
+ <!-- Form -->
13875
+ <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
13876
+ <form ${isEdit ? `hx-put="/admin/faq/${faq?.id}"` : 'hx-post="/admin/faq"'}
13877
+ hx-target="body"
13878
+ hx-swap="outerHTML"
13879
+ class="space-y-6 p-6">
13880
+
13881
+ <!-- Question -->
13882
+ <div>
13883
+ <label for="question" class="block text-sm font-medium text-white">
13884
+ Question <span class="text-red-400">*</span>
13885
+ </label>
13886
+ <div class="mt-1">
13887
+ <textarea name="question"
13888
+ id="question"
13889
+ rows="3"
13890
+ required
13891
+ maxlength="500"
13892
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13893
+ placeholder="Enter the frequently asked question...">${faq?.question || ""}</textarea>
13894
+ <p class="mt-1 text-sm text-gray-300">
13895
+ <span id="question-count">0</span>/500 characters
13896
+ </p>
13897
+ </div>
13898
+ ${errors?.question ? `
13899
+ <div class="mt-1">
13900
+ ${errors.question.map((error) => `
13901
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13902
+ `).join("")}
13903
+ </div>
13904
+ ` : ""}
13905
+ </div>
13906
+
13907
+ <!-- Answer -->
13908
+ <div>
13909
+ <label for="answer" class="block text-sm font-medium text-white">
13910
+ Answer <span class="text-red-400">*</span>
13911
+ </label>
13912
+ <div class="mt-1">
13913
+ <textarea name="answer"
13914
+ id="answer"
13915
+ rows="6"
13916
+ required
13917
+ maxlength="2000"
13918
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
13919
+ placeholder="Enter the detailed answer...">${faq?.answer || ""}</textarea>
13920
+ <p class="mt-1 text-sm text-gray-300">
13921
+ <span id="answer-count">0</span>/2000 characters. You can use basic HTML for formatting.
13922
+ </p>
13923
+ </div>
13924
+ ${errors?.answer ? `
13925
+ <div class="mt-1">
13926
+ ${errors.answer.map((error) => `
13927
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13928
+ `).join("")}
13929
+ </div>
13930
+ ` : ""}
13931
+ </div>
13932
+
13933
+ <!-- Category and Tags Row -->
13934
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
13935
+ <!-- Category -->
13936
+ <div>
13937
+ <label for="category" class="block text-sm font-medium text-white">Category</label>
13938
+ <div class="mt-1">
13939
+ <select name="category"
13940
+ id="category"
13941
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
13942
+ <option value="">Select a category</option>
13943
+ <option value="general" ${faq?.category === "general" ? "selected" : ""}>General</option>
13944
+ <option value="technical" ${faq?.category === "technical" ? "selected" : ""}>Technical</option>
13945
+ <option value="billing" ${faq?.category === "billing" ? "selected" : ""}>Billing</option>
13946
+ <option value="support" ${faq?.category === "support" ? "selected" : ""}>Support</option>
13947
+ <option value="account" ${faq?.category === "account" ? "selected" : ""}>Account</option>
13948
+ <option value="features" ${faq?.category === "features" ? "selected" : ""}>Features</option>
13949
+ </select>
13950
+ </div>
13951
+ ${errors?.category ? `
13952
+ <div class="mt-1">
13953
+ ${errors.category.map((error) => `
13954
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13955
+ `).join("")}
13956
+ </div>
13957
+ ` : ""}
13958
+ </div>
13959
+
13960
+ <!-- Tags -->
13961
+ <div>
13962
+ <label for="tags" class="block text-sm font-medium text-white">Tags</label>
13963
+ <div class="mt-1">
13964
+ <input type="text"
13965
+ name="tags"
13966
+ id="tags"
13967
+ value="${faq?.tags || ""}"
13968
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
13969
+ placeholder="e.g., payment, setup, troubleshooting">
13970
+ <p class="mt-1 text-sm text-gray-300">Separate multiple tags with commas</p>
13971
+ </div>
13972
+ ${errors?.tags ? `
13973
+ <div class="mt-1">
13974
+ ${errors.tags.map((error) => `
13975
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
13976
+ `).join("")}
13977
+ </div>
13978
+ ` : ""}
13979
+ </div>
13980
+ </div>
13981
+
13982
+ <!-- Status and Sort Order Row -->
13983
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
13984
+ <!-- Published Status -->
13985
+ <div>
13986
+ <label class="block text-sm font-medium text-white">Status</label>
13987
+ <div class="mt-2 space-y-2">
13988
+ <div class="flex items-center">
13989
+ <input id="published"
13990
+ name="isPublished"
13991
+ type="radio"
13992
+ value="true"
13993
+ ${!faq || faq.isPublished ? "checked" : ""}
13994
+ class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
13995
+ <label for="published" class="ml-2 block text-sm text-white">
13996
+ Published <span class="text-gray-300">(visible to users)</span>
13997
+ </label>
13998
+ </div>
13999
+ <div class="flex items-center">
14000
+ <input id="draft"
14001
+ name="isPublished"
14002
+ type="radio"
14003
+ value="false"
14004
+ ${faq && !faq.isPublished ? "checked" : ""}
14005
+ class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
14006
+ <label for="draft" class="ml-2 block text-sm text-white">
14007
+ Draft <span class="text-gray-300">(not visible to users)</span>
14008
+ </label>
14009
+ </div>
14010
+ </div>
14011
+ </div>
14012
+
14013
+ <!-- Sort Order -->
14014
+ <div>
14015
+ <label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
14016
+ <div class="mt-1">
14017
+ <input type="number"
14018
+ name="sortOrder"
14019
+ id="sortOrder"
14020
+ value="${faq?.sortOrder || 0}"
14021
+ min="0"
14022
+ step="1"
14023
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
14024
+ <p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
14025
+ </div>
14026
+ ${errors?.sortOrder ? `
14027
+ <div class="mt-1">
14028
+ ${errors.sortOrder.map((error) => `
14029
+ <p class="text-sm text-red-400">${escapeHtml4(error)}</p>
14030
+ `).join("")}
14031
+ </div>
14032
+ ` : ""}
14033
+ </div>
14034
+ </div>
14035
+
14036
+ <!-- Form Actions -->
14037
+ <div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
14038
+ <a href="/admin/faq"
14039
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
14040
+ Cancel
14041
+ </a>
14042
+ <button type="submit"
14043
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-blue-500/80 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-blue-500 transition-all">
14044
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14045
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
14046
+ </svg>
14047
+ ${isEdit ? "Update FAQ" : "Create FAQ"}
14048
+ </button>
14049
+ </div>
14050
+ </form>
14051
+ </div>
14052
+ </div>
14053
+
14054
+ <script>
14055
+ // Character count for question
14056
+ const questionTextarea = document.getElementById('question');
14057
+ const questionCount = document.getElementById('question-count');
14058
+
14059
+ function updateQuestionCount() {
14060
+ questionCount.textContent = questionTextarea.value.length;
14061
+ }
14062
+
14063
+ questionTextarea.addEventListener('input', updateQuestionCount);
14064
+ updateQuestionCount(); // Initial count
14065
+
14066
+ // Character count for answer
14067
+ const answerTextarea = document.getElementById('answer');
14068
+ const answerCount = document.getElementById('answer-count');
14069
+
14070
+ function updateAnswerCount() {
14071
+ answerCount.textContent = answerTextarea.value.length;
14072
+ }
14073
+
14074
+ answerTextarea.addEventListener('input', updateAnswerCount);
14075
+ updateAnswerCount(); // Initial count
14076
+ </script>
14077
+ `;
14078
+ const layoutData = {
14079
+ title: `${pageTitle} - Admin`,
14080
+ pageTitle,
14081
+ currentPath: isEdit ? `/admin/faq/${faq?.id}` : "/admin/faq/new",
14082
+ user: data.user,
14083
+ content: pageContent
14084
+ };
14085
+ return renderAdminLayout(layoutData);
14086
+ }
14087
+ function escapeHtml4(unsafe) {
14088
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14089
+ }
14090
+
14091
+ // src/routes/admin-faq.ts
14092
+ var faqSchema = z.object({
14093
+ question: z.string().min(1, "Question is required").max(500, "Question must be under 500 characters"),
14094
+ answer: z.string().min(1, "Answer is required").max(2e3, "Answer must be under 2000 characters"),
14095
+ category: z.string().optional(),
14096
+ tags: z.string().optional(),
14097
+ isPublished: z.string().transform((val) => val === "true"),
14098
+ sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
14099
+ });
14100
+ var adminFAQRoutes = new Hono();
14101
+ adminFAQRoutes.get("/", async (c) => {
14102
+ try {
14103
+ const user = c.get("user");
14104
+ const { category, published, search, page = "1" } = c.req.query();
14105
+ const currentPage = parseInt(page, 10) || 1;
14106
+ const limit = 20;
14107
+ const offset = (currentPage - 1) * limit;
14108
+ const db = c.env?.DB;
14109
+ if (!db) {
14110
+ return c.html(renderFAQList({
14111
+ faqs: [],
14112
+ totalCount: 0,
14113
+ currentPage: 1,
14114
+ totalPages: 1,
14115
+ user: user ? {
14116
+ name: user.email,
14117
+ email: user.email,
14118
+ role: user.role
14119
+ } : void 0,
14120
+ message: "Database not available",
14121
+ messageType: "error"
14122
+ }));
14123
+ }
14124
+ let whereClause = "WHERE 1=1";
14125
+ const params = [];
14126
+ if (category) {
14127
+ whereClause += " AND category = ?";
14128
+ params.push(category);
14129
+ }
14130
+ if (published !== void 0) {
14131
+ whereClause += " AND isPublished = ?";
14132
+ params.push(published === "true" ? 1 : 0);
14133
+ }
14134
+ if (search) {
14135
+ whereClause += " AND (question LIKE ? OR answer LIKE ? OR tags LIKE ?)";
14136
+ const searchTerm = `%${search}%`;
14137
+ params.push(searchTerm, searchTerm, searchTerm);
14138
+ }
14139
+ const countQuery = `SELECT COUNT(*) as count FROM faqs ${whereClause}`;
14140
+ const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
14141
+ const totalCount = countResults?.[0]?.count || 0;
14142
+ const dataQuery = `
14143
+ SELECT * FROM faqs
14144
+ ${whereClause}
14145
+ ORDER BY sortOrder ASC, created_at DESC
14146
+ LIMIT ? OFFSET ?
14147
+ `;
14148
+ const { results: faqs } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
14149
+ const totalPages = Math.ceil(totalCount / limit);
14150
+ return c.html(renderFAQList({
14151
+ faqs: faqs || [],
14152
+ totalCount,
14153
+ currentPage,
14154
+ totalPages,
14155
+ user: user ? {
14156
+ name: user.email,
14157
+ email: user.email,
14158
+ role: user.role
14159
+ } : void 0
14160
+ }));
14161
+ } catch (error) {
14162
+ console.error("Error fetching FAQs:", error);
14163
+ const user = c.get("user");
14164
+ return c.html(renderFAQList({
14165
+ faqs: [],
14166
+ totalCount: 0,
14167
+ currentPage: 1,
14168
+ totalPages: 1,
14169
+ user: user ? {
14170
+ name: user.email,
14171
+ email: user.email,
14172
+ role: user.role
14173
+ } : void 0,
14174
+ message: "Failed to load FAQs",
14175
+ messageType: "error"
14176
+ }));
14177
+ }
14178
+ });
14179
+ adminFAQRoutes.get("/new", async (c) => {
14180
+ const user = c.get("user");
14181
+ return c.html(renderFAQForm({
14182
+ isEdit: false,
14183
+ user: user ? {
14184
+ name: user.email,
14185
+ email: user.email,
14186
+ role: user.role
14187
+ } : void 0
14188
+ }));
14189
+ });
14190
+ adminFAQRoutes.post("/", async (c) => {
14191
+ try {
14192
+ const formData = await c.req.formData();
14193
+ const data = Object.fromEntries(formData.entries());
14194
+ const validatedData = faqSchema.parse(data);
14195
+ const user = c.get("user");
14196
+ const db = c.env?.DB;
14197
+ if (!db) {
14198
+ return c.html(renderFAQForm({
14199
+ isEdit: false,
14200
+ user: user ? {
14201
+ name: user.email,
14202
+ email: user.email,
14203
+ role: user.role
14204
+ } : void 0,
14205
+ message: "Database not available",
14206
+ messageType: "error"
14207
+ }));
14208
+ }
14209
+ const { results } = await db.prepare(`
14210
+ INSERT INTO faqs (question, answer, category, tags, isPublished, sortOrder)
14211
+ VALUES (?, ?, ?, ?, ?, ?)
14212
+ RETURNING *
14213
+ `).bind(
14214
+ validatedData.question,
14215
+ validatedData.answer,
14216
+ validatedData.category || null,
14217
+ validatedData.tags || null,
14218
+ validatedData.isPublished ? 1 : 0,
14219
+ validatedData.sortOrder
14220
+ ).all();
14221
+ if (results && results.length > 0) {
14222
+ return c.redirect("/admin/faq?message=FAQ created successfully");
14223
+ } else {
14224
+ return c.html(renderFAQForm({
14225
+ isEdit: false,
14226
+ user: user ? {
14227
+ name: user.email,
14228
+ email: user.email,
14229
+ role: user.role
14230
+ } : void 0,
14231
+ message: "Failed to create FAQ",
14232
+ messageType: "error"
14233
+ }));
14234
+ }
14235
+ } catch (error) {
14236
+ console.error("Error creating FAQ:", error);
14237
+ const user = c.get("user");
14238
+ if (error instanceof z.ZodError) {
14239
+ const errors = {};
14240
+ error.errors.forEach((err) => {
14241
+ const field = err.path[0];
14242
+ if (!errors[field]) errors[field] = [];
14243
+ errors[field].push(err.message);
14244
+ });
14245
+ return c.html(renderFAQForm({
14246
+ isEdit: false,
14247
+ user: user ? {
14248
+ name: user.email,
14249
+ email: user.email,
14250
+ role: user.role
14251
+ } : void 0,
14252
+ errors,
14253
+ message: "Please correct the errors below",
14254
+ messageType: "error"
14255
+ }));
14256
+ }
14257
+ return c.html(renderFAQForm({
14258
+ isEdit: false,
14259
+ user: user ? {
14260
+ name: user.email,
14261
+ email: user.email,
14262
+ role: user.role
14263
+ } : void 0,
14264
+ message: "Failed to create FAQ",
14265
+ messageType: "error"
14266
+ }));
14267
+ }
14268
+ });
14269
+ adminFAQRoutes.get("/:id", async (c) => {
14270
+ try {
14271
+ const id = parseInt(c.req.param("id"));
14272
+ const user = c.get("user");
14273
+ const db = c.env?.DB;
14274
+ if (!db) {
14275
+ return c.html(renderFAQForm({
14276
+ isEdit: true,
14277
+ user: user ? {
14278
+ name: user.email,
14279
+ email: user.email,
14280
+ role: user.role
14281
+ } : void 0,
14282
+ message: "Database not available",
14283
+ messageType: "error"
14284
+ }));
14285
+ }
14286
+ const { results } = await db.prepare("SELECT * FROM faqs WHERE id = ?").bind(id).all();
14287
+ if (!results || results.length === 0) {
14288
+ return c.redirect("/admin/faq?message=FAQ not found&type=error");
14289
+ }
14290
+ const faq = results[0];
14291
+ return c.html(renderFAQForm({
14292
+ faq: {
14293
+ id: faq.id,
14294
+ question: faq.question,
14295
+ answer: faq.answer,
14296
+ category: faq.category,
14297
+ tags: faq.tags,
14298
+ isPublished: Boolean(faq.isPublished),
14299
+ sortOrder: faq.sortOrder
14300
+ },
14301
+ isEdit: true,
14302
+ user: user ? {
14303
+ name: user.email,
14304
+ email: user.email,
14305
+ role: user.role
14306
+ } : void 0
14307
+ }));
14308
+ } catch (error) {
14309
+ console.error("Error fetching FAQ:", error);
14310
+ const user = c.get("user");
14311
+ return c.html(renderFAQForm({
14312
+ isEdit: true,
14313
+ user: user ? {
14314
+ name: user.email,
14315
+ email: user.email,
14316
+ role: user.role
14317
+ } : void 0,
14318
+ message: "Failed to load FAQ",
14319
+ messageType: "error"
14320
+ }));
14321
+ }
14322
+ });
14323
+ adminFAQRoutes.put("/:id", async (c) => {
14324
+ try {
14325
+ const id = parseInt(c.req.param("id"));
14326
+ const formData = await c.req.formData();
14327
+ const data = Object.fromEntries(formData.entries());
14328
+ const validatedData = faqSchema.parse(data);
14329
+ const user = c.get("user");
14330
+ const db = c.env?.DB;
14331
+ if (!db) {
14332
+ return c.html(renderFAQForm({
14333
+ isEdit: true,
14334
+ user: user ? {
14335
+ name: user.email,
14336
+ email: user.email,
14337
+ role: user.role
14338
+ } : void 0,
14339
+ message: "Database not available",
14340
+ messageType: "error"
14341
+ }));
14342
+ }
14343
+ const { results } = await db.prepare(`
14344
+ UPDATE faqs
14345
+ SET question = ?, answer = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
14346
+ WHERE id = ?
14347
+ RETURNING *
14348
+ `).bind(
14349
+ validatedData.question,
14350
+ validatedData.answer,
14351
+ validatedData.category || null,
14352
+ validatedData.tags || null,
14353
+ validatedData.isPublished ? 1 : 0,
14354
+ validatedData.sortOrder,
14355
+ id
14356
+ ).all();
14357
+ if (results && results.length > 0) {
14358
+ return c.redirect("/admin/faq?message=FAQ updated successfully");
14359
+ } else {
14360
+ return c.html(renderFAQForm({
14361
+ faq: {
14362
+ id,
14363
+ question: validatedData.question,
14364
+ answer: validatedData.answer,
14365
+ category: validatedData.category,
14366
+ tags: validatedData.tags,
14367
+ isPublished: validatedData.isPublished,
14368
+ sortOrder: validatedData.sortOrder
14369
+ },
14370
+ isEdit: true,
14371
+ user: user ? {
14372
+ name: user.email,
14373
+ email: user.email,
14374
+ role: user.role
14375
+ } : void 0,
14376
+ message: "FAQ not found",
14377
+ messageType: "error"
14378
+ }));
14379
+ }
14380
+ } catch (error) {
14381
+ console.error("Error updating FAQ:", error);
14382
+ const user = c.get("user");
14383
+ const id = parseInt(c.req.param("id"));
14384
+ if (error instanceof z.ZodError) {
14385
+ const errors = {};
14386
+ error.errors.forEach((err) => {
14387
+ const field = err.path[0];
14388
+ if (!errors[field]) errors[field] = [];
14389
+ errors[field].push(err.message);
14390
+ });
14391
+ return c.html(renderFAQForm({
14392
+ faq: {
14393
+ id,
14394
+ question: "",
14395
+ answer: "",
14396
+ category: "",
14397
+ tags: "",
14398
+ isPublished: true,
14399
+ sortOrder: 0
14400
+ },
14401
+ isEdit: true,
14402
+ user: user ? {
14403
+ name: user.email,
14404
+ email: user.email,
14405
+ role: user.role
14406
+ } : void 0,
14407
+ errors,
14408
+ message: "Please correct the errors below",
14409
+ messageType: "error"
14410
+ }));
14411
+ }
14412
+ return c.html(renderFAQForm({
14413
+ faq: {
14414
+ id,
14415
+ question: "",
14416
+ answer: "",
14417
+ category: "",
14418
+ tags: "",
14419
+ isPublished: true,
14420
+ sortOrder: 0
14421
+ },
14422
+ isEdit: true,
14423
+ user: user ? {
14424
+ name: user.email,
14425
+ email: user.email,
14426
+ role: user.role
14427
+ } : void 0,
14428
+ message: "Failed to update FAQ",
14429
+ messageType: "error"
14430
+ }));
14431
+ }
14432
+ });
14433
+ adminFAQRoutes.delete("/:id", async (c) => {
14434
+ try {
14435
+ const id = parseInt(c.req.param("id"));
14436
+ const db = c.env?.DB;
14437
+ if (!db) {
14438
+ return c.json({ error: "Database not available" }, 500);
14439
+ }
14440
+ const { changes } = await db.prepare("DELETE FROM faqs WHERE id = ?").bind(id).run();
14441
+ if (changes === 0) {
14442
+ return c.json({ error: "FAQ not found" }, 404);
14443
+ }
14444
+ return c.redirect("/admin/faq?message=FAQ deleted successfully");
14445
+ } catch (error) {
14446
+ console.error("Error deleting FAQ:", error);
14447
+ return c.json({ error: "Failed to delete FAQ" }, 500);
14448
+ }
14449
+ });
14450
+ var admin_faq_default = adminFAQRoutes;
14451
+
14452
+ // src/templates/pages/admin-testimonials-form.template.ts
14453
+ function renderTestimonialsForm(data) {
14454
+ const { testimonial, isEdit, errors, message, messageType } = data;
14455
+ const pageTitle = isEdit ? "Edit Testimonial" : "New Testimonial";
14456
+ const pageContent = `
14457
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
14458
+ <!-- Header -->
14459
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
14460
+ <div>
14461
+ <h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
14462
+ <p class="mt-2 text-sm text-gray-300">
14463
+ ${isEdit ? "Update the testimonial details below" : "Create a new customer testimonial"}
14464
+ </p>
14465
+ </div>
14466
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
14467
+ <a href="/admin/testimonials"
14468
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
14469
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14470
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
14471
+ </svg>
14472
+ Back to List
14473
+ </a>
14474
+ </div>
14475
+ </div>
14476
+
14477
+ ${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
14478
+
14479
+ <!-- Form -->
14480
+ <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
14481
+ <form ${isEdit ? `hx-put="/admin/testimonials/${testimonial?.id}"` : 'hx-post="/admin/testimonials"'}
14482
+ hx-target="body"
14483
+ hx-swap="outerHTML"
14484
+ class="space-y-6 p-6">
14485
+
14486
+ <!-- Author Information Section -->
14487
+ <div>
14488
+ <h2 class="text-lg font-medium text-white mb-4">Author Information</h2>
14489
+
14490
+ <!-- Author Name -->
14491
+ <div class="mb-4">
14492
+ <label for="authorName" class="block text-sm font-medium text-white">
14493
+ Author Name <span class="text-red-400">*</span>
14494
+ </label>
14495
+ <div class="mt-1">
14496
+ <input type="text"
14497
+ name="authorName"
14498
+ id="authorName"
14499
+ value="${testimonial?.authorName || ""}"
14500
+ required
14501
+ maxlength="100"
14502
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
14503
+ placeholder="John Doe">
14504
+ </div>
14505
+ ${errors?.authorName ? `
14506
+ <div class="mt-1">
14507
+ ${errors.authorName.map((error) => `
14508
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14509
+ `).join("")}
14510
+ </div>
14511
+ ` : ""}
14512
+ </div>
14513
+
14514
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
14515
+ <!-- Author Title -->
14516
+ <div>
14517
+ <label for="authorTitle" class="block text-sm font-medium text-white">Title/Position</label>
14518
+ <div class="mt-1">
14519
+ <input type="text"
14520
+ name="authorTitle"
14521
+ id="authorTitle"
14522
+ value="${testimonial?.authorTitle || ""}"
14523
+ maxlength="100"
14524
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
14525
+ placeholder="CEO">
14526
+ </div>
14527
+ ${errors?.authorTitle ? `
14528
+ <div class="mt-1">
14529
+ ${errors.authorTitle.map((error) => `
14530
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14531
+ `).join("")}
14532
+ </div>
14533
+ ` : ""}
14534
+ </div>
14535
+
14536
+ <!-- Author Company -->
14537
+ <div>
14538
+ <label for="authorCompany" class="block text-sm font-medium text-white">Company</label>
14539
+ <div class="mt-1">
14540
+ <input type="text"
14541
+ name="authorCompany"
14542
+ id="authorCompany"
14543
+ value="${testimonial?.authorCompany || ""}"
14544
+ maxlength="100"
14545
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
14546
+ placeholder="Acme Corp">
14547
+ </div>
14548
+ ${errors?.authorCompany ? `
14549
+ <div class="mt-1">
14550
+ ${errors.authorCompany.map((error) => `
14551
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14552
+ `).join("")}
14553
+ </div>
14554
+ ` : ""}
14555
+ </div>
14556
+ </div>
14557
+ </div>
14558
+
14559
+ <!-- Testimonial Content Section -->
14560
+ <div>
14561
+ <h2 class="text-lg font-medium text-white mb-4">Testimonial</h2>
14562
+
14563
+ <!-- Testimonial Text -->
14564
+ <div class="mb-4">
14565
+ <label for="testimonialText" class="block text-sm font-medium text-white">
14566
+ Testimonial <span class="text-red-400">*</span>
14567
+ </label>
14568
+ <div class="mt-1">
14569
+ <textarea name="testimonialText"
14570
+ id="testimonialText"
14571
+ rows="6"
14572
+ required
14573
+ maxlength="1000"
14574
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
14575
+ placeholder="Enter the customer's testimonial...">${testimonial?.testimonialText || ""}</textarea>
14576
+ <p class="mt-1 text-sm text-gray-300">
14577
+ <span id="testimonial-count">0</span>/1000 characters
14578
+ </p>
14579
+ </div>
14580
+ ${errors?.testimonialText ? `
14581
+ <div class="mt-1">
14582
+ ${errors.testimonialText.map((error) => `
14583
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14584
+ `).join("")}
14585
+ </div>
14586
+ ` : ""}
14587
+ </div>
14588
+
14589
+ <!-- Rating -->
14590
+ <div>
14591
+ <label for="rating" class="block text-sm font-medium text-white">Rating (Optional)</label>
14592
+ <div class="mt-1">
14593
+ <select name="rating"
14594
+ id="rating"
14595
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
14596
+ <option value="">No rating</option>
14597
+ <option value="5" ${testimonial?.rating === 5 ? "selected" : ""}>\u2B50\u2B50\u2B50\u2B50\u2B50 (5 stars)</option>
14598
+ <option value="4" ${testimonial?.rating === 4 ? "selected" : ""}>\u2B50\u2B50\u2B50\u2B50 (4 stars)</option>
14599
+ <option value="3" ${testimonial?.rating === 3 ? "selected" : ""}>\u2B50\u2B50\u2B50 (3 stars)</option>
14600
+ <option value="2" ${testimonial?.rating === 2 ? "selected" : ""}>\u2B50\u2B50 (2 stars)</option>
14601
+ <option value="1" ${testimonial?.rating === 1 ? "selected" : ""}>\u2B50 (1 star)</option>
14602
+ </select>
14603
+ </div>
14604
+ ${errors?.rating ? `
14605
+ <div class="mt-1">
14606
+ ${errors.rating.map((error) => `
14607
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14608
+ `).join("")}
14609
+ </div>
14610
+ ` : ""}
14611
+ </div>
14612
+ </div>
14613
+
14614
+ <!-- Status and Sort Order Row -->
14615
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
14616
+ <!-- Published Status -->
14617
+ <div>
14618
+ <label class="block text-sm font-medium text-white">Status</label>
14619
+ <div class="mt-2 space-y-2">
14620
+ <div class="flex items-center">
14621
+ <input id="published"
14622
+ name="isPublished"
14623
+ type="radio"
14624
+ value="true"
14625
+ ${!testimonial || testimonial.isPublished ? "checked" : ""}
14626
+ class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
14627
+ <label for="published" class="ml-2 block text-sm text-white">
14628
+ Published <span class="text-gray-300">(visible to users)</span>
14629
+ </label>
14630
+ </div>
14631
+ <div class="flex items-center">
14632
+ <input id="draft"
14633
+ name="isPublished"
14634
+ type="radio"
14635
+ value="false"
14636
+ ${testimonial && !testimonial.isPublished ? "checked" : ""}
14637
+ class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
14638
+ <label for="draft" class="ml-2 block text-sm text-white">
14639
+ Draft <span class="text-gray-300">(not visible to users)</span>
14640
+ </label>
14641
+ </div>
14642
+ </div>
14643
+ </div>
14644
+
14645
+ <!-- Sort Order -->
14646
+ <div>
14647
+ <label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
14648
+ <div class="mt-1">
14649
+ <input type="number"
14650
+ name="sortOrder"
14651
+ id="sortOrder"
14652
+ value="${testimonial?.sortOrder || 0}"
14653
+ min="0"
14654
+ step="1"
14655
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
14656
+ <p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
14657
+ </div>
14658
+ ${errors?.sortOrder ? `
14659
+ <div class="mt-1">
14660
+ ${errors.sortOrder.map((error) => `
14661
+ <p class="text-sm text-red-400">${escapeHtml5(error)}</p>
14662
+ `).join("")}
14663
+ </div>
14664
+ ` : ""}
14665
+ </div>
14666
+ </div>
14667
+
14668
+ <!-- Form Actions -->
14669
+ <div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
14670
+ <a href="/admin/testimonials"
14671
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
14672
+ Cancel
14673
+ </a>
14674
+ <button type="submit"
14675
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-blue-500/80 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-blue-500 transition-all">
14676
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14677
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
14678
+ </svg>
14679
+ ${isEdit ? "Update Testimonial" : "Create Testimonial"}
14680
+ </button>
14681
+ </div>
14682
+ </form>
14683
+ </div>
14684
+ </div>
14685
+
14686
+ <script>
14687
+ // Character count for testimonial
14688
+ const testimonialTextarea = document.getElementById('testimonialText');
14689
+ const testimonialCount = document.getElementById('testimonial-count');
14690
+
14691
+ function updateTestimonialCount() {
14692
+ testimonialCount.textContent = testimonialTextarea.value.length;
14693
+ }
14694
+
14695
+ testimonialTextarea.addEventListener('input', updateTestimonialCount);
14696
+ updateTestimonialCount(); // Initial count
14697
+ </script>
14698
+ `;
14699
+ const layoutData = {
14700
+ title: `${pageTitle} - Admin`,
14701
+ pageTitle,
14702
+ currentPath: isEdit ? `/admin/testimonials/${testimonial?.id}` : "/admin/testimonials/new",
14703
+ user: data.user,
14704
+ content: pageContent
14705
+ };
14706
+ return renderAdminLayout(layoutData);
14707
+ }
14708
+ function escapeHtml5(unsafe) {
14709
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
14710
+ }
14711
+
14712
+ // src/routes/admin-testimonials.ts
14713
+ var testimonialSchema = z.object({
14714
+ authorName: z.string().min(1, "Author name is required").max(100, "Author name must be under 100 characters"),
14715
+ authorTitle: z.string().optional(),
14716
+ authorCompany: z.string().optional(),
14717
+ testimonialText: z.string().min(1, "Testimonial is required").max(1e3, "Testimonial must be under 1000 characters"),
14718
+ rating: z.string().transform((val) => val ? parseInt(val, 10) : void 0).pipe(z.number().min(1).max(5).optional()),
14719
+ isPublished: z.string().transform((val) => val === "true"),
14720
+ sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
14721
+ });
14722
+ var adminTestimonialsRoutes = new Hono();
14723
+ adminTestimonialsRoutes.get("/", async (c) => {
14724
+ try {
14725
+ const user = c.get("user");
14726
+ const { published, minRating, search, page = "1" } = c.req.query();
14727
+ const currentPage = parseInt(page, 10) || 1;
14728
+ const limit = 20;
14729
+ const offset = (currentPage - 1) * limit;
14730
+ const db = c.env?.DB;
14731
+ if (!db) {
14732
+ return c.html(renderTestimonialsList({
14733
+ testimonials: [],
14734
+ totalCount: 0,
14735
+ currentPage: 1,
14736
+ totalPages: 1,
14737
+ user: user ? {
14738
+ name: user.email,
14739
+ email: user.email,
14740
+ role: user.role
14741
+ } : void 0,
14742
+ message: "Database not available",
14743
+ messageType: "error"
14744
+ }));
14745
+ }
14746
+ let whereClause = "WHERE 1=1";
14747
+ const params = [];
14748
+ if (published !== void 0) {
14749
+ whereClause += " AND isPublished = ?";
14750
+ params.push(published === "true" ? 1 : 0);
14751
+ }
14752
+ if (minRating) {
14753
+ whereClause += " AND rating >= ?";
14754
+ params.push(parseInt(minRating, 10));
14755
+ }
14756
+ if (search) {
14757
+ whereClause += " AND (author_name LIKE ? OR testimonial_text LIKE ? OR author_company LIKE ?)";
14758
+ const searchTerm = `%${search}%`;
14759
+ params.push(searchTerm, searchTerm, searchTerm);
14760
+ }
14761
+ const countQuery = `SELECT COUNT(*) as count FROM testimonials ${whereClause}`;
14762
+ const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
14763
+ const totalCount = countResults?.[0]?.count || 0;
14764
+ const dataQuery = `
14765
+ SELECT * FROM testimonials
14766
+ ${whereClause}
14767
+ ORDER BY sortOrder ASC, created_at DESC
14768
+ LIMIT ? OFFSET ?
14769
+ `;
14770
+ const { results: testimonials } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
14771
+ const totalPages = Math.ceil(totalCount / limit);
14772
+ return c.html(renderTestimonialsList({
14773
+ testimonials: testimonials || [],
14774
+ totalCount,
14775
+ currentPage,
14776
+ totalPages,
14777
+ user: user ? {
14778
+ name: user.email,
14779
+ email: user.email,
14780
+ role: user.role
14781
+ } : void 0
14782
+ }));
14783
+ } catch (error) {
14784
+ console.error("Error fetching testimonials:", error);
14785
+ const user = c.get("user");
14786
+ return c.html(renderTestimonialsList({
14787
+ testimonials: [],
14788
+ totalCount: 0,
14789
+ currentPage: 1,
14790
+ totalPages: 1,
14791
+ user: user ? {
14792
+ name: user.email,
14793
+ email: user.email,
14794
+ role: user.role
14795
+ } : void 0,
14796
+ message: "Failed to load testimonials",
14797
+ messageType: "error"
14798
+ }));
14799
+ }
14800
+ });
14801
+ adminTestimonialsRoutes.get("/new", async (c) => {
14802
+ const user = c.get("user");
14803
+ return c.html(renderTestimonialsForm({
14804
+ isEdit: false,
14805
+ user: user ? {
14806
+ name: user.email,
14807
+ email: user.email,
14808
+ role: user.role
14809
+ } : void 0
14810
+ }));
14811
+ });
14812
+ adminTestimonialsRoutes.post("/", async (c) => {
14813
+ try {
14814
+ const formData = await c.req.formData();
14815
+ const data = Object.fromEntries(formData.entries());
14816
+ const validatedData = testimonialSchema.parse(data);
14817
+ const user = c.get("user");
14818
+ const db = c.env?.DB;
14819
+ if (!db) {
14820
+ return c.html(renderTestimonialsForm({
14821
+ isEdit: false,
14822
+ user: user ? {
14823
+ name: user.email,
14824
+ email: user.email,
14825
+ role: user.role
14826
+ } : void 0,
14827
+ message: "Database not available",
14828
+ messageType: "error"
14829
+ }));
14830
+ }
14831
+ const { results } = await db.prepare(`
14832
+ INSERT INTO testimonials (author_name, author_title, author_company, testimonial_text, rating, isPublished, sortOrder)
14833
+ VALUES (?, ?, ?, ?, ?, ?, ?)
14834
+ RETURNING *
14835
+ `).bind(
14836
+ validatedData.authorName,
14837
+ validatedData.authorTitle || null,
14838
+ validatedData.authorCompany || null,
14839
+ validatedData.testimonialText,
14840
+ validatedData.rating || null,
14841
+ validatedData.isPublished ? 1 : 0,
14842
+ validatedData.sortOrder
14843
+ ).all();
14844
+ if (results && results.length > 0) {
14845
+ return c.redirect("/admin/testimonials?message=Testimonial created successfully");
14846
+ } else {
14847
+ return c.html(renderTestimonialsForm({
14848
+ isEdit: false,
14849
+ user: user ? {
14850
+ name: user.email,
14851
+ email: user.email,
14852
+ role: user.role
14853
+ } : void 0,
14854
+ message: "Failed to create testimonial",
14855
+ messageType: "error"
14856
+ }));
14857
+ }
14858
+ } catch (error) {
14859
+ console.error("Error creating testimonial:", error);
14860
+ const user = c.get("user");
14861
+ if (error instanceof z.ZodError) {
14862
+ const errors = {};
14863
+ error.errors.forEach((err) => {
14864
+ const field = err.path[0];
14865
+ if (!errors[field]) errors[field] = [];
14866
+ errors[field].push(err.message);
14867
+ });
14868
+ return c.html(renderTestimonialsForm({
14869
+ isEdit: false,
14870
+ user: user ? {
14871
+ name: user.email,
14872
+ email: user.email,
14873
+ role: user.role
14874
+ } : void 0,
14875
+ errors,
14876
+ message: "Please correct the errors below",
14877
+ messageType: "error"
14878
+ }));
14879
+ }
14880
+ return c.html(renderTestimonialsForm({
14881
+ isEdit: false,
14882
+ user: user ? {
14883
+ name: user.email,
14884
+ email: user.email,
14885
+ role: user.role
14886
+ } : void 0,
14887
+ message: "Failed to create testimonial",
14888
+ messageType: "error"
14889
+ }));
14890
+ }
14891
+ });
14892
+ adminTestimonialsRoutes.get("/:id", async (c) => {
14893
+ try {
14894
+ const id = parseInt(c.req.param("id"));
14895
+ const user = c.get("user");
14896
+ const db = c.env?.DB;
14897
+ if (!db) {
14898
+ return c.html(renderTestimonialsForm({
14899
+ isEdit: true,
14900
+ user: user ? {
14901
+ name: user.email,
14902
+ email: user.email,
14903
+ role: user.role
14904
+ } : void 0,
14905
+ message: "Database not available",
14906
+ messageType: "error"
14907
+ }));
14908
+ }
14909
+ const { results } = await db.prepare("SELECT * FROM testimonials WHERE id = ?").bind(id).all();
14910
+ if (!results || results.length === 0) {
14911
+ return c.redirect("/admin/testimonials?message=Testimonial not found&type=error");
14912
+ }
14913
+ const testimonial = results[0];
14914
+ return c.html(renderTestimonialsForm({
14915
+ testimonial: {
14916
+ id: testimonial.id,
14917
+ authorName: testimonial.author_name,
14918
+ authorTitle: testimonial.author_title,
14919
+ authorCompany: testimonial.author_company,
14920
+ testimonialText: testimonial.testimonial_text,
14921
+ rating: testimonial.rating,
14922
+ isPublished: Boolean(testimonial.isPublished),
14923
+ sortOrder: testimonial.sortOrder
14924
+ },
14925
+ isEdit: true,
14926
+ user: user ? {
14927
+ name: user.email,
14928
+ email: user.email,
14929
+ role: user.role
14930
+ } : void 0
14931
+ }));
14932
+ } catch (error) {
14933
+ console.error("Error fetching testimonial:", error);
14934
+ const user = c.get("user");
14935
+ return c.html(renderTestimonialsForm({
14936
+ isEdit: true,
14937
+ user: user ? {
14938
+ name: user.email,
14939
+ email: user.email,
14940
+ role: user.role
14941
+ } : void 0,
14942
+ message: "Failed to load testimonial",
14943
+ messageType: "error"
14944
+ }));
14945
+ }
14946
+ });
14947
+ adminTestimonialsRoutes.put("/:id", async (c) => {
14948
+ try {
14949
+ const id = parseInt(c.req.param("id"));
14950
+ const formData = await c.req.formData();
14951
+ const data = Object.fromEntries(formData.entries());
14952
+ const validatedData = testimonialSchema.parse(data);
14953
+ const user = c.get("user");
14954
+ const db = c.env?.DB;
14955
+ if (!db) {
14956
+ return c.html(renderTestimonialsForm({
14957
+ isEdit: true,
14958
+ user: user ? {
14959
+ name: user.email,
14960
+ email: user.email,
14961
+ role: user.role
14962
+ } : void 0,
14963
+ message: "Database not available",
14964
+ messageType: "error"
14965
+ }));
14966
+ }
14967
+ const { results } = await db.prepare(`
14968
+ UPDATE testimonials
14969
+ SET author_name = ?, author_title = ?, author_company = ?, testimonial_text = ?, rating = ?, isPublished = ?, sortOrder = ?
14970
+ WHERE id = ?
14971
+ RETURNING *
14972
+ `).bind(
14973
+ validatedData.authorName,
14974
+ validatedData.authorTitle || null,
14975
+ validatedData.authorCompany || null,
14976
+ validatedData.testimonialText,
14977
+ validatedData.rating || null,
14978
+ validatedData.isPublished ? 1 : 0,
14979
+ validatedData.sortOrder,
14980
+ id
14981
+ ).all();
14982
+ if (results && results.length > 0) {
14983
+ return c.redirect("/admin/testimonials?message=Testimonial updated successfully");
14984
+ } else {
14985
+ return c.html(renderTestimonialsForm({
14986
+ testimonial: {
14987
+ id,
14988
+ authorName: validatedData.authorName,
14989
+ authorTitle: validatedData.authorTitle,
14990
+ authorCompany: validatedData.authorCompany,
14991
+ testimonialText: validatedData.testimonialText,
14992
+ rating: validatedData.rating,
14993
+ isPublished: validatedData.isPublished,
14994
+ sortOrder: validatedData.sortOrder
14995
+ },
14996
+ isEdit: true,
14997
+ user: user ? {
14998
+ name: user.email,
14999
+ email: user.email,
15000
+ role: user.role
15001
+ } : void 0,
15002
+ message: "Testimonial not found",
15003
+ messageType: "error"
15004
+ }));
15005
+ }
15006
+ } catch (error) {
15007
+ console.error("Error updating testimonial:", error);
15008
+ const user = c.get("user");
15009
+ const id = parseInt(c.req.param("id"));
15010
+ if (error instanceof z.ZodError) {
15011
+ const errors = {};
15012
+ error.errors.forEach((err) => {
15013
+ const field = err.path[0];
15014
+ if (!errors[field]) errors[field] = [];
15015
+ errors[field].push(err.message);
15016
+ });
15017
+ return c.html(renderTestimonialsForm({
15018
+ testimonial: {
15019
+ id,
15020
+ authorName: "",
15021
+ authorTitle: "",
15022
+ authorCompany: "",
15023
+ testimonialText: "",
15024
+ rating: void 0,
15025
+ isPublished: true,
15026
+ sortOrder: 0
15027
+ },
15028
+ isEdit: true,
15029
+ user: user ? {
15030
+ name: user.email,
15031
+ email: user.email,
15032
+ role: user.role
15033
+ } : void 0,
15034
+ errors,
15035
+ message: "Please correct the errors below",
15036
+ messageType: "error"
15037
+ }));
15038
+ }
15039
+ return c.html(renderTestimonialsForm({
15040
+ testimonial: {
15041
+ id,
15042
+ authorName: "",
15043
+ authorTitle: "",
15044
+ authorCompany: "",
15045
+ testimonialText: "",
15046
+ rating: void 0,
15047
+ isPublished: true,
15048
+ sortOrder: 0
15049
+ },
15050
+ isEdit: true,
15051
+ user: user ? {
15052
+ name: user.email,
15053
+ email: user.email,
15054
+ role: user.role
15055
+ } : void 0,
15056
+ message: "Failed to update testimonial",
15057
+ messageType: "error"
15058
+ }));
15059
+ }
15060
+ });
15061
+ adminTestimonialsRoutes.delete("/:id", async (c) => {
15062
+ try {
15063
+ const id = parseInt(c.req.param("id"));
15064
+ const db = c.env?.DB;
15065
+ if (!db) {
15066
+ return c.json({ error: "Database not available" }, 500);
15067
+ }
15068
+ const { changes } = await db.prepare("DELETE FROM testimonials WHERE id = ?").bind(id).run();
15069
+ if (changes === 0) {
15070
+ return c.json({ error: "Testimonial not found" }, 404);
15071
+ }
15072
+ return c.redirect("/admin/testimonials?message=Testimonial deleted successfully");
15073
+ } catch (error) {
15074
+ console.error("Error deleting testimonial:", error);
15075
+ return c.json({ error: "Failed to delete testimonial" }, 500);
15076
+ }
15077
+ });
15078
+ var admin_testimonials_default = adminTestimonialsRoutes;
15079
+
15080
+ // src/templates/pages/admin-code-examples-form.template.ts
15081
+ function renderCodeExamplesForm(data) {
15082
+ const { codeExample, isEdit, errors, message, messageType } = data;
15083
+ const pageTitle = isEdit ? "Edit Code Example" : "New Code Example";
15084
+ const pageContent = `
15085
+ <div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
15086
+ <!-- Header -->
15087
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
15088
+ <div>
15089
+ <h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
15090
+ <p class="mt-2 text-sm text-gray-300">
15091
+ ${isEdit ? "Update the code example details below" : "Create a new code snippet or example"}
15092
+ </p>
15093
+ </div>
15094
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
15095
+ <a href="/admin/code-examples"
15096
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
15097
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
15098
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
15099
+ </svg>
15100
+ Back to List
15101
+ </a>
15102
+ </div>
15103
+ </div>
15104
+
15105
+ ${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
15106
+
15107
+ <!-- Form -->
15108
+ <div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
15109
+ <form ${isEdit ? `hx-put="/admin/code-examples/${codeExample?.id}"` : 'hx-post="/admin/code-examples"'}
15110
+ hx-target="body"
15111
+ hx-swap="outerHTML"
15112
+ class="space-y-6 p-6">
15113
+
15114
+ <!-- Basic Information Section -->
15115
+ <div>
15116
+ <h2 class="text-lg font-medium text-white mb-4">Basic Information</h2>
15117
+
15118
+ <!-- Title -->
15119
+ <div class="mb-4">
15120
+ <label for="title" class="block text-sm font-medium text-white">
15121
+ Title <span class="text-red-400">*</span>
15122
+ </label>
15123
+ <div class="mt-1">
15124
+ <input type="text"
15125
+ name="title"
15126
+ id="title"
15127
+ value="${codeExample?.title || ""}"
15128
+ required
15129
+ maxlength="200"
15130
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-purple-600 sm:text-sm sm:leading-6"
15131
+ placeholder="e.g., React useState Hook Example">
15132
+ </div>
15133
+ ${errors?.title ? `
15134
+ <div class="mt-1">
15135
+ ${errors.title.map((error) => `
15136
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15137
+ `).join("")}
15138
+ </div>
15139
+ ` : ""}
15140
+ </div>
15141
+
15142
+ <!-- Description -->
15143
+ <div class="mb-4">
15144
+ <label for="description" class="block text-sm font-medium text-white">Description</label>
15145
+ <div class="mt-1">
15146
+ <textarea name="description"
15147
+ id="description"
15148
+ rows="3"
15149
+ maxlength="500"
15150
+ class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-purple-400 focus:outline-none transition-colors w-full"
15151
+ placeholder="Briefly describe what this code example demonstrates...">${codeExample?.description || ""}</textarea>
15152
+ <p class="mt-1 text-sm text-gray-300">
15153
+ <span id="description-count">0</span>/500 characters
15154
+ </p>
15155
+ </div>
15156
+ ${errors?.description ? `
15157
+ <div class="mt-1">
15158
+ ${errors.description.map((error) => `
15159
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15160
+ `).join("")}
15161
+ </div>
15162
+ ` : ""}
15163
+ </div>
15164
+
15165
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
15166
+ <!-- Language -->
15167
+ <div>
15168
+ <label for="language" class="block text-sm font-medium text-white">
15169
+ Language <span class="text-red-400">*</span>
15170
+ </label>
15171
+ <div class="mt-1">
15172
+ <select name="language"
15173
+ id="language"
15174
+ required
15175
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 focus:ring-2 focus:ring-inset focus:ring-purple-600 sm:text-sm sm:leading-6">
15176
+ <option value="">Select language...</option>
15177
+ <option value="javascript" ${codeExample?.language === "javascript" ? "selected" : ""}>JavaScript</option>
15178
+ <option value="typescript" ${codeExample?.language === "typescript" ? "selected" : ""}>TypeScript</option>
15179
+ <option value="python" ${codeExample?.language === "python" ? "selected" : ""}>Python</option>
15180
+ <option value="go" ${codeExample?.language === "go" ? "selected" : ""}>Go</option>
15181
+ <option value="rust" ${codeExample?.language === "rust" ? "selected" : ""}>Rust</option>
15182
+ <option value="java" ${codeExample?.language === "java" ? "selected" : ""}>Java</option>
15183
+ <option value="php" ${codeExample?.language === "php" ? "selected" : ""}>PHP</option>
15184
+ <option value="ruby" ${codeExample?.language === "ruby" ? "selected" : ""}>Ruby</option>
15185
+ <option value="sql" ${codeExample?.language === "sql" ? "selected" : ""}>SQL</option>
15186
+ </select>
15187
+ </div>
15188
+ ${errors?.language ? `
15189
+ <div class="mt-1">
15190
+ ${errors.language.map((error) => `
15191
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15192
+ `).join("")}
15193
+ </div>
15194
+ ` : ""}
15195
+ </div>
15196
+
15197
+ <!-- Category -->
15198
+ <div>
15199
+ <label for="category" class="block text-sm font-medium text-white">Category</label>
15200
+ <div class="mt-1">
15201
+ <input type="text"
15202
+ name="category"
15203
+ id="category"
15204
+ value="${codeExample?.category || ""}"
15205
+ maxlength="50"
15206
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-purple-600 sm:text-sm sm:leading-6"
15207
+ placeholder="e.g., frontend, backend">
15208
+ </div>
15209
+ ${errors?.category ? `
15210
+ <div class="mt-1">
15211
+ ${errors.category.map((error) => `
15212
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15213
+ `).join("")}
15214
+ </div>
15215
+ ` : ""}
15216
+ </div>
15217
+
15218
+ <!-- Tags -->
15219
+ <div>
15220
+ <label for="tags" class="block text-sm font-medium text-white">Tags</label>
15221
+ <div class="mt-1">
15222
+ <input type="text"
15223
+ name="tags"
15224
+ id="tags"
15225
+ value="${codeExample?.tags || ""}"
15226
+ maxlength="200"
15227
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-purple-600 sm:text-sm sm:leading-6"
15228
+ placeholder="e.g., react, hooks, state">
15229
+ <p class="mt-1 text-sm text-gray-300">Comma-separated tags</p>
15230
+ </div>
15231
+ ${errors?.tags ? `
15232
+ <div class="mt-1">
15233
+ ${errors.tags.map((error) => `
15234
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15235
+ `).join("")}
15236
+ </div>
15237
+ ` : ""}
15238
+ </div>
15239
+ </div>
15240
+ </div>
15241
+
15242
+ <!-- Code Section -->
15243
+ <div>
15244
+ <h2 class="text-lg font-medium text-white mb-4">Code</h2>
15245
+
15246
+ <!-- Code Editor -->
15247
+ <div class="mb-4">
15248
+ <label for="code" class="block text-sm font-medium text-white">
15249
+ Code <span class="text-red-400">*</span>
15250
+ </label>
15251
+ <div class="mt-1">
15252
+ <textarea name="code"
15253
+ id="code"
15254
+ rows="20"
15255
+ required
15256
+ class="backdrop-blur-sm bg-gray-800/90 border border-white/20 rounded-xl px-3 py-2 text-white placeholder-gray-300 focus:border-purple-400 focus:outline-none transition-colors w-full font-mono text-sm"
15257
+ placeholder="Paste your code here...">${codeExample?.code || ""}</textarea>
15258
+ <p class="mt-1 text-sm text-gray-300">
15259
+ <span id="code-count">0</span> characters
15260
+ </p>
15261
+ </div>
15262
+ ${errors?.code ? `
15263
+ <div class="mt-1">
15264
+ ${errors.code.map((error) => `
15265
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15266
+ `).join("")}
15267
+ </div>
15268
+ ` : ""}
15269
+ </div>
15270
+ </div>
15271
+
15272
+ <!-- Status and Sort Order Row -->
15273
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
15274
+ <!-- Published Status -->
15275
+ <div>
15276
+ <label class="block text-sm font-medium text-white">Status</label>
15277
+ <div class="mt-2 space-y-2">
15278
+ <div class="flex items-center">
15279
+ <input id="published"
15280
+ name="isPublished"
15281
+ type="radio"
15282
+ value="true"
15283
+ ${!codeExample || codeExample.isPublished ? "checked" : ""}
15284
+ class="h-4 w-4 text-purple-600 focus:ring-purple-600 border-gray-600 bg-gray-700">
15285
+ <label for="published" class="ml-2 block text-sm text-white">
15286
+ Published <span class="text-gray-300">(visible to users)</span>
15287
+ </label>
15288
+ </div>
15289
+ <div class="flex items-center">
15290
+ <input id="draft"
15291
+ name="isPublished"
15292
+ type="radio"
15293
+ value="false"
15294
+ ${codeExample && !codeExample.isPublished ? "checked" : ""}
15295
+ class="h-4 w-4 text-purple-600 focus:ring-purple-600 border-gray-600 bg-gray-700">
15296
+ <label for="draft" class="ml-2 block text-sm text-white">
15297
+ Draft <span class="text-gray-300">(not visible to users)</span>
15298
+ </label>
15299
+ </div>
15300
+ </div>
15301
+ </div>
15302
+
15303
+ <!-- Sort Order -->
15304
+ <div>
15305
+ <label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
15306
+ <div class="mt-1">
15307
+ <input type="number"
15308
+ name="sortOrder"
15309
+ id="sortOrder"
15310
+ value="${codeExample?.sortOrder || 0}"
15311
+ min="0"
15312
+ step="1"
15313
+ class="block w-full rounded-md border-0 bg-gray-700 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-purple-600 sm:text-sm sm:leading-6">
15314
+ <p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
15315
+ </div>
15316
+ ${errors?.sortOrder ? `
15317
+ <div class="mt-1">
15318
+ ${errors.sortOrder.map((error) => `
15319
+ <p class="text-sm text-red-400">${escapeHtml6(error)}</p>
15320
+ `).join("")}
15321
+ </div>
15322
+ ` : ""}
15323
+ </div>
15324
+ </div>
15325
+
15326
+ <!-- Form Actions -->
15327
+ <div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
15328
+ <a href="/admin/code-examples"
15329
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-white/10 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-white/20 transition-all">
15330
+ Cancel
15331
+ </a>
15332
+ <button type="submit"
15333
+ class="inline-flex items-center justify-center rounded-xl backdrop-blur-sm bg-purple-500/80 px-4 py-2 text-sm font-semibold text-white border border-white/20 hover:bg-purple-500 transition-all">
15334
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
15335
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
15336
+ </svg>
15337
+ ${isEdit ? "Update Code Example" : "Create Code Example"}
15338
+ </button>
15339
+ </div>
15340
+ </form>
15341
+ </div>
15342
+ </div>
15343
+
15344
+ <script>
15345
+ // Character count for description
15346
+ const descriptionTextarea = document.getElementById('description');
15347
+ const descriptionCount = document.getElementById('description-count');
15348
+
15349
+ function updateDescriptionCount() {
15350
+ descriptionCount.textContent = descriptionTextarea.value.length;
15351
+ }
15352
+
15353
+ descriptionTextarea.addEventListener('input', updateDescriptionCount);
15354
+ updateDescriptionCount(); // Initial count
15355
+
15356
+ // Character count for code
15357
+ const codeTextarea = document.getElementById('code');
15358
+ const codeCount = document.getElementById('code-count');
15359
+
15360
+ function updateCodeCount() {
15361
+ codeCount.textContent = codeTextarea.value.length;
15362
+ }
15363
+
15364
+ codeTextarea.addEventListener('input', updateCodeCount);
15365
+ updateCodeCount(); // Initial count
15366
+ </script>
15367
+ `;
15368
+ const layoutData = {
15369
+ title: `${pageTitle} - Admin`,
15370
+ pageTitle,
15371
+ currentPath: isEdit ? `/admin/code-examples/${codeExample?.id}` : "/admin/code-examples/new",
15372
+ user: data.user,
15373
+ content: pageContent
15374
+ };
15375
+ return renderAdminLayout(layoutData);
15376
+ }
15377
+ function escapeHtml6(unsafe) {
15378
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
15379
+ }
15380
+
15381
+ // src/routes/admin-code-examples.ts
15382
+ var codeExampleSchema = z.object({
15383
+ title: z.string().min(1, "Title is required").max(200, "Title must be under 200 characters"),
15384
+ description: z.string().max(500, "Description must be under 500 characters").optional(),
15385
+ code: z.string().min(1, "Code is required"),
15386
+ language: z.string().min(1, "Language is required"),
15387
+ category: z.string().max(50, "Category must be under 50 characters").optional(),
15388
+ tags: z.string().max(200, "Tags must be under 200 characters").optional(),
15389
+ isPublished: z.string().transform((val) => val === "true"),
15390
+ sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
15391
+ });
15392
+ var adminCodeExamplesRoutes = new Hono();
15393
+ adminCodeExamplesRoutes.get("/", async (c) => {
15394
+ try {
15395
+ const user = c.get("user");
15396
+ const { published, language, search, page = "1" } = c.req.query();
15397
+ const currentPage = parseInt(page, 10) || 1;
15398
+ const limit = 20;
15399
+ const offset = (currentPage - 1) * limit;
15400
+ const db = c.env?.DB;
15401
+ if (!db) {
15402
+ return c.html(renderCodeExamplesList({
15403
+ codeExamples: [],
15404
+ totalCount: 0,
15405
+ currentPage: 1,
15406
+ totalPages: 1,
15407
+ user: user ? {
15408
+ name: user.email,
15409
+ email: user.email,
15410
+ role: user.role
15411
+ } : void 0,
15412
+ message: "Database not available",
15413
+ messageType: "error"
15414
+ }));
15415
+ }
15416
+ let whereClause = "WHERE 1=1";
15417
+ const params = [];
15418
+ if (published !== void 0) {
15419
+ whereClause += " AND isPublished = ?";
15420
+ params.push(published === "true" ? 1 : 0);
15421
+ }
15422
+ if (language) {
15423
+ whereClause += " AND language = ?";
15424
+ params.push(language);
15425
+ }
15426
+ if (search) {
15427
+ whereClause += " AND (title LIKE ? OR description LIKE ? OR code LIKE ? OR tags LIKE ?)";
15428
+ const searchTerm = `%${search}%`;
15429
+ params.push(searchTerm, searchTerm, searchTerm, searchTerm);
15430
+ }
15431
+ const countQuery = `SELECT COUNT(*) as count FROM code_examples ${whereClause}`;
15432
+ const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
15433
+ const totalCount = countResults?.[0]?.count || 0;
15434
+ const dataQuery = `
15435
+ SELECT * FROM code_examples
15436
+ ${whereClause}
15437
+ ORDER BY sortOrder ASC, created_at DESC
15438
+ LIMIT ? OFFSET ?
15439
+ `;
15440
+ const { results: codeExamples } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
15441
+ const totalPages = Math.ceil(totalCount / limit);
15442
+ return c.html(renderCodeExamplesList({
15443
+ codeExamples: codeExamples || [],
15444
+ totalCount,
15445
+ currentPage,
15446
+ totalPages,
15447
+ user: user ? {
15448
+ name: user.email,
15449
+ email: user.email,
15450
+ role: user.role
15451
+ } : void 0
15452
+ }));
15453
+ } catch (error) {
15454
+ console.error("Error fetching code examples:", error);
15455
+ const user = c.get("user");
15456
+ return c.html(renderCodeExamplesList({
15457
+ codeExamples: [],
15458
+ totalCount: 0,
15459
+ currentPage: 1,
15460
+ totalPages: 1,
15461
+ user: user ? {
15462
+ name: user.email,
15463
+ email: user.email,
15464
+ role: user.role
15465
+ } : void 0,
15466
+ message: "Failed to load code examples",
15467
+ messageType: "error"
15468
+ }));
15469
+ }
15470
+ });
15471
+ adminCodeExamplesRoutes.get("/new", async (c) => {
15472
+ const user = c.get("user");
15473
+ return c.html(renderCodeExamplesForm({
15474
+ isEdit: false,
15475
+ user: user ? {
15476
+ name: user.email,
15477
+ email: user.email,
15478
+ role: user.role
15479
+ } : void 0
15480
+ }));
15481
+ });
15482
+ adminCodeExamplesRoutes.post("/", async (c) => {
15483
+ try {
15484
+ const formData = await c.req.formData();
15485
+ const data = Object.fromEntries(formData.entries());
15486
+ const validatedData = codeExampleSchema.parse(data);
15487
+ const user = c.get("user");
15488
+ const db = c.env?.DB;
15489
+ if (!db) {
15490
+ return c.html(renderCodeExamplesForm({
15491
+ isEdit: false,
15492
+ user: user ? {
15493
+ name: user.email,
15494
+ email: user.email,
15495
+ role: user.role
15496
+ } : void 0,
15497
+ message: "Database not available",
15498
+ messageType: "error"
15499
+ }));
15500
+ }
15501
+ const { results } = await db.prepare(`
15502
+ INSERT INTO code_examples (title, description, code, language, category, tags, isPublished, sortOrder)
15503
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
15504
+ RETURNING *
15505
+ `).bind(
15506
+ validatedData.title,
15507
+ validatedData.description || null,
15508
+ validatedData.code,
15509
+ validatedData.language,
15510
+ validatedData.category || null,
15511
+ validatedData.tags || null,
15512
+ validatedData.isPublished ? 1 : 0,
15513
+ validatedData.sortOrder
15514
+ ).all();
15515
+ if (results && results.length > 0) {
15516
+ return c.redirect("/admin/code-examples?message=Code example created successfully");
15517
+ } else {
15518
+ return c.html(renderCodeExamplesForm({
15519
+ isEdit: false,
15520
+ user: user ? {
15521
+ name: user.email,
15522
+ email: user.email,
15523
+ role: user.role
15524
+ } : void 0,
15525
+ message: "Failed to create code example",
15526
+ messageType: "error"
15527
+ }));
15528
+ }
15529
+ } catch (error) {
15530
+ console.error("Error creating code example:", error);
15531
+ const user = c.get("user");
15532
+ if (error instanceof z.ZodError) {
15533
+ const errors = {};
15534
+ error.errors.forEach((err) => {
15535
+ const field = err.path[0];
15536
+ if (!errors[field]) errors[field] = [];
15537
+ errors[field].push(err.message);
15538
+ });
15539
+ return c.html(renderCodeExamplesForm({
15540
+ isEdit: false,
15541
+ user: user ? {
15542
+ name: user.email,
15543
+ email: user.email,
15544
+ role: user.role
15545
+ } : void 0,
15546
+ errors,
15547
+ message: "Please correct the errors below",
15548
+ messageType: "error"
15549
+ }));
15550
+ }
15551
+ return c.html(renderCodeExamplesForm({
15552
+ isEdit: false,
15553
+ user: user ? {
15554
+ name: user.email,
15555
+ email: user.email,
15556
+ role: user.role
15557
+ } : void 0,
15558
+ message: "Failed to create code example",
15559
+ messageType: "error"
15560
+ }));
15561
+ }
15562
+ });
15563
+ adminCodeExamplesRoutes.get("/:id", async (c) => {
15564
+ try {
15565
+ const id = parseInt(c.req.param("id"));
15566
+ const user = c.get("user");
15567
+ const db = c.env?.DB;
15568
+ if (!db) {
15569
+ return c.html(renderCodeExamplesForm({
15570
+ isEdit: true,
15571
+ user: user ? {
15572
+ name: user.email,
15573
+ email: user.email,
15574
+ role: user.role
15575
+ } : void 0,
15576
+ message: "Database not available",
15577
+ messageType: "error"
15578
+ }));
15579
+ }
15580
+ const { results } = await db.prepare("SELECT * FROM code_examples WHERE id = ?").bind(id).all();
15581
+ if (!results || results.length === 0) {
15582
+ return c.redirect("/admin/code-examples?message=Code example not found&type=error");
15583
+ }
15584
+ const example = results[0];
15585
+ return c.html(renderCodeExamplesForm({
15586
+ codeExample: {
15587
+ id: example.id,
15588
+ title: example.title,
15589
+ description: example.description,
15590
+ code: example.code,
15591
+ language: example.language,
15592
+ category: example.category,
15593
+ tags: example.tags,
15594
+ isPublished: Boolean(example.isPublished),
15595
+ sortOrder: example.sortOrder
15596
+ },
15597
+ isEdit: true,
15598
+ user: user ? {
15599
+ name: user.email,
15600
+ email: user.email,
15601
+ role: user.role
15602
+ } : void 0
15603
+ }));
15604
+ } catch (error) {
15605
+ console.error("Error fetching code example:", error);
15606
+ const user = c.get("user");
15607
+ return c.html(renderCodeExamplesForm({
15608
+ isEdit: true,
15609
+ user: user ? {
15610
+ name: user.email,
15611
+ email: user.email,
15612
+ role: user.role
15613
+ } : void 0,
15614
+ message: "Failed to load code example",
15615
+ messageType: "error"
15616
+ }));
15617
+ }
15618
+ });
15619
+ adminCodeExamplesRoutes.put("/:id", async (c) => {
15620
+ try {
15621
+ const id = parseInt(c.req.param("id"));
15622
+ const formData = await c.req.formData();
15623
+ const data = Object.fromEntries(formData.entries());
15624
+ const validatedData = codeExampleSchema.parse(data);
15625
+ const user = c.get("user");
15626
+ const db = c.env?.DB;
15627
+ if (!db) {
15628
+ return c.html(renderCodeExamplesForm({
15629
+ isEdit: true,
15630
+ user: user ? {
15631
+ name: user.email,
15632
+ email: user.email,
15633
+ role: user.role
15634
+ } : void 0,
15635
+ message: "Database not available",
15636
+ messageType: "error"
15637
+ }));
15638
+ }
15639
+ const { results } = await db.prepare(`
15640
+ UPDATE code_examples
15641
+ SET title = ?, description = ?, code = ?, language = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
15642
+ WHERE id = ?
15643
+ RETURNING *
15644
+ `).bind(
15645
+ validatedData.title,
15646
+ validatedData.description || null,
15647
+ validatedData.code,
15648
+ validatedData.language,
15649
+ validatedData.category || null,
15650
+ validatedData.tags || null,
15651
+ validatedData.isPublished ? 1 : 0,
15652
+ validatedData.sortOrder,
15653
+ id
15654
+ ).all();
15655
+ if (results && results.length > 0) {
15656
+ return c.redirect("/admin/code-examples?message=Code example updated successfully");
15657
+ } else {
15658
+ return c.html(renderCodeExamplesForm({
15659
+ codeExample: {
15660
+ id,
15661
+ title: validatedData.title,
15662
+ description: validatedData.description,
15663
+ code: validatedData.code,
15664
+ language: validatedData.language,
15665
+ category: validatedData.category,
15666
+ tags: validatedData.tags,
15667
+ isPublished: validatedData.isPublished,
15668
+ sortOrder: validatedData.sortOrder
15669
+ },
15670
+ isEdit: true,
15671
+ user: user ? {
15672
+ name: user.email,
15673
+ email: user.email,
15674
+ role: user.role
15675
+ } : void 0,
15676
+ message: "Code example not found",
15677
+ messageType: "error"
15678
+ }));
15679
+ }
15680
+ } catch (error) {
15681
+ console.error("Error updating code example:", error);
15682
+ const user = c.get("user");
15683
+ const id = parseInt(c.req.param("id"));
15684
+ if (error instanceof z.ZodError) {
15685
+ const errors = {};
15686
+ error.errors.forEach((err) => {
15687
+ const field = err.path[0];
15688
+ if (!errors[field]) errors[field] = [];
15689
+ errors[field].push(err.message);
15690
+ });
15691
+ return c.html(renderCodeExamplesForm({
15692
+ codeExample: {
15693
+ id,
15694
+ title: "",
15695
+ description: "",
15696
+ code: "",
15697
+ language: "",
15698
+ category: "",
15699
+ tags: "",
15700
+ isPublished: true,
15701
+ sortOrder: 0
15702
+ },
15703
+ isEdit: true,
15704
+ user: user ? {
15705
+ name: user.email,
15706
+ email: user.email,
15707
+ role: user.role
15708
+ } : void 0,
15709
+ errors,
15710
+ message: "Please correct the errors below",
15711
+ messageType: "error"
15712
+ }));
15713
+ }
15714
+ return c.html(renderCodeExamplesForm({
15715
+ codeExample: {
15716
+ id,
15717
+ title: "",
15718
+ description: "",
15719
+ code: "",
15720
+ language: "",
15721
+ category: "",
15722
+ tags: "",
15723
+ isPublished: true,
15724
+ sortOrder: 0
15725
+ },
15726
+ isEdit: true,
15727
+ user: user ? {
15728
+ name: user.email,
15729
+ email: user.email,
15730
+ role: user.role
15731
+ } : void 0,
15732
+ message: "Failed to update code example",
15733
+ messageType: "error"
15734
+ }));
15735
+ }
15736
+ });
15737
+ adminCodeExamplesRoutes.delete("/:id", async (c) => {
15738
+ try {
15739
+ const id = parseInt(c.req.param("id"));
15740
+ const db = c.env?.DB;
15741
+ if (!db) {
15742
+ return c.json({ error: "Database not available" }, 500);
15743
+ }
15744
+ const { changes } = await db.prepare("DELETE FROM code_examples WHERE id = ?").bind(id).run();
15745
+ if (changes === 0) {
15746
+ return c.json({ error: "Code example not found" }, 404);
15747
+ }
15748
+ return c.redirect("/admin/code-examples?message=Code example deleted successfully");
15749
+ } catch (error) {
15750
+ console.error("Error deleting code example:", error);
15751
+ return c.json({ error: "Failed to delete code example" }, 500);
15752
+ }
15753
+ });
15754
+ var admin_code_examples_default = adminCodeExamplesRoutes;
14210
15755
 
14211
15756
  // src/routes/index.ts
14212
15757
  var ROUTES_INFO = {
14213
- message: "Routes migration in progress",
15758
+ message: "Core routes available",
14214
15759
  available: [
14215
15760
  "apiRoutes",
14216
15761
  "apiContentCrudRoutes",
@@ -14222,12 +15767,17 @@ var ROUTES_INFO = {
14222
15767
  "adminUsersRoutes",
14223
15768
  "adminMediaRoutes",
14224
15769
  "adminPluginRoutes",
14225
- "adminLogsRoutes"
15770
+ "adminLogsRoutes",
15771
+ "adminDesignRoutes",
15772
+ "adminCheckboxRoutes",
15773
+ "adminFAQRoutes",
15774
+ "adminTestimonialsRoutes",
15775
+ "adminCodeExamplesRoutes"
14226
15776
  ],
14227
- status: "Routes are being added incrementally",
15777
+ status: "Core package routes ready",
14228
15778
  reference: "https://github.com/sonicjs/sonicjs"
14229
15779
  };
14230
15780
 
14231
- export { ROUTES_INFO, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, admin_api_default, admin_content_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, userRoutes };
14232
- //# sourceMappingURL=chunk-FDUDHGI6.js.map
14233
- //# sourceMappingURL=chunk-FDUDHGI6.js.map
15781
+ export { ROUTES_INFO, adminCheckboxRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, 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, userRoutes };
15782
+ //# sourceMappingURL=chunk-JETM2U2D.js.map
15783
+ //# sourceMappingURL=chunk-JETM2U2D.js.map