@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.
- package/dist/chunk-4XI3YBKU.cjs +266 -0
- package/dist/chunk-4XI3YBKU.cjs.map +1 -0
- package/dist/chunk-ET5I4GBD.cjs +3515 -0
- package/dist/chunk-ET5I4GBD.cjs.map +1 -0
- package/dist/{chunk-FDUDHGI6.js → chunk-JETM2U2D.js} +2345 -795
- package/dist/chunk-JETM2U2D.js.map +1 -0
- package/dist/chunk-LU6J53IX.js +262 -0
- package/dist/chunk-LU6J53IX.js.map +1 -0
- package/dist/chunk-P3VS4DV3.js +3498 -0
- package/dist/chunk-P3VS4DV3.js.map +1 -0
- package/dist/{chunk-DG4INX36.cjs → chunk-QUMBDPNJ.cjs} +2392 -837
- package/dist/chunk-QUMBDPNJ.cjs.map +1 -0
- package/dist/index.cjs +56 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/routes.cjs +34 -14
- package/dist/routes.js +2 -2
- package/dist/templates.cjs +42 -22
- package/dist/templates.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-6BHDKYLU.cjs +0 -798
- package/dist/chunk-6BHDKYLU.cjs.map +0 -1
- package/dist/chunk-DG4INX36.cjs.map +0 -1
- package/dist/chunk-FDUDHGI6.js.map +0 -1
- package/dist/chunk-FZYF43TN.js +0 -789
- package/dist/chunk-FZYF43TN.js.map +0 -1
- package/dist/chunk-G4JGVZAF.js +0 -1337
- package/dist/chunk-G4JGVZAF.js.map +0 -1
- package/dist/chunk-LEGKJ5DI.cjs +0 -1344
- package/dist/chunk-LEGKJ5DI.cjs.map +0 -1
|
@@ -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-
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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/
|
|
3566
|
-
function
|
|
3567
|
-
const
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
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
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
|
7470
|
+
const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7859
7471
|
"&": "&",
|
|
7860
7472
|
"<": "<",
|
|
7861
7473
|
">": ">",
|
|
@@ -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 =
|
|
7479
|
+
const fullName = escapeHtml7(`${truncatedFirstName} ${truncatedLastName}`);
|
|
7868
7480
|
const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
|
|
7869
|
-
const username =
|
|
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
|
|
7497
|
+
const escapeHtml7 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7886
7498
|
"&": "&",
|
|
7887
7499
|
"<": "<",
|
|
7888
7500
|
">": ">",
|
|
7889
7501
|
'"': """,
|
|
7890
7502
|
"'": "'"
|
|
7891
7503
|
})[char] || char);
|
|
7892
|
-
const escapedEmail =
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
8408
|
+
return c.html(renderAlert2({
|
|
8797
8409
|
type: "error",
|
|
8798
|
-
message: "Failed to create user
|
|
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
|
|
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(
|
|
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(
|
|
8513
|
+
return c.html(renderAlert2({
|
|
8902
8514
|
type: "error",
|
|
8903
|
-
message: "Failed to load user
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
8599
|
+
return c.html(renderAlert2({
|
|
8988
8600
|
type: "error",
|
|
8989
|
-
message: "Failed to update user
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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: "
|
|
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: "
|
|
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-
|
|
14233
|
-
//# sourceMappingURL=chunk-
|
|
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
|