@sonicjs-cms/core 2.0.9 → 2.0.11
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-VEC5MLT3.js → chunk-5RKQB2JG.js} +10 -228
- package/dist/chunk-5RKQB2JG.js.map +1 -0
- package/dist/chunk-AMSTLQFI.cjs +801 -0
- package/dist/chunk-AMSTLQFI.cjs.map +1 -0
- package/dist/{chunk-ABYMIXRN.js → chunk-CLLJFZ5U.js} +2018 -1054
- package/dist/chunk-CLLJFZ5U.js.map +1 -0
- package/dist/{chunk-WRRLB6KG.js → chunk-DU7JJZN7.js} +5 -4
- package/dist/chunk-DU7JJZN7.js.map +1 -0
- package/dist/{chunk-OKPDQO2Y.js → chunk-FYWJMETG.js} +30 -10
- package/dist/chunk-FYWJMETG.js.map +1 -0
- package/dist/chunk-I5ZPYKNX.js +787 -0
- package/dist/chunk-I5ZPYKNX.js.map +1 -0
- package/dist/{chunk-4I25AGUR.cjs → chunk-IM2LGCYD.cjs} +2166 -1202
- package/dist/chunk-IM2LGCYD.cjs.map +1 -0
- package/dist/{chunk-TMIRVVQ7.cjs → chunk-NNXPAPUD.cjs} +5 -4
- package/dist/chunk-NNXPAPUD.cjs.map +1 -0
- package/dist/{chunk-OPGDMS7L.js → chunk-QNWYQZ55.js} +3 -3
- package/dist/{chunk-OPGDMS7L.js.map → chunk-QNWYQZ55.js.map} +1 -1
- package/dist/{chunk-COBUPOMD.js → chunk-T7IYBGGO.cjs} +5 -770
- package/dist/chunk-T7IYBGGO.cjs.map +1 -0
- package/dist/{chunk-DYYAXDXI.cjs → chunk-X2VADBA4.cjs} +31 -11
- package/dist/chunk-X2VADBA4.cjs.map +1 -0
- package/dist/{chunk-EYMHWJTW.cjs → chunk-YU6QFFI4.cjs} +9 -228
- package/dist/chunk-YU6QFFI4.cjs.map +1 -0
- package/dist/{chunk-MABBKINE.cjs → chunk-ZMSYKV62.cjs} +5 -5
- package/dist/{chunk-MABBKINE.cjs.map → chunk-ZMSYKV62.cjs.map} +1 -1
- package/dist/{chunk-NBDPIRQS.cjs → chunk-ZPMFT2JW.js} +4 -786
- package/dist/chunk-ZPMFT2JW.js.map +1 -0
- package/dist/index.cjs +475 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +386 -11
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +24 -23
- package/dist/middleware.js +3 -2
- package/dist/migrations-IHERIQVD.js +4 -0
- package/dist/migrations-IHERIQVD.js.map +1 -0
- package/dist/migrations-POFD5KNG.cjs +13 -0
- package/dist/migrations-POFD5KNG.cjs.map +1 -0
- package/dist/routes.cjs +25 -28
- package/dist/routes.js +6 -5
- package/dist/services.cjs +19 -18
- package/dist/services.js +2 -1
- package/dist/templates.cjs +17 -21
- package/dist/templates.js +2 -2
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/migrations/001_initial_schema.sql +2 -2
- package/migrations/007_demo_login_plugin.sql +1 -1
- package/migrations/020_add_email_plugin.sql +22 -0
- package/migrations/021_add_magic_link_auth_plugin.sql +42 -0
- package/migrations/021_add_otp_login.sql +42 -0
- package/migrations/022_add_tinymce_plugin.sql +25 -0
- package/migrations/023_add_mdxeditor_plugin.sql +25 -0
- package/migrations/024_add_quill_editor_plugin.sql +25 -0
- package/migrations/025_add_easymde_plugin.sql +25 -0
- package/package.json +3 -2
- package/dist/chunk-4I25AGUR.cjs.map +0 -1
- package/dist/chunk-ABYMIXRN.js.map +0 -1
- package/dist/chunk-COBUPOMD.js.map +0 -1
- package/dist/chunk-DYYAXDXI.cjs.map +0 -1
- package/dist/chunk-EYMHWJTW.cjs.map +0 -1
- package/dist/chunk-NBDPIRQS.cjs.map +0 -1
- package/dist/chunk-OKPDQO2Y.js.map +0 -1
- package/dist/chunk-TMIRVVQ7.cjs.map +0 -1
- package/dist/chunk-VEC5MLT3.js.map +0 -1
- package/dist/chunk-WRRLB6KG.js.map +0 -1
- package/migrations/002_faq_plugin.sql +0 -86
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-6FR25MPC.js';
|
|
2
|
-
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-
|
|
3
|
-
import { PluginService
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-FYWJMETG.js';
|
|
3
|
+
import { PluginService } from './chunk-I5ZPYKNX.js';
|
|
4
|
+
import { MigrationService } from './chunk-ZPMFT2JW.js';
|
|
5
|
+
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-5RKQB2JG.js';
|
|
6
|
+
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-DU7JJZN7.js';
|
|
6
7
|
import { metricsTracker } from './chunk-FICTAGD4.js';
|
|
7
8
|
import { Hono } from 'hono';
|
|
8
9
|
import { cors } from 'hono/cors';
|
|
@@ -337,8 +338,8 @@ apiRoutes.get("/content", async (c) => {
|
|
|
337
338
|
filter.limit = 50;
|
|
338
339
|
}
|
|
339
340
|
filter.limit = Math.min(filter.limit, 1e3);
|
|
340
|
-
const
|
|
341
|
-
const queryResult =
|
|
341
|
+
const builder3 = new QueryFilterBuilder();
|
|
342
|
+
const queryResult = builder3.build("content", filter);
|
|
342
343
|
if (queryResult.errors.length > 0) {
|
|
343
344
|
return c.json({
|
|
344
345
|
error: "Invalid filter parameters",
|
|
@@ -440,8 +441,8 @@ apiRoutes.get("/collections/:collection/content", async (c) => {
|
|
|
440
441
|
filter.limit = 50;
|
|
441
442
|
}
|
|
442
443
|
filter.limit = Math.min(filter.limit, 1e3);
|
|
443
|
-
const
|
|
444
|
-
const queryResult =
|
|
444
|
+
const builder3 = new QueryFilterBuilder();
|
|
445
|
+
const queryResult = builder3.build("content", filter);
|
|
445
446
|
if (queryResult.errors.length > 0) {
|
|
446
447
|
return c.json({
|
|
447
448
|
error: "Invalid filter parameters",
|
|
@@ -1084,10 +1085,10 @@ apiMediaRoutes.patch("/:id", async (c) => {
|
|
|
1084
1085
|
const allowedFields = ["alt", "caption", "tags", "folder"];
|
|
1085
1086
|
const updates = [];
|
|
1086
1087
|
const values = [];
|
|
1087
|
-
for (const [key,
|
|
1088
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1088
1089
|
if (allowedFields.includes(key)) {
|
|
1089
1090
|
updates.push(`${key} = ?`);
|
|
1090
|
-
values.push(key === "tags" ? JSON.stringify(
|
|
1091
|
+
values.push(key === "tags" ? JSON.stringify(value) : value);
|
|
1091
1092
|
}
|
|
1092
1093
|
}
|
|
1093
1094
|
if (updates.length === 0) {
|
|
@@ -1717,6 +1718,68 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
|
|
|
1717
1718
|
return c.json({ error: "Failed to delete collection" }, 500);
|
|
1718
1719
|
}
|
|
1719
1720
|
});
|
|
1721
|
+
adminApiRoutes.get("/migrations/status", async (c) => {
|
|
1722
|
+
try {
|
|
1723
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
|
|
1724
|
+
const db = c.env.DB;
|
|
1725
|
+
const migrationService = new MigrationService2(db);
|
|
1726
|
+
const status = await migrationService.getMigrationStatus();
|
|
1727
|
+
return c.json({
|
|
1728
|
+
success: true,
|
|
1729
|
+
data: status
|
|
1730
|
+
});
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
console.error("Error fetching migration status:", error);
|
|
1733
|
+
return c.json({
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: "Failed to fetch migration status"
|
|
1736
|
+
}, 500);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
adminApiRoutes.post("/migrations/run", async (c) => {
|
|
1740
|
+
try {
|
|
1741
|
+
const user = c.get("user");
|
|
1742
|
+
if (!user || user.role !== "admin") {
|
|
1743
|
+
return c.json({
|
|
1744
|
+
success: false,
|
|
1745
|
+
error: "Unauthorized. Admin access required."
|
|
1746
|
+
}, 403);
|
|
1747
|
+
}
|
|
1748
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
|
|
1749
|
+
const db = c.env.DB;
|
|
1750
|
+
const migrationService = new MigrationService2(db);
|
|
1751
|
+
const result = await migrationService.runPendingMigrations();
|
|
1752
|
+
return c.json({
|
|
1753
|
+
success: result.success,
|
|
1754
|
+
message: result.message,
|
|
1755
|
+
applied: result.applied
|
|
1756
|
+
});
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
console.error("Error running migrations:", error);
|
|
1759
|
+
return c.json({
|
|
1760
|
+
success: false,
|
|
1761
|
+
error: "Failed to run migrations"
|
|
1762
|
+
}, 500);
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
adminApiRoutes.get("/migrations/validate", async (c) => {
|
|
1766
|
+
try {
|
|
1767
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-IHERIQVD.js');
|
|
1768
|
+
const db = c.env.DB;
|
|
1769
|
+
const migrationService = new MigrationService2(db);
|
|
1770
|
+
const validation = await migrationService.validateSchema();
|
|
1771
|
+
return c.json({
|
|
1772
|
+
success: true,
|
|
1773
|
+
data: validation
|
|
1774
|
+
});
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
console.error("Error validating schema:", error);
|
|
1777
|
+
return c.json({
|
|
1778
|
+
success: false,
|
|
1779
|
+
error: "Failed to validate schema"
|
|
1780
|
+
}, 500);
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1720
1783
|
var admin_api_default = adminApiRoutes;
|
|
1721
1784
|
|
|
1722
1785
|
// src/templates/pages/auth-login.template.ts
|
|
@@ -1861,7 +1924,7 @@ function renderLoginPage(data, demoLoginActive = false) {
|
|
|
1861
1924
|
|
|
1862
1925
|
if (emailInput && passwordInput) {
|
|
1863
1926
|
emailInput.value = 'admin@sonicjs.com';
|
|
1864
|
-
passwordInput.value = '
|
|
1927
|
+
passwordInput.value = 'sonicjs!';
|
|
1865
1928
|
|
|
1866
1929
|
// Add visual indication that form is prefilled (only if not already present)
|
|
1867
1930
|
const form = emailInput.closest('form');
|
|
@@ -2120,16 +2183,22 @@ authRoutes.post(
|
|
|
2120
2183
|
async (c) => {
|
|
2121
2184
|
try {
|
|
2122
2185
|
const db = c.env.DB;
|
|
2123
|
-
|
|
2186
|
+
let requestData;
|
|
2187
|
+
try {
|
|
2188
|
+
requestData = await c.req.json();
|
|
2189
|
+
} catch (parseError) {
|
|
2190
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
2191
|
+
}
|
|
2124
2192
|
const validationSchema = await authValidationService.buildRegistrationSchema(db);
|
|
2125
|
-
|
|
2126
|
-
|
|
2193
|
+
let validatedData;
|
|
2194
|
+
try {
|
|
2195
|
+
validatedData = await validationSchema.parseAsync(requestData);
|
|
2196
|
+
} catch (validationError) {
|
|
2127
2197
|
return c.json({
|
|
2128
2198
|
error: "Validation failed",
|
|
2129
|
-
details:
|
|
2199
|
+
details: validationError.errors?.map((e) => e.message) || [validationError.message || "Invalid request data"]
|
|
2130
2200
|
}, 400);
|
|
2131
2201
|
}
|
|
2132
|
-
const validatedData = validationResult.data;
|
|
2133
2202
|
const email = validatedData.email;
|
|
2134
2203
|
const password = validatedData.password;
|
|
2135
2204
|
const username = validatedData.username || authValidationService.generateDefaultValue("username", validatedData);
|
|
@@ -2181,7 +2250,13 @@ authRoutes.post(
|
|
|
2181
2250
|
}, 201);
|
|
2182
2251
|
} catch (error) {
|
|
2183
2252
|
console.error("Registration error:", error);
|
|
2184
|
-
|
|
2253
|
+
if (error instanceof Error && error.message.includes("validation")) {
|
|
2254
|
+
return c.json({ error: error.message }, 400);
|
|
2255
|
+
}
|
|
2256
|
+
return c.json({
|
|
2257
|
+
error: "Registration failed",
|
|
2258
|
+
details: error instanceof Error ? error.message : String(error)
|
|
2259
|
+
}, 500);
|
|
2185
2260
|
}
|
|
2186
2261
|
}
|
|
2187
2262
|
);
|
|
@@ -2227,8 +2302,8 @@ authRoutes.post("/login", async (c) => {
|
|
|
2227
2302
|
id: user.id,
|
|
2228
2303
|
email: user.email,
|
|
2229
2304
|
username: user.username,
|
|
2230
|
-
firstName: user.
|
|
2231
|
-
lastName: user.
|
|
2305
|
+
firstName: user.first_name,
|
|
2306
|
+
lastName: user.last_name,
|
|
2232
2307
|
role: user.role
|
|
2233
2308
|
},
|
|
2234
2309
|
token
|
|
@@ -2470,8 +2545,10 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2470
2545
|
`).run();
|
|
2471
2546
|
const existingAdmin = await db.prepare("SELECT id FROM users WHERE email = ? OR username = ?").bind("admin@sonicjs.com", "admin").first();
|
|
2472
2547
|
if (existingAdmin) {
|
|
2548
|
+
const passwordHash2 = await AuthManager.hashPassword("sonicjs!");
|
|
2549
|
+
await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
|
|
2473
2550
|
return c.json({
|
|
2474
|
-
message: "Admin user already exists",
|
|
2551
|
+
message: "Admin user already exists (password updated)",
|
|
2475
2552
|
user: {
|
|
2476
2553
|
id: existingAdmin.id,
|
|
2477
2554
|
email: "admin@sonicjs.com",
|
|
@@ -2480,7 +2557,7 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2480
2557
|
}
|
|
2481
2558
|
});
|
|
2482
2559
|
}
|
|
2483
|
-
const passwordHash = await AuthManager.hashPassword("
|
|
2560
|
+
const passwordHash = await AuthManager.hashPassword("sonicjs!");
|
|
2484
2561
|
const userId = "admin-user-id";
|
|
2485
2562
|
const now = Date.now();
|
|
2486
2563
|
const adminEmail = "admin@sonicjs.com".toLowerCase();
|
|
@@ -2990,7 +3067,7 @@ init_admin_layout_catalyst_template();
|
|
|
2990
3067
|
|
|
2991
3068
|
// src/templates/components/dynamic-field.template.ts
|
|
2992
3069
|
function renderDynamicField(field, options = {}) {
|
|
2993
|
-
const { value
|
|
3070
|
+
const { value = "", errors = [], disabled = false, className = "" } = options;
|
|
2994
3071
|
const opts = field.field_options || {};
|
|
2995
3072
|
const required = field.is_required ? "required" : "";
|
|
2996
3073
|
const baseClasses = `w-full rounded-lg px-3 py-2 text-sm text-zinc-950 dark:text-white bg-white dark:bg-zinc-800 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow ${className}`;
|
|
@@ -3047,7 +3124,7 @@ function renderDynamicField(field, options = {}) {
|
|
|
3047
3124
|
type="text"
|
|
3048
3125
|
id="${fieldId}"
|
|
3049
3126
|
name="${fieldName}"
|
|
3050
|
-
value="${escapeHtml2(
|
|
3127
|
+
value="${escapeHtml2(value)}"
|
|
3051
3128
|
placeholder="${opts.placeholder || ""}"
|
|
3052
3129
|
maxlength="${opts.maxLength || ""}"
|
|
3053
3130
|
${opts.pattern ? `data-pattern="${opts.pattern}"` : ""}
|
|
@@ -3083,40 +3160,66 @@ function renderDynamicField(field, options = {}) {
|
|
|
3083
3160
|
` : ""}
|
|
3084
3161
|
`;
|
|
3085
3162
|
break;
|
|
3163
|
+
case "textarea":
|
|
3164
|
+
fieldHTML = `
|
|
3165
|
+
<textarea
|
|
3166
|
+
id="${fieldId}"
|
|
3167
|
+
name="${fieldName}"
|
|
3168
|
+
rows="${opts.rows || 6}"
|
|
3169
|
+
placeholder="${opts.placeholder || ""}"
|
|
3170
|
+
maxlength="${opts.maxLength || ""}"
|
|
3171
|
+
class="${baseClasses} ${errorClasses} resize-y"
|
|
3172
|
+
${required}
|
|
3173
|
+
${disabled ? "disabled" : ""}
|
|
3174
|
+
>${escapeHtml2(value)}</textarea>
|
|
3175
|
+
`;
|
|
3176
|
+
break;
|
|
3086
3177
|
case "richtext":
|
|
3087
3178
|
fieldHTML = `
|
|
3088
|
-
<div class="richtext-container">
|
|
3089
|
-
<textarea
|
|
3179
|
+
<div class="richtext-container" data-height="${opts.height || 300}" data-toolbar="${opts.toolbar || "full"}">
|
|
3180
|
+
<textarea
|
|
3090
3181
|
id="${fieldId}"
|
|
3091
3182
|
name="${fieldName}"
|
|
3092
3183
|
class="${baseClasses} ${errorClasses} min-h-[${opts.height || 300}px]"
|
|
3093
3184
|
${required}
|
|
3094
3185
|
${disabled ? "disabled" : ""}
|
|
3095
|
-
>${escapeHtml2(
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3186
|
+
>${escapeHtml2(value)}</textarea>
|
|
3187
|
+
</div>
|
|
3188
|
+
`;
|
|
3189
|
+
break;
|
|
3190
|
+
case "quill":
|
|
3191
|
+
fieldHTML = `
|
|
3192
|
+
<div class="quill-editor-container" data-field-id="${fieldId}">
|
|
3193
|
+
<!-- Quill Editor Container -->
|
|
3194
|
+
<div
|
|
3195
|
+
id="${fieldId}-editor"
|
|
3196
|
+
class="quill-editor bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100"
|
|
3197
|
+
data-theme="${opts.theme || "snow"}"
|
|
3198
|
+
data-toolbar="${opts.toolbar || "full"}"
|
|
3199
|
+
data-placeholder="${opts.placeholder || "Enter content..."}"
|
|
3200
|
+
data-height="${opts.height || 300}"
|
|
3201
|
+
>${value}</div>
|
|
3202
|
+
|
|
3203
|
+
<!-- Hidden input to store the actual content for form submission -->
|
|
3204
|
+
<input
|
|
3205
|
+
type="hidden"
|
|
3206
|
+
id="${fieldId}"
|
|
3207
|
+
name="${fieldName}"
|
|
3208
|
+
value="${escapeHtml2(value)}"
|
|
3209
|
+
>
|
|
3210
|
+
</div>
|
|
3211
|
+
`;
|
|
3212
|
+
break;
|
|
3213
|
+
case "mdxeditor":
|
|
3214
|
+
fieldHTML = `
|
|
3215
|
+
<div class="richtext-container" data-height="${opts.height || 300}" data-toolbar="${opts.toolbar || "full"}">
|
|
3216
|
+
<textarea
|
|
3217
|
+
id="${fieldId}"
|
|
3218
|
+
name="${fieldName}"
|
|
3219
|
+
class="${baseClasses} ${errorClasses} min-h-[${opts.height || 300}px]"
|
|
3220
|
+
${required}
|
|
3221
|
+
${disabled ? "disabled" : ""}
|
|
3222
|
+
>${escapeHtml2(value)}</textarea>
|
|
3120
3223
|
</div>
|
|
3121
3224
|
`;
|
|
3122
3225
|
break;
|
|
@@ -3126,7 +3229,7 @@ function renderDynamicField(field, options = {}) {
|
|
|
3126
3229
|
type="number"
|
|
3127
3230
|
id="${fieldId}"
|
|
3128
3231
|
name="${fieldName}"
|
|
3129
|
-
value="${
|
|
3232
|
+
value="${value}"
|
|
3130
3233
|
min="${opts.min || ""}"
|
|
3131
3234
|
max="${opts.max || ""}"
|
|
3132
3235
|
step="${opts.step || ""}"
|
|
@@ -3138,7 +3241,7 @@ function renderDynamicField(field, options = {}) {
|
|
|
3138
3241
|
`;
|
|
3139
3242
|
break;
|
|
3140
3243
|
case "boolean":
|
|
3141
|
-
const checked =
|
|
3244
|
+
const checked = value === true || value === "true" || value === "1" ? "checked" : "";
|
|
3142
3245
|
fieldHTML = `
|
|
3143
3246
|
<div class="flex items-center space-x-3">
|
|
3144
3247
|
<input
|
|
@@ -3159,11 +3262,26 @@ function renderDynamicField(field, options = {}) {
|
|
|
3159
3262
|
break;
|
|
3160
3263
|
case "date":
|
|
3161
3264
|
fieldHTML = `
|
|
3162
|
-
<input
|
|
3163
|
-
type="date"
|
|
3265
|
+
<input
|
|
3266
|
+
type="date"
|
|
3267
|
+
id="${fieldId}"
|
|
3268
|
+
name="${fieldName}"
|
|
3269
|
+
value="${value}"
|
|
3270
|
+
min="${opts.min || ""}"
|
|
3271
|
+
max="${opts.max || ""}"
|
|
3272
|
+
class="${baseClasses} ${errorClasses}"
|
|
3273
|
+
${required}
|
|
3274
|
+
${disabled ? "disabled" : ""}
|
|
3275
|
+
>
|
|
3276
|
+
`;
|
|
3277
|
+
break;
|
|
3278
|
+
case "datetime":
|
|
3279
|
+
fieldHTML = `
|
|
3280
|
+
<input
|
|
3281
|
+
type="datetime-local"
|
|
3164
3282
|
id="${fieldId}"
|
|
3165
3283
|
name="${fieldName}"
|
|
3166
|
-
value="${
|
|
3284
|
+
value="${value}"
|
|
3167
3285
|
min="${opts.min || ""}"
|
|
3168
3286
|
max="${opts.max || ""}"
|
|
3169
3287
|
class="${baseClasses} ${errorClasses}"
|
|
@@ -3172,10 +3290,75 @@ function renderDynamicField(field, options = {}) {
|
|
|
3172
3290
|
>
|
|
3173
3291
|
`;
|
|
3174
3292
|
break;
|
|
3293
|
+
case "slug":
|
|
3294
|
+
let slugPattern = opts.pattern || "^[a-z0-9-]+$";
|
|
3295
|
+
let slugHelp = '<p class="mt-2 text-xs text-zinc-500 dark:text-zinc-400">Use lowercase letters, numbers, and hyphens only</p>';
|
|
3296
|
+
slugHelp += `<button type="button" class="mt-1 text-xs text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300" onclick="generateSlugFromTitle('\${fieldId}')">Generate from title</button>`;
|
|
3297
|
+
fieldHTML = `
|
|
3298
|
+
<input
|
|
3299
|
+
type="text"
|
|
3300
|
+
id="${fieldId}"
|
|
3301
|
+
name="${fieldName}"
|
|
3302
|
+
value="${escapeHtml2(value)}"
|
|
3303
|
+
placeholder="${opts.placeholder || "url-friendly-slug"}"
|
|
3304
|
+
maxlength="${opts.maxLength || ""}"
|
|
3305
|
+
data-pattern="${slugPattern}"
|
|
3306
|
+
class="${baseClasses} ${errorClasses}"
|
|
3307
|
+
${required}
|
|
3308
|
+
${disabled ? "disabled" : ""}
|
|
3309
|
+
>
|
|
3310
|
+
${slugHelp}
|
|
3311
|
+
<script>
|
|
3312
|
+
(function() {
|
|
3313
|
+
const field = document.getElementById('${fieldId}');
|
|
3314
|
+
const pattern = new RegExp('${slugPattern}');
|
|
3315
|
+
|
|
3316
|
+
field.addEventListener('input', function() {
|
|
3317
|
+
if (this.value && !pattern.test(this.value)) {
|
|
3318
|
+
this.setCustomValidity('Please use only lowercase letters, numbers, and hyphens.');
|
|
3319
|
+
} else {
|
|
3320
|
+
this.setCustomValidity('');
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3324
|
+
field.addEventListener('blur', function() {
|
|
3325
|
+
this.reportValidity();
|
|
3326
|
+
});
|
|
3327
|
+
})();
|
|
3328
|
+
|
|
3329
|
+
function generateSlugFromTitle(slugFieldId) {
|
|
3330
|
+
const titleField = document.querySelector('input[name="title"]');
|
|
3331
|
+
const slugField = document.getElementById(slugFieldId);
|
|
3332
|
+
if (titleField && slugField) {
|
|
3333
|
+
const slug = titleField.value
|
|
3334
|
+
.toLowerCase()
|
|
3335
|
+
.replace(/[^a-z0-9\\s_-]/g, '')
|
|
3336
|
+
.replace(/\\s+/g, '-')
|
|
3337
|
+
.replace(/[-_]+/g, '-')
|
|
3338
|
+
.replace(/^[-_]|[-_]$/g, '');
|
|
3339
|
+
slugField.value = slug;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
// Auto-generate slug when title changes
|
|
3344
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3345
|
+
const titleField = document.querySelector('input[name="title"]');
|
|
3346
|
+
const slugField = document.getElementById('${fieldId}');
|
|
3347
|
+
if (titleField && slugField && !slugField.value) {
|
|
3348
|
+
titleField.addEventListener('input', function() {
|
|
3349
|
+
if (!slugField.value) {
|
|
3350
|
+
generateSlugFromTitle('${fieldId}');
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
});
|
|
3355
|
+
</script>
|
|
3356
|
+
`;
|
|
3357
|
+
break;
|
|
3175
3358
|
case "select":
|
|
3176
3359
|
const options2 = opts.options || [];
|
|
3177
3360
|
const multiple = opts.multiple ? "multiple" : "";
|
|
3178
|
-
const selectedValues = Array.isArray(
|
|
3361
|
+
const selectedValues = Array.isArray(value) ? value : [value];
|
|
3179
3362
|
fieldHTML = `
|
|
3180
3363
|
<select
|
|
3181
3364
|
id="${fieldId}"
|
|
@@ -3208,9 +3391,9 @@ function renderDynamicField(field, options = {}) {
|
|
|
3208
3391
|
case "media":
|
|
3209
3392
|
fieldHTML = `
|
|
3210
3393
|
<div class="media-field-container">
|
|
3211
|
-
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${
|
|
3212
|
-
<div class="media-preview ${
|
|
3213
|
-
${
|
|
3394
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${value}">
|
|
3395
|
+
<div class="media-preview ${value ? "" : "hidden"}" id="${fieldId}-preview">
|
|
3396
|
+
${value ? `<img src="${value}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg border border-white/20">` : ""}
|
|
3214
3397
|
</div>
|
|
3215
3398
|
<div class="media-actions mt-2 space-x-2">
|
|
3216
3399
|
<button
|
|
@@ -3224,7 +3407,7 @@ function renderDynamicField(field, options = {}) {
|
|
|
3224
3407
|
</svg>
|
|
3225
3408
|
Select Media
|
|
3226
3409
|
</button>
|
|
3227
|
-
${
|
|
3410
|
+
${value ? `
|
|
3228
3411
|
<button
|
|
3229
3412
|
type="button"
|
|
3230
3413
|
onclick="clearMediaField('${fieldId}')"
|
|
@@ -3238,36 +3421,13 @@ function renderDynamicField(field, options = {}) {
|
|
|
3238
3421
|
</div>
|
|
3239
3422
|
`;
|
|
3240
3423
|
break;
|
|
3241
|
-
case "guid":
|
|
3242
|
-
fieldHTML = `
|
|
3243
|
-
<div class="guid-field-container">
|
|
3244
|
-
<input
|
|
3245
|
-
type="text"
|
|
3246
|
-
id="${fieldId}"
|
|
3247
|
-
name="${fieldName}"
|
|
3248
|
-
value="${escapeHtml2(value2)}"
|
|
3249
|
-
class="${baseClasses} bg-zinc-100 dark:bg-zinc-800/50 cursor-not-allowed"
|
|
3250
|
-
readonly
|
|
3251
|
-
disabled
|
|
3252
|
-
>
|
|
3253
|
-
<div class="mt-2 flex items-start gap-x-2">
|
|
3254
|
-
<svg class="h-5 w-5 text-cyan-600 dark:text-cyan-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
3255
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"/>
|
|
3256
|
-
</svg>
|
|
3257
|
-
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
|
3258
|
-
${value2 ? "This unique identifier was automatically generated and cannot be changed." : "A unique identifier (UUID) will be automatically generated when you save this content."}
|
|
3259
|
-
</div>
|
|
3260
|
-
</div>
|
|
3261
|
-
</div>
|
|
3262
|
-
`;
|
|
3263
|
-
break;
|
|
3264
3424
|
default:
|
|
3265
3425
|
fieldHTML = `
|
|
3266
3426
|
<input
|
|
3267
3427
|
type="text"
|
|
3268
3428
|
id="${fieldId}"
|
|
3269
3429
|
name="${fieldName}"
|
|
3270
|
-
value="${escapeHtml2(
|
|
3430
|
+
value="${escapeHtml2(value)}"
|
|
3271
3431
|
class="${baseClasses} ${errorClasses}"
|
|
3272
3432
|
${required}
|
|
3273
3433
|
${disabled ? "disabled" : ""}
|
|
@@ -3324,34 +3484,750 @@ function escapeHtml2(text) {
|
|
|
3324
3484
|
"'": "'"
|
|
3325
3485
|
})[char] || char);
|
|
3326
3486
|
}
|
|
3487
|
+
var PluginBuilder = class _PluginBuilder {
|
|
3488
|
+
plugin;
|
|
3489
|
+
constructor(options) {
|
|
3490
|
+
this.plugin = {
|
|
3491
|
+
name: options.name,
|
|
3492
|
+
version: options.version,
|
|
3493
|
+
description: options.description,
|
|
3494
|
+
author: options.author,
|
|
3495
|
+
dependencies: options.dependencies,
|
|
3496
|
+
routes: [],
|
|
3497
|
+
middleware: [],
|
|
3498
|
+
models: [],
|
|
3499
|
+
services: [],
|
|
3500
|
+
adminPages: [],
|
|
3501
|
+
adminComponents: [],
|
|
3502
|
+
menuItems: [],
|
|
3503
|
+
hooks: []
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Create a new plugin builder
|
|
3508
|
+
*/
|
|
3509
|
+
static create(options) {
|
|
3510
|
+
return new _PluginBuilder(options);
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Add metadata to the plugin
|
|
3514
|
+
*/
|
|
3515
|
+
metadata(metadata) {
|
|
3516
|
+
Object.assign(this.plugin, metadata);
|
|
3517
|
+
return this;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Add routes to plugin
|
|
3521
|
+
*/
|
|
3522
|
+
addRoutes(routes) {
|
|
3523
|
+
this.plugin.routes = [...this.plugin.routes || [], ...routes];
|
|
3524
|
+
return this;
|
|
3525
|
+
}
|
|
3526
|
+
/**
|
|
3527
|
+
* Add a single route to plugin
|
|
3528
|
+
*/
|
|
3529
|
+
addRoute(path, handler, options) {
|
|
3530
|
+
const route = {
|
|
3531
|
+
path,
|
|
3532
|
+
handler,
|
|
3533
|
+
...options
|
|
3534
|
+
};
|
|
3535
|
+
this.plugin.routes = [...this.plugin.routes || [], route];
|
|
3536
|
+
return this;
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Add middleware to plugin
|
|
3540
|
+
*/
|
|
3541
|
+
addMiddleware(middleware) {
|
|
3542
|
+
this.plugin.middleware = [...this.plugin.middleware || [], ...middleware];
|
|
3543
|
+
return this;
|
|
3544
|
+
}
|
|
3545
|
+
/**
|
|
3546
|
+
* Add a single middleware to plugin
|
|
3547
|
+
*/
|
|
3548
|
+
addSingleMiddleware(name, handler, options) {
|
|
3549
|
+
const middleware = {
|
|
3550
|
+
name,
|
|
3551
|
+
handler,
|
|
3552
|
+
...options
|
|
3553
|
+
};
|
|
3554
|
+
this.plugin.middleware = [...this.plugin.middleware || [], middleware];
|
|
3555
|
+
return this;
|
|
3556
|
+
}
|
|
3557
|
+
/**
|
|
3558
|
+
* Add models to plugin
|
|
3559
|
+
*/
|
|
3560
|
+
addModels(models) {
|
|
3561
|
+
this.plugin.models = [...this.plugin.models || [], ...models];
|
|
3562
|
+
return this;
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Add a single model to plugin
|
|
3566
|
+
*/
|
|
3567
|
+
addModel(name, options) {
|
|
3568
|
+
const model = {
|
|
3569
|
+
name,
|
|
3570
|
+
...options
|
|
3571
|
+
};
|
|
3572
|
+
this.plugin.models = [...this.plugin.models || [], model];
|
|
3573
|
+
return this;
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Add services to plugin
|
|
3577
|
+
*/
|
|
3578
|
+
addServices(services) {
|
|
3579
|
+
this.plugin.services = [...this.plugin.services || [], ...services];
|
|
3580
|
+
return this;
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Add a single service to plugin
|
|
3584
|
+
*/
|
|
3585
|
+
addService(name, implementation, options) {
|
|
3586
|
+
const service = {
|
|
3587
|
+
name,
|
|
3588
|
+
implementation,
|
|
3589
|
+
...options
|
|
3590
|
+
};
|
|
3591
|
+
this.plugin.services = [...this.plugin.services || [], service];
|
|
3592
|
+
return this;
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Add admin pages to plugin
|
|
3596
|
+
*/
|
|
3597
|
+
addAdminPages(pages) {
|
|
3598
|
+
this.plugin.adminPages = [...this.plugin.adminPages || [], ...pages];
|
|
3599
|
+
return this;
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Add a single admin page to plugin
|
|
3603
|
+
*/
|
|
3604
|
+
addAdminPage(path, title, component, options) {
|
|
3605
|
+
const page = {
|
|
3606
|
+
path,
|
|
3607
|
+
title,
|
|
3608
|
+
component,
|
|
3609
|
+
...options
|
|
3610
|
+
};
|
|
3611
|
+
this.plugin.adminPages = [...this.plugin.adminPages || [], page];
|
|
3612
|
+
return this;
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Add admin components to plugin
|
|
3616
|
+
*/
|
|
3617
|
+
addComponents(components) {
|
|
3618
|
+
this.plugin.adminComponents = [...this.plugin.adminComponents || [], ...components];
|
|
3619
|
+
return this;
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Add a single admin component to plugin
|
|
3623
|
+
*/
|
|
3624
|
+
addComponent(name, template, options) {
|
|
3625
|
+
const component = {
|
|
3626
|
+
name,
|
|
3627
|
+
template,
|
|
3628
|
+
...options
|
|
3629
|
+
};
|
|
3630
|
+
this.plugin.adminComponents = [...this.plugin.adminComponents || [], component];
|
|
3631
|
+
return this;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Add menu items to plugin
|
|
3635
|
+
*/
|
|
3636
|
+
addMenuItems(items) {
|
|
3637
|
+
this.plugin.menuItems = [...this.plugin.menuItems || [], ...items];
|
|
3638
|
+
return this;
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Add a single menu item to plugin
|
|
3642
|
+
*/
|
|
3643
|
+
addMenuItem(label, path, options) {
|
|
3644
|
+
const menuItem = {
|
|
3645
|
+
label,
|
|
3646
|
+
path,
|
|
3647
|
+
...options
|
|
3648
|
+
};
|
|
3649
|
+
this.plugin.menuItems = [...this.plugin.menuItems || [], menuItem];
|
|
3650
|
+
return this;
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Add hooks to plugin
|
|
3654
|
+
*/
|
|
3655
|
+
addHooks(hooks) {
|
|
3656
|
+
this.plugin.hooks = [...this.plugin.hooks || [], ...hooks];
|
|
3657
|
+
return this;
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Add a single hook to plugin
|
|
3661
|
+
*/
|
|
3662
|
+
addHook(name, handler, options) {
|
|
3663
|
+
const hook = {
|
|
3664
|
+
name,
|
|
3665
|
+
handler,
|
|
3666
|
+
...options
|
|
3667
|
+
};
|
|
3668
|
+
this.plugin.hooks = [...this.plugin.hooks || [], hook];
|
|
3669
|
+
return this;
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Add lifecycle hooks
|
|
3673
|
+
*/
|
|
3674
|
+
lifecycle(hooks) {
|
|
3675
|
+
Object.assign(this.plugin, hooks);
|
|
3676
|
+
return this;
|
|
3677
|
+
}
|
|
3678
|
+
/**
|
|
3679
|
+
* Build the plugin
|
|
3680
|
+
*/
|
|
3681
|
+
build() {
|
|
3682
|
+
if (!this.plugin.name || !this.plugin.version) {
|
|
3683
|
+
throw new Error("Plugin name and version are required");
|
|
3684
|
+
}
|
|
3685
|
+
return this.plugin;
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3327
3688
|
|
|
3328
|
-
// src/
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
|
-
|
|
3689
|
+
// src/plugins/available/tinymce-plugin/index.ts
|
|
3690
|
+
var builder = PluginBuilder.create({
|
|
3691
|
+
name: "tinymce-plugin",
|
|
3692
|
+
version: "1.0.0",
|
|
3693
|
+
description: "Powerful WYSIWYG rich text editor for content creation"
|
|
3694
|
+
});
|
|
3695
|
+
builder.metadata({
|
|
3696
|
+
author: {
|
|
3697
|
+
name: "SonicJS Team",
|
|
3698
|
+
email: "team@sonicjs.com"
|
|
3699
|
+
},
|
|
3700
|
+
license: "MIT",
|
|
3701
|
+
compatibility: "^2.0.0"
|
|
3702
|
+
});
|
|
3703
|
+
builder.lifecycle({
|
|
3704
|
+
activate: async () => {
|
|
3705
|
+
console.info("\u2705 TinyMCE plugin activated");
|
|
3706
|
+
},
|
|
3707
|
+
deactivate: async () => {
|
|
3708
|
+
console.info("\u274C TinyMCE plugin deactivated");
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
builder.build();
|
|
3712
|
+
function getTinyMCEScript(apiKey = "no-api-key") {
|
|
3713
|
+
return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
|
|
3714
|
+
}
|
|
3715
|
+
function getTinyMCEInitScript(config) {
|
|
3716
|
+
const skin = config?.skin || "oxide-dark";
|
|
3717
|
+
const contentCss = skin.includes("dark") ? "dark" : "default";
|
|
3718
|
+
const defaultHeight = config?.defaultHeight || 300;
|
|
3719
|
+
return `
|
|
3720
|
+
// Initialize TinyMCE for all richtext fields
|
|
3721
|
+
function initializeTinyMCE() {
|
|
3722
|
+
if (typeof tinymce !== 'undefined') {
|
|
3723
|
+
// Find all textareas that need TinyMCE
|
|
3724
|
+
document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
|
|
3725
|
+
// Skip if already initialized
|
|
3726
|
+
if (tinymce.get(textarea.id)) {
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// Get configuration from data attributes
|
|
3731
|
+
const container = textarea.closest('.richtext-container');
|
|
3732
|
+
const height = container?.dataset.height || ${defaultHeight};
|
|
3733
|
+
const toolbar = container?.dataset.toolbar || 'full';
|
|
3734
|
+
|
|
3735
|
+
tinymce.init({
|
|
3736
|
+
selector: '#' + textarea.id,
|
|
3737
|
+
skin: '${skin}',
|
|
3738
|
+
content_css: '${contentCss}',
|
|
3739
|
+
height: parseInt(height),
|
|
3740
|
+
menubar: false,
|
|
3741
|
+
plugins: [
|
|
3742
|
+
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
3743
|
+
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
3744
|
+
'insertdatetime', 'media', 'table', 'help', 'wordcount'
|
|
3745
|
+
],
|
|
3746
|
+
toolbar: toolbar === 'simple'
|
|
3747
|
+
? 'bold italic underline | bullist numlist | link'
|
|
3748
|
+
: toolbar === 'minimal'
|
|
3749
|
+
? 'bold italic | link'
|
|
3750
|
+
: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
|
|
3751
|
+
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-size: 14px }'
|
|
3752
|
+
});
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// Initialize on DOMContentLoaded
|
|
3758
|
+
if (document.readyState === 'loading') {
|
|
3759
|
+
document.addEventListener('DOMContentLoaded', initializeTinyMCE);
|
|
3760
|
+
} else {
|
|
3761
|
+
// DOM already loaded, initialize immediately
|
|
3762
|
+
initializeTinyMCE();
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// Also reinitialize after HTMX swaps (for dynamic content)
|
|
3766
|
+
document.addEventListener('htmx:afterSwap', function(event) {
|
|
3767
|
+
// Give the DOM a moment to settle
|
|
3768
|
+
setTimeout(initializeTinyMCE, 100);
|
|
3769
|
+
});
|
|
3770
|
+
`;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/plugins/core-plugins/quill-editor/index.ts
|
|
3774
|
+
var QUILL_TOOLBARS = {
|
|
3775
|
+
full: [
|
|
3776
|
+
[{ "header": [1, 2, 3, 4, 5, 6, false] }],
|
|
3777
|
+
["bold", "italic", "underline", "strike"],
|
|
3778
|
+
[{ "color": [] }, { "background": [] }],
|
|
3779
|
+
[{ "align": [] }],
|
|
3780
|
+
[{ "list": "ordered" }, { "list": "bullet" }],
|
|
3781
|
+
[{ "indent": "-1" }, { "indent": "+1" }],
|
|
3782
|
+
["blockquote", "code-block"],
|
|
3783
|
+
["link", "image", "video"],
|
|
3784
|
+
["clean"]
|
|
3785
|
+
],
|
|
3786
|
+
simple: [
|
|
3787
|
+
["bold", "italic", "underline"],
|
|
3788
|
+
[{ "list": "ordered" }, { "list": "bullet" }],
|
|
3789
|
+
["link"]
|
|
3790
|
+
],
|
|
3791
|
+
minimal: [
|
|
3792
|
+
["bold", "italic"],
|
|
3793
|
+
["link"]
|
|
3794
|
+
]
|
|
3795
|
+
};
|
|
3796
|
+
function getQuillInitScript() {
|
|
3797
|
+
return `
|
|
3798
|
+
<script>
|
|
3799
|
+
// Global Quill initialization function
|
|
3800
|
+
window.initializeQuillEditors = function() {
|
|
3801
|
+
console.log('[Quill] initializeQuillEditors called');
|
|
3802
|
+
if (typeof Quill === 'undefined') {
|
|
3803
|
+
console.warn('[Quill] Quill is not loaded yet. Retrying...');
|
|
3804
|
+
setTimeout(window.initializeQuillEditors, 100);
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
console.log('[Quill] Quill is loaded, searching for editors...');
|
|
3809
|
+
// Find all Quill editor containers that haven't been initialized
|
|
3810
|
+
const containers = document.querySelectorAll('.quill-editor-container');
|
|
3811
|
+
console.log('[Quill] Found', containers.length, 'editor containers');
|
|
3812
|
+
|
|
3813
|
+
containers.forEach((container, index) => {
|
|
3814
|
+
console.log('[Quill] Processing container', index);
|
|
3815
|
+
try {
|
|
3816
|
+
const editorDiv = container.querySelector('.quill-editor');
|
|
3817
|
+
if (!editorDiv || editorDiv.classList.contains('ql-container')) {
|
|
3818
|
+
return; // Already initialized or invalid
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
const fieldId = container.getAttribute('data-field-id');
|
|
3822
|
+
const hiddenInput = document.getElementById(fieldId);
|
|
3823
|
+
const theme = editorDiv.getAttribute('data-theme') || 'snow';
|
|
3824
|
+
const toolbarPreset = editorDiv.getAttribute('data-toolbar') || 'full';
|
|
3825
|
+
const placeholder = editorDiv.getAttribute('data-placeholder') || 'Enter content...';
|
|
3826
|
+
const height = parseInt(editorDiv.getAttribute('data-height') || '300');
|
|
3827
|
+
|
|
3828
|
+
// Get toolbar configuration
|
|
3829
|
+
const toolbarConfig = ${JSON.stringify(QUILL_TOOLBARS)};
|
|
3830
|
+
const toolbar = toolbarConfig[toolbarPreset] || toolbarConfig.full;
|
|
3831
|
+
|
|
3832
|
+
// Initialize Quill
|
|
3833
|
+
const quill = new Quill('#' + editorDiv.id, {
|
|
3834
|
+
theme: theme,
|
|
3835
|
+
placeholder: placeholder,
|
|
3836
|
+
modules: {
|
|
3837
|
+
toolbar: toolbar
|
|
3838
|
+
},
|
|
3839
|
+
formats: [
|
|
3840
|
+
'header', 'bold', 'italic', 'underline', 'strike',
|
|
3841
|
+
'color', 'background', 'align',
|
|
3842
|
+
'list', 'bullet', 'indent',
|
|
3843
|
+
'blockquote', 'code-block',
|
|
3844
|
+
'link', 'image', 'video'
|
|
3845
|
+
]
|
|
3846
|
+
});
|
|
3847
|
+
|
|
3848
|
+
// Set editor height
|
|
3849
|
+
const editorElement = editorDiv.querySelector('.ql-editor');
|
|
3850
|
+
if (editorElement) {
|
|
3851
|
+
editorElement.style.minHeight = height + 'px';
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
// Sync content to hidden input on change
|
|
3855
|
+
quill.on('text-change', function() {
|
|
3856
|
+
const html = quill.root.innerHTML;
|
|
3857
|
+
if (hiddenInput) {
|
|
3858
|
+
hiddenInput.value = html;
|
|
3859
|
+
}
|
|
3860
|
+
});
|
|
3861
|
+
|
|
3862
|
+
// Store quill instance for potential later access
|
|
3863
|
+
editorDiv.quillInstance = quill;
|
|
3864
|
+
console.log('[Quill] Successfully initialized editor', index);
|
|
3865
|
+
} catch (error) {
|
|
3866
|
+
console.error('[Quill] Error initializing editor', index, ':', error);
|
|
3867
|
+
}
|
|
3868
|
+
});
|
|
3869
|
+
console.log('[Quill] Initialization complete');
|
|
3870
|
+
};
|
|
3871
|
+
|
|
3872
|
+
// Initialize on DOM ready
|
|
3873
|
+
if (document.readyState === 'loading') {
|
|
3874
|
+
document.addEventListener('DOMContentLoaded', window.initializeQuillEditors);
|
|
3875
|
+
} else {
|
|
3876
|
+
window.initializeQuillEditors();
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
// Re-initialize after HTMX swaps
|
|
3880
|
+
if (typeof htmx !== 'undefined') {
|
|
3881
|
+
document.body.addEventListener('htmx:afterSwap', window.initializeQuillEditors);
|
|
3882
|
+
}
|
|
3883
|
+
</script>
|
|
3884
|
+
`;
|
|
3885
|
+
}
|
|
3886
|
+
function getQuillCDN(version = "2.0.2") {
|
|
3887
|
+
return `
|
|
3888
|
+
<!-- Quill Editor CSS -->
|
|
3889
|
+
<link href="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.snow.css" rel="stylesheet">
|
|
3890
|
+
<link href="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.bubble.css" rel="stylesheet">
|
|
3891
|
+
|
|
3892
|
+
<!-- Quill Editor JS -->
|
|
3893
|
+
<script src="https://cdn.jsdelivr.net/npm/quill@${version}/dist/quill.js"></script>
|
|
3894
|
+
|
|
3895
|
+
<!-- Quill Dark Mode Styles -->
|
|
3896
|
+
<style>
|
|
3897
|
+
/* Dark mode styles for Quill editor */
|
|
3898
|
+
.dark .ql-toolbar {
|
|
3899
|
+
background-color: #18181b !important;
|
|
3900
|
+
border-color: #3f3f46 !important;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
.dark .ql-toolbar button,
|
|
3904
|
+
.dark .ql-toolbar .ql-picker-label {
|
|
3905
|
+
color: #e4e4e7 !important;
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
.dark .ql-toolbar button:hover,
|
|
3909
|
+
.dark .ql-toolbar .ql-picker-label:hover {
|
|
3910
|
+
color: #fff !important;
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
.dark .ql-toolbar button.ql-active,
|
|
3914
|
+
.dark .ql-toolbar .ql-picker-label.ql-active {
|
|
3915
|
+
color: #3b82f6 !important;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
.dark .ql-stroke {
|
|
3919
|
+
stroke: #e4e4e7 !important;
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
.dark .ql-fill {
|
|
3923
|
+
fill: #e4e4e7 !important;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
.dark .ql-picker-options {
|
|
3927
|
+
background-color: #18181b !important;
|
|
3928
|
+
border-color: #3f3f46 !important;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
.dark .ql-picker-item:hover {
|
|
3932
|
+
background-color: #27272a !important;
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
.dark .ql-container {
|
|
3936
|
+
background-color: #09090b !important;
|
|
3937
|
+
border-color: #3f3f46 !important;
|
|
3938
|
+
color: #e4e4e7 !important;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
.dark .ql-editor {
|
|
3942
|
+
color: #e4e4e7 !important;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
.dark .ql-editor.ql-blank::before {
|
|
3946
|
+
color: #71717a !important;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
/* Improve contrast for dark mode */
|
|
3950
|
+
.dark .ql-snow .ql-stroke {
|
|
3951
|
+
stroke: #e4e4e7 !important;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
.dark .ql-snow .ql-stroke.ql-fill {
|
|
3955
|
+
fill: #e4e4e7 !important;
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
.dark .ql-snow .ql-fill {
|
|
3959
|
+
fill: #e4e4e7 !important;
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
.dark .ql-snow .ql-picker-options {
|
|
3963
|
+
background-color: #18181b !important;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
.dark .ql-snow .ql-picker-item:hover {
|
|
3967
|
+
color: #3b82f6 !important;
|
|
3968
|
+
}
|
|
3969
|
+
</style>
|
|
3970
|
+
`;
|
|
3971
|
+
}
|
|
3972
|
+
function createQuillEditorPlugin() {
|
|
3973
|
+
const builder3 = PluginBuilder.create({
|
|
3974
|
+
name: "quill-editor",
|
|
3975
|
+
version: "1.0.0",
|
|
3976
|
+
description: "Quill rich text editor integration for SonicJS"
|
|
3977
|
+
});
|
|
3978
|
+
builder3.metadata({
|
|
3979
|
+
author: {
|
|
3980
|
+
name: "SonicJS Team",
|
|
3981
|
+
email: "team@sonicjs.com"
|
|
3982
|
+
},
|
|
3983
|
+
license: "MIT",
|
|
3984
|
+
compatibility: "^2.0.0",
|
|
3985
|
+
tags: ["editor", "rich-text", "wysiwyg", "quill"]
|
|
3986
|
+
});
|
|
3987
|
+
builder3.lifecycle({
|
|
3988
|
+
activate: async () => {
|
|
3989
|
+
console.info("\u2705 Quill Editor plugin activated");
|
|
3990
|
+
},
|
|
3991
|
+
deactivate: async () => {
|
|
3992
|
+
console.info("\u274C Quill Editor plugin deactivated");
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
return builder3.build();
|
|
3996
|
+
}
|
|
3997
|
+
createQuillEditorPlugin();
|
|
3998
|
+
|
|
3999
|
+
// src/plugins/available/mdxeditor-plugin/index.ts
|
|
4000
|
+
var builder2 = PluginBuilder.create({
|
|
4001
|
+
name: "mdxeditor-plugin",
|
|
4002
|
+
version: "1.0.0",
|
|
4003
|
+
description: "Lightweight markdown editor with live preview"
|
|
4004
|
+
});
|
|
4005
|
+
builder2.metadata({
|
|
4006
|
+
author: {
|
|
4007
|
+
name: "SonicJS Team",
|
|
4008
|
+
email: "team@sonicjs.com"
|
|
4009
|
+
},
|
|
4010
|
+
license: "MIT",
|
|
4011
|
+
compatibility: "^2.0.0"
|
|
4012
|
+
});
|
|
4013
|
+
builder2.lifecycle({
|
|
4014
|
+
activate: async () => {
|
|
4015
|
+
console.info("\u2705 EasyMDE editor plugin activated");
|
|
4016
|
+
},
|
|
4017
|
+
deactivate: async () => {
|
|
4018
|
+
console.info("\u274C EasyMDE editor plugin deactivated");
|
|
4019
|
+
}
|
|
4020
|
+
});
|
|
4021
|
+
builder2.build();
|
|
4022
|
+
function getMDXEditorScripts() {
|
|
4023
|
+
return `
|
|
4024
|
+
<!-- EasyMDE Markdown Editor -->
|
|
4025
|
+
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
|
4026
|
+
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
|
4027
|
+
<style>
|
|
4028
|
+
/* Dark mode styling for EasyMDE */
|
|
4029
|
+
.EasyMDEContainer {
|
|
4030
|
+
background-color: #1e293b;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
.EasyMDEContainer .CodeMirror {
|
|
4034
|
+
background-color: #1e293b;
|
|
4035
|
+
color: #e2e8f0;
|
|
4036
|
+
border-color: #334155;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
.EasyMDEContainer .CodeMirror-scroll {
|
|
4040
|
+
background-color: #1e293b;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
.EasyMDEContainer .CodeMirror-cursor {
|
|
4044
|
+
border-left-color: #e2e8f0;
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
.EasyMDEContainer .CodeMirror-gutters {
|
|
4048
|
+
background-color: #0f172a;
|
|
4049
|
+
border-right-color: #334155;
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
.EasyMDEContainer .CodeMirror-linenumber {
|
|
4053
|
+
color: #64748b;
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
.editor-toolbar {
|
|
4057
|
+
background-color: #0f172a;
|
|
4058
|
+
border-color: #334155;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
.editor-toolbar button {
|
|
4062
|
+
color: #94a3b8 !important;
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
.editor-toolbar button:hover,
|
|
4066
|
+
.editor-toolbar button.active {
|
|
4067
|
+
background-color: #334155;
|
|
4068
|
+
border-color: #475569;
|
|
4069
|
+
color: #e2e8f0 !important;
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
.editor-toolbar i.separator {
|
|
4073
|
+
border-left-color: #334155;
|
|
4074
|
+
border-right-color: #334155;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
.editor-statusbar {
|
|
4078
|
+
background-color: #0f172a;
|
|
4079
|
+
color: #64748b;
|
|
4080
|
+
border-top-color: #334155;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
.editor-preview,
|
|
4084
|
+
.editor-preview-side {
|
|
4085
|
+
background-color: #1e293b;
|
|
4086
|
+
color: #e2e8f0;
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
.CodeMirror-selected {
|
|
4090
|
+
background-color: #334155 !important;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
.CodeMirror-focused .CodeMirror-selected {
|
|
4094
|
+
background-color: #475569 !important;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
/* Syntax highlighting for dark mode */
|
|
4098
|
+
.cm-header {
|
|
4099
|
+
color: #60a5fa;
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
.cm-strong {
|
|
4103
|
+
color: #fbbf24;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
.cm-em {
|
|
4107
|
+
color: #a78bfa;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
.cm-link {
|
|
4111
|
+
color: #34d399;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
.cm-url {
|
|
4115
|
+
color: #34d399;
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
.cm-quote {
|
|
4119
|
+
color: #94a3b8;
|
|
4120
|
+
font-style: italic;
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
.cm-comment {
|
|
4124
|
+
color: #64748b;
|
|
4125
|
+
}
|
|
4126
|
+
</style>
|
|
4127
|
+
`;
|
|
4128
|
+
}
|
|
4129
|
+
function getMDXEditorInitScript(config) {
|
|
4130
|
+
const defaultHeight = config?.defaultHeight || 400;
|
|
4131
|
+
const toolbar = config?.toolbar || "full";
|
|
4132
|
+
const placeholder = config?.placeholder || "Start writing your content...";
|
|
4133
|
+
return `
|
|
4134
|
+
// Initialize EasyMDE (Markdown Editor) for all richtext fields
|
|
4135
|
+
function initializeMDXEditor() {
|
|
4136
|
+
if (typeof EasyMDE === 'undefined') {
|
|
4137
|
+
console.warn('EasyMDE not loaded yet, retrying...');
|
|
4138
|
+
setTimeout(initializeMDXEditor, 100);
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// Find all textareas that need EasyMDE
|
|
4143
|
+
document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
|
|
4144
|
+
// Skip if already initialized
|
|
4145
|
+
if (textarea.dataset.mdxeditorInitialized === 'true') {
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
// Mark as initialized
|
|
4150
|
+
textarea.dataset.mdxeditorInitialized = 'true';
|
|
4151
|
+
|
|
4152
|
+
// Get configuration from data attributes
|
|
4153
|
+
const container = textarea.closest('.richtext-container');
|
|
4154
|
+
const height = container?.dataset.height || ${defaultHeight};
|
|
4155
|
+
const editorToolbar = container?.dataset.toolbar || '${toolbar}';
|
|
4156
|
+
|
|
4157
|
+
// Initialize EasyMDE
|
|
4158
|
+
try {
|
|
4159
|
+
const toolbarButtons = editorToolbar === 'minimal'
|
|
4160
|
+
? ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'preview']
|
|
4161
|
+
: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', '|', 'preview', 'side-by-side', 'fullscreen', '|', 'guide'];
|
|
4162
|
+
|
|
4163
|
+
const easyMDE = new EasyMDE({
|
|
4164
|
+
element: textarea,
|
|
4165
|
+
placeholder: '${placeholder}',
|
|
4166
|
+
spellChecker: false,
|
|
4167
|
+
minHeight: height + 'px',
|
|
4168
|
+
toolbar: toolbarButtons,
|
|
4169
|
+
status: ['lines', 'words', 'cursor'],
|
|
4170
|
+
renderingConfig: {
|
|
4171
|
+
singleLineBreaks: false,
|
|
4172
|
+
codeSyntaxHighlighting: true
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
|
|
4176
|
+
// Store reference to editor instance
|
|
4177
|
+
textarea.easyMDEInstance = easyMDE;
|
|
4178
|
+
|
|
4179
|
+
console.log('EasyMDE initialized for field:', textarea.id || textarea.name);
|
|
4180
|
+
} catch (error) {
|
|
4181
|
+
console.error('Error initializing EasyMDE:', error);
|
|
4182
|
+
// Show textarea as fallback
|
|
4183
|
+
textarea.style.display = 'block';
|
|
4184
|
+
}
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// Initialize on DOMContentLoaded
|
|
4189
|
+
if (document.readyState === 'loading') {
|
|
4190
|
+
document.addEventListener('DOMContentLoaded', initializeMDXEditor);
|
|
4191
|
+
} else {
|
|
4192
|
+
// DOM already loaded, initialize immediately
|
|
4193
|
+
initializeMDXEditor();
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
// Also reinitialize after HTMX swaps (for dynamic content)
|
|
4197
|
+
document.addEventListener('htmx:afterSwap', function(event) {
|
|
4198
|
+
// Give the DOM a moment to settle
|
|
4199
|
+
setTimeout(initializeMDXEditor, 100);
|
|
4200
|
+
});
|
|
4201
|
+
`;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// src/templates/pages/admin-content-form.template.ts
|
|
4205
|
+
function renderContentFormPage(data) {
|
|
4206
|
+
const isEdit = data.isEdit || !!data.id;
|
|
4207
|
+
const title = isEdit ? `Edit: ${data.title || "Content"}` : `New ${data.collection.display_name}`;
|
|
4208
|
+
const backUrl = data.referrerParams ? `/admin/content?${data.referrerParams}` : `/admin/content?collection=${data.collection.id}`;
|
|
4209
|
+
const coreFields = data.fields.filter((f) => ["title", "slug", "content"].includes(f.field_name));
|
|
4210
|
+
const contentFields = data.fields.filter((f) => !["title", "slug", "content"].includes(f.field_name) && !f.field_name.startsWith("meta_"));
|
|
4211
|
+
const metaFields = data.fields.filter((f) => f.field_name.startsWith("meta_"));
|
|
4212
|
+
const getFieldValue = (fieldName) => {
|
|
4213
|
+
if (fieldName === "title") return data.title || data.data?.[fieldName] || "";
|
|
4214
|
+
if (fieldName === "slug") return data.slug || data.data?.[fieldName] || "";
|
|
4215
|
+
return data.data?.[fieldName] || "";
|
|
4216
|
+
};
|
|
4217
|
+
const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4218
|
+
value: getFieldValue(field.field_name),
|
|
4219
|
+
errors: data.validationErrors?.[field.field_name] || []
|
|
4220
|
+
}));
|
|
4221
|
+
const contentFieldsHTML = contentFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4222
|
+
value: getFieldValue(field.field_name),
|
|
4223
|
+
errors: data.validationErrors?.[field.field_name] || []
|
|
4224
|
+
}));
|
|
4225
|
+
const metaFieldsHTML = metaFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4226
|
+
value: getFieldValue(field.field_name),
|
|
4227
|
+
errors: data.validationErrors?.[field.field_name] || []
|
|
4228
|
+
}));
|
|
4229
|
+
const pageContent = `
|
|
4230
|
+
<div class="space-y-6">
|
|
3355
4231
|
<!-- Header -->
|
|
3356
4232
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
3357
4233
|
<div>
|
|
@@ -3650,8 +4526,13 @@ function renderContentFormPage(data) {
|
|
|
3650
4526
|
|
|
3651
4527
|
${getConfirmationDialogScript()}
|
|
3652
4528
|
|
|
3653
|
-
<!-- TinyMCE
|
|
3654
|
-
|
|
4529
|
+
${data.tinymceEnabled ? getTinyMCEScript(data.tinymceSettings?.apiKey) : "<!-- TinyMCE plugin not active -->"}
|
|
4530
|
+
|
|
4531
|
+
${data.quillEnabled ? getQuillCDN(data.quillSettings?.version) : "<!-- Quill plugin not active -->"}
|
|
4532
|
+
|
|
4533
|
+
${data.quillEnabled ? getQuillInitScript() : "<!-- Quill init script not needed -->"}
|
|
4534
|
+
|
|
4535
|
+
${data.mdxeditorEnabled ? getMDXEditorScripts() : "<!-- MDXEditor plugin not active -->"}
|
|
3655
4536
|
|
|
3656
4537
|
<!-- Dynamic Field Scripts -->
|
|
3657
4538
|
<script>
|
|
@@ -3926,6 +4807,19 @@ function renderContentFormPage(data) {
|
|
|
3926
4807
|
form.addEventListener('input', scheduleAutoSave);
|
|
3927
4808
|
form.addEventListener('change', scheduleAutoSave);
|
|
3928
4809
|
});
|
|
4810
|
+
|
|
4811
|
+
${data.tinymceEnabled ? `<script>${getTinyMCEInitScript({
|
|
4812
|
+
skin: data.tinymceSettings?.skin,
|
|
4813
|
+
defaultHeight: data.tinymceSettings?.defaultHeight,
|
|
4814
|
+
defaultToolbar: data.tinymceSettings?.defaultToolbar
|
|
4815
|
+
})}</script>` : ""}
|
|
4816
|
+
|
|
4817
|
+
${data.mdxeditorEnabled ? `<script>${getMDXEditorInitScript({
|
|
4818
|
+
theme: data.mdxeditorSettings?.theme,
|
|
4819
|
+
defaultHeight: data.mdxeditorSettings?.defaultHeight,
|
|
4820
|
+
toolbar: data.mdxeditorSettings?.toolbar,
|
|
4821
|
+
placeholder: data.mdxeditorSettings?.placeholder
|
|
4822
|
+
})}</script>` : ""}
|
|
3929
4823
|
</script>
|
|
3930
4824
|
`;
|
|
3931
4825
|
const layoutData = {
|
|
@@ -3996,11 +4890,11 @@ function renderContentListPage(data) {
|
|
|
3996
4890
|
label: "Title",
|
|
3997
4891
|
sortable: true,
|
|
3998
4892
|
sortType: "string",
|
|
3999
|
-
render: (
|
|
4893
|
+
render: (value, row) => `
|
|
4000
4894
|
<div class="flex items-center">
|
|
4001
4895
|
<div>
|
|
4002
4896
|
<div class="text-sm font-medium text-zinc-950 dark:text-white">
|
|
4003
|
-
<a href="/content/${row.id}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${row.title}</a>
|
|
4897
|
+
<a href="/admin/content/${row.id}/edit${currentParams ? `?ref=${encodeURIComponent(currentParams)}` : ""}" class="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">${row.title}</a>
|
|
4004
4898
|
</div>
|
|
4005
4899
|
<div class="text-sm text-zinc-500 dark:text-zinc-400">${row.slug}</div>
|
|
4006
4900
|
</div>
|
|
@@ -4019,7 +4913,7 @@ function renderContentListPage(data) {
|
|
|
4019
4913
|
label: "Status",
|
|
4020
4914
|
sortable: true,
|
|
4021
4915
|
sortType: "string",
|
|
4022
|
-
render: (
|
|
4916
|
+
render: (value) => value
|
|
4023
4917
|
},
|
|
4024
4918
|
{
|
|
4025
4919
|
key: "authorName",
|
|
@@ -4040,7 +4934,7 @@ function renderContentListPage(data) {
|
|
|
4040
4934
|
label: "Actions",
|
|
4041
4935
|
sortable: false,
|
|
4042
4936
|
className: "text-sm font-medium",
|
|
4043
|
-
render: (
|
|
4937
|
+
render: (value, row) => `
|
|
4044
4938
|
<div class="flex space-x-2">
|
|
4045
4939
|
<button
|
|
4046
4940
|
class="inline-flex items-center justify-center p-1.5 rounded-lg bg-cyan-50 dark:bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 ring-1 ring-inset ring-cyan-600/20 dark:ring-cyan-500/20 hover:bg-cyan-100 dark:hover:bg-cyan-500/20 transition-colors"
|
|
@@ -4051,6 +4945,15 @@ function renderContentListPage(data) {
|
|
|
4051
4945
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/>
|
|
4052
4946
|
</svg>
|
|
4053
4947
|
</button>
|
|
4948
|
+
<button
|
|
4949
|
+
class="inline-flex items-center justify-center p-1.5 rounded-lg bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-400 ring-1 ring-inset ring-purple-600/20 dark:ring-purple-500/20 hover:bg-purple-100 dark:hover:bg-purple-500/20 transition-colors"
|
|
4950
|
+
onclick="window.open('/api/content/${row.id}', '_blank')"
|
|
4951
|
+
title="View API"
|
|
4952
|
+
>
|
|
4953
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
4954
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
|
4955
|
+
</svg>
|
|
4956
|
+
</button>
|
|
4054
4957
|
<button
|
|
4055
4958
|
class="inline-flex items-center justify-center p-1.5 rounded-lg bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 hover:bg-red-100 dark:hover:bg-red-500/20 transition-colors"
|
|
4056
4959
|
hx-delete="/admin/content/${row.id}"
|
|
@@ -4230,7 +5133,7 @@ function renderContentListPage(data) {
|
|
|
4230
5133
|
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 ${filter.name === "status" ? "pl-8" : "pl-3"} pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48"
|
|
4231
5134
|
>
|
|
4232
5135
|
${filter.options.map((opt) => `
|
|
4233
|
-
<option value="${opt.
|
|
5136
|
+
<option value="${opt.__value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
|
|
4234
5137
|
`).join("")}
|
|
4235
5138
|
</select>
|
|
4236
5139
|
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
@@ -4750,7 +5653,15 @@ function escapeHtml3(text) {
|
|
|
4750
5653
|
}
|
|
4751
5654
|
|
|
4752
5655
|
// src/middleware/plugin-middleware.ts
|
|
4753
|
-
|
|
5656
|
+
async function isPluginActive2(db, pluginId) {
|
|
5657
|
+
try {
|
|
5658
|
+
const result = await db.prepare("SELECT status FROM plugins WHERE id = ?").bind(pluginId).first();
|
|
5659
|
+
return result?.status === "active";
|
|
5660
|
+
} catch (error) {
|
|
5661
|
+
console.error(`[isPluginActive] Error checking plugin status for ${pluginId}:`, error);
|
|
5662
|
+
return false;
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
4754
5665
|
|
|
4755
5666
|
// src/routes/admin-content.ts
|
|
4756
5667
|
var adminContentRoutes = new Hono();
|
|
@@ -4760,6 +5671,37 @@ async function getCollectionFields(db, collectionId) {
|
|
|
4760
5671
|
return cache.getOrSet(
|
|
4761
5672
|
cache.generateKey("fields", collectionId),
|
|
4762
5673
|
async () => {
|
|
5674
|
+
const collectionStmt = db.prepare("SELECT schema FROM collections WHERE id = ?");
|
|
5675
|
+
const collectionRow = await collectionStmt.bind(collectionId).first();
|
|
5676
|
+
if (collectionRow && collectionRow.schema) {
|
|
5677
|
+
try {
|
|
5678
|
+
const schema = typeof collectionRow.schema === "string" ? JSON.parse(collectionRow.schema) : collectionRow.schema;
|
|
5679
|
+
if (schema && schema.properties) {
|
|
5680
|
+
let fieldOrder = 0;
|
|
5681
|
+
return Object.entries(schema.properties).map(([fieldName, fieldConfig]) => {
|
|
5682
|
+
let fieldOptions = { ...fieldConfig };
|
|
5683
|
+
if (fieldConfig.type === "select" && fieldConfig.enum) {
|
|
5684
|
+
fieldOptions.options = fieldConfig.enum.map((value, index) => ({
|
|
5685
|
+
value,
|
|
5686
|
+
label: fieldConfig.enumLabels?.[index] || value
|
|
5687
|
+
}));
|
|
5688
|
+
}
|
|
5689
|
+
return {
|
|
5690
|
+
id: `schema-${fieldName}`,
|
|
5691
|
+
field_name: fieldName,
|
|
5692
|
+
field_type: fieldConfig.type || "string",
|
|
5693
|
+
field_label: fieldConfig.title || fieldName,
|
|
5694
|
+
field_options: fieldOptions,
|
|
5695
|
+
field_order: fieldOrder++,
|
|
5696
|
+
is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
|
|
5697
|
+
is_searchable: false
|
|
5698
|
+
};
|
|
5699
|
+
});
|
|
5700
|
+
}
|
|
5701
|
+
} catch (e) {
|
|
5702
|
+
console.error("Error parsing collection schema:", e);
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
4763
5705
|
const stmt = db.prepare(`
|
|
4764
5706
|
SELECT * FROM content_fields
|
|
4765
5707
|
WHERE collection_id = ?
|
|
@@ -5002,11 +5944,44 @@ adminContentRoutes.get("/new", async (c) => {
|
|
|
5002
5944
|
}
|
|
5003
5945
|
const fields = await getCollectionFields(db, collectionId);
|
|
5004
5946
|
const workflowEnabled = await isPluginActive2(db, "workflow");
|
|
5947
|
+
const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
|
|
5948
|
+
let tinymceSettings;
|
|
5949
|
+
if (tinymceEnabled) {
|
|
5950
|
+
const pluginService = new PluginService(db);
|
|
5951
|
+
const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
|
|
5952
|
+
tinymceSettings = tinymcePlugin2?.settings;
|
|
5953
|
+
}
|
|
5954
|
+
const quillEnabled = await isPluginActive2(db, "quill-editor");
|
|
5955
|
+
let quillSettings;
|
|
5956
|
+
if (quillEnabled) {
|
|
5957
|
+
const pluginService = new PluginService(db);
|
|
5958
|
+
const quillPlugin = await pluginService.getPlugin("quill-editor");
|
|
5959
|
+
quillSettings = quillPlugin?.settings;
|
|
5960
|
+
}
|
|
5961
|
+
const mdxeditorEnabled = await isPluginActive2(db, "mdxeditor-plugin");
|
|
5962
|
+
let mdxeditorSettings;
|
|
5963
|
+
if (mdxeditorEnabled) {
|
|
5964
|
+
const pluginService = new PluginService(db);
|
|
5965
|
+
const mdxeditorPlugin2 = await pluginService.getPlugin("mdxeditor-plugin");
|
|
5966
|
+
mdxeditorSettings = mdxeditorPlugin2?.settings;
|
|
5967
|
+
}
|
|
5968
|
+
console.log("[Content Form /new] Editor plugins status:", {
|
|
5969
|
+
tinymce: tinymceEnabled,
|
|
5970
|
+
quill: quillEnabled,
|
|
5971
|
+
mdxeditor: mdxeditorEnabled,
|
|
5972
|
+
mdxeditorSettings
|
|
5973
|
+
});
|
|
5005
5974
|
const formData = {
|
|
5006
5975
|
collection,
|
|
5007
5976
|
fields,
|
|
5008
5977
|
isEdit: false,
|
|
5009
5978
|
workflowEnabled,
|
|
5979
|
+
tinymceEnabled,
|
|
5980
|
+
tinymceSettings,
|
|
5981
|
+
quillEnabled,
|
|
5982
|
+
quillSettings,
|
|
5983
|
+
mdxeditorEnabled,
|
|
5984
|
+
mdxeditorSettings,
|
|
5010
5985
|
user: user ? {
|
|
5011
5986
|
name: user.email,
|
|
5012
5987
|
email: user.email,
|
|
@@ -5074,6 +6049,27 @@ adminContentRoutes.get("/:id/edit", async (c) => {
|
|
|
5074
6049
|
const fields = await getCollectionFields(db, content.collection_id);
|
|
5075
6050
|
const contentData = content.data ? JSON.parse(content.data) : {};
|
|
5076
6051
|
const workflowEnabled = await isPluginActive2(db, "workflow");
|
|
6052
|
+
const tinymceEnabled = await isPluginActive2(db, "tinymce-plugin");
|
|
6053
|
+
let tinymceSettings;
|
|
6054
|
+
if (tinymceEnabled) {
|
|
6055
|
+
const pluginService = new PluginService(db);
|
|
6056
|
+
const tinymcePlugin2 = await pluginService.getPlugin("tinymce-plugin");
|
|
6057
|
+
tinymceSettings = tinymcePlugin2?.settings;
|
|
6058
|
+
}
|
|
6059
|
+
const quillEnabled = await isPluginActive2(db, "quill-editor");
|
|
6060
|
+
let quillSettings;
|
|
6061
|
+
if (quillEnabled) {
|
|
6062
|
+
const pluginService = new PluginService(db);
|
|
6063
|
+
const quillPlugin = await pluginService.getPlugin("quill-editor");
|
|
6064
|
+
quillSettings = quillPlugin?.settings;
|
|
6065
|
+
}
|
|
6066
|
+
const mdxeditorEnabled = await isPluginActive2(db, "mdxeditor-plugin");
|
|
6067
|
+
let mdxeditorSettings;
|
|
6068
|
+
if (mdxeditorEnabled) {
|
|
6069
|
+
const pluginService = new PluginService(db);
|
|
6070
|
+
const mdxeditorPlugin2 = await pluginService.getPlugin("mdxeditor-plugin");
|
|
6071
|
+
mdxeditorSettings = mdxeditorPlugin2?.settings;
|
|
6072
|
+
}
|
|
5077
6073
|
const formData = {
|
|
5078
6074
|
id: content.id,
|
|
5079
6075
|
title: content.title,
|
|
@@ -5089,6 +6085,12 @@ adminContentRoutes.get("/:id/edit", async (c) => {
|
|
|
5089
6085
|
fields,
|
|
5090
6086
|
isEdit: true,
|
|
5091
6087
|
workflowEnabled,
|
|
6088
|
+
tinymceEnabled,
|
|
6089
|
+
tinymceSettings,
|
|
6090
|
+
quillEnabled,
|
|
6091
|
+
quillSettings,
|
|
6092
|
+
mdxeditorEnabled,
|
|
6093
|
+
mdxeditorSettings,
|
|
5092
6094
|
referrerParams,
|
|
5093
6095
|
user: user ? {
|
|
5094
6096
|
name: user.email,
|
|
@@ -5139,41 +6141,31 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5139
6141
|
const data = {};
|
|
5140
6142
|
const errors = {};
|
|
5141
6143
|
for (const field of fields) {
|
|
5142
|
-
const
|
|
5143
|
-
if (field.
|
|
5144
|
-
const options = field.field_options || {};
|
|
5145
|
-
if (options.autoGenerate) {
|
|
5146
|
-
data[field.field_name] = crypto.randomUUID();
|
|
5147
|
-
continue;
|
|
5148
|
-
}
|
|
5149
|
-
}
|
|
5150
|
-
if (field.is_required && (!value2 || value2.toString().trim() === "")) {
|
|
6144
|
+
const value = formData.get(field.field_name);
|
|
6145
|
+
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
5151
6146
|
errors[field.field_name] = [`${field.field_label} is required`];
|
|
5152
6147
|
continue;
|
|
5153
6148
|
}
|
|
5154
6149
|
switch (field.field_type) {
|
|
5155
6150
|
case "number":
|
|
5156
|
-
if (
|
|
6151
|
+
if (value && isNaN(Number(value))) {
|
|
5157
6152
|
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
5158
6153
|
} else {
|
|
5159
|
-
data[field.field_name] =
|
|
6154
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5160
6155
|
}
|
|
5161
6156
|
break;
|
|
5162
6157
|
case "boolean":
|
|
5163
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ?
|
|
6158
|
+
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
5164
6159
|
break;
|
|
5165
6160
|
case "select":
|
|
5166
6161
|
if (field.field_options?.multiple) {
|
|
5167
6162
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5168
6163
|
} else {
|
|
5169
|
-
data[field.field_name] =
|
|
6164
|
+
data[field.field_name] = value;
|
|
5170
6165
|
}
|
|
5171
6166
|
break;
|
|
5172
|
-
case "guid":
|
|
5173
|
-
data[field.field_name] = value2 || null;
|
|
5174
|
-
break;
|
|
5175
6167
|
default:
|
|
5176
|
-
data[field.field_name] =
|
|
6168
|
+
data[field.field_name] = value;
|
|
5177
6169
|
}
|
|
5178
6170
|
}
|
|
5179
6171
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5207,9 +6199,9 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5207
6199
|
INSERT INTO content (
|
|
5208
6200
|
id, collection_id, slug, title, data, status,
|
|
5209
6201
|
scheduled_publish_at, scheduled_unpublish_at,
|
|
5210
|
-
meta_title, meta_description, author_id,
|
|
6202
|
+
meta_title, meta_description, author_id, created_at, updated_at
|
|
5211
6203
|
)
|
|
5212
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
6204
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5213
6205
|
`);
|
|
5214
6206
|
await insertStmt.bind(
|
|
5215
6207
|
contentId,
|
|
@@ -5223,7 +6215,6 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5223
6215
|
data.meta_title || null,
|
|
5224
6216
|
data.meta_description || null,
|
|
5225
6217
|
user?.userId || "unknown",
|
|
5226
|
-
user?.userId || "unknown",
|
|
5227
6218
|
now,
|
|
5228
6219
|
now
|
|
5229
6220
|
).run();
|
|
@@ -5301,31 +6292,31 @@ adminContentRoutes.put("/:id", async (c) => {
|
|
|
5301
6292
|
const data = {};
|
|
5302
6293
|
const errors = {};
|
|
5303
6294
|
for (const field of fields) {
|
|
5304
|
-
const
|
|
5305
|
-
if (field.is_required && (!
|
|
6295
|
+
const value = formData.get(field.field_name);
|
|
6296
|
+
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
5306
6297
|
errors[field.field_name] = [`${field.field_label} is required`];
|
|
5307
6298
|
continue;
|
|
5308
6299
|
}
|
|
5309
6300
|
switch (field.field_type) {
|
|
5310
6301
|
case "number":
|
|
5311
|
-
if (
|
|
6302
|
+
if (value && isNaN(Number(value))) {
|
|
5312
6303
|
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
5313
6304
|
} else {
|
|
5314
|
-
data[field.field_name] =
|
|
6305
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5315
6306
|
}
|
|
5316
6307
|
break;
|
|
5317
6308
|
case "boolean":
|
|
5318
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ?
|
|
6309
|
+
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
5319
6310
|
break;
|
|
5320
6311
|
case "select":
|
|
5321
6312
|
if (field.field_options?.multiple) {
|
|
5322
6313
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5323
6314
|
} else {
|
|
5324
|
-
data[field.field_name] =
|
|
6315
|
+
data[field.field_name] = value;
|
|
5325
6316
|
}
|
|
5326
6317
|
break;
|
|
5327
6318
|
default:
|
|
5328
|
-
data[field.field_name] =
|
|
6319
|
+
data[field.field_name] = value;
|
|
5329
6320
|
}
|
|
5330
6321
|
}
|
|
5331
6322
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5442,23 +6433,23 @@ adminContentRoutes.post("/preview", async (c) => {
|
|
|
5442
6433
|
const fields = await getCollectionFields(db, collectionId);
|
|
5443
6434
|
const data = {};
|
|
5444
6435
|
for (const field of fields) {
|
|
5445
|
-
const
|
|
6436
|
+
const value = formData.get(field.field_name);
|
|
5446
6437
|
switch (field.field_type) {
|
|
5447
6438
|
case "number":
|
|
5448
|
-
data[field.field_name] =
|
|
6439
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5449
6440
|
break;
|
|
5450
6441
|
case "boolean":
|
|
5451
|
-
data[field.field_name] =
|
|
6442
|
+
data[field.field_name] = value === "true";
|
|
5452
6443
|
break;
|
|
5453
6444
|
case "select":
|
|
5454
6445
|
if (field.field_options?.multiple) {
|
|
5455
6446
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5456
6447
|
} else {
|
|
5457
|
-
data[field.field_name] =
|
|
6448
|
+
data[field.field_name] = value;
|
|
5458
6449
|
}
|
|
5459
6450
|
break;
|
|
5460
6451
|
default:
|
|
5461
|
-
data[field.field_name] =
|
|
6452
|
+
data[field.field_name] = value;
|
|
5462
6453
|
}
|
|
5463
6454
|
}
|
|
5464
6455
|
const previewHTML = `
|
|
@@ -5526,9 +6517,9 @@ adminContentRoutes.post("/duplicate", async (c) => {
|
|
|
5526
6517
|
const insertStmt = db.prepare(`
|
|
5527
6518
|
INSERT INTO content (
|
|
5528
6519
|
id, collection_id, slug, title, data, status,
|
|
5529
|
-
author_id,
|
|
6520
|
+
author_id, created_at, updated_at
|
|
5530
6521
|
)
|
|
5531
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
6522
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5532
6523
|
`);
|
|
5533
6524
|
await insertStmt.bind(
|
|
5534
6525
|
newId,
|
|
@@ -5539,7 +6530,6 @@ adminContentRoutes.post("/duplicate", async (c) => {
|
|
|
5539
6530
|
"draft",
|
|
5540
6531
|
// Always start as draft
|
|
5541
6532
|
user?.userId || "unknown",
|
|
5542
|
-
user?.userId || "unknown",
|
|
5543
6533
|
now,
|
|
5544
6534
|
now
|
|
5545
6535
|
).run();
|
|
@@ -7315,10 +8305,10 @@ function renderUsersListPage(data) {
|
|
|
7315
8305
|
label: "",
|
|
7316
8306
|
className: "w-12",
|
|
7317
8307
|
sortable: false,
|
|
7318
|
-
render: (
|
|
8308
|
+
render: (value, row) => {
|
|
7319
8309
|
const initials = `${row.firstName.charAt(0)}${row.lastName.charAt(0)}`.toUpperCase();
|
|
7320
|
-
if (
|
|
7321
|
-
return `<img src="${
|
|
8310
|
+
if (value) {
|
|
8311
|
+
return `<img src="${value}" alt="${row.firstName} ${row.lastName}" class="w-8 h-8 rounded-full">`;
|
|
7322
8312
|
}
|
|
7323
8313
|
return `
|
|
7324
8314
|
<div class="w-8 h-8 bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 rounded-full flex items-center justify-center">
|
|
@@ -7333,7 +8323,7 @@ function renderUsersListPage(data) {
|
|
|
7333
8323
|
sortable: true,
|
|
7334
8324
|
sortType: "string",
|
|
7335
8325
|
render: (_value, row) => {
|
|
7336
|
-
const
|
|
8326
|
+
const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7337
8327
|
"&": "&",
|
|
7338
8328
|
"<": "<",
|
|
7339
8329
|
">": ">",
|
|
@@ -7342,9 +8332,9 @@ function renderUsersListPage(data) {
|
|
|
7342
8332
|
})[char] || char);
|
|
7343
8333
|
const truncatedFirstName = row.firstName.length > 25 ? row.firstName.substring(0, 25) + "..." : row.firstName;
|
|
7344
8334
|
const truncatedLastName = row.lastName.length > 25 ? row.lastName.substring(0, 25) + "..." : row.lastName;
|
|
7345
|
-
const fullName =
|
|
8335
|
+
const fullName = escapeHtml6(`${truncatedFirstName} ${truncatedLastName}`);
|
|
7346
8336
|
const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
|
|
7347
|
-
const username =
|
|
8337
|
+
const username = escapeHtml6(truncatedUsername);
|
|
7348
8338
|
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>';
|
|
7349
8339
|
return `
|
|
7350
8340
|
<div>
|
|
@@ -7359,15 +8349,15 @@ function renderUsersListPage(data) {
|
|
|
7359
8349
|
label: "Email",
|
|
7360
8350
|
sortable: true,
|
|
7361
8351
|
sortType: "string",
|
|
7362
|
-
render: (
|
|
7363
|
-
const
|
|
8352
|
+
render: (value) => {
|
|
8353
|
+
const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7364
8354
|
"&": "&",
|
|
7365
8355
|
"<": "<",
|
|
7366
8356
|
">": ">",
|
|
7367
8357
|
'"': """,
|
|
7368
8358
|
"'": "'"
|
|
7369
8359
|
})[char] || char);
|
|
7370
|
-
const escapedEmail =
|
|
8360
|
+
const escapedEmail = escapeHtml6(value);
|
|
7371
8361
|
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>`;
|
|
7372
8362
|
}
|
|
7373
8363
|
},
|
|
@@ -7376,7 +8366,7 @@ function renderUsersListPage(data) {
|
|
|
7376
8366
|
label: "Role",
|
|
7377
8367
|
sortable: true,
|
|
7378
8368
|
sortType: "string",
|
|
7379
|
-
render: (
|
|
8369
|
+
render: (value) => {
|
|
7380
8370
|
const roleColors = {
|
|
7381
8371
|
admin: "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",
|
|
7382
8372
|
editor: "bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-500/20",
|
|
@@ -7392,7 +8382,7 @@ function renderUsersListPage(data) {
|
|
|
7392
8382
|
label: "Last Login",
|
|
7393
8383
|
sortable: true,
|
|
7394
8384
|
sortType: "date",
|
|
7395
|
-
render: (
|
|
8385
|
+
render: (value) => {
|
|
7396
8386
|
if (!value) return '<span class="text-zinc-500 dark:text-zinc-400">Never</span>';
|
|
7397
8387
|
return `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`;
|
|
7398
8388
|
}
|
|
@@ -7402,7 +8392,7 @@ function renderUsersListPage(data) {
|
|
|
7402
8392
|
label: "Created",
|
|
7403
8393
|
sortable: true,
|
|
7404
8394
|
sortType: "date",
|
|
7405
|
-
render: (
|
|
8395
|
+
render: (value) => `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`
|
|
7406
8396
|
},
|
|
7407
8397
|
{
|
|
7408
8398
|
key: "actions",
|
|
@@ -7900,7 +8890,7 @@ userRoutes.post("/profile/avatar", async (c) => {
|
|
|
7900
8890
|
try {
|
|
7901
8891
|
const formData = await c.req.formData();
|
|
7902
8892
|
const avatarFile = formData.get("avatar");
|
|
7903
|
-
if (!avatarFile ||
|
|
8893
|
+
if (!avatarFile || typeof avatarFile === "string" || !avatarFile.name) {
|
|
7904
8894
|
return c.html(renderAlert2({
|
|
7905
8895
|
type: "error",
|
|
7906
8896
|
message: "Please select an image file.",
|
|
@@ -8483,35 +9473,75 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
8483
9473
|
}));
|
|
8484
9474
|
}
|
|
8485
9475
|
});
|
|
8486
|
-
userRoutes.
|
|
9476
|
+
userRoutes.post("/users/:id/toggle", async (c) => {
|
|
8487
9477
|
const db = c.env.DB;
|
|
8488
9478
|
const user = c.get("user");
|
|
8489
9479
|
const userId = c.req.param("id");
|
|
8490
9480
|
try {
|
|
8491
|
-
const body = await c.req.json().catch(() => ({
|
|
8492
|
-
const
|
|
8493
|
-
if (userId === user.userId) {
|
|
8494
|
-
return c.json({ error: "You cannot
|
|
9481
|
+
const body = await c.req.json().catch(() => ({ active: true }));
|
|
9482
|
+
const active = body.active === true;
|
|
9483
|
+
if (userId === user.userId && !active) {
|
|
9484
|
+
return c.json({ error: "You cannot deactivate your own account" }, 400);
|
|
8495
9485
|
}
|
|
8496
9486
|
const userStmt = db.prepare(`
|
|
8497
9487
|
SELECT id, email FROM users WHERE id = ?
|
|
8498
9488
|
`);
|
|
8499
|
-
const
|
|
8500
|
-
if (!
|
|
9489
|
+
const userToToggle = await userStmt.bind(userId).first();
|
|
9490
|
+
if (!userToToggle) {
|
|
8501
9491
|
return c.json({ error: "User not found" }, 404);
|
|
8502
9492
|
}
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
9493
|
+
const toggleStmt = db.prepare(`
|
|
9494
|
+
UPDATE users SET is_active = ?, updated_at = ? WHERE id = ?
|
|
9495
|
+
`);
|
|
9496
|
+
await toggleStmt.bind(active ? 1 : 0, Date.now(), userId).run();
|
|
9497
|
+
await logActivity(
|
|
9498
|
+
db,
|
|
9499
|
+
user.userId,
|
|
9500
|
+
active ? "user.activate" : "user.deactivate",
|
|
9501
|
+
"users",
|
|
9502
|
+
userId,
|
|
9503
|
+
{ email: userToToggle.email },
|
|
9504
|
+
c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
|
|
9505
|
+
c.req.header("user-agent")
|
|
9506
|
+
);
|
|
9507
|
+
return c.json({
|
|
9508
|
+
success: true,
|
|
9509
|
+
message: active ? "User activated successfully" : "User deactivated successfully"
|
|
9510
|
+
});
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
console.error("User toggle error:", error);
|
|
9513
|
+
return c.json({ error: "Failed to toggle user status" }, 500);
|
|
9514
|
+
}
|
|
9515
|
+
});
|
|
9516
|
+
userRoutes.delete("/users/:id", async (c) => {
|
|
9517
|
+
const db = c.env.DB;
|
|
9518
|
+
const user = c.get("user");
|
|
9519
|
+
const userId = c.req.param("id");
|
|
9520
|
+
try {
|
|
9521
|
+
const body = await c.req.json().catch(() => ({ hardDelete: false }));
|
|
9522
|
+
const hardDelete = body.hardDelete === true;
|
|
9523
|
+
if (userId === user.userId) {
|
|
9524
|
+
return c.json({ error: "You cannot delete your own account" }, 400);
|
|
9525
|
+
}
|
|
9526
|
+
const userStmt = db.prepare(`
|
|
9527
|
+
SELECT id, email FROM users WHERE id = ?
|
|
9528
|
+
`);
|
|
9529
|
+
const userToDelete = await userStmt.bind(userId).first();
|
|
9530
|
+
if (!userToDelete) {
|
|
9531
|
+
return c.json({ error: "User not found" }, 404);
|
|
9532
|
+
}
|
|
9533
|
+
if (hardDelete) {
|
|
9534
|
+
const deleteStmt = db.prepare(`
|
|
9535
|
+
DELETE FROM users WHERE id = ?
|
|
9536
|
+
`);
|
|
9537
|
+
await deleteStmt.bind(userId).run();
|
|
9538
|
+
await logActivity(
|
|
9539
|
+
db,
|
|
9540
|
+
user.userId,
|
|
9541
|
+
"user!.hard_delete",
|
|
9542
|
+
"users",
|
|
9543
|
+
userId,
|
|
9544
|
+
{ email: userToDelete.email, permanent: true },
|
|
8515
9545
|
c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
|
|
8516
9546
|
c.req.header("user-agent")
|
|
8517
9547
|
);
|
|
@@ -11090,10 +12120,32 @@ function renderPluginsListPage(data) {
|
|
|
11090
12120
|
</div>
|
|
11091
12121
|
</div>
|
|
11092
12122
|
|
|
12123
|
+
<!-- Experimental Notice -->
|
|
12124
|
+
<div class="mb-6 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 p-4">
|
|
12125
|
+
<div class="flex items-start">
|
|
12126
|
+
<div class="flex-shrink-0">
|
|
12127
|
+
<svg class="h-5 w-5 text-amber-600 dark:text-amber-400" viewBox="0 0 20 20" fill="currentColor">
|
|
12128
|
+
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
|
12129
|
+
</svg>
|
|
12130
|
+
</div>
|
|
12131
|
+
<div class="ml-3 flex-1">
|
|
12132
|
+
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">
|
|
12133
|
+
Experimental Feature
|
|
12134
|
+
</h3>
|
|
12135
|
+
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
|
|
12136
|
+
<p>
|
|
12137
|
+
Plugin management is currently under active development. While functional, some features may change or have limitations.
|
|
12138
|
+
Please report any issues you encounter on our <a href="https://discord.gg/8bMy6bv3sZ" target="_blank" class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100">Discord community</a>.
|
|
12139
|
+
</p>
|
|
12140
|
+
</div>
|
|
12141
|
+
</div>
|
|
12142
|
+
</div>
|
|
12143
|
+
</div>
|
|
12144
|
+
|
|
11093
12145
|
<!-- Stats -->
|
|
11094
12146
|
<div class="mb-6">
|
|
11095
12147
|
<h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
|
|
11096
|
-
<dl class="mt-5 grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-
|
|
12148
|
+
<dl class="mt-5 grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-5 md:divide-x md:divide-y-0">
|
|
11097
12149
|
<div class="px-4 py-5 sm:p-6">
|
|
11098
12150
|
<dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
|
|
11099
12151
|
<dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
|
|
@@ -11154,6 +12206,21 @@ function renderPluginsListPage(data) {
|
|
|
11154
12206
|
</div>
|
|
11155
12207
|
</dd>
|
|
11156
12208
|
</div>
|
|
12209
|
+
<div class="px-4 py-5 sm:p-6">
|
|
12210
|
+
<dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Available to Install</dt>
|
|
12211
|
+
<dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
|
|
12212
|
+
<div class="flex items-baseline text-2xl font-semibold text-zinc-400">
|
|
12213
|
+
${data.stats?.uninstalled || 0}
|
|
12214
|
+
</div>
|
|
12215
|
+
<div class="inline-flex items-baseline rounded-full bg-zinc-400/10 text-zinc-600 dark:text-zinc-400 px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
|
|
12216
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
|
|
12217
|
+
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
|
12218
|
+
</svg>
|
|
12219
|
+
<span class="sr-only">Available</span>
|
|
12220
|
+
Ready
|
|
12221
|
+
</div>
|
|
12222
|
+
</dd>
|
|
12223
|
+
</div>
|
|
11157
12224
|
</dl>
|
|
11158
12225
|
</div>
|
|
11159
12226
|
|
|
@@ -11169,13 +12236,16 @@ function renderPluginsListPage(data) {
|
|
|
11169
12236
|
<div>
|
|
11170
12237
|
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
|
|
11171
12238
|
<div class="mt-2 grid grid-cols-1">
|
|
11172
|
-
<select 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-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
|
|
12239
|
+
<select id="category-filter" onchange="filterPlugins()" 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-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
|
|
11173
12240
|
<option value="">All Categories</option>
|
|
11174
12241
|
<option value="content">Content Management</option>
|
|
11175
12242
|
<option value="media">Media</option>
|
|
11176
12243
|
<option value="seo">SEO & Analytics</option>
|
|
11177
12244
|
<option value="security">Security</option>
|
|
11178
12245
|
<option value="utilities">Utilities</option>
|
|
12246
|
+
<option value="system">System</option>
|
|
12247
|
+
<option value="development">Development</option>
|
|
12248
|
+
<option value="demo">Demo</option>
|
|
11179
12249
|
</select>
|
|
11180
12250
|
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
11181
12251
|
<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" />
|
|
@@ -11185,10 +12255,11 @@ function renderPluginsListPage(data) {
|
|
|
11185
12255
|
<div>
|
|
11186
12256
|
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
|
|
11187
12257
|
<div class="mt-2 grid grid-cols-1">
|
|
11188
|
-
<select 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-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
|
|
12258
|
+
<select id="status-filter" onchange="filterPlugins()" 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-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48">
|
|
11189
12259
|
<option value="">All Status</option>
|
|
11190
12260
|
<option value="active">Active</option>
|
|
11191
12261
|
<option value="inactive">Inactive</option>
|
|
12262
|
+
<option value="uninstalled">Available to Install</option>
|
|
11192
12263
|
<option value="error">Error</option>
|
|
11193
12264
|
</select>
|
|
11194
12265
|
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
@@ -11205,8 +12276,10 @@ function renderPluginsListPage(data) {
|
|
|
11205
12276
|
</svg>
|
|
11206
12277
|
</div>
|
|
11207
12278
|
<input
|
|
12279
|
+
id="search-input"
|
|
11208
12280
|
type="text"
|
|
11209
12281
|
placeholder="Search plugins..."
|
|
12282
|
+
oninput="filterPlugins()"
|
|
11210
12283
|
class="w-full rounded-full bg-transparent px-11 py-2 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 border-2 border-cyan-200/50 dark:border-cyan-700/50 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
|
|
11211
12284
|
/>
|
|
11212
12285
|
</div>
|
|
@@ -11229,7 +12302,7 @@ function renderPluginsListPage(data) {
|
|
|
11229
12302
|
</div>
|
|
11230
12303
|
|
|
11231
12304
|
<!-- Plugins Grid -->
|
|
11232
|
-
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
12305
|
+
<div id="plugins-grid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
11233
12306
|
${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
|
|
11234
12307
|
</div>
|
|
11235
12308
|
|
|
@@ -11356,7 +12429,12 @@ function renderPluginsListPage(data) {
|
|
|
11356
12429
|
}
|
|
11357
12430
|
|
|
11358
12431
|
function openPluginSettings(pluginId) {
|
|
11359
|
-
|
|
12432
|
+
// Email plugin has a custom settings page
|
|
12433
|
+
if (pluginId === 'email') {
|
|
12434
|
+
window.location.href = '/admin/plugins/email/settings';
|
|
12435
|
+
} else {
|
|
12436
|
+
window.location.href = \`/admin/plugins/\${pluginId}\`;
|
|
12437
|
+
}
|
|
11360
12438
|
}
|
|
11361
12439
|
|
|
11362
12440
|
function showPluginDetails(pluginId) {
|
|
@@ -11380,12 +12458,78 @@ function renderPluginsListPage(data) {
|
|
|
11380
12458
|
const dropdown = document.getElementById('plugin-dropdown');
|
|
11381
12459
|
dropdown.classList.toggle('hidden');
|
|
11382
12460
|
}
|
|
11383
|
-
|
|
12461
|
+
|
|
12462
|
+
function filterPlugins() {
|
|
12463
|
+
const categoryFilter = document.getElementById('category-filter').value.toLowerCase();
|
|
12464
|
+
const statusFilter = document.getElementById('status-filter').value.toLowerCase();
|
|
12465
|
+
const searchInput = document.getElementById('search-input').value.toLowerCase();
|
|
12466
|
+
|
|
12467
|
+
const pluginCards = document.querySelectorAll('.plugin-card');
|
|
12468
|
+
let visibleCount = 0;
|
|
12469
|
+
|
|
12470
|
+
pluginCards.forEach(card => {
|
|
12471
|
+
// Get plugin data from card attributes
|
|
12472
|
+
const category = card.getAttribute('data-category')?.toLowerCase() || '';
|
|
12473
|
+
const status = card.getAttribute('data-status')?.toLowerCase() || '';
|
|
12474
|
+
const name = card.getAttribute('data-name')?.toLowerCase() || '';
|
|
12475
|
+
const description = card.getAttribute('data-description')?.toLowerCase() || '';
|
|
12476
|
+
|
|
12477
|
+
// Check if plugin matches all filters
|
|
12478
|
+
let matches = true;
|
|
12479
|
+
|
|
12480
|
+
// Category filter
|
|
12481
|
+
if (categoryFilter && category !== categoryFilter) {
|
|
12482
|
+
matches = false;
|
|
12483
|
+
}
|
|
12484
|
+
|
|
12485
|
+
// Status filter
|
|
12486
|
+
if (statusFilter && status !== statusFilter) {
|
|
12487
|
+
matches = false;
|
|
12488
|
+
}
|
|
12489
|
+
|
|
12490
|
+
// Search filter - check if search term is in name or description
|
|
12491
|
+
if (searchInput && !name.includes(searchInput) && !description.includes(searchInput)) {
|
|
12492
|
+
matches = false;
|
|
12493
|
+
}
|
|
12494
|
+
|
|
12495
|
+
// Show/hide card
|
|
12496
|
+
if (matches) {
|
|
12497
|
+
card.style.display = '';
|
|
12498
|
+
visibleCount++;
|
|
12499
|
+
} else {
|
|
12500
|
+
card.style.display = 'none';
|
|
12501
|
+
}
|
|
12502
|
+
});
|
|
12503
|
+
|
|
12504
|
+
// Show/hide "no results" message
|
|
12505
|
+
let noResultsMsg = document.getElementById('no-results-message');
|
|
12506
|
+
if (visibleCount === 0) {
|
|
12507
|
+
if (!noResultsMsg) {
|
|
12508
|
+
noResultsMsg = document.createElement('div');
|
|
12509
|
+
noResultsMsg.id = 'no-results-message';
|
|
12510
|
+
noResultsMsg.className = 'col-span-full text-center py-12';
|
|
12511
|
+
noResultsMsg.innerHTML = \`
|
|
12512
|
+
<div class="flex flex-col items-center">
|
|
12513
|
+
<svg class="w-16 h-16 text-zinc-400 dark:text-zinc-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12514
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
12515
|
+
</svg>
|
|
12516
|
+
<h3 class="text-lg font-semibold text-zinc-950 dark:text-white mb-2">No plugins found</h3>
|
|
12517
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400">Try adjusting your filters or search terms</p>
|
|
12518
|
+
</div>
|
|
12519
|
+
\`;
|
|
12520
|
+
document.getElementById('plugins-grid').appendChild(noResultsMsg);
|
|
12521
|
+
}
|
|
12522
|
+
noResultsMsg.style.display = '';
|
|
12523
|
+
} else if (noResultsMsg) {
|
|
12524
|
+
noResultsMsg.style.display = 'none';
|
|
12525
|
+
}
|
|
12526
|
+
}
|
|
12527
|
+
|
|
11384
12528
|
// Close dropdown when clicking outside
|
|
11385
12529
|
document.addEventListener('click', (event) => {
|
|
11386
12530
|
const dropdown = document.getElementById('plugin-dropdown');
|
|
11387
12531
|
const button = event.target.closest('button[onclick="toggleDropdown()"]');
|
|
11388
|
-
|
|
12532
|
+
|
|
11389
12533
|
if (!button && !dropdown.contains(event.target)) {
|
|
11390
12534
|
dropdown.classList.add('hidden');
|
|
11391
12535
|
}
|
|
@@ -11420,23 +12564,33 @@ function renderPluginCard(plugin) {
|
|
|
11420
12564
|
const statusColors = {
|
|
11421
12565
|
active: "bg-lime-50 dark:bg-lime-500/10 text-lime-700 dark:text-lime-300 ring-lime-700/10 dark:ring-lime-400/20",
|
|
11422
12566
|
inactive: "bg-zinc-50 dark:bg-zinc-500/10 text-zinc-700 dark:text-zinc-400 ring-zinc-700/10 dark:ring-zinc-400/20",
|
|
11423
|
-
error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-700/10 dark:ring-red-400/20"
|
|
12567
|
+
error: "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-red-700/10 dark:ring-red-400/20",
|
|
12568
|
+
uninstalled: "bg-zinc-100 dark:bg-zinc-600/10 text-zinc-600 dark:text-zinc-500 ring-zinc-600/10 dark:ring-zinc-500/20"
|
|
11424
12569
|
};
|
|
11425
12570
|
const statusIcons = {
|
|
11426
12571
|
active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
|
|
11427
12572
|
inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
|
|
11428
|
-
error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>'
|
|
12573
|
+
error: '<div class="w-2 h-2 bg-red-500 dark:bg-red-400 rounded-full mr-2"></div>',
|
|
12574
|
+
uninstalled: '<div class="w-2 h-2 bg-zinc-400 dark:bg-zinc-600 rounded-full mr-2"></div>'
|
|
11429
12575
|
};
|
|
11430
12576
|
const borderColors = {
|
|
11431
12577
|
active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
|
|
11432
12578
|
inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
|
|
11433
|
-
error: "ring-[3px] ring-red-500 dark:ring-red-400"
|
|
12579
|
+
error: "ring-[3px] ring-red-500 dark:ring-red-400",
|
|
12580
|
+
uninstalled: "ring-[3px] ring-zinc-400 dark:ring-zinc-600"
|
|
11434
12581
|
};
|
|
11435
12582
|
const criticalCorePlugins = ["core-auth", "core-media"];
|
|
11436
12583
|
const canToggle = !criticalCorePlugins.includes(plugin.id);
|
|
11437
|
-
|
|
12584
|
+
let actionButton = "";
|
|
12585
|
+
if (plugin.status === "uninstalled") {
|
|
12586
|
+
actionButton = `<button onclick="installPlugin('${plugin.name}')" class="bg-cyan-600 dark:bg-cyan-700 hover:bg-cyan-700 dark:hover:bg-cyan-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Install</button>`;
|
|
12587
|
+
} else if (plugin.status === "active") {
|
|
12588
|
+
actionButton = `<button onclick="togglePlugin('${plugin.id}', 'deactivate')" class="bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Deactivate</button>`;
|
|
12589
|
+
} else {
|
|
12590
|
+
actionButton = `<button onclick="togglePlugin('${plugin.id}', 'activate')" class="bg-lime-600 dark:bg-lime-700 hover:bg-lime-700 dark:hover:bg-lime-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">Activate</button>`;
|
|
12591
|
+
}
|
|
11438
12592
|
return `
|
|
11439
|
-
<div class="plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ${borderColors[plugin.status]} p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all">
|
|
12593
|
+
<div class="plugin-card rounded-xl bg-white dark:bg-zinc-900 shadow-sm ${borderColors[plugin.status]} p-6 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all" data-category="${plugin.category}" data-status="${plugin.status}" data-name="${plugin.displayName}" data-description="${plugin.description}">
|
|
11440
12594
|
<div class="flex items-start justify-between mb-4">
|
|
11441
12595
|
<div class="flex items-center gap-3">
|
|
11442
12596
|
<div class="w-12 h-12 rounded-lg flex items-center justify-center ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 bg-zinc-50 dark:bg-zinc-800">
|
|
@@ -11497,20 +12651,24 @@ function renderPluginCard(plugin) {
|
|
|
11497
12651
|
|
|
11498
12652
|
<div class="flex items-center justify-between">
|
|
11499
12653
|
<div class="flex gap-2">
|
|
11500
|
-
${canToggle ? actionButton : ""}
|
|
12654
|
+
${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
|
|
12655
|
+
${plugin.status !== "uninstalled" ? `
|
|
11501
12656
|
<button onclick="openPluginSettings('${plugin.id}')" class="bg-white dark:bg-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors">
|
|
11502
12657
|
Settings
|
|
11503
12658
|
</button>
|
|
12659
|
+
` : ""}
|
|
11504
12660
|
</div>
|
|
11505
12661
|
|
|
11506
12662
|
<div class="flex items-center gap-2">
|
|
12663
|
+
${plugin.status !== "uninstalled" ? `
|
|
11507
12664
|
<button onclick="showPluginDetails('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Plugin Details">
|
|
11508
12665
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
11509
12666
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
11510
12667
|
</svg>
|
|
11511
12668
|
</button>
|
|
12669
|
+
` : ""}
|
|
11512
12670
|
|
|
11513
|
-
${!plugin.isCore ? `
|
|
12671
|
+
${!plugin.isCore && plugin.status !== "uninstalled" ? `
|
|
11514
12672
|
<button onclick="uninstallPlugin('${plugin.id}')" class="text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 p-1.5 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" title="Uninstall Plugin">
|
|
11515
12673
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
11516
12674
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
@@ -11813,28 +12971,22 @@ function renderPluginSettingsPage(data) {
|
|
|
11813
12971
|
const { plugin, activity = [], user } = data;
|
|
11814
12972
|
const pageContent = `
|
|
11815
12973
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-6">
|
|
11816
|
-
<!-- Header with
|
|
11817
|
-
<div class="flex items-center mb-6">
|
|
11818
|
-
<
|
|
11819
|
-
<
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
</li>
|
|
11833
|
-
<li>
|
|
11834
|
-
<span class="text-gray-300">${plugin.displayName}</span>
|
|
11835
|
-
</li>
|
|
11836
|
-
</ol>
|
|
11837
|
-
</nav>
|
|
12974
|
+
<!-- Header with Back Button -->
|
|
12975
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
12976
|
+
<div>
|
|
12977
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Plugin Settings</h1>
|
|
12978
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
12979
|
+
${plugin.description}
|
|
12980
|
+
</p>
|
|
12981
|
+
</div>
|
|
12982
|
+
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
12983
|
+
<a href="/admin/plugins" class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
|
|
12984
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12985
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
12986
|
+
</svg>
|
|
12987
|
+
Back to Plugins
|
|
12988
|
+
</a>
|
|
12989
|
+
</div>
|
|
11838
12990
|
</div>
|
|
11839
12991
|
|
|
11840
12992
|
<!-- Plugin Header -->
|
|
@@ -11845,9 +12997,8 @@ function renderPluginSettingsPage(data) {
|
|
|
11845
12997
|
${plugin.icon || plugin.displayName.charAt(0).toUpperCase()}
|
|
11846
12998
|
</div>
|
|
11847
12999
|
<div>
|
|
11848
|
-
<
|
|
11849
|
-
<
|
|
11850
|
-
<div class="flex items-center gap-4 text-sm text-gray-400">
|
|
13000
|
+
<h2 class="text-2xl font-semibold text-white mb-1">${plugin.displayName}</h2>
|
|
13001
|
+
<div class="flex items-center gap-4 text-sm text-gray-400 mt-2">
|
|
11851
13002
|
<span>v${plugin.version}</span>
|
|
11852
13003
|
<span>by ${plugin.author}</span>
|
|
11853
13004
|
<span>${plugin.category}</span>
|
|
@@ -11856,7 +13007,7 @@ function renderPluginSettingsPage(data) {
|
|
|
11856
13007
|
</div>
|
|
11857
13008
|
</div>
|
|
11858
13009
|
</div>
|
|
11859
|
-
|
|
13010
|
+
|
|
11860
13011
|
<div class="flex items-center gap-3">
|
|
11861
13012
|
${renderStatusBadge(plugin.status)}
|
|
11862
13013
|
${renderToggleButton(plugin)}
|
|
@@ -12143,10 +13294,10 @@ function renderSettingsTab(plugin) {
|
|
|
12143
13294
|
`;
|
|
12144
13295
|
}
|
|
12145
13296
|
function renderSettingsFields(settings) {
|
|
12146
|
-
return Object.entries(settings).map(([key,
|
|
13297
|
+
return Object.entries(settings).map(([key, value]) => {
|
|
12147
13298
|
const fieldId = `setting_${key}`;
|
|
12148
13299
|
const displayName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
|
12149
|
-
if (typeof
|
|
13300
|
+
if (typeof value === "boolean") {
|
|
12150
13301
|
return `
|
|
12151
13302
|
<div class="flex items-center justify-between">
|
|
12152
13303
|
<div>
|
|
@@ -12154,12 +13305,12 @@ function renderSettingsFields(settings) {
|
|
|
12154
13305
|
<p class="text-xs text-gray-400">Enable or disable this feature</p>
|
|
12155
13306
|
</div>
|
|
12156
13307
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
12157
|
-
<input type="checkbox" name="${fieldId}" id="${fieldId}" ${
|
|
13308
|
+
<input type="checkbox" name="${fieldId}" id="${fieldId}" ${value ? "checked" : ""} class="sr-only peer">
|
|
12158
13309
|
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
12159
13310
|
</label>
|
|
12160
13311
|
</div>
|
|
12161
13312
|
`;
|
|
12162
|
-
} else if (typeof
|
|
13313
|
+
} else if (typeof value === "number") {
|
|
12163
13314
|
return `
|
|
12164
13315
|
<div>
|
|
12165
13316
|
<label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
|
|
@@ -12167,7 +13318,7 @@ function renderSettingsFields(settings) {
|
|
|
12167
13318
|
type="number"
|
|
12168
13319
|
name="${fieldId}"
|
|
12169
13320
|
id="${fieldId}"
|
|
12170
|
-
value="${
|
|
13321
|
+
value="${value}"
|
|
12171
13322
|
class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
|
|
12172
13323
|
>
|
|
12173
13324
|
</div>
|
|
@@ -12180,7 +13331,7 @@ function renderSettingsFields(settings) {
|
|
|
12180
13331
|
type="text"
|
|
12181
13332
|
name="${fieldId}"
|
|
12182
13333
|
id="${fieldId}"
|
|
12183
|
-
value="${
|
|
13334
|
+
value="${value}"
|
|
12184
13335
|
class="backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full"
|
|
12185
13336
|
>
|
|
12186
13337
|
</div>
|
|
@@ -12328,6 +13479,86 @@ function formatTimestamp(timestamp) {
|
|
|
12328
13479
|
// src/routes/admin-plugins.ts
|
|
12329
13480
|
var adminPluginRoutes = new Hono();
|
|
12330
13481
|
adminPluginRoutes.use("*", requireAuth());
|
|
13482
|
+
var AVAILABLE_PLUGINS = [
|
|
13483
|
+
{
|
|
13484
|
+
id: "third-party-faq",
|
|
13485
|
+
name: "faq-plugin",
|
|
13486
|
+
display_name: "FAQ System",
|
|
13487
|
+
description: "Frequently Asked Questions management system with categories, search, and custom styling",
|
|
13488
|
+
version: "2.0.0",
|
|
13489
|
+
author: "Community Developer",
|
|
13490
|
+
category: "content",
|
|
13491
|
+
icon: "\u2753",
|
|
13492
|
+
permissions: ["manage:faqs"],
|
|
13493
|
+
dependencies: [],
|
|
13494
|
+
is_core: false
|
|
13495
|
+
},
|
|
13496
|
+
{
|
|
13497
|
+
id: "demo-login-prefill",
|
|
13498
|
+
name: "demo-login-plugin",
|
|
13499
|
+
display_name: "Demo Login Prefill",
|
|
13500
|
+
description: "Prefills login form with demo credentials (admin@sonicjs.com/sonicjs!) for easy site demonstration",
|
|
13501
|
+
version: "1.0.0-beta.1",
|
|
13502
|
+
author: "SonicJS",
|
|
13503
|
+
category: "demo",
|
|
13504
|
+
icon: "\u{1F3AF}",
|
|
13505
|
+
permissions: [],
|
|
13506
|
+
dependencies: [],
|
|
13507
|
+
is_core: false
|
|
13508
|
+
},
|
|
13509
|
+
{
|
|
13510
|
+
id: "database-tools",
|
|
13511
|
+
name: "database-tools",
|
|
13512
|
+
display_name: "Database Tools",
|
|
13513
|
+
description: "Database management tools including truncate, backup, and validation",
|
|
13514
|
+
version: "1.0.0-beta.1",
|
|
13515
|
+
author: "SonicJS Team",
|
|
13516
|
+
category: "system",
|
|
13517
|
+
icon: "\u{1F5C4}\uFE0F",
|
|
13518
|
+
permissions: ["manage:database", "admin"],
|
|
13519
|
+
dependencies: [],
|
|
13520
|
+
is_core: false
|
|
13521
|
+
},
|
|
13522
|
+
{
|
|
13523
|
+
id: "seed-data",
|
|
13524
|
+
name: "seed-data",
|
|
13525
|
+
display_name: "Seed Data",
|
|
13526
|
+
description: "Generate realistic example users and content for testing and development",
|
|
13527
|
+
version: "1.0.0-beta.1",
|
|
13528
|
+
author: "SonicJS Team",
|
|
13529
|
+
category: "development",
|
|
13530
|
+
icon: "\u{1F331}",
|
|
13531
|
+
permissions: ["admin"],
|
|
13532
|
+
dependencies: [],
|
|
13533
|
+
is_core: false
|
|
13534
|
+
},
|
|
13535
|
+
{
|
|
13536
|
+
id: "quill-editor",
|
|
13537
|
+
name: "quill-editor",
|
|
13538
|
+
display_name: "Quill Rich Text Editor",
|
|
13539
|
+
description: "Quill WYSIWYG editor integration for rich text editing. Lightweight, modern editor with customizable toolbars and dark mode support.",
|
|
13540
|
+
version: "1.0.0",
|
|
13541
|
+
author: "SonicJS Team",
|
|
13542
|
+
category: "editor",
|
|
13543
|
+
icon: "\u270D\uFE0F",
|
|
13544
|
+
permissions: [],
|
|
13545
|
+
dependencies: [],
|
|
13546
|
+
is_core: true
|
|
13547
|
+
},
|
|
13548
|
+
{
|
|
13549
|
+
id: "tinymce-plugin",
|
|
13550
|
+
name: "tinymce-plugin",
|
|
13551
|
+
display_name: "TinyMCE Rich Text Editor",
|
|
13552
|
+
description: "Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.",
|
|
13553
|
+
version: "1.0.0",
|
|
13554
|
+
author: "SonicJS Team",
|
|
13555
|
+
category: "editor",
|
|
13556
|
+
icon: "\u{1F4DD}",
|
|
13557
|
+
permissions: [],
|
|
13558
|
+
dependencies: [],
|
|
13559
|
+
is_core: false
|
|
13560
|
+
}
|
|
13561
|
+
];
|
|
12331
13562
|
adminPluginRoutes.get("/", async (c) => {
|
|
12332
13563
|
try {
|
|
12333
13564
|
const user = c.get("user");
|
|
@@ -12336,15 +13567,17 @@ adminPluginRoutes.get("/", async (c) => {
|
|
|
12336
13567
|
return c.text("Access denied", 403);
|
|
12337
13568
|
}
|
|
12338
13569
|
const pluginService = new PluginService(db);
|
|
12339
|
-
let
|
|
12340
|
-
let stats = { total: 0, active: 0, inactive: 0, errors: 0 };
|
|
13570
|
+
let installedPlugins = [];
|
|
13571
|
+
let stats = { total: 0, active: 0, inactive: 0, errors: 0, uninstalled: 0 };
|
|
12341
13572
|
try {
|
|
12342
|
-
|
|
13573
|
+
installedPlugins = await pluginService.getAllPlugins();
|
|
12343
13574
|
stats = await pluginService.getPluginStats();
|
|
12344
13575
|
} catch (error) {
|
|
12345
13576
|
console.error("Error loading plugins:", error);
|
|
12346
13577
|
}
|
|
12347
|
-
const
|
|
13578
|
+
const installedPluginIds = new Set(installedPlugins.map((p) => p.id));
|
|
13579
|
+
const uninstalledPlugins = AVAILABLE_PLUGINS.filter((p) => !installedPluginIds.has(p.id));
|
|
13580
|
+
const templatePlugins = installedPlugins.map((p) => ({
|
|
12348
13581
|
id: p.id,
|
|
12349
13582
|
name: p.name,
|
|
12350
13583
|
displayName: p.display_name,
|
|
@@ -12361,8 +13594,28 @@ adminPluginRoutes.get("/", async (c) => {
|
|
|
12361
13594
|
permissions: p.permissions,
|
|
12362
13595
|
isCore: p.is_core
|
|
12363
13596
|
}));
|
|
13597
|
+
const uninstalledTemplatePlugins = uninstalledPlugins.map((p) => ({
|
|
13598
|
+
id: p.id,
|
|
13599
|
+
name: p.name,
|
|
13600
|
+
displayName: p.display_name,
|
|
13601
|
+
description: p.description,
|
|
13602
|
+
version: p.version,
|
|
13603
|
+
author: p.author,
|
|
13604
|
+
status: "uninstalled",
|
|
13605
|
+
category: p.category,
|
|
13606
|
+
icon: p.icon,
|
|
13607
|
+
downloadCount: 0,
|
|
13608
|
+
rating: 0,
|
|
13609
|
+
lastUpdated: "Not installed",
|
|
13610
|
+
dependencies: p.dependencies,
|
|
13611
|
+
permissions: p.permissions,
|
|
13612
|
+
isCore: p.is_core
|
|
13613
|
+
}));
|
|
13614
|
+
const allPlugins = [...templatePlugins, ...uninstalledTemplatePlugins];
|
|
13615
|
+
stats.uninstalled = uninstalledPlugins.length;
|
|
13616
|
+
stats.total = installedPlugins.length + uninstalledPlugins.length;
|
|
12364
13617
|
const pageData = {
|
|
12365
|
-
plugins:
|
|
13618
|
+
plugins: allPlugins,
|
|
12366
13619
|
stats,
|
|
12367
13620
|
user: {
|
|
12368
13621
|
name: user?.email || "User",
|
|
@@ -12499,7 +13752,7 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12499
13752
|
id: "demo-login-prefill",
|
|
12500
13753
|
name: "demo-login-plugin",
|
|
12501
13754
|
display_name: "Demo Login Prefill",
|
|
12502
|
-
description: "Prefills login form with demo credentials (admin@sonicjs.com/
|
|
13755
|
+
description: "Prefills login form with demo credentials (admin@sonicjs.com/sonicjs!) for easy site demonstration",
|
|
12503
13756
|
version: "1.0.0-beta.1",
|
|
12504
13757
|
author: "SonicJS",
|
|
12505
13758
|
category: "demo",
|
|
@@ -12509,7 +13762,7 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12509
13762
|
settings: {
|
|
12510
13763
|
enableNotice: true,
|
|
12511
13764
|
demoEmail: "admin@sonicjs.com",
|
|
12512
|
-
demoPassword: "
|
|
13765
|
+
demoPassword: "sonicjs!"
|
|
12513
13766
|
}
|
|
12514
13767
|
});
|
|
12515
13768
|
return c.json({ success: true, plugin: demoPlugin });
|
|
@@ -12608,6 +13861,50 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12608
13861
|
});
|
|
12609
13862
|
return c.json({ success: true, plugin: seedDataPlugin });
|
|
12610
13863
|
}
|
|
13864
|
+
if (body.name === "quill-editor") {
|
|
13865
|
+
const quillPlugin = await pluginService.installPlugin({
|
|
13866
|
+
id: "quill-editor",
|
|
13867
|
+
name: "quill-editor",
|
|
13868
|
+
display_name: "Quill Rich Text Editor",
|
|
13869
|
+
description: "Quill WYSIWYG editor integration for rich text editing. Lightweight, modern editor with customizable toolbars and dark mode support.",
|
|
13870
|
+
version: "1.0.0",
|
|
13871
|
+
author: "SonicJS Team",
|
|
13872
|
+
category: "editor",
|
|
13873
|
+
icon: "\u270D\uFE0F",
|
|
13874
|
+
permissions: [],
|
|
13875
|
+
dependencies: [],
|
|
13876
|
+
is_core: true,
|
|
13877
|
+
settings: {
|
|
13878
|
+
version: "2.0.2",
|
|
13879
|
+
defaultHeight: 300,
|
|
13880
|
+
defaultToolbar: "full",
|
|
13881
|
+
theme: "snow"
|
|
13882
|
+
}
|
|
13883
|
+
});
|
|
13884
|
+
return c.json({ success: true, plugin: quillPlugin });
|
|
13885
|
+
}
|
|
13886
|
+
if (body.name === "tinymce-plugin") {
|
|
13887
|
+
const tinymcePlugin2 = await pluginService.installPlugin({
|
|
13888
|
+
id: "tinymce-plugin",
|
|
13889
|
+
name: "tinymce-plugin",
|
|
13890
|
+
display_name: "TinyMCE Rich Text Editor",
|
|
13891
|
+
description: "Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.",
|
|
13892
|
+
version: "1.0.0",
|
|
13893
|
+
author: "SonicJS Team",
|
|
13894
|
+
category: "editor",
|
|
13895
|
+
icon: "\u{1F4DD}",
|
|
13896
|
+
permissions: [],
|
|
13897
|
+
dependencies: [],
|
|
13898
|
+
is_core: false,
|
|
13899
|
+
settings: {
|
|
13900
|
+
apiKey: "no-api-key",
|
|
13901
|
+
defaultHeight: 300,
|
|
13902
|
+
defaultToolbar: "full",
|
|
13903
|
+
skin: "oxide-dark"
|
|
13904
|
+
}
|
|
13905
|
+
});
|
|
13906
|
+
return c.json({ success: true, plugin: tinymcePlugin2 });
|
|
13907
|
+
}
|
|
12611
13908
|
return c.json({ error: "Plugin not found in registry" }, 404);
|
|
12612
13909
|
} catch (error) {
|
|
12613
13910
|
console.error("Error installing plugin:", error);
|
|
@@ -13683,751 +14980,146 @@ adminLogsRoutes.get("/export", async (c) => {
|
|
|
13683
14980
|
return c.json({ error: "Failed to export logs" }, 500);
|
|
13684
14981
|
}
|
|
13685
14982
|
});
|
|
13686
|
-
adminLogsRoutes.post("/cleanup", async (c) => {
|
|
13687
|
-
try {
|
|
13688
|
-
const user = c.get("user");
|
|
13689
|
-
if (!user || user.role !== "admin") {
|
|
13690
|
-
return c.json({
|
|
13691
|
-
success: false,
|
|
13692
|
-
error: "Unauthorized. Admin access required."
|
|
13693
|
-
}, 403);
|
|
13694
|
-
}
|
|
13695
|
-
const logger = getLogger(c.env.DB);
|
|
13696
|
-
await logger.cleanupByRetention();
|
|
13697
|
-
return c.html(html`
|
|
13698
|
-
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
|
13699
|
-
Log cleanup completed successfully!
|
|
13700
|
-
</div>
|
|
13701
|
-
`);
|
|
13702
|
-
} catch (error) {
|
|
13703
|
-
console.error("Error cleaning up logs:", error);
|
|
13704
|
-
return c.html(html`
|
|
13705
|
-
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
13706
|
-
Failed to clean up logs. Please try again.
|
|
13707
|
-
</div>
|
|
13708
|
-
`);
|
|
13709
|
-
}
|
|
13710
|
-
});
|
|
13711
|
-
adminLogsRoutes.post("/search", async (c) => {
|
|
13712
|
-
try {
|
|
13713
|
-
const formData = await c.req.formData();
|
|
13714
|
-
const search = formData.get("search");
|
|
13715
|
-
const level = formData.get("level");
|
|
13716
|
-
const category = formData.get("category");
|
|
13717
|
-
const logger = getLogger(c.env.DB);
|
|
13718
|
-
const filter = {
|
|
13719
|
-
limit: 20,
|
|
13720
|
-
offset: 0,
|
|
13721
|
-
sortBy: "created_at",
|
|
13722
|
-
sortOrder: "desc"
|
|
13723
|
-
};
|
|
13724
|
-
if (search) filter.search = search;
|
|
13725
|
-
if (level) filter.level = [level];
|
|
13726
|
-
if (category) filter.category = [category];
|
|
13727
|
-
const { logs } = await logger.getLogs(filter);
|
|
13728
|
-
const rows = logs.map((log) => {
|
|
13729
|
-
const formattedLog = {
|
|
13730
|
-
...log,
|
|
13731
|
-
formattedDate: new Date(log.createdAt).toLocaleString(),
|
|
13732
|
-
levelClass: getLevelClass(log.level),
|
|
13733
|
-
categoryClass: getCategoryClass(log.category)
|
|
13734
|
-
};
|
|
13735
|
-
return `
|
|
13736
|
-
<tr class="hover:bg-gray-50">
|
|
13737
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
13738
|
-
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
|
|
13739
|
-
${formattedLog.level}
|
|
13740
|
-
</span>
|
|
13741
|
-
</td>
|
|
13742
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
13743
|
-
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
|
|
13744
|
-
${formattedLog.category}
|
|
13745
|
-
</span>
|
|
13746
|
-
</td>
|
|
13747
|
-
<td class="px-6 py-4">
|
|
13748
|
-
<div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
|
|
13749
|
-
</td>
|
|
13750
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
|
|
13751
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
|
|
13752
|
-
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
13753
|
-
<a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
|
|
13754
|
-
</td>
|
|
13755
|
-
</tr>
|
|
13756
|
-
`;
|
|
13757
|
-
}).join("");
|
|
13758
|
-
return c.html(rows);
|
|
13759
|
-
} catch (error) {
|
|
13760
|
-
console.error("Error searching logs:", error);
|
|
13761
|
-
return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
|
|
13762
|
-
}
|
|
13763
|
-
});
|
|
13764
|
-
function getLevelClass(level) {
|
|
13765
|
-
switch (level) {
|
|
13766
|
-
case "debug":
|
|
13767
|
-
return "bg-gray-100 text-gray-800";
|
|
13768
|
-
case "info":
|
|
13769
|
-
return "bg-blue-100 text-blue-800";
|
|
13770
|
-
case "warn":
|
|
13771
|
-
return "bg-yellow-100 text-yellow-800";
|
|
13772
|
-
case "error":
|
|
13773
|
-
return "bg-red-100 text-red-800";
|
|
13774
|
-
case "fatal":
|
|
13775
|
-
return "bg-purple-100 text-purple-800";
|
|
13776
|
-
default:
|
|
13777
|
-
return "bg-gray-100 text-gray-800";
|
|
13778
|
-
}
|
|
13779
|
-
}
|
|
13780
|
-
function getCategoryClass(category) {
|
|
13781
|
-
switch (category) {
|
|
13782
|
-
case "auth":
|
|
13783
|
-
return "bg-green-100 text-green-800";
|
|
13784
|
-
case "api":
|
|
13785
|
-
return "bg-blue-100 text-blue-800";
|
|
13786
|
-
case "workflow":
|
|
13787
|
-
return "bg-purple-100 text-purple-800";
|
|
13788
|
-
case "plugin":
|
|
13789
|
-
return "bg-indigo-100 text-indigo-800";
|
|
13790
|
-
case "media":
|
|
13791
|
-
return "bg-pink-100 text-pink-800";
|
|
13792
|
-
case "system":
|
|
13793
|
-
return "bg-gray-100 text-gray-800";
|
|
13794
|
-
case "security":
|
|
13795
|
-
return "bg-red-100 text-red-800";
|
|
13796
|
-
case "error":
|
|
13797
|
-
return "bg-red-100 text-red-800";
|
|
13798
|
-
default:
|
|
13799
|
-
return "bg-gray-100 text-gray-800";
|
|
13800
|
-
}
|
|
13801
|
-
}
|
|
13802
|
-
var adminDesignRoutes = new Hono();
|
|
13803
|
-
adminDesignRoutes.get("/", (c) => {
|
|
13804
|
-
const user = c.get("user");
|
|
13805
|
-
const pageData = {
|
|
13806
|
-
user: user ? {
|
|
13807
|
-
name: user.email,
|
|
13808
|
-
email: user.email,
|
|
13809
|
-
role: user.role
|
|
13810
|
-
} : void 0
|
|
13811
|
-
};
|
|
13812
|
-
return c.html(renderDesignPage(pageData));
|
|
13813
|
-
});
|
|
13814
|
-
var adminCheckboxRoutes = new Hono();
|
|
13815
|
-
adminCheckboxRoutes.get("/", (c) => {
|
|
13816
|
-
const user = c.get("user");
|
|
13817
|
-
const pageData = {
|
|
13818
|
-
user: user ? {
|
|
13819
|
-
name: user.email,
|
|
13820
|
-
email: user.email,
|
|
13821
|
-
role: user.role
|
|
13822
|
-
} : void 0
|
|
13823
|
-
};
|
|
13824
|
-
return c.html(renderCheckboxPage(pageData));
|
|
13825
|
-
});
|
|
13826
|
-
|
|
13827
|
-
// src/templates/pages/admin-faq-form.template.ts
|
|
13828
|
-
function renderFAQForm(data) {
|
|
13829
|
-
const { faq, isEdit, errors, message, messageType } = data;
|
|
13830
|
-
const pageTitle = isEdit ? "Edit FAQ" : "New FAQ";
|
|
13831
|
-
const pageContent = `
|
|
13832
|
-
<div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
|
13833
|
-
<!-- Header -->
|
|
13834
|
-
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
13835
|
-
<div>
|
|
13836
|
-
<h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
|
|
13837
|
-
<p class="mt-2 text-sm text-gray-300">
|
|
13838
|
-
${isEdit ? "Update the FAQ details below" : "Create a new frequently asked question"}
|
|
13839
|
-
</p>
|
|
13840
|
-
</div>
|
|
13841
|
-
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
13842
|
-
<a href="/admin/faq"
|
|
13843
|
-
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">
|
|
13844
|
-
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
13845
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
13846
|
-
</svg>
|
|
13847
|
-
Back to List
|
|
13848
|
-
</a>
|
|
13849
|
-
</div>
|
|
13850
|
-
</div>
|
|
13851
|
-
|
|
13852
|
-
${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
|
|
13853
|
-
|
|
13854
|
-
<!-- Form -->
|
|
13855
|
-
<div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
|
|
13856
|
-
<form ${isEdit ? `hx-put="/admin/faq/${faq?.id}"` : 'hx-post="/admin/faq"'}
|
|
13857
|
-
hx-target="body"
|
|
13858
|
-
hx-swap="outerHTML"
|
|
13859
|
-
class="space-y-6 p-6">
|
|
13860
|
-
|
|
13861
|
-
<!-- Question -->
|
|
13862
|
-
<div>
|
|
13863
|
-
<label for="question" class="block text-sm font-medium text-white">
|
|
13864
|
-
Question <span class="text-red-400">*</span>
|
|
13865
|
-
</label>
|
|
13866
|
-
<div class="mt-1">
|
|
13867
|
-
<textarea name="question"
|
|
13868
|
-
id="question"
|
|
13869
|
-
rows="3"
|
|
13870
|
-
required
|
|
13871
|
-
maxlength="500"
|
|
13872
|
-
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"
|
|
13873
|
-
placeholder="Enter the frequently asked question...">${faq?.question || ""}</textarea>
|
|
13874
|
-
<p class="mt-1 text-sm text-gray-300">
|
|
13875
|
-
<span id="question-count">0</span>/500 characters
|
|
13876
|
-
</p>
|
|
13877
|
-
</div>
|
|
13878
|
-
${errors?.question ? `
|
|
13879
|
-
<div class="mt-1">
|
|
13880
|
-
${errors.question.map((error) => `
|
|
13881
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13882
|
-
`).join("")}
|
|
13883
|
-
</div>
|
|
13884
|
-
` : ""}
|
|
13885
|
-
</div>
|
|
13886
|
-
|
|
13887
|
-
<!-- Answer -->
|
|
13888
|
-
<div>
|
|
13889
|
-
<label for="answer" class="block text-sm font-medium text-white">
|
|
13890
|
-
Answer <span class="text-red-400">*</span>
|
|
13891
|
-
</label>
|
|
13892
|
-
<div class="mt-1">
|
|
13893
|
-
<textarea name="answer"
|
|
13894
|
-
id="answer"
|
|
13895
|
-
rows="6"
|
|
13896
|
-
required
|
|
13897
|
-
maxlength="2000"
|
|
13898
|
-
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"
|
|
13899
|
-
placeholder="Enter the detailed answer...">${faq?.answer || ""}</textarea>
|
|
13900
|
-
<p class="mt-1 text-sm text-gray-300">
|
|
13901
|
-
<span id="answer-count">0</span>/2000 characters. You can use basic HTML for formatting.
|
|
13902
|
-
</p>
|
|
13903
|
-
</div>
|
|
13904
|
-
${errors?.answer ? `
|
|
13905
|
-
<div class="mt-1">
|
|
13906
|
-
${errors.answer.map((error) => `
|
|
13907
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13908
|
-
`).join("")}
|
|
13909
|
-
</div>
|
|
13910
|
-
` : ""}
|
|
13911
|
-
</div>
|
|
13912
|
-
|
|
13913
|
-
<!-- Category and Tags Row -->
|
|
13914
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
13915
|
-
<!-- Category -->
|
|
13916
|
-
<div>
|
|
13917
|
-
<label for="category" class="block text-sm font-medium text-white">Category</label>
|
|
13918
|
-
<div class="mt-1">
|
|
13919
|
-
<select name="category"
|
|
13920
|
-
id="category"
|
|
13921
|
-
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">
|
|
13922
|
-
<option value="">Select a category</option>
|
|
13923
|
-
<option value="general" ${faq?.category === "general" ? "selected" : ""}>General</option>
|
|
13924
|
-
<option value="technical" ${faq?.category === "technical" ? "selected" : ""}>Technical</option>
|
|
13925
|
-
<option value="billing" ${faq?.category === "billing" ? "selected" : ""}>Billing</option>
|
|
13926
|
-
<option value="support" ${faq?.category === "support" ? "selected" : ""}>Support</option>
|
|
13927
|
-
<option value="account" ${faq?.category === "account" ? "selected" : ""}>Account</option>
|
|
13928
|
-
<option value="features" ${faq?.category === "features" ? "selected" : ""}>Features</option>
|
|
13929
|
-
</select>
|
|
13930
|
-
</div>
|
|
13931
|
-
${errors?.category ? `
|
|
13932
|
-
<div class="mt-1">
|
|
13933
|
-
${errors.category.map((error) => `
|
|
13934
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13935
|
-
`).join("")}
|
|
13936
|
-
</div>
|
|
13937
|
-
` : ""}
|
|
13938
|
-
</div>
|
|
13939
|
-
|
|
13940
|
-
<!-- Tags -->
|
|
13941
|
-
<div>
|
|
13942
|
-
<label for="tags" class="block text-sm font-medium text-white">Tags</label>
|
|
13943
|
-
<div class="mt-1">
|
|
13944
|
-
<input type="text"
|
|
13945
|
-
name="tags"
|
|
13946
|
-
id="tags"
|
|
13947
|
-
value="${faq?.tags || ""}"
|
|
13948
|
-
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"
|
|
13949
|
-
placeholder="e.g., payment, setup, troubleshooting">
|
|
13950
|
-
<p class="mt-1 text-sm text-gray-300">Separate multiple tags with commas</p>
|
|
13951
|
-
</div>
|
|
13952
|
-
${errors?.tags ? `
|
|
13953
|
-
<div class="mt-1">
|
|
13954
|
-
${errors.tags.map((error) => `
|
|
13955
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13956
|
-
`).join("")}
|
|
13957
|
-
</div>
|
|
13958
|
-
` : ""}
|
|
13959
|
-
</div>
|
|
13960
|
-
</div>
|
|
13961
|
-
|
|
13962
|
-
<!-- Status and Sort Order Row -->
|
|
13963
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
13964
|
-
<!-- Published Status -->
|
|
13965
|
-
<div>
|
|
13966
|
-
<label class="block text-sm font-medium text-white">Status</label>
|
|
13967
|
-
<div class="mt-2 space-y-2">
|
|
13968
|
-
<div class="flex items-center">
|
|
13969
|
-
<input id="published"
|
|
13970
|
-
name="isPublished"
|
|
13971
|
-
type="radio"
|
|
13972
|
-
value="true"
|
|
13973
|
-
${!faq || faq.isPublished ? "checked" : ""}
|
|
13974
|
-
class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
|
|
13975
|
-
<label for="published" class="ml-2 block text-sm text-white">
|
|
13976
|
-
Published <span class="text-gray-300">(visible to users)</span>
|
|
13977
|
-
</label>
|
|
13978
|
-
</div>
|
|
13979
|
-
<div class="flex items-center">
|
|
13980
|
-
<input id="draft"
|
|
13981
|
-
name="isPublished"
|
|
13982
|
-
type="radio"
|
|
13983
|
-
value="false"
|
|
13984
|
-
${faq && !faq.isPublished ? "checked" : ""}
|
|
13985
|
-
class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
|
|
13986
|
-
<label for="draft" class="ml-2 block text-sm text-white">
|
|
13987
|
-
Draft <span class="text-gray-300">(not visible to users)</span>
|
|
13988
|
-
</label>
|
|
13989
|
-
</div>
|
|
13990
|
-
</div>
|
|
13991
|
-
</div>
|
|
13992
|
-
|
|
13993
|
-
<!-- Sort Order -->
|
|
13994
|
-
<div>
|
|
13995
|
-
<label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
|
|
13996
|
-
<div class="mt-1">
|
|
13997
|
-
<input type="number"
|
|
13998
|
-
name="sortOrder"
|
|
13999
|
-
id="sortOrder"
|
|
14000
|
-
value="${faq?.sortOrder || 0}"
|
|
14001
|
-
min="0"
|
|
14002
|
-
step="1"
|
|
14003
|
-
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">
|
|
14004
|
-
<p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
|
|
14005
|
-
</div>
|
|
14006
|
-
${errors?.sortOrder ? `
|
|
14007
|
-
<div class="mt-1">
|
|
14008
|
-
${errors.sortOrder.map((error) => `
|
|
14009
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14010
|
-
`).join("")}
|
|
14011
|
-
</div>
|
|
14012
|
-
` : ""}
|
|
14013
|
-
</div>
|
|
14014
|
-
</div>
|
|
14015
|
-
|
|
14016
|
-
<!-- Form Actions -->
|
|
14017
|
-
<div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
|
|
14018
|
-
<a href="/admin/faq"
|
|
14019
|
-
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">
|
|
14020
|
-
Cancel
|
|
14021
|
-
</a>
|
|
14022
|
-
<button type="submit"
|
|
14023
|
-
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">
|
|
14024
|
-
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
14025
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
14026
|
-
</svg>
|
|
14027
|
-
${isEdit ? "Update FAQ" : "Create FAQ"}
|
|
14028
|
-
</button>
|
|
14029
|
-
</div>
|
|
14030
|
-
</form>
|
|
14031
|
-
</div>
|
|
14032
|
-
</div>
|
|
14033
|
-
|
|
14034
|
-
<script>
|
|
14035
|
-
// Character count for question
|
|
14036
|
-
const questionTextarea = document.getElementById('question');
|
|
14037
|
-
const questionCount = document.getElementById('question-count');
|
|
14038
|
-
|
|
14039
|
-
function updateQuestionCount() {
|
|
14040
|
-
questionCount.textContent = questionTextarea.value.length;
|
|
14041
|
-
}
|
|
14042
|
-
|
|
14043
|
-
questionTextarea.addEventListener('input', updateQuestionCount);
|
|
14044
|
-
updateQuestionCount(); // Initial count
|
|
14045
|
-
|
|
14046
|
-
// Character count for answer
|
|
14047
|
-
const answerTextarea = document.getElementById('answer');
|
|
14048
|
-
const answerCount = document.getElementById('answer-count');
|
|
14049
|
-
|
|
14050
|
-
function updateAnswerCount() {
|
|
14051
|
-
answerCount.textContent = answerTextarea.value.length;
|
|
14052
|
-
}
|
|
14053
|
-
|
|
14054
|
-
answerTextarea.addEventListener('input', updateAnswerCount);
|
|
14055
|
-
updateAnswerCount(); // Initial count
|
|
14056
|
-
</script>
|
|
14057
|
-
`;
|
|
14058
|
-
const layoutData = {
|
|
14059
|
-
title: `${pageTitle} - Admin`,
|
|
14060
|
-
pageTitle,
|
|
14061
|
-
currentPath: isEdit ? `/admin/faq/${faq?.id}` : "/admin/faq/new",
|
|
14062
|
-
user: data.user,
|
|
14063
|
-
content: pageContent
|
|
14064
|
-
};
|
|
14065
|
-
return renderAdminLayout(layoutData);
|
|
14066
|
-
}
|
|
14067
|
-
function escapeHtml4(unsafe) {
|
|
14068
|
-
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14069
|
-
}
|
|
14070
|
-
|
|
14071
|
-
// src/routes/admin-faq.ts
|
|
14072
|
-
var faqSchema = z.object({
|
|
14073
|
-
question: z.string().min(1, "Question is required").max(500, "Question must be under 500 characters"),
|
|
14074
|
-
answer: z.string().min(1, "Answer is required").max(2e3, "Answer must be under 2000 characters"),
|
|
14075
|
-
category: z.string().optional(),
|
|
14076
|
-
tags: z.string().optional(),
|
|
14077
|
-
isPublished: z.string().transform((val) => val === "true"),
|
|
14078
|
-
sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
|
|
14079
|
-
});
|
|
14080
|
-
var adminFAQRoutes = new Hono();
|
|
14081
|
-
adminFAQRoutes.get("/", async (c) => {
|
|
14082
|
-
try {
|
|
14083
|
-
const user = c.get("user");
|
|
14084
|
-
const { category, published, search, page = "1" } = c.req.query();
|
|
14085
|
-
const currentPage = parseInt(page, 10) || 1;
|
|
14086
|
-
const limit = 20;
|
|
14087
|
-
const offset = (currentPage - 1) * limit;
|
|
14088
|
-
const db = c.env?.DB;
|
|
14089
|
-
if (!db) {
|
|
14090
|
-
return c.html(renderFAQList({
|
|
14091
|
-
faqs: [],
|
|
14092
|
-
totalCount: 0,
|
|
14093
|
-
currentPage: 1,
|
|
14094
|
-
totalPages: 1,
|
|
14095
|
-
user: user ? {
|
|
14096
|
-
name: user.email,
|
|
14097
|
-
email: user.email,
|
|
14098
|
-
role: user.role
|
|
14099
|
-
} : void 0,
|
|
14100
|
-
message: "Database not available",
|
|
14101
|
-
messageType: "error"
|
|
14102
|
-
}));
|
|
14103
|
-
}
|
|
14104
|
-
let whereClause = "WHERE 1=1";
|
|
14105
|
-
const params = [];
|
|
14106
|
-
if (category) {
|
|
14107
|
-
whereClause += " AND category = ?";
|
|
14108
|
-
params.push(category);
|
|
14109
|
-
}
|
|
14110
|
-
if (published !== void 0) {
|
|
14111
|
-
whereClause += " AND isPublished = ?";
|
|
14112
|
-
params.push(published === "true" ? 1 : 0);
|
|
14113
|
-
}
|
|
14114
|
-
if (search) {
|
|
14115
|
-
whereClause += " AND (question LIKE ? OR answer LIKE ? OR tags LIKE ?)";
|
|
14116
|
-
const searchTerm = `%${search}%`;
|
|
14117
|
-
params.push(searchTerm, searchTerm, searchTerm);
|
|
14118
|
-
}
|
|
14119
|
-
const countQuery = `SELECT COUNT(*) as count FROM faqs ${whereClause}`;
|
|
14120
|
-
const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
|
|
14121
|
-
const totalCount = countResults?.[0]?.count || 0;
|
|
14122
|
-
const dataQuery = `
|
|
14123
|
-
SELECT * FROM faqs
|
|
14124
|
-
${whereClause}
|
|
14125
|
-
ORDER BY sortOrder ASC, created_at DESC
|
|
14126
|
-
LIMIT ? OFFSET ?
|
|
14127
|
-
`;
|
|
14128
|
-
const { results: faqs } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
|
|
14129
|
-
const totalPages = Math.ceil(totalCount / limit);
|
|
14130
|
-
return c.html(renderFAQList({
|
|
14131
|
-
faqs: faqs || [],
|
|
14132
|
-
totalCount,
|
|
14133
|
-
currentPage,
|
|
14134
|
-
totalPages,
|
|
14135
|
-
user: user ? {
|
|
14136
|
-
name: user.email,
|
|
14137
|
-
email: user.email,
|
|
14138
|
-
role: user.role
|
|
14139
|
-
} : void 0
|
|
14140
|
-
}));
|
|
14141
|
-
} catch (error) {
|
|
14142
|
-
console.error("Error fetching FAQs:", error);
|
|
14143
|
-
const user = c.get("user");
|
|
14144
|
-
return c.html(renderFAQList({
|
|
14145
|
-
faqs: [],
|
|
14146
|
-
totalCount: 0,
|
|
14147
|
-
currentPage: 1,
|
|
14148
|
-
totalPages: 1,
|
|
14149
|
-
user: user ? {
|
|
14150
|
-
name: user.email,
|
|
14151
|
-
email: user.email,
|
|
14152
|
-
role: user.role
|
|
14153
|
-
} : void 0,
|
|
14154
|
-
message: "Failed to load FAQs",
|
|
14155
|
-
messageType: "error"
|
|
14156
|
-
}));
|
|
14157
|
-
}
|
|
14158
|
-
});
|
|
14159
|
-
adminFAQRoutes.get("/new", async (c) => {
|
|
14160
|
-
const user = c.get("user");
|
|
14161
|
-
return c.html(renderFAQForm({
|
|
14162
|
-
isEdit: false,
|
|
14163
|
-
user: user ? {
|
|
14164
|
-
name: user.email,
|
|
14165
|
-
email: user.email,
|
|
14166
|
-
role: user.role
|
|
14167
|
-
} : void 0
|
|
14168
|
-
}));
|
|
14169
|
-
});
|
|
14170
|
-
adminFAQRoutes.post("/", async (c) => {
|
|
14171
|
-
try {
|
|
14172
|
-
const formData = await c.req.formData();
|
|
14173
|
-
const data = Object.fromEntries(formData.entries());
|
|
14174
|
-
const validatedData = faqSchema.parse(data);
|
|
14175
|
-
const user = c.get("user");
|
|
14176
|
-
const db = c.env?.DB;
|
|
14177
|
-
if (!db) {
|
|
14178
|
-
return c.html(renderFAQForm({
|
|
14179
|
-
isEdit: false,
|
|
14180
|
-
user: user ? {
|
|
14181
|
-
name: user.email,
|
|
14182
|
-
email: user.email,
|
|
14183
|
-
role: user.role
|
|
14184
|
-
} : void 0,
|
|
14185
|
-
message: "Database not available",
|
|
14186
|
-
messageType: "error"
|
|
14187
|
-
}));
|
|
14188
|
-
}
|
|
14189
|
-
const { results } = await db.prepare(`
|
|
14190
|
-
INSERT INTO faqs (question, answer, category, tags, isPublished, sortOrder)
|
|
14191
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
14192
|
-
RETURNING *
|
|
14193
|
-
`).bind(
|
|
14194
|
-
validatedData.question,
|
|
14195
|
-
validatedData.answer,
|
|
14196
|
-
validatedData.category || null,
|
|
14197
|
-
validatedData.tags || null,
|
|
14198
|
-
validatedData.isPublished ? 1 : 0,
|
|
14199
|
-
validatedData.sortOrder
|
|
14200
|
-
).all();
|
|
14201
|
-
if (results && results.length > 0) {
|
|
14202
|
-
return c.redirect("/admin/faq?message=FAQ created successfully");
|
|
14203
|
-
} else {
|
|
14204
|
-
return c.html(renderFAQForm({
|
|
14205
|
-
isEdit: false,
|
|
14206
|
-
user: user ? {
|
|
14207
|
-
name: user.email,
|
|
14208
|
-
email: user.email,
|
|
14209
|
-
role: user.role
|
|
14210
|
-
} : void 0,
|
|
14211
|
-
message: "Failed to create FAQ",
|
|
14212
|
-
messageType: "error"
|
|
14213
|
-
}));
|
|
14214
|
-
}
|
|
14215
|
-
} catch (error) {
|
|
14216
|
-
console.error("Error creating FAQ:", error);
|
|
14217
|
-
const user = c.get("user");
|
|
14218
|
-
if (error instanceof z.ZodError) {
|
|
14219
|
-
const errors = {};
|
|
14220
|
-
error.errors.forEach((err) => {
|
|
14221
|
-
const field = err.path[0];
|
|
14222
|
-
if (!errors[field]) errors[field] = [];
|
|
14223
|
-
errors[field].push(err.message);
|
|
14224
|
-
});
|
|
14225
|
-
return c.html(renderFAQForm({
|
|
14226
|
-
isEdit: false,
|
|
14227
|
-
user: user ? {
|
|
14228
|
-
name: user.email,
|
|
14229
|
-
email: user.email,
|
|
14230
|
-
role: user.role
|
|
14231
|
-
} : void 0,
|
|
14232
|
-
errors,
|
|
14233
|
-
message: "Please correct the errors below",
|
|
14234
|
-
messageType: "error"
|
|
14235
|
-
}));
|
|
14236
|
-
}
|
|
14237
|
-
return c.html(renderFAQForm({
|
|
14238
|
-
isEdit: false,
|
|
14239
|
-
user: user ? {
|
|
14240
|
-
name: user.email,
|
|
14241
|
-
email: user.email,
|
|
14242
|
-
role: user.role
|
|
14243
|
-
} : void 0,
|
|
14244
|
-
message: "Failed to create FAQ",
|
|
14245
|
-
messageType: "error"
|
|
14246
|
-
}));
|
|
14247
|
-
}
|
|
14248
|
-
});
|
|
14249
|
-
adminFAQRoutes.get("/:id", async (c) => {
|
|
14250
|
-
try {
|
|
14251
|
-
const id = parseInt(c.req.param("id"));
|
|
14252
|
-
const user = c.get("user");
|
|
14253
|
-
const db = c.env?.DB;
|
|
14254
|
-
if (!db) {
|
|
14255
|
-
return c.html(renderFAQForm({
|
|
14256
|
-
isEdit: true,
|
|
14257
|
-
user: user ? {
|
|
14258
|
-
name: user.email,
|
|
14259
|
-
email: user.email,
|
|
14260
|
-
role: user.role
|
|
14261
|
-
} : void 0,
|
|
14262
|
-
message: "Database not available",
|
|
14263
|
-
messageType: "error"
|
|
14264
|
-
}));
|
|
14265
|
-
}
|
|
14266
|
-
const { results } = await db.prepare("SELECT * FROM faqs WHERE id = ?").bind(id).all();
|
|
14267
|
-
if (!results || results.length === 0) {
|
|
14268
|
-
return c.redirect("/admin/faq?message=FAQ not found&type=error");
|
|
14269
|
-
}
|
|
14270
|
-
const faq = results[0];
|
|
14271
|
-
return c.html(renderFAQForm({
|
|
14272
|
-
faq: {
|
|
14273
|
-
id: faq.id,
|
|
14274
|
-
question: faq.question,
|
|
14275
|
-
answer: faq.answer,
|
|
14276
|
-
category: faq.category,
|
|
14277
|
-
tags: faq.tags,
|
|
14278
|
-
isPublished: Boolean(faq.isPublished),
|
|
14279
|
-
sortOrder: faq.sortOrder
|
|
14280
|
-
},
|
|
14281
|
-
isEdit: true,
|
|
14282
|
-
user: user ? {
|
|
14283
|
-
name: user.email,
|
|
14284
|
-
email: user.email,
|
|
14285
|
-
role: user.role
|
|
14286
|
-
} : void 0
|
|
14287
|
-
}));
|
|
14288
|
-
} catch (error) {
|
|
14289
|
-
console.error("Error fetching FAQ:", error);
|
|
14290
|
-
const user = c.get("user");
|
|
14291
|
-
return c.html(renderFAQForm({
|
|
14292
|
-
isEdit: true,
|
|
14293
|
-
user: user ? {
|
|
14294
|
-
name: user.email,
|
|
14295
|
-
email: user.email,
|
|
14296
|
-
role: user.role
|
|
14297
|
-
} : void 0,
|
|
14298
|
-
message: "Failed to load FAQ",
|
|
14299
|
-
messageType: "error"
|
|
14300
|
-
}));
|
|
14301
|
-
}
|
|
14302
|
-
});
|
|
14303
|
-
adminFAQRoutes.put("/:id", async (c) => {
|
|
14304
|
-
try {
|
|
14305
|
-
const id = parseInt(c.req.param("id"));
|
|
14306
|
-
const formData = await c.req.formData();
|
|
14307
|
-
const data = Object.fromEntries(formData.entries());
|
|
14308
|
-
const validatedData = faqSchema.parse(data);
|
|
14309
|
-
const user = c.get("user");
|
|
14310
|
-
const db = c.env?.DB;
|
|
14311
|
-
if (!db) {
|
|
14312
|
-
return c.html(renderFAQForm({
|
|
14313
|
-
isEdit: true,
|
|
14314
|
-
user: user ? {
|
|
14315
|
-
name: user.email,
|
|
14316
|
-
email: user.email,
|
|
14317
|
-
role: user.role
|
|
14318
|
-
} : void 0,
|
|
14319
|
-
message: "Database not available",
|
|
14320
|
-
messageType: "error"
|
|
14321
|
-
}));
|
|
14322
|
-
}
|
|
14323
|
-
const { results } = await db.prepare(`
|
|
14324
|
-
UPDATE faqs
|
|
14325
|
-
SET question = ?, answer = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
|
|
14326
|
-
WHERE id = ?
|
|
14327
|
-
RETURNING *
|
|
14328
|
-
`).bind(
|
|
14329
|
-
validatedData.question,
|
|
14330
|
-
validatedData.answer,
|
|
14331
|
-
validatedData.category || null,
|
|
14332
|
-
validatedData.tags || null,
|
|
14333
|
-
validatedData.isPublished ? 1 : 0,
|
|
14334
|
-
validatedData.sortOrder,
|
|
14335
|
-
id
|
|
14336
|
-
).all();
|
|
14337
|
-
if (results && results.length > 0) {
|
|
14338
|
-
return c.redirect("/admin/faq?message=FAQ updated successfully");
|
|
14339
|
-
} else {
|
|
14340
|
-
return c.html(renderFAQForm({
|
|
14341
|
-
faq: {
|
|
14342
|
-
id,
|
|
14343
|
-
question: validatedData.question,
|
|
14344
|
-
answer: validatedData.answer,
|
|
14345
|
-
category: validatedData.category,
|
|
14346
|
-
tags: validatedData.tags,
|
|
14347
|
-
isPublished: validatedData.isPublished,
|
|
14348
|
-
sortOrder: validatedData.sortOrder
|
|
14349
|
-
},
|
|
14350
|
-
isEdit: true,
|
|
14351
|
-
user: user ? {
|
|
14352
|
-
name: user.email,
|
|
14353
|
-
email: user.email,
|
|
14354
|
-
role: user.role
|
|
14355
|
-
} : void 0,
|
|
14356
|
-
message: "FAQ not found",
|
|
14357
|
-
messageType: "error"
|
|
14358
|
-
}));
|
|
14359
|
-
}
|
|
14360
|
-
} catch (error) {
|
|
14361
|
-
console.error("Error updating FAQ:", error);
|
|
14362
|
-
const user = c.get("user");
|
|
14363
|
-
const id = parseInt(c.req.param("id"));
|
|
14364
|
-
if (error instanceof z.ZodError) {
|
|
14365
|
-
const errors = {};
|
|
14366
|
-
error.errors.forEach((err) => {
|
|
14367
|
-
const field = err.path[0];
|
|
14368
|
-
if (!errors[field]) errors[field] = [];
|
|
14369
|
-
errors[field].push(err.message);
|
|
14370
|
-
});
|
|
14371
|
-
return c.html(renderFAQForm({
|
|
14372
|
-
faq: {
|
|
14373
|
-
id,
|
|
14374
|
-
question: "",
|
|
14375
|
-
answer: "",
|
|
14376
|
-
category: "",
|
|
14377
|
-
tags: "",
|
|
14378
|
-
isPublished: true,
|
|
14379
|
-
sortOrder: 0
|
|
14380
|
-
},
|
|
14381
|
-
isEdit: true,
|
|
14382
|
-
user: user ? {
|
|
14383
|
-
name: user.email,
|
|
14384
|
-
email: user.email,
|
|
14385
|
-
role: user.role
|
|
14386
|
-
} : void 0,
|
|
14387
|
-
errors,
|
|
14388
|
-
message: "Please correct the errors below",
|
|
14389
|
-
messageType: "error"
|
|
14390
|
-
}));
|
|
14391
|
-
}
|
|
14392
|
-
return c.html(renderFAQForm({
|
|
14393
|
-
faq: {
|
|
14394
|
-
id,
|
|
14395
|
-
question: "",
|
|
14396
|
-
answer: "",
|
|
14397
|
-
category: "",
|
|
14398
|
-
tags: "",
|
|
14399
|
-
isPublished: true,
|
|
14400
|
-
sortOrder: 0
|
|
14401
|
-
},
|
|
14402
|
-
isEdit: true,
|
|
14403
|
-
user: user ? {
|
|
14404
|
-
name: user.email,
|
|
14405
|
-
email: user.email,
|
|
14406
|
-
role: user.role
|
|
14407
|
-
} : void 0,
|
|
14408
|
-
message: "Failed to update FAQ",
|
|
14409
|
-
messageType: "error"
|
|
14410
|
-
}));
|
|
14411
|
-
}
|
|
14412
|
-
});
|
|
14413
|
-
adminFAQRoutes.delete("/:id", async (c) => {
|
|
14983
|
+
adminLogsRoutes.post("/cleanup", async (c) => {
|
|
14414
14984
|
try {
|
|
14415
|
-
const
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
if (changes === 0) {
|
|
14422
|
-
return c.json({ error: "FAQ not found" }, 404);
|
|
14985
|
+
const user = c.get("user");
|
|
14986
|
+
if (!user || user.role !== "admin") {
|
|
14987
|
+
return c.json({
|
|
14988
|
+
success: false,
|
|
14989
|
+
error: "Unauthorized. Admin access required."
|
|
14990
|
+
}, 403);
|
|
14423
14991
|
}
|
|
14424
|
-
|
|
14992
|
+
const logger = getLogger(c.env.DB);
|
|
14993
|
+
await logger.cleanupByRetention();
|
|
14994
|
+
return c.html(html`
|
|
14995
|
+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
|
14996
|
+
Log cleanup completed successfully!
|
|
14997
|
+
</div>
|
|
14998
|
+
`);
|
|
14999
|
+
} catch (error) {
|
|
15000
|
+
console.error("Error cleaning up logs:", error);
|
|
15001
|
+
return c.html(html`
|
|
15002
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
15003
|
+
Failed to clean up logs. Please try again.
|
|
15004
|
+
</div>
|
|
15005
|
+
`);
|
|
15006
|
+
}
|
|
15007
|
+
});
|
|
15008
|
+
adminLogsRoutes.post("/search", async (c) => {
|
|
15009
|
+
try {
|
|
15010
|
+
const formData = await c.req.formData();
|
|
15011
|
+
const search = formData.get("search");
|
|
15012
|
+
const level = formData.get("level");
|
|
15013
|
+
const category = formData.get("category");
|
|
15014
|
+
const logger = getLogger(c.env.DB);
|
|
15015
|
+
const filter = {
|
|
15016
|
+
limit: 20,
|
|
15017
|
+
offset: 0,
|
|
15018
|
+
sortBy: "created_at",
|
|
15019
|
+
sortOrder: "desc"
|
|
15020
|
+
};
|
|
15021
|
+
if (search) filter.search = search;
|
|
15022
|
+
if (level) filter.level = [level];
|
|
15023
|
+
if (category) filter.category = [category];
|
|
15024
|
+
const { logs } = await logger.getLogs(filter);
|
|
15025
|
+
const rows = logs.map((log) => {
|
|
15026
|
+
const formattedLog = {
|
|
15027
|
+
...log,
|
|
15028
|
+
formattedDate: new Date(log.createdAt).toLocaleString(),
|
|
15029
|
+
levelClass: getLevelClass(log.level),
|
|
15030
|
+
categoryClass: getCategoryClass(log.category)
|
|
15031
|
+
};
|
|
15032
|
+
return `
|
|
15033
|
+
<tr class="hover:bg-gray-50">
|
|
15034
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
15035
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
|
|
15036
|
+
${formattedLog.level}
|
|
15037
|
+
</span>
|
|
15038
|
+
</td>
|
|
15039
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
15040
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
|
|
15041
|
+
${formattedLog.category}
|
|
15042
|
+
</span>
|
|
15043
|
+
</td>
|
|
15044
|
+
<td class="px-6 py-4">
|
|
15045
|
+
<div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
|
|
15046
|
+
</td>
|
|
15047
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
|
|
15048
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
|
|
15049
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
15050
|
+
<a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
|
|
15051
|
+
</td>
|
|
15052
|
+
</tr>
|
|
15053
|
+
`;
|
|
15054
|
+
}).join("");
|
|
15055
|
+
return c.html(rows);
|
|
14425
15056
|
} catch (error) {
|
|
14426
|
-
console.error("Error
|
|
14427
|
-
return c.
|
|
15057
|
+
console.error("Error searching logs:", error);
|
|
15058
|
+
return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
|
|
15059
|
+
}
|
|
15060
|
+
});
|
|
15061
|
+
function getLevelClass(level) {
|
|
15062
|
+
switch (level) {
|
|
15063
|
+
case "debug":
|
|
15064
|
+
return "bg-gray-100 text-gray-800";
|
|
15065
|
+
case "info":
|
|
15066
|
+
return "bg-blue-100 text-blue-800";
|
|
15067
|
+
case "warn":
|
|
15068
|
+
return "bg-yellow-100 text-yellow-800";
|
|
15069
|
+
case "error":
|
|
15070
|
+
return "bg-red-100 text-red-800";
|
|
15071
|
+
case "fatal":
|
|
15072
|
+
return "bg-purple-100 text-purple-800";
|
|
15073
|
+
default:
|
|
15074
|
+
return "bg-gray-100 text-gray-800";
|
|
15075
|
+
}
|
|
15076
|
+
}
|
|
15077
|
+
function getCategoryClass(category) {
|
|
15078
|
+
switch (category) {
|
|
15079
|
+
case "auth":
|
|
15080
|
+
return "bg-green-100 text-green-800";
|
|
15081
|
+
case "api":
|
|
15082
|
+
return "bg-blue-100 text-blue-800";
|
|
15083
|
+
case "workflow":
|
|
15084
|
+
return "bg-purple-100 text-purple-800";
|
|
15085
|
+
case "plugin":
|
|
15086
|
+
return "bg-indigo-100 text-indigo-800";
|
|
15087
|
+
case "media":
|
|
15088
|
+
return "bg-pink-100 text-pink-800";
|
|
15089
|
+
case "system":
|
|
15090
|
+
return "bg-gray-100 text-gray-800";
|
|
15091
|
+
case "security":
|
|
15092
|
+
return "bg-red-100 text-red-800";
|
|
15093
|
+
case "error":
|
|
15094
|
+
return "bg-red-100 text-red-800";
|
|
15095
|
+
default:
|
|
15096
|
+
return "bg-gray-100 text-gray-800";
|
|
14428
15097
|
}
|
|
15098
|
+
}
|
|
15099
|
+
var adminDesignRoutes = new Hono();
|
|
15100
|
+
adminDesignRoutes.get("/", (c) => {
|
|
15101
|
+
const user = c.get("user");
|
|
15102
|
+
const pageData = {
|
|
15103
|
+
user: user ? {
|
|
15104
|
+
name: user.email,
|
|
15105
|
+
email: user.email,
|
|
15106
|
+
role: user.role
|
|
15107
|
+
} : void 0
|
|
15108
|
+
};
|
|
15109
|
+
return c.html(renderDesignPage(pageData));
|
|
15110
|
+
});
|
|
15111
|
+
var adminCheckboxRoutes = new Hono();
|
|
15112
|
+
adminCheckboxRoutes.get("/", (c) => {
|
|
15113
|
+
const user = c.get("user");
|
|
15114
|
+
const pageData = {
|
|
15115
|
+
user: user ? {
|
|
15116
|
+
name: user.email,
|
|
15117
|
+
email: user.email,
|
|
15118
|
+
role: user.role
|
|
15119
|
+
} : void 0
|
|
15120
|
+
};
|
|
15121
|
+
return c.html(renderCheckboxPage(pageData));
|
|
14429
15122
|
});
|
|
14430
|
-
var admin_faq_default = adminFAQRoutes;
|
|
14431
15123
|
|
|
14432
15124
|
// src/templates/pages/admin-testimonials-form.template.ts
|
|
14433
15125
|
function renderTestimonialsForm(data) {
|
|
@@ -14485,7 +15177,7 @@ function renderTestimonialsForm(data) {
|
|
|
14485
15177
|
${errors?.authorName ? `
|
|
14486
15178
|
<div class="mt-1">
|
|
14487
15179
|
${errors.authorName.map((error) => `
|
|
14488
|
-
<p class="text-sm text-red-400">${
|
|
15180
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14489
15181
|
`).join("")}
|
|
14490
15182
|
</div>
|
|
14491
15183
|
` : ""}
|
|
@@ -14507,7 +15199,7 @@ function renderTestimonialsForm(data) {
|
|
|
14507
15199
|
${errors?.authorTitle ? `
|
|
14508
15200
|
<div class="mt-1">
|
|
14509
15201
|
${errors.authorTitle.map((error) => `
|
|
14510
|
-
<p class="text-sm text-red-400">${
|
|
15202
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14511
15203
|
`).join("")}
|
|
14512
15204
|
</div>
|
|
14513
15205
|
` : ""}
|
|
@@ -14528,7 +15220,7 @@ function renderTestimonialsForm(data) {
|
|
|
14528
15220
|
${errors?.authorCompany ? `
|
|
14529
15221
|
<div class="mt-1">
|
|
14530
15222
|
${errors.authorCompany.map((error) => `
|
|
14531
|
-
<p class="text-sm text-red-400">${
|
|
15223
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14532
15224
|
`).join("")}
|
|
14533
15225
|
</div>
|
|
14534
15226
|
` : ""}
|
|
@@ -14560,7 +15252,7 @@ function renderTestimonialsForm(data) {
|
|
|
14560
15252
|
${errors?.testimonialText ? `
|
|
14561
15253
|
<div class="mt-1">
|
|
14562
15254
|
${errors.testimonialText.map((error) => `
|
|
14563
|
-
<p class="text-sm text-red-400">${
|
|
15255
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14564
15256
|
`).join("")}
|
|
14565
15257
|
</div>
|
|
14566
15258
|
` : ""}
|
|
@@ -14584,7 +15276,7 @@ function renderTestimonialsForm(data) {
|
|
|
14584
15276
|
${errors?.rating ? `
|
|
14585
15277
|
<div class="mt-1">
|
|
14586
15278
|
${errors.rating.map((error) => `
|
|
14587
|
-
<p class="text-sm text-red-400">${
|
|
15279
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14588
15280
|
`).join("")}
|
|
14589
15281
|
</div>
|
|
14590
15282
|
` : ""}
|
|
@@ -14638,7 +15330,7 @@ function renderTestimonialsForm(data) {
|
|
|
14638
15330
|
${errors?.sortOrder ? `
|
|
14639
15331
|
<div class="mt-1">
|
|
14640
15332
|
${errors.sortOrder.map((error) => `
|
|
14641
|
-
<p class="text-sm text-red-400">${
|
|
15333
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14642
15334
|
`).join("")}
|
|
14643
15335
|
</div>
|
|
14644
15336
|
` : ""}
|
|
@@ -14685,7 +15377,7 @@ function renderTestimonialsForm(data) {
|
|
|
14685
15377
|
};
|
|
14686
15378
|
return renderAdminLayout(layoutData);
|
|
14687
15379
|
}
|
|
14688
|
-
function
|
|
15380
|
+
function escapeHtml4(unsafe) {
|
|
14689
15381
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14690
15382
|
}
|
|
14691
15383
|
|
|
@@ -15113,7 +15805,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15113
15805
|
${errors?.title ? `
|
|
15114
15806
|
<div class="mt-1">
|
|
15115
15807
|
${errors.title.map((error) => `
|
|
15116
|
-
<p class="text-sm text-red-400">${
|
|
15808
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15117
15809
|
`).join("")}
|
|
15118
15810
|
</div>
|
|
15119
15811
|
` : ""}
|
|
@@ -15136,7 +15828,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15136
15828
|
${errors?.description ? `
|
|
15137
15829
|
<div class="mt-1">
|
|
15138
15830
|
${errors.description.map((error) => `
|
|
15139
|
-
<p class="text-sm text-red-400">${
|
|
15831
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15140
15832
|
`).join("")}
|
|
15141
15833
|
</div>
|
|
15142
15834
|
` : ""}
|
|
@@ -15168,7 +15860,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15168
15860
|
${errors?.language ? `
|
|
15169
15861
|
<div class="mt-1">
|
|
15170
15862
|
${errors.language.map((error) => `
|
|
15171
|
-
<p class="text-sm text-red-400">${
|
|
15863
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15172
15864
|
`).join("")}
|
|
15173
15865
|
</div>
|
|
15174
15866
|
` : ""}
|
|
@@ -15189,7 +15881,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15189
15881
|
${errors?.category ? `
|
|
15190
15882
|
<div class="mt-1">
|
|
15191
15883
|
${errors.category.map((error) => `
|
|
15192
|
-
<p class="text-sm text-red-400">${
|
|
15884
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15193
15885
|
`).join("")}
|
|
15194
15886
|
</div>
|
|
15195
15887
|
` : ""}
|
|
@@ -15211,7 +15903,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15211
15903
|
${errors?.tags ? `
|
|
15212
15904
|
<div class="mt-1">
|
|
15213
15905
|
${errors.tags.map((error) => `
|
|
15214
|
-
<p class="text-sm text-red-400">${
|
|
15906
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15215
15907
|
`).join("")}
|
|
15216
15908
|
</div>
|
|
15217
15909
|
` : ""}
|
|
@@ -15242,7 +15934,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15242
15934
|
${errors?.code ? `
|
|
15243
15935
|
<div class="mt-1">
|
|
15244
15936
|
${errors.code.map((error) => `
|
|
15245
|
-
<p class="text-sm text-red-400">${
|
|
15937
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15246
15938
|
`).join("")}
|
|
15247
15939
|
</div>
|
|
15248
15940
|
` : ""}
|
|
@@ -15296,7 +15988,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15296
15988
|
${errors?.sortOrder ? `
|
|
15297
15989
|
<div class="mt-1">
|
|
15298
15990
|
${errors.sortOrder.map((error) => `
|
|
15299
|
-
<p class="text-sm text-red-400">${
|
|
15991
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15300
15992
|
`).join("")}
|
|
15301
15993
|
</div>
|
|
15302
15994
|
` : ""}
|
|
@@ -15354,7 +16046,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15354
16046
|
};
|
|
15355
16047
|
return renderAdminLayout(layoutData);
|
|
15356
16048
|
}
|
|
15357
|
-
function
|
|
16049
|
+
function escapeHtml5(unsafe) {
|
|
15358
16050
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15359
16051
|
}
|
|
15360
16052
|
|
|
@@ -16688,8 +17380,8 @@ function renderTable2(data) {
|
|
|
16688
17380
|
</td>
|
|
16689
17381
|
` : ""}
|
|
16690
17382
|
${data.columns.map((column, colIndex) => {
|
|
16691
|
-
const
|
|
16692
|
-
const displayValue = column.render ? column.render(
|
|
17383
|
+
const value = row[column.key];
|
|
17384
|
+
const displayValue = column.render ? column.render(value, row) : value;
|
|
16693
17385
|
const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
|
|
16694
17386
|
const isFirst = colIndex === 0 && !data.selectable;
|
|
16695
17387
|
const isLast = colIndex === data.columns.length - 1;
|
|
@@ -17076,10 +17768,41 @@ function renderCollectionsListPage(data) {
|
|
|
17076
17768
|
|
|
17077
17769
|
// src/templates/pages/admin-collections-form.template.ts
|
|
17078
17770
|
init_admin_layout_catalyst_template();
|
|
17771
|
+
function getFieldTypeBadge(fieldType) {
|
|
17772
|
+
const typeLabels = {
|
|
17773
|
+
"text": "Text",
|
|
17774
|
+
"richtext": "Rich Text (TinyMCE)",
|
|
17775
|
+
"quill": "Rich Text (Quill)",
|
|
17776
|
+
"mdxeditor": "Rich Text (MDXEditor)",
|
|
17777
|
+
"number": "Number",
|
|
17778
|
+
"boolean": "Boolean",
|
|
17779
|
+
"date": "Date",
|
|
17780
|
+
"select": "Select",
|
|
17781
|
+
"media": "Media"
|
|
17782
|
+
};
|
|
17783
|
+
const typeColors = {
|
|
17784
|
+
"text": "bg-blue-500/10 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300 ring-blue-500/20 dark:ring-blue-400/20",
|
|
17785
|
+
"richtext": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
|
|
17786
|
+
"quill": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
|
|
17787
|
+
"mdxeditor": "bg-purple-500/10 dark:bg-purple-400/10 text-purple-700 dark:text-purple-300 ring-purple-500/20 dark:ring-purple-400/20",
|
|
17788
|
+
"number": "bg-green-500/10 dark:bg-green-400/10 text-green-700 dark:text-green-300 ring-green-500/20 dark:ring-green-400/20",
|
|
17789
|
+
"boolean": "bg-amber-500/10 dark:bg-amber-400/10 text-amber-700 dark:text-amber-300 ring-amber-500/20 dark:ring-amber-400/20",
|
|
17790
|
+
"date": "bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-cyan-500/20 dark:ring-cyan-400/20",
|
|
17791
|
+
"select": "bg-indigo-500/10 dark:bg-indigo-400/10 text-indigo-700 dark:text-indigo-300 ring-indigo-500/20 dark:ring-indigo-400/20",
|
|
17792
|
+
"media": "bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-rose-500/20 dark:ring-rose-400/20"
|
|
17793
|
+
};
|
|
17794
|
+
const label = typeLabels[fieldType] || fieldType;
|
|
17795
|
+
const color = typeColors[fieldType] || "bg-zinc-500/10 dark:bg-zinc-400/10 text-zinc-700 dark:text-zinc-300 ring-zinc-500/20 dark:ring-zinc-400/20";
|
|
17796
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${color} ring-1 ring-inset">${label}</span>`;
|
|
17797
|
+
}
|
|
17079
17798
|
function renderCollectionFormPage(data) {
|
|
17080
17799
|
const isEdit = data.isEdit || !!data.id;
|
|
17081
17800
|
const title = isEdit ? "Edit Collection" : "Create New Collection";
|
|
17082
17801
|
const subtitle = isEdit ? `Update collection: ${data.display_name}` : "Define a new content collection with custom fields and settings.";
|
|
17802
|
+
const fieldsWithData = (data.fields || []).map((field) => ({
|
|
17803
|
+
...field,
|
|
17804
|
+
dataFieldJSON: JSON.stringify(JSON.stringify(field))
|
|
17805
|
+
}));
|
|
17083
17806
|
const fields = [
|
|
17084
17807
|
{
|
|
17085
17808
|
name: "displayName",
|
|
@@ -17316,21 +18039,24 @@ function renderCollectionFormPage(data) {
|
|
|
17316
18039
|
|
|
17317
18040
|
<!-- Fields List (Read-Only) -->
|
|
17318
18041
|
<div class="space-y-3">
|
|
17319
|
-
${
|
|
18042
|
+
${fieldsWithData.map((field) => `
|
|
17320
18043
|
<div class="bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4">
|
|
17321
18044
|
<div class="flex items-center justify-between">
|
|
17322
18045
|
<div class="flex items-center gap-x-4">
|
|
17323
18046
|
<div>
|
|
17324
18047
|
<div class="flex items-center gap-x-2">
|
|
17325
18048
|
<span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
|
|
17326
|
-
|
|
17327
|
-
${field.field_type}
|
|
17328
|
-
</span>
|
|
18049
|
+
${getFieldTypeBadge(field.field_type)}
|
|
17329
18050
|
${field.is_required ? `
|
|
17330
18051
|
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-1 ring-inset ring-rose-500/20 dark:ring-rose-400/20">
|
|
17331
18052
|
Required
|
|
17332
18053
|
</span>
|
|
17333
18054
|
` : ""}
|
|
18055
|
+
${field.is_searchable ? `
|
|
18056
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-emerald-500/10 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300 ring-1 ring-inset ring-emerald-500/20 dark:ring-emerald-400/20">
|
|
18057
|
+
Searchable
|
|
18058
|
+
</span>
|
|
18059
|
+
` : ""}
|
|
17334
18060
|
</div>
|
|
17335
18061
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
|
17336
18062
|
<code class="px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">${field.field_name}</code>
|
|
@@ -17376,8 +18102,10 @@ function renderCollectionFormPage(data) {
|
|
|
17376
18102
|
|
|
17377
18103
|
<!-- Fields List -->
|
|
17378
18104
|
<div id="fields-list" class="space-y-3">
|
|
17379
|
-
${
|
|
17380
|
-
<div class="field-item bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4"
|
|
18105
|
+
${fieldsWithData.map((field) => `
|
|
18106
|
+
<div class="field-item bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4"
|
|
18107
|
+
data-field-id="${field.id}"
|
|
18108
|
+
data-field-data="${field.dataFieldJSON}">
|
|
17381
18109
|
<div class="flex items-center justify-between">
|
|
17382
18110
|
<div class="flex items-center gap-x-4">
|
|
17383
18111
|
<div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
|
|
@@ -17388,14 +18116,17 @@ function renderCollectionFormPage(data) {
|
|
|
17388
18116
|
<div>
|
|
17389
18117
|
<div class="flex items-center gap-x-2">
|
|
17390
18118
|
<span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
|
|
17391
|
-
|
|
17392
|
-
${field.field_type}
|
|
17393
|
-
</span>
|
|
18119
|
+
${getFieldTypeBadge(field.field_type)}
|
|
17394
18120
|
${field.is_required ? `
|
|
17395
18121
|
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-pink-500/10 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300 ring-1 ring-inset ring-pink-500/20 dark:ring-pink-400/20">
|
|
17396
18122
|
Required
|
|
17397
18123
|
</span>
|
|
17398
18124
|
` : ""}
|
|
18125
|
+
${field.is_searchable ? `
|
|
18126
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-emerald-500/10 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300 ring-1 ring-inset ring-emerald-500/20 dark:ring-emerald-400/20">
|
|
18127
|
+
Searchable
|
|
18128
|
+
</span>
|
|
18129
|
+
` : ""}
|
|
17399
18130
|
</div>
|
|
17400
18131
|
<div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
|
|
17401
18132
|
Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
|
|
@@ -17506,7 +18237,7 @@ function renderCollectionFormPage(data) {
|
|
|
17506
18237
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
|
|
17507
18238
|
<input
|
|
17508
18239
|
type="text"
|
|
17509
|
-
id="field-name"
|
|
18240
|
+
id="modal-field-name"
|
|
17510
18241
|
name="field_name"
|
|
17511
18242
|
required
|
|
17512
18243
|
pattern="[a-z0-9_]+"
|
|
@@ -17527,13 +18258,14 @@ function renderCollectionFormPage(data) {
|
|
|
17527
18258
|
>
|
|
17528
18259
|
<option value="">Select field type...</option>
|
|
17529
18260
|
<option value="text">Text</option>
|
|
17530
|
-
<option value="richtext">Rich Text</option>
|
|
18261
|
+
${data.editorPlugins?.tinymce ? '<option value="richtext">Rich Text (TinyMCE)</option>' : ""}
|
|
18262
|
+
${data.editorPlugins?.quill ? '<option value="quill">Rich Text (Quill)</option>' : ""}
|
|
18263
|
+
${data.editorPlugins?.mdxeditor ? '<option value="mdxeditor">Rich Text (MDXEditor)</option>' : ""}
|
|
17531
18264
|
<option value="number">Number</option>
|
|
17532
18265
|
<option value="boolean">Boolean</option>
|
|
17533
18266
|
<option value="date">Date</option>
|
|
17534
18267
|
<option value="select">Select</option>
|
|
17535
18268
|
<option value="media">Media</option>
|
|
17536
|
-
<option value="guid">GUID (Auto-generated)</option>
|
|
17537
18269
|
</select>
|
|
17538
18270
|
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-blue-600 dark:text-blue-400 sm:size-4">
|
|
17539
18271
|
<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" />
|
|
@@ -17558,6 +18290,7 @@ function renderCollectionFormPage(data) {
|
|
|
17558
18290
|
<div class="flex gap-3">
|
|
17559
18291
|
<div class="flex h-6 shrink-0 items-center">
|
|
17560
18292
|
<div class="group grid size-4 grid-cols-1">
|
|
18293
|
+
<input type="hidden" name="is_required" value="0">
|
|
17561
18294
|
<input
|
|
17562
18295
|
type="checkbox"
|
|
17563
18296
|
id="field-required"
|
|
@@ -17579,6 +18312,7 @@ function renderCollectionFormPage(data) {
|
|
|
17579
18312
|
<div class="flex gap-3">
|
|
17580
18313
|
<div class="flex h-6 shrink-0 items-center">
|
|
17581
18314
|
<div class="group grid size-4 grid-cols-1">
|
|
18315
|
+
<input type="hidden" name="is_searchable" value="0">
|
|
17582
18316
|
<input
|
|
17583
18317
|
type="checkbox"
|
|
17584
18318
|
id="field-searchable"
|
|
@@ -17641,18 +18375,52 @@ function renderCollectionFormPage(data) {
|
|
|
17641
18375
|
document.getElementById('submit-text').textContent = 'Add Field';
|
|
17642
18376
|
document.getElementById('field-form').reset();
|
|
17643
18377
|
document.getElementById('field-id').value = '';
|
|
17644
|
-
document.getElementById('field-name').disabled = false;
|
|
18378
|
+
document.getElementById('modal-field-name').disabled = false;
|
|
17645
18379
|
currentEditingField = null;
|
|
18380
|
+
isEditingField = false; // Allow change handlers for add mode
|
|
17646
18381
|
document.getElementById('field-modal').classList.remove('hidden');
|
|
17647
18382
|
}
|
|
17648
18383
|
|
|
17649
18384
|
function editField(fieldId) {
|
|
17650
18385
|
const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
|
|
17651
|
-
if (!fieldItem)
|
|
18386
|
+
if (!fieldItem) {
|
|
18387
|
+
console.error('Field item not found:', fieldId);
|
|
18388
|
+
return;
|
|
18389
|
+
}
|
|
18390
|
+
|
|
18391
|
+
// Get field data from data attribute (primary source) or embedded array (fallback)
|
|
18392
|
+
let field = null;
|
|
18393
|
+
|
|
18394
|
+
// Try to get from data attribute first
|
|
18395
|
+
const fieldDataAttr = fieldItem.getAttribute('data-field-data');
|
|
18396
|
+
if (fieldDataAttr) {
|
|
18397
|
+
try {
|
|
18398
|
+
// Data is double-JSON encoded to properly escape all special characters
|
|
18399
|
+
field = JSON.parse(JSON.parse(fieldDataAttr));
|
|
18400
|
+
console.log('Loaded field data from data attribute:', field);
|
|
18401
|
+
} catch (e) {
|
|
18402
|
+
console.error('Error parsing field data from attribute:', e);
|
|
18403
|
+
// Try single parse as fallback for backwards compatibility
|
|
18404
|
+
try {
|
|
18405
|
+
field = JSON.parse(fieldDataAttr);
|
|
18406
|
+
console.log('Loaded field data from data attribute (single parse):', field);
|
|
18407
|
+
} catch (e2) {
|
|
18408
|
+
console.error('Error parsing field data (fallback):', e2);
|
|
18409
|
+
}
|
|
18410
|
+
}
|
|
18411
|
+
}
|
|
18412
|
+
|
|
18413
|
+
// Fallback to embedded array
|
|
18414
|
+
if (!field) {
|
|
18415
|
+
const fields = ${JSON.stringify(data.fields || [])};
|
|
18416
|
+
field = fields.find(f => f.id === fieldId);
|
|
18417
|
+
console.log('Loaded field data from embedded array:', field);
|
|
18418
|
+
}
|
|
17652
18419
|
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
18420
|
+
if (!field) {
|
|
18421
|
+
console.error('Field data not found for id:', fieldId);
|
|
18422
|
+
return;
|
|
18423
|
+
}
|
|
17656
18424
|
|
|
17657
18425
|
// Set up the modal for editing
|
|
17658
18426
|
document.getElementById('modal-title').textContent = 'Edit Field';
|
|
@@ -17660,18 +18428,102 @@ function renderCollectionFormPage(data) {
|
|
|
17660
18428
|
document.getElementById('field-id').value = fieldId;
|
|
17661
18429
|
currentEditingField = fieldId;
|
|
17662
18430
|
|
|
17663
|
-
//
|
|
17664
|
-
document.getElementById('field-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
18431
|
+
// Show modal FIRST before populating fields
|
|
18432
|
+
document.getElementById('field-modal').classList.remove('hidden');
|
|
18433
|
+
|
|
18434
|
+
// Set flag to prevent change event handlers from interfering
|
|
18435
|
+
isEditingField = true;
|
|
18436
|
+
|
|
18437
|
+
// Use setTimeout to ensure modal is rendered before setting values
|
|
18438
|
+
setTimeout(() => {
|
|
18439
|
+
// Populate form with existing field data
|
|
18440
|
+
console.log('Field object for editing:', field);
|
|
18441
|
+
console.log('field.field_name:', field.field_name);
|
|
18442
|
+
console.log('field.field_type:', field.field_type);
|
|
18443
|
+
console.log('field.field_label:', field.field_label);
|
|
18444
|
+
|
|
18445
|
+
const fieldNameInput = document.getElementById('modal-field-name');
|
|
18446
|
+
const fieldTypeSelect = document.getElementById('field-type');
|
|
18447
|
+
const fieldLabelInput = document.getElementById('field-label');
|
|
18448
|
+
|
|
18449
|
+
console.log('Field name input element:', fieldNameInput);
|
|
18450
|
+
console.log('Field type select element:', fieldTypeSelect);
|
|
18451
|
+
console.log('Field label input element:', fieldLabelInput);
|
|
18452
|
+
|
|
18453
|
+
if (fieldNameInput) {
|
|
18454
|
+
console.log('Before setting - field-name value:', fieldNameInput.value);
|
|
18455
|
+
console.log('Before setting - field-name disabled:', fieldNameInput.disabled);
|
|
18456
|
+
|
|
18457
|
+
fieldNameInput.disabled = false; // Enable first to ensure value can be set
|
|
18458
|
+
fieldNameInput.value = field.field_name || '';
|
|
18459
|
+
fieldNameInput.disabled = true; // Then disable
|
|
18460
|
+
|
|
18461
|
+
console.log('After setting - field-name value:', fieldNameInput.value);
|
|
18462
|
+
console.log('After setting - field-name disabled:', fieldNameInput.disabled);
|
|
18463
|
+
|
|
18464
|
+
// Verify the value stuck
|
|
18465
|
+
setTimeout(() => {
|
|
18466
|
+
console.log('After 100ms - field-name value:', fieldNameInput.value);
|
|
18467
|
+
}, 100);
|
|
18468
|
+
} else {
|
|
18469
|
+
console.error('field-name input not found!');
|
|
18470
|
+
}
|
|
18471
|
+
|
|
18472
|
+
if (fieldLabelInput) {
|
|
18473
|
+
fieldLabelInput.value = field.field_label || '';
|
|
18474
|
+
console.log('Set field-label to:', fieldLabelInput.value);
|
|
18475
|
+
} else {
|
|
18476
|
+
console.error('field-label input not found!');
|
|
18477
|
+
}
|
|
18478
|
+
|
|
18479
|
+
if (fieldTypeSelect) {
|
|
18480
|
+
// Map schema types to UI field types
|
|
18481
|
+
let uiFieldType = field.field_type || '';
|
|
18482
|
+
|
|
18483
|
+
// Check if it's a schema field with field_options that might indicate the actual type
|
|
18484
|
+
if (field.field_options && typeof field.field_options === 'object') {
|
|
18485
|
+
// Check for richtext format
|
|
18486
|
+
if (field.field_options.format === 'richtext') {
|
|
18487
|
+
uiFieldType = 'richtext';
|
|
18488
|
+
}
|
|
18489
|
+
// Check for other format indicators
|
|
18490
|
+
else if (field.field_options.type) {
|
|
18491
|
+
uiFieldType = field.field_options.type;
|
|
18492
|
+
}
|
|
18493
|
+
}
|
|
18494
|
+
|
|
18495
|
+
// Map common schema types to UI types
|
|
18496
|
+
const typeMapping = {
|
|
18497
|
+
'string': 'text',
|
|
18498
|
+
'integer': 'number',
|
|
18499
|
+
'bool': 'boolean'
|
|
18500
|
+
};
|
|
18501
|
+
|
|
18502
|
+
if (typeMapping[uiFieldType]) {
|
|
18503
|
+
uiFieldType = typeMapping[uiFieldType];
|
|
18504
|
+
}
|
|
18505
|
+
|
|
18506
|
+
fieldTypeSelect.value = uiFieldType;
|
|
18507
|
+
console.log('Set field-type to:', fieldTypeSelect.value, '(original:', field.field_type, ')');
|
|
18508
|
+
} else {
|
|
18509
|
+
console.error('field-type select not found!');
|
|
18510
|
+
}
|
|
18511
|
+
|
|
18512
|
+
const requiredCheckbox = document.getElementById('field-required');
|
|
18513
|
+
const searchableCheckbox = document.getElementById('field-searchable');
|
|
18514
|
+
|
|
18515
|
+
if (requiredCheckbox) {
|
|
18516
|
+
requiredCheckbox.checked = Boolean(field.is_required);
|
|
18517
|
+
}
|
|
18518
|
+
|
|
18519
|
+
if (searchableCheckbox) {
|
|
18520
|
+
searchableCheckbox.checked = Boolean(field.is_searchable);
|
|
18521
|
+
}
|
|
18522
|
+
|
|
17671
18523
|
// Handle field options - serialize object back to JSON string
|
|
17672
18524
|
if (field.field_options) {
|
|
17673
|
-
document.getElementById('field-options').value = typeof field.field_options === 'string'
|
|
17674
|
-
? field.field_options
|
|
18525
|
+
document.getElementById('field-options').value = typeof field.field_options === 'string'
|
|
18526
|
+
? field.field_options
|
|
17675
18527
|
: JSON.stringify(field.field_options, null, 2);
|
|
17676
18528
|
} else {
|
|
17677
18529
|
document.getElementById('field-options').value = '';
|
|
@@ -17682,7 +18534,7 @@ function renderCollectionFormPage(data) {
|
|
|
17682
18534
|
const optionsContainer = document.getElementById('field-options-container');
|
|
17683
18535
|
const helpText = document.getElementById('field-type-help');
|
|
17684
18536
|
|
|
17685
|
-
if (['select', 'media', 'richtext'
|
|
18537
|
+
if (['select', 'media', 'richtext'].includes(fieldType)) {
|
|
17686
18538
|
optionsContainer.classList.remove('hidden');
|
|
17687
18539
|
|
|
17688
18540
|
// Set help text based on type
|
|
@@ -17696,9 +18548,6 @@ function renderCollectionFormPage(data) {
|
|
|
17696
18548
|
case 'richtext':
|
|
17697
18549
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17698
18550
|
break;
|
|
17699
|
-
case 'guid':
|
|
17700
|
-
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17701
|
-
break;
|
|
17702
18551
|
}
|
|
17703
18552
|
} else {
|
|
17704
18553
|
optionsContainer.classList.add('hidden');
|
|
@@ -17722,12 +18571,19 @@ function renderCollectionFormPage(data) {
|
|
|
17722
18571
|
}
|
|
17723
18572
|
}
|
|
17724
18573
|
|
|
17725
|
-
|
|
18574
|
+
// Clear the flag after a short delay to allow all events to settle
|
|
18575
|
+
setTimeout(() => {
|
|
18576
|
+
isEditingField = false;
|
|
18577
|
+
console.log('Cleared isEditingField flag');
|
|
18578
|
+
}, 100);
|
|
18579
|
+
|
|
18580
|
+
}, 10); // Small delay to ensure modal is fully rendered
|
|
17726
18581
|
}
|
|
17727
18582
|
|
|
17728
18583
|
function closeFieldModal() {
|
|
17729
18584
|
document.getElementById('field-modal').classList.add('hidden');
|
|
17730
18585
|
currentEditingField = null;
|
|
18586
|
+
isEditingField = false; // Clear the flag when closing
|
|
17731
18587
|
}
|
|
17732
18588
|
|
|
17733
18589
|
let fieldToDelete = null;
|
|
@@ -17801,12 +18657,21 @@ function renderCollectionFormPage(data) {
|
|
|
17801
18657
|
});
|
|
17802
18658
|
});
|
|
17803
18659
|
|
|
18660
|
+
// Flag to prevent change handler during programmatic edits
|
|
18661
|
+
let isEditingField = false;
|
|
18662
|
+
|
|
17804
18663
|
// Field type change handler
|
|
17805
18664
|
document.getElementById('field-type').addEventListener('change', function() {
|
|
18665
|
+
// Skip if we're programmatically setting values during edit
|
|
18666
|
+
if (isEditingField) {
|
|
18667
|
+
console.log('Skipping change handler - field is being edited');
|
|
18668
|
+
return;
|
|
18669
|
+
}
|
|
18670
|
+
|
|
17806
18671
|
const optionsContainer = document.getElementById('field-options-container');
|
|
17807
18672
|
const fieldOptions = document.getElementById('field-options');
|
|
17808
18673
|
const helpText = document.getElementById('field-type-help');
|
|
17809
|
-
const fieldNameInput = document.getElementById('field-name');
|
|
18674
|
+
const fieldNameInput = document.getElementById('modal-field-name');
|
|
17810
18675
|
|
|
17811
18676
|
// Show/hide options based on field type
|
|
17812
18677
|
if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
|
|
@@ -17826,14 +18691,6 @@ function renderCollectionFormPage(data) {
|
|
|
17826
18691
|
fieldOptions.value = '{"toolbar": "full", "height": 400}';
|
|
17827
18692
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17828
18693
|
break;
|
|
17829
|
-
case 'guid':
|
|
17830
|
-
fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
|
|
17831
|
-
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17832
|
-
// Suggest 'id' as field name for GUID fields
|
|
17833
|
-
if (!fieldNameInput.value || fieldNameInput.value === '') {
|
|
17834
|
-
fieldNameInput.value = 'id';
|
|
17835
|
-
}
|
|
17836
|
-
break;
|
|
17837
18694
|
}
|
|
17838
18695
|
} else {
|
|
17839
18696
|
optionsContainer.classList.add('hidden');
|
|
@@ -17970,8 +18827,19 @@ adminCollectionsRoutes.get("/", async (c) => {
|
|
|
17970
18827
|
return c.html(html`<p>Error loading collections</p>`);
|
|
17971
18828
|
}
|
|
17972
18829
|
});
|
|
17973
|
-
adminCollectionsRoutes.get("/new", (c) => {
|
|
18830
|
+
adminCollectionsRoutes.get("/new", async (c) => {
|
|
17974
18831
|
const user = c.get("user");
|
|
18832
|
+
const db = c.env.DB;
|
|
18833
|
+
const [tinymceActive, quillActive, mdxeditorActive] = await Promise.all([
|
|
18834
|
+
isPluginActive2(db, "tinymce-plugin"),
|
|
18835
|
+
isPluginActive2(db, "quill-editor"),
|
|
18836
|
+
isPluginActive2(db, "mdxeditor-plugin")
|
|
18837
|
+
]);
|
|
18838
|
+
console.log("[Collections /new] Editor plugins status:", {
|
|
18839
|
+
tinymce: tinymceActive,
|
|
18840
|
+
quill: quillActive,
|
|
18841
|
+
mdxeditor: mdxeditorActive
|
|
18842
|
+
});
|
|
17975
18843
|
const formData = {
|
|
17976
18844
|
isEdit: false,
|
|
17977
18845
|
user: user ? {
|
|
@@ -17979,7 +18847,12 @@ adminCollectionsRoutes.get("/new", (c) => {
|
|
|
17979
18847
|
email: user.email,
|
|
17980
18848
|
role: user.role
|
|
17981
18849
|
} : void 0,
|
|
17982
|
-
version: c.get("appVersion")
|
|
18850
|
+
version: c.get("appVersion"),
|
|
18851
|
+
editorPlugins: {
|
|
18852
|
+
tinymce: tinymceActive,
|
|
18853
|
+
quill: quillActive,
|
|
18854
|
+
mdxeditor: mdxeditorActive
|
|
18855
|
+
}
|
|
17983
18856
|
};
|
|
17984
18857
|
return c.html(renderCollectionFormPage(formData));
|
|
17985
18858
|
});
|
|
@@ -18079,16 +18952,16 @@ adminCollectionsRoutes.post("/", async (c) => {
|
|
|
18079
18952
|
if (isHtmx) {
|
|
18080
18953
|
return c.html(html`
|
|
18081
18954
|
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
18082
|
-
Collection created successfully! Redirecting...
|
|
18955
|
+
Collection created successfully! Redirecting to edit mode...
|
|
18083
18956
|
<script>
|
|
18084
18957
|
setTimeout(() => {
|
|
18085
|
-
window.location.href = '/admin/collections';
|
|
18958
|
+
window.location.href = '/admin/collections/${collectionId}';
|
|
18086
18959
|
}, 1500);
|
|
18087
18960
|
</script>
|
|
18088
18961
|
</div>
|
|
18089
18962
|
`);
|
|
18090
18963
|
} else {
|
|
18091
|
-
return c.redirect(
|
|
18964
|
+
return c.redirect(`/admin/collections/${collectionId}`);
|
|
18092
18965
|
}
|
|
18093
18966
|
} catch (error) {
|
|
18094
18967
|
console.error("Error creating collection:", error);
|
|
@@ -18138,7 +19011,7 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18138
19011
|
field_options: fieldConfig,
|
|
18139
19012
|
field_order: fieldOrder++,
|
|
18140
19013
|
is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
|
|
18141
|
-
is_searchable: false
|
|
19014
|
+
is_searchable: fieldConfig.searchable === true || false
|
|
18142
19015
|
}));
|
|
18143
19016
|
}
|
|
18144
19017
|
} catch (e) {
|
|
@@ -18163,6 +19036,11 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18163
19036
|
is_searchable: row.is_searchable === 1
|
|
18164
19037
|
}));
|
|
18165
19038
|
}
|
|
19039
|
+
const [tinymceActive, quillActive, mdxeditorActive] = await Promise.all([
|
|
19040
|
+
isPluginActive2(db, "tinymce-plugin"),
|
|
19041
|
+
isPluginActive2(db, "quill-editor"),
|
|
19042
|
+
isPluginActive2(db, "mdxeditor-plugin")
|
|
19043
|
+
]);
|
|
18166
19044
|
const formData = {
|
|
18167
19045
|
id: collection.id,
|
|
18168
19046
|
name: collection.name,
|
|
@@ -18176,7 +19054,12 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18176
19054
|
email: user.email,
|
|
18177
19055
|
role: user.role
|
|
18178
19056
|
} : void 0,
|
|
18179
|
-
version: c.get("appVersion")
|
|
19057
|
+
version: c.get("appVersion"),
|
|
19058
|
+
editorPlugins: {
|
|
19059
|
+
tinymce: tinymceActive,
|
|
19060
|
+
quill: quillActive,
|
|
19061
|
+
mdxeditor: mdxeditorActive
|
|
19062
|
+
}
|
|
18180
19063
|
};
|
|
18181
19064
|
return c.html(renderCollectionFormPage(formData));
|
|
18182
19065
|
} catch (error) {
|
|
@@ -18316,21 +19199,103 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
|
|
|
18316
19199
|
adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
|
|
18317
19200
|
try {
|
|
18318
19201
|
const fieldId = c.req.param("fieldId");
|
|
19202
|
+
const collectionId = c.req.param("collectionId");
|
|
18319
19203
|
const formData = await c.req.formData();
|
|
18320
19204
|
const fieldLabel = formData.get("field_label");
|
|
18321
|
-
const
|
|
18322
|
-
const
|
|
19205
|
+
const fieldType = formData.get("field_type");
|
|
19206
|
+
const isRequiredValues = formData.getAll("is_required");
|
|
19207
|
+
const isSearchableValues = formData.getAll("is_searchable");
|
|
19208
|
+
const isRequired = isRequiredValues[isRequiredValues.length - 1] === "1";
|
|
19209
|
+
const isSearchable = isSearchableValues[isSearchableValues.length - 1] === "1";
|
|
18323
19210
|
const fieldOptions = formData.get("field_options") || "{}";
|
|
19211
|
+
console.log("[Field Update] Field ID:", fieldId);
|
|
19212
|
+
console.log("[Field Update] Form data received:", {
|
|
19213
|
+
field_label: fieldLabel,
|
|
19214
|
+
field_type: fieldType,
|
|
19215
|
+
is_required: formData.get("is_required"),
|
|
19216
|
+
is_searchable: formData.get("is_searchable"),
|
|
19217
|
+
field_options: fieldOptions
|
|
19218
|
+
});
|
|
18324
19219
|
if (!fieldLabel) {
|
|
18325
19220
|
return c.json({ success: false, error: "Field label is required." });
|
|
18326
19221
|
}
|
|
18327
19222
|
const db = c.env.DB;
|
|
19223
|
+
if (fieldId.startsWith("schema-")) {
|
|
19224
|
+
const fieldName = fieldId.replace("schema-", "");
|
|
19225
|
+
console.log("[Field Update] Updating schema field:", fieldName);
|
|
19226
|
+
const getCollectionStmt = db.prepare("SELECT * FROM collections WHERE id = ?");
|
|
19227
|
+
const collection = await getCollectionStmt.bind(collectionId).first();
|
|
19228
|
+
if (!collection) {
|
|
19229
|
+
return c.json({ success: false, error: "Collection not found." });
|
|
19230
|
+
}
|
|
19231
|
+
let schema = typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema;
|
|
19232
|
+
if (!schema) {
|
|
19233
|
+
schema = { type: "object", properties: {}, required: [] };
|
|
19234
|
+
}
|
|
19235
|
+
if (!schema.properties) {
|
|
19236
|
+
schema.properties = {};
|
|
19237
|
+
}
|
|
19238
|
+
if (!schema.required) {
|
|
19239
|
+
schema.required = [];
|
|
19240
|
+
}
|
|
19241
|
+
if (schema.properties[fieldName]) {
|
|
19242
|
+
const updatedFieldConfig = {
|
|
19243
|
+
...schema.properties[fieldName],
|
|
19244
|
+
type: fieldType,
|
|
19245
|
+
title: fieldLabel,
|
|
19246
|
+
searchable: isSearchable
|
|
19247
|
+
};
|
|
19248
|
+
if (isRequired) {
|
|
19249
|
+
updatedFieldConfig.required = true;
|
|
19250
|
+
} else {
|
|
19251
|
+
delete updatedFieldConfig.required;
|
|
19252
|
+
}
|
|
19253
|
+
schema.properties[fieldName] = updatedFieldConfig;
|
|
19254
|
+
const requiredIndex = schema.required.indexOf(fieldName);
|
|
19255
|
+
console.log("[Field Update] Required field handling:", {
|
|
19256
|
+
fieldName,
|
|
19257
|
+
isRequired,
|
|
19258
|
+
currentRequiredArray: schema.required,
|
|
19259
|
+
requiredIndex
|
|
19260
|
+
});
|
|
19261
|
+
if (isRequired && requiredIndex === -1) {
|
|
19262
|
+
schema.required.push(fieldName);
|
|
19263
|
+
console.log("[Field Update] Added field to required array");
|
|
19264
|
+
} else if (!isRequired && requiredIndex !== -1) {
|
|
19265
|
+
schema.required.splice(requiredIndex, 1);
|
|
19266
|
+
console.log("[Field Update] Removed field from required array");
|
|
19267
|
+
}
|
|
19268
|
+
console.log("[Field Update] Final required array:", schema.required);
|
|
19269
|
+
console.log("[Field Update] Final field config:", schema.properties[fieldName]);
|
|
19270
|
+
}
|
|
19271
|
+
const updateCollectionStmt = db.prepare(`
|
|
19272
|
+
UPDATE collections
|
|
19273
|
+
SET schema = ?, updated_at = ?
|
|
19274
|
+
WHERE id = ?
|
|
19275
|
+
`);
|
|
19276
|
+
const result2 = await updateCollectionStmt.bind(JSON.stringify(schema), Date.now(), collectionId).run();
|
|
19277
|
+
console.log("[Field Update] Schema update result:", {
|
|
19278
|
+
success: result2.success,
|
|
19279
|
+
changes: result2.meta?.changes
|
|
19280
|
+
});
|
|
19281
|
+
return c.json({ success: true });
|
|
19282
|
+
}
|
|
18328
19283
|
const updateStmt = db.prepare(`
|
|
18329
19284
|
UPDATE content_fields
|
|
18330
|
-
SET field_label = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
|
|
19285
|
+
SET field_label = ?, field_type = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
|
|
18331
19286
|
WHERE id = ?
|
|
18332
19287
|
`);
|
|
18333
|
-
await updateStmt.bind(fieldLabel, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
|
|
19288
|
+
const result = await updateStmt.bind(fieldLabel, fieldType, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
|
|
19289
|
+
console.log("[Field Update] Update result:", {
|
|
19290
|
+
success: result.success,
|
|
19291
|
+
meta: result.meta,
|
|
19292
|
+
changes: result.meta?.changes,
|
|
19293
|
+
last_row_id: result.meta?.last_row_id
|
|
19294
|
+
});
|
|
19295
|
+
const verifyStmt = db.prepare("SELECT * FROM content_fields WHERE id = ?");
|
|
19296
|
+
const verifyResult = await verifyStmt.bind(fieldId).first();
|
|
19297
|
+
console.log("[Field Update] Verification - field after update:", verifyResult);
|
|
19298
|
+
console.log("[Field Update] Successfully updated field with type:", fieldType);
|
|
18334
19299
|
return c.json({ success: true });
|
|
18335
19300
|
} catch (error) {
|
|
18336
19301
|
console.error("Error updating field:", error);
|
|
@@ -19591,7 +20556,7 @@ function renderMigrationSettings(settings) {
|
|
|
19591
20556
|
btn.innerHTML = 'Running...';
|
|
19592
20557
|
|
|
19593
20558
|
try {
|
|
19594
|
-
const response = await fetch('/admin/api/migrations/run', {
|
|
20559
|
+
const response = await fetch('/admin/settings/api/migrations/run', {
|
|
19595
20560
|
method: 'POST'
|
|
19596
20561
|
});
|
|
19597
20562
|
const result = await response.json();
|
|
@@ -19612,7 +20577,7 @@ function renderMigrationSettings(settings) {
|
|
|
19612
20577
|
|
|
19613
20578
|
window.validateSchema = async function() {
|
|
19614
20579
|
try {
|
|
19615
|
-
const response = await fetch('/admin/api/migrations/validate');
|
|
20580
|
+
const response = await fetch('/admin/settings/api/migrations/validate');
|
|
19616
20581
|
const result = await response.json();
|
|
19617
20582
|
|
|
19618
20583
|
if (result.success) {
|
|
@@ -20240,7 +21205,6 @@ var ROUTES_INFO = {
|
|
|
20240
21205
|
"adminLogsRoutes",
|
|
20241
21206
|
"adminDesignRoutes",
|
|
20242
21207
|
"adminCheckboxRoutes",
|
|
20243
|
-
"adminFAQRoutes",
|
|
20244
21208
|
"adminTestimonialsRoutes",
|
|
20245
21209
|
"adminCodeExamplesRoutes",
|
|
20246
21210
|
"adminDashboardRoutes",
|
|
@@ -20251,6 +21215,6 @@ var ROUTES_INFO = {
|
|
|
20251
21215
|
reference: "https://github.com/sonicjs/sonicjs"
|
|
20252
21216
|
};
|
|
20253
21217
|
|
|
20254
|
-
export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default,
|
|
20255
|
-
//# sourceMappingURL=chunk-
|
|
20256
|
-
//# sourceMappingURL=chunk-
|
|
21218
|
+
export { PluginBuilder, ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
|
|
21219
|
+
//# sourceMappingURL=chunk-CLLJFZ5U.js.map
|
|
21220
|
+
//# sourceMappingURL=chunk-CLLJFZ5U.js.map
|