@sonicjs-cms/core 2.0.10 → 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-LW33AOBF.js → chunk-5RKQB2JG.js} +6 -222
- 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-Z4H6DBVF.js → chunk-CLLJFZ5U.js} +1965 -1043
- package/dist/chunk-CLLJFZ5U.js.map +1 -0
- package/dist/{chunk-MXJJN4IA.js → chunk-DU7JJZN7.js} +5 -4
- package/dist/chunk-DU7JJZN7.js.map +1 -0
- package/dist/{chunk-YHG45LMU.js → chunk-FYWJMETG.js} +20 -4
- 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-Q7SL7U43.cjs → chunk-IM2LGCYD.cjs} +2114 -1192
- package/dist/chunk-IM2LGCYD.cjs.map +1 -0
- package/dist/{chunk-3PHG75W4.cjs → chunk-NNXPAPUD.cjs} +5 -4
- package/dist/chunk-NNXPAPUD.cjs.map +1 -0
- package/dist/{chunk-FTMKKKNH.js → chunk-QNWYQZ55.js} +3 -3
- package/dist/{chunk-FTMKKKNH.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-HXA5QSI3.cjs → chunk-X2VADBA4.cjs} +22 -6
- package/dist/chunk-X2VADBA4.cjs.map +1 -0
- package/dist/{chunk-MU3MR2QR.cjs → chunk-YU6QFFI4.cjs} +5 -222
- package/dist/chunk-YU6QFFI4.cjs.map +1 -0
- package/dist/{chunk-CAP6QQR2.cjs → chunk-ZMSYKV62.cjs} +5 -5
- package/dist/{chunk-CAP6QQR2.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 +385 -10
- 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-3PHG75W4.cjs.map +0 -1
- package/dist/chunk-COBUPOMD.js.map +0 -1
- package/dist/chunk-HXA5QSI3.cjs.map +0 -1
- package/dist/chunk-LW33AOBF.js.map +0 -1
- package/dist/chunk-MU3MR2QR.cjs.map +0 -1
- package/dist/chunk-MXJJN4IA.js.map +0 -1
- package/dist/chunk-NBDPIRQS.cjs.map +0 -1
- package/dist/chunk-Q7SL7U43.cjs.map +0 -1
- package/dist/chunk-YHG45LMU.js.map +0 -1
- package/dist/chunk-Z4H6DBVF.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();
|
|
@@ -4767,16 +5678,25 @@ async function getCollectionFields(db, collectionId) {
|
|
|
4767
5678
|
const schema = typeof collectionRow.schema === "string" ? JSON.parse(collectionRow.schema) : collectionRow.schema;
|
|
4768
5679
|
if (schema && schema.properties) {
|
|
4769
5680
|
let fieldOrder = 0;
|
|
4770
|
-
return Object.entries(schema.properties).map(([fieldName, fieldConfig]) =>
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
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
|
+
});
|
|
4780
5700
|
}
|
|
4781
5701
|
} catch (e) {
|
|
4782
5702
|
console.error("Error parsing collection schema:", e);
|
|
@@ -5024,11 +5944,44 @@ adminContentRoutes.get("/new", async (c) => {
|
|
|
5024
5944
|
}
|
|
5025
5945
|
const fields = await getCollectionFields(db, collectionId);
|
|
5026
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
|
+
});
|
|
5027
5974
|
const formData = {
|
|
5028
5975
|
collection,
|
|
5029
5976
|
fields,
|
|
5030
5977
|
isEdit: false,
|
|
5031
5978
|
workflowEnabled,
|
|
5979
|
+
tinymceEnabled,
|
|
5980
|
+
tinymceSettings,
|
|
5981
|
+
quillEnabled,
|
|
5982
|
+
quillSettings,
|
|
5983
|
+
mdxeditorEnabled,
|
|
5984
|
+
mdxeditorSettings,
|
|
5032
5985
|
user: user ? {
|
|
5033
5986
|
name: user.email,
|
|
5034
5987
|
email: user.email,
|
|
@@ -5096,6 +6049,27 @@ adminContentRoutes.get("/:id/edit", async (c) => {
|
|
|
5096
6049
|
const fields = await getCollectionFields(db, content.collection_id);
|
|
5097
6050
|
const contentData = content.data ? JSON.parse(content.data) : {};
|
|
5098
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
|
+
}
|
|
5099
6073
|
const formData = {
|
|
5100
6074
|
id: content.id,
|
|
5101
6075
|
title: content.title,
|
|
@@ -5111,6 +6085,12 @@ adminContentRoutes.get("/:id/edit", async (c) => {
|
|
|
5111
6085
|
fields,
|
|
5112
6086
|
isEdit: true,
|
|
5113
6087
|
workflowEnabled,
|
|
6088
|
+
tinymceEnabled,
|
|
6089
|
+
tinymceSettings,
|
|
6090
|
+
quillEnabled,
|
|
6091
|
+
quillSettings,
|
|
6092
|
+
mdxeditorEnabled,
|
|
6093
|
+
mdxeditorSettings,
|
|
5114
6094
|
referrerParams,
|
|
5115
6095
|
user: user ? {
|
|
5116
6096
|
name: user.email,
|
|
@@ -5161,41 +6141,31 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
5161
6141
|
const data = {};
|
|
5162
6142
|
const errors = {};
|
|
5163
6143
|
for (const field of fields) {
|
|
5164
|
-
const
|
|
5165
|
-
if (field.
|
|
5166
|
-
const options = field.field_options || {};
|
|
5167
|
-
if (options.autoGenerate) {
|
|
5168
|
-
data[field.field_name] = crypto.randomUUID();
|
|
5169
|
-
continue;
|
|
5170
|
-
}
|
|
5171
|
-
}
|
|
5172
|
-
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() === "")) {
|
|
5173
6146
|
errors[field.field_name] = [`${field.field_label} is required`];
|
|
5174
6147
|
continue;
|
|
5175
6148
|
}
|
|
5176
6149
|
switch (field.field_type) {
|
|
5177
6150
|
case "number":
|
|
5178
|
-
if (
|
|
6151
|
+
if (value && isNaN(Number(value))) {
|
|
5179
6152
|
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
5180
6153
|
} else {
|
|
5181
|
-
data[field.field_name] =
|
|
6154
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5182
6155
|
}
|
|
5183
6156
|
break;
|
|
5184
6157
|
case "boolean":
|
|
5185
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ?
|
|
6158
|
+
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
5186
6159
|
break;
|
|
5187
6160
|
case "select":
|
|
5188
6161
|
if (field.field_options?.multiple) {
|
|
5189
6162
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5190
6163
|
} else {
|
|
5191
|
-
data[field.field_name] =
|
|
6164
|
+
data[field.field_name] = value;
|
|
5192
6165
|
}
|
|
5193
6166
|
break;
|
|
5194
|
-
case "guid":
|
|
5195
|
-
data[field.field_name] = value2 || null;
|
|
5196
|
-
break;
|
|
5197
6167
|
default:
|
|
5198
|
-
data[field.field_name] =
|
|
6168
|
+
data[field.field_name] = value;
|
|
5199
6169
|
}
|
|
5200
6170
|
}
|
|
5201
6171
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5322,31 +6292,31 @@ adminContentRoutes.put("/:id", async (c) => {
|
|
|
5322
6292
|
const data = {};
|
|
5323
6293
|
const errors = {};
|
|
5324
6294
|
for (const field of fields) {
|
|
5325
|
-
const
|
|
5326
|
-
if (field.is_required && (!
|
|
6295
|
+
const value = formData.get(field.field_name);
|
|
6296
|
+
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
5327
6297
|
errors[field.field_name] = [`${field.field_label} is required`];
|
|
5328
6298
|
continue;
|
|
5329
6299
|
}
|
|
5330
6300
|
switch (field.field_type) {
|
|
5331
6301
|
case "number":
|
|
5332
|
-
if (
|
|
6302
|
+
if (value && isNaN(Number(value))) {
|
|
5333
6303
|
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
5334
6304
|
} else {
|
|
5335
|
-
data[field.field_name] =
|
|
6305
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5336
6306
|
}
|
|
5337
6307
|
break;
|
|
5338
6308
|
case "boolean":
|
|
5339
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ?
|
|
6309
|
+
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
5340
6310
|
break;
|
|
5341
6311
|
case "select":
|
|
5342
6312
|
if (field.field_options?.multiple) {
|
|
5343
6313
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5344
6314
|
} else {
|
|
5345
|
-
data[field.field_name] =
|
|
6315
|
+
data[field.field_name] = value;
|
|
5346
6316
|
}
|
|
5347
6317
|
break;
|
|
5348
6318
|
default:
|
|
5349
|
-
data[field.field_name] =
|
|
6319
|
+
data[field.field_name] = value;
|
|
5350
6320
|
}
|
|
5351
6321
|
}
|
|
5352
6322
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5463,23 +6433,23 @@ adminContentRoutes.post("/preview", async (c) => {
|
|
|
5463
6433
|
const fields = await getCollectionFields(db, collectionId);
|
|
5464
6434
|
const data = {};
|
|
5465
6435
|
for (const field of fields) {
|
|
5466
|
-
const
|
|
6436
|
+
const value = formData.get(field.field_name);
|
|
5467
6437
|
switch (field.field_type) {
|
|
5468
6438
|
case "number":
|
|
5469
|
-
data[field.field_name] =
|
|
6439
|
+
data[field.field_name] = value ? Number(value) : null;
|
|
5470
6440
|
break;
|
|
5471
6441
|
case "boolean":
|
|
5472
|
-
data[field.field_name] =
|
|
6442
|
+
data[field.field_name] = value === "true";
|
|
5473
6443
|
break;
|
|
5474
6444
|
case "select":
|
|
5475
6445
|
if (field.field_options?.multiple) {
|
|
5476
6446
|
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
5477
6447
|
} else {
|
|
5478
|
-
data[field.field_name] =
|
|
6448
|
+
data[field.field_name] = value;
|
|
5479
6449
|
}
|
|
5480
6450
|
break;
|
|
5481
6451
|
default:
|
|
5482
|
-
data[field.field_name] =
|
|
6452
|
+
data[field.field_name] = value;
|
|
5483
6453
|
}
|
|
5484
6454
|
}
|
|
5485
6455
|
const previewHTML = `
|
|
@@ -7335,10 +8305,10 @@ function renderUsersListPage(data) {
|
|
|
7335
8305
|
label: "",
|
|
7336
8306
|
className: "w-12",
|
|
7337
8307
|
sortable: false,
|
|
7338
|
-
render: (
|
|
8308
|
+
render: (value, row) => {
|
|
7339
8309
|
const initials = `${row.firstName.charAt(0)}${row.lastName.charAt(0)}`.toUpperCase();
|
|
7340
|
-
if (
|
|
7341
|
-
return `<img src="${
|
|
8310
|
+
if (value) {
|
|
8311
|
+
return `<img src="${value}" alt="${row.firstName} ${row.lastName}" class="w-8 h-8 rounded-full">`;
|
|
7342
8312
|
}
|
|
7343
8313
|
return `
|
|
7344
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">
|
|
@@ -7353,7 +8323,7 @@ function renderUsersListPage(data) {
|
|
|
7353
8323
|
sortable: true,
|
|
7354
8324
|
sortType: "string",
|
|
7355
8325
|
render: (_value, row) => {
|
|
7356
|
-
const
|
|
8326
|
+
const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7357
8327
|
"&": "&",
|
|
7358
8328
|
"<": "<",
|
|
7359
8329
|
">": ">",
|
|
@@ -7362,9 +8332,9 @@ function renderUsersListPage(data) {
|
|
|
7362
8332
|
})[char] || char);
|
|
7363
8333
|
const truncatedFirstName = row.firstName.length > 25 ? row.firstName.substring(0, 25) + "..." : row.firstName;
|
|
7364
8334
|
const truncatedLastName = row.lastName.length > 25 ? row.lastName.substring(0, 25) + "..." : row.lastName;
|
|
7365
|
-
const fullName =
|
|
8335
|
+
const fullName = escapeHtml6(`${truncatedFirstName} ${truncatedLastName}`);
|
|
7366
8336
|
const truncatedUsername = row.username.length > 100 ? row.username.substring(0, 100) + "..." : row.username;
|
|
7367
|
-
const username =
|
|
8337
|
+
const username = escapeHtml6(truncatedUsername);
|
|
7368
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>';
|
|
7369
8339
|
return `
|
|
7370
8340
|
<div>
|
|
@@ -7379,15 +8349,15 @@ function renderUsersListPage(data) {
|
|
|
7379
8349
|
label: "Email",
|
|
7380
8350
|
sortable: true,
|
|
7381
8351
|
sortType: "string",
|
|
7382
|
-
render: (
|
|
7383
|
-
const
|
|
8352
|
+
render: (value) => {
|
|
8353
|
+
const escapeHtml6 = (text) => text.replace(/[&<>"']/g, (char) => ({
|
|
7384
8354
|
"&": "&",
|
|
7385
8355
|
"<": "<",
|
|
7386
8356
|
">": ">",
|
|
7387
8357
|
'"': """,
|
|
7388
8358
|
"'": "'"
|
|
7389
8359
|
})[char] || char);
|
|
7390
|
-
const escapedEmail =
|
|
8360
|
+
const escapedEmail = escapeHtml6(value);
|
|
7391
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>`;
|
|
7392
8362
|
}
|
|
7393
8363
|
},
|
|
@@ -7396,7 +8366,7 @@ function renderUsersListPage(data) {
|
|
|
7396
8366
|
label: "Role",
|
|
7397
8367
|
sortable: true,
|
|
7398
8368
|
sortType: "string",
|
|
7399
|
-
render: (
|
|
8369
|
+
render: (value) => {
|
|
7400
8370
|
const roleColors = {
|
|
7401
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",
|
|
7402
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",
|
|
@@ -7412,7 +8382,7 @@ function renderUsersListPage(data) {
|
|
|
7412
8382
|
label: "Last Login",
|
|
7413
8383
|
sortable: true,
|
|
7414
8384
|
sortType: "date",
|
|
7415
|
-
render: (
|
|
8385
|
+
render: (value) => {
|
|
7416
8386
|
if (!value) return '<span class="text-zinc-500 dark:text-zinc-400">Never</span>';
|
|
7417
8387
|
return `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`;
|
|
7418
8388
|
}
|
|
@@ -7422,7 +8392,7 @@ function renderUsersListPage(data) {
|
|
|
7422
8392
|
label: "Created",
|
|
7423
8393
|
sortable: true,
|
|
7424
8394
|
sortType: "date",
|
|
7425
|
-
render: (
|
|
8395
|
+
render: (value) => `<span class="text-sm text-zinc-500 dark:text-zinc-400">${new Date(value).toLocaleDateString()}</span>`
|
|
7426
8396
|
},
|
|
7427
8397
|
{
|
|
7428
8398
|
key: "actions",
|
|
@@ -7920,7 +8890,7 @@ userRoutes.post("/profile/avatar", async (c) => {
|
|
|
7920
8890
|
try {
|
|
7921
8891
|
const formData = await c.req.formData();
|
|
7922
8892
|
const avatarFile = formData.get("avatar");
|
|
7923
|
-
if (!avatarFile ||
|
|
8893
|
+
if (!avatarFile || typeof avatarFile === "string" || !avatarFile.name) {
|
|
7924
8894
|
return c.html(renderAlert2({
|
|
7925
8895
|
type: "error",
|
|
7926
8896
|
message: "Please select an image file.",
|
|
@@ -8503,6 +9473,46 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
8503
9473
|
}));
|
|
8504
9474
|
}
|
|
8505
9475
|
});
|
|
9476
|
+
userRoutes.post("/users/:id/toggle", async (c) => {
|
|
9477
|
+
const db = c.env.DB;
|
|
9478
|
+
const user = c.get("user");
|
|
9479
|
+
const userId = c.req.param("id");
|
|
9480
|
+
try {
|
|
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);
|
|
9485
|
+
}
|
|
9486
|
+
const userStmt = db.prepare(`
|
|
9487
|
+
SELECT id, email FROM users WHERE id = ?
|
|
9488
|
+
`);
|
|
9489
|
+
const userToToggle = await userStmt.bind(userId).first();
|
|
9490
|
+
if (!userToToggle) {
|
|
9491
|
+
return c.json({ error: "User not found" }, 404);
|
|
9492
|
+
}
|
|
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
|
+
});
|
|
8506
9516
|
userRoutes.delete("/users/:id", async (c) => {
|
|
8507
9517
|
const db = c.env.DB;
|
|
8508
9518
|
const user = c.get("user");
|
|
@@ -11135,7 +12145,7 @@ function renderPluginsListPage(data) {
|
|
|
11135
12145
|
<!-- Stats -->
|
|
11136
12146
|
<div class="mb-6">
|
|
11137
12147
|
<h3 class="text-base font-semibold text-zinc-950 dark:text-white">Plugin Statistics</h3>
|
|
11138
|
-
<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">
|
|
11139
12149
|
<div class="px-4 py-5 sm:p-6">
|
|
11140
12150
|
<dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">Total Plugins</dt>
|
|
11141
12151
|
<dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
|
|
@@ -11196,10 +12206,25 @@ function renderPluginsListPage(data) {
|
|
|
11196
12206
|
</div>
|
|
11197
12207
|
</dd>
|
|
11198
12208
|
</div>
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
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>
|
|
12224
|
+
</dl>
|
|
12225
|
+
</div>
|
|
12226
|
+
|
|
12227
|
+
<!-- Filters -->
|
|
11203
12228
|
<div class="relative rounded-xl overflow-hidden mb-6">
|
|
11204
12229
|
<!-- Gradient Background -->
|
|
11205
12230
|
<div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10 dark:from-cyan-400/20 dark:via-blue-400/20 dark:to-purple-400/20"></div>
|
|
@@ -11211,13 +12236,16 @@ function renderPluginsListPage(data) {
|
|
|
11211
12236
|
<div>
|
|
11212
12237
|
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Category</label>
|
|
11213
12238
|
<div class="mt-2 grid grid-cols-1">
|
|
11214
|
-
<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">
|
|
11215
12240
|
<option value="">All Categories</option>
|
|
11216
12241
|
<option value="content">Content Management</option>
|
|
11217
12242
|
<option value="media">Media</option>
|
|
11218
12243
|
<option value="seo">SEO & Analytics</option>
|
|
11219
12244
|
<option value="security">Security</option>
|
|
11220
12245
|
<option value="utilities">Utilities</option>
|
|
12246
|
+
<option value="system">System</option>
|
|
12247
|
+
<option value="development">Development</option>
|
|
12248
|
+
<option value="demo">Demo</option>
|
|
11221
12249
|
</select>
|
|
11222
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">
|
|
11223
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" />
|
|
@@ -11227,10 +12255,11 @@ function renderPluginsListPage(data) {
|
|
|
11227
12255
|
<div>
|
|
11228
12256
|
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Status</label>
|
|
11229
12257
|
<div class="mt-2 grid grid-cols-1">
|
|
11230
|
-
<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">
|
|
11231
12259
|
<option value="">All Status</option>
|
|
11232
12260
|
<option value="active">Active</option>
|
|
11233
12261
|
<option value="inactive">Inactive</option>
|
|
12262
|
+
<option value="uninstalled">Available to Install</option>
|
|
11234
12263
|
<option value="error">Error</option>
|
|
11235
12264
|
</select>
|
|
11236
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">
|
|
@@ -11247,8 +12276,10 @@ function renderPluginsListPage(data) {
|
|
|
11247
12276
|
</svg>
|
|
11248
12277
|
</div>
|
|
11249
12278
|
<input
|
|
12279
|
+
id="search-input"
|
|
11250
12280
|
type="text"
|
|
11251
12281
|
placeholder="Search plugins..."
|
|
12282
|
+
oninput="filterPlugins()"
|
|
11252
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"
|
|
11253
12284
|
/>
|
|
11254
12285
|
</div>
|
|
@@ -11271,7 +12302,7 @@ function renderPluginsListPage(data) {
|
|
|
11271
12302
|
</div>
|
|
11272
12303
|
|
|
11273
12304
|
<!-- Plugins Grid -->
|
|
11274
|
-
<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">
|
|
11275
12306
|
${data.plugins.map((plugin) => renderPluginCard(plugin)).join("")}
|
|
11276
12307
|
</div>
|
|
11277
12308
|
|
|
@@ -11398,7 +12429,12 @@ function renderPluginsListPage(data) {
|
|
|
11398
12429
|
}
|
|
11399
12430
|
|
|
11400
12431
|
function openPluginSettings(pluginId) {
|
|
11401
|
-
|
|
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
|
+
}
|
|
11402
12438
|
}
|
|
11403
12439
|
|
|
11404
12440
|
function showPluginDetails(pluginId) {
|
|
@@ -11422,12 +12458,78 @@ function renderPluginsListPage(data) {
|
|
|
11422
12458
|
const dropdown = document.getElementById('plugin-dropdown');
|
|
11423
12459
|
dropdown.classList.toggle('hidden');
|
|
11424
12460
|
}
|
|
11425
|
-
|
|
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
|
+
|
|
11426
12528
|
// Close dropdown when clicking outside
|
|
11427
12529
|
document.addEventListener('click', (event) => {
|
|
11428
12530
|
const dropdown = document.getElementById('plugin-dropdown');
|
|
11429
12531
|
const button = event.target.closest('button[onclick="toggleDropdown()"]');
|
|
11430
|
-
|
|
12532
|
+
|
|
11431
12533
|
if (!button && !dropdown.contains(event.target)) {
|
|
11432
12534
|
dropdown.classList.add('hidden');
|
|
11433
12535
|
}
|
|
@@ -11462,23 +12564,33 @@ function renderPluginCard(plugin) {
|
|
|
11462
12564
|
const statusColors = {
|
|
11463
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",
|
|
11464
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",
|
|
11465
|
-
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"
|
|
11466
12569
|
};
|
|
11467
12570
|
const statusIcons = {
|
|
11468
12571
|
active: '<div class="w-2 h-2 bg-lime-500 dark:bg-lime-400 rounded-full mr-2"></div>',
|
|
11469
12572
|
inactive: '<div class="w-2 h-2 bg-zinc-500 dark:bg-zinc-400 rounded-full mr-2"></div>',
|
|
11470
|
-
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>'
|
|
11471
12575
|
};
|
|
11472
12576
|
const borderColors = {
|
|
11473
12577
|
active: "ring-[3px] ring-lime-500 dark:ring-lime-400",
|
|
11474
12578
|
inactive: "ring-[3px] ring-pink-500 dark:ring-pink-400",
|
|
11475
|
-
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"
|
|
11476
12581
|
};
|
|
11477
12582
|
const criticalCorePlugins = ["core-auth", "core-media"];
|
|
11478
12583
|
const canToggle = !criticalCorePlugins.includes(plugin.id);
|
|
11479
|
-
|
|
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
|
+
}
|
|
11480
12592
|
return `
|
|
11481
|
-
<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}">
|
|
11482
12594
|
<div class="flex items-start justify-between mb-4">
|
|
11483
12595
|
<div class="flex items-center gap-3">
|
|
11484
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">
|
|
@@ -11539,20 +12651,24 @@ function renderPluginCard(plugin) {
|
|
|
11539
12651
|
|
|
11540
12652
|
<div class="flex items-center justify-between">
|
|
11541
12653
|
<div class="flex gap-2">
|
|
11542
|
-
${canToggle ? actionButton : ""}
|
|
12654
|
+
${plugin.status === "uninstalled" ? actionButton : canToggle ? actionButton : ""}
|
|
12655
|
+
${plugin.status !== "uninstalled" ? `
|
|
11543
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">
|
|
11544
12657
|
Settings
|
|
11545
12658
|
</button>
|
|
12659
|
+
` : ""}
|
|
11546
12660
|
</div>
|
|
11547
12661
|
|
|
11548
12662
|
<div class="flex items-center gap-2">
|
|
12663
|
+
${plugin.status !== "uninstalled" ? `
|
|
11549
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">
|
|
11550
12665
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
11551
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"/>
|
|
11552
12667
|
</svg>
|
|
11553
12668
|
</button>
|
|
12669
|
+
` : ""}
|
|
11554
12670
|
|
|
11555
|
-
${!plugin.isCore ? `
|
|
12671
|
+
${!plugin.isCore && plugin.status !== "uninstalled" ? `
|
|
11556
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">
|
|
11557
12673
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
11558
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"/>
|
|
@@ -11855,28 +12971,22 @@ function renderPluginSettingsPage(data) {
|
|
|
11855
12971
|
const { plugin, activity = [], user } = data;
|
|
11856
12972
|
const pageContent = `
|
|
11857
12973
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-6">
|
|
11858
|
-
<!-- Header with
|
|
11859
|
-
<div class="flex items-center mb-6">
|
|
11860
|
-
<
|
|
11861
|
-
<
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
</li>
|
|
11875
|
-
<li>
|
|
11876
|
-
<span class="text-gray-300">${plugin.displayName}</span>
|
|
11877
|
-
</li>
|
|
11878
|
-
</ol>
|
|
11879
|
-
</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>
|
|
11880
12990
|
</div>
|
|
11881
12991
|
|
|
11882
12992
|
<!-- Plugin Header -->
|
|
@@ -11887,9 +12997,8 @@ function renderPluginSettingsPage(data) {
|
|
|
11887
12997
|
${plugin.icon || plugin.displayName.charAt(0).toUpperCase()}
|
|
11888
12998
|
</div>
|
|
11889
12999
|
<div>
|
|
11890
|
-
<
|
|
11891
|
-
<
|
|
11892
|
-
<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">
|
|
11893
13002
|
<span>v${plugin.version}</span>
|
|
11894
13003
|
<span>by ${plugin.author}</span>
|
|
11895
13004
|
<span>${plugin.category}</span>
|
|
@@ -11898,7 +13007,7 @@ function renderPluginSettingsPage(data) {
|
|
|
11898
13007
|
</div>
|
|
11899
13008
|
</div>
|
|
11900
13009
|
</div>
|
|
11901
|
-
|
|
13010
|
+
|
|
11902
13011
|
<div class="flex items-center gap-3">
|
|
11903
13012
|
${renderStatusBadge(plugin.status)}
|
|
11904
13013
|
${renderToggleButton(plugin)}
|
|
@@ -12185,10 +13294,10 @@ function renderSettingsTab(plugin) {
|
|
|
12185
13294
|
`;
|
|
12186
13295
|
}
|
|
12187
13296
|
function renderSettingsFields(settings) {
|
|
12188
|
-
return Object.entries(settings).map(([key,
|
|
13297
|
+
return Object.entries(settings).map(([key, value]) => {
|
|
12189
13298
|
const fieldId = `setting_${key}`;
|
|
12190
13299
|
const displayName = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
|
12191
|
-
if (typeof
|
|
13300
|
+
if (typeof value === "boolean") {
|
|
12192
13301
|
return `
|
|
12193
13302
|
<div class="flex items-center justify-between">
|
|
12194
13303
|
<div>
|
|
@@ -12196,12 +13305,12 @@ function renderSettingsFields(settings) {
|
|
|
12196
13305
|
<p class="text-xs text-gray-400">Enable or disable this feature</p>
|
|
12197
13306
|
</div>
|
|
12198
13307
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
12199
|
-
<input type="checkbox" name="${fieldId}" id="${fieldId}" ${
|
|
13308
|
+
<input type="checkbox" name="${fieldId}" id="${fieldId}" ${value ? "checked" : ""} class="sr-only peer">
|
|
12200
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>
|
|
12201
13310
|
</label>
|
|
12202
13311
|
</div>
|
|
12203
13312
|
`;
|
|
12204
|
-
} else if (typeof
|
|
13313
|
+
} else if (typeof value === "number") {
|
|
12205
13314
|
return `
|
|
12206
13315
|
<div>
|
|
12207
13316
|
<label for="${fieldId}" class="block text-sm font-medium text-gray-300 mb-2">${displayName}</label>
|
|
@@ -12209,7 +13318,7 @@ function renderSettingsFields(settings) {
|
|
|
12209
13318
|
type="number"
|
|
12210
13319
|
name="${fieldId}"
|
|
12211
13320
|
id="${fieldId}"
|
|
12212
|
-
value="${
|
|
13321
|
+
value="${value}"
|
|
12213
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"
|
|
12214
13323
|
>
|
|
12215
13324
|
</div>
|
|
@@ -12222,7 +13331,7 @@ function renderSettingsFields(settings) {
|
|
|
12222
13331
|
type="text"
|
|
12223
13332
|
name="${fieldId}"
|
|
12224
13333
|
id="${fieldId}"
|
|
12225
|
-
value="${
|
|
13334
|
+
value="${value}"
|
|
12226
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"
|
|
12227
13336
|
>
|
|
12228
13337
|
</div>
|
|
@@ -12370,6 +13479,86 @@ function formatTimestamp(timestamp) {
|
|
|
12370
13479
|
// src/routes/admin-plugins.ts
|
|
12371
13480
|
var adminPluginRoutes = new Hono();
|
|
12372
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
|
+
];
|
|
12373
13562
|
adminPluginRoutes.get("/", async (c) => {
|
|
12374
13563
|
try {
|
|
12375
13564
|
const user = c.get("user");
|
|
@@ -12378,15 +13567,17 @@ adminPluginRoutes.get("/", async (c) => {
|
|
|
12378
13567
|
return c.text("Access denied", 403);
|
|
12379
13568
|
}
|
|
12380
13569
|
const pluginService = new PluginService(db);
|
|
12381
|
-
let
|
|
12382
|
-
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 };
|
|
12383
13572
|
try {
|
|
12384
|
-
|
|
13573
|
+
installedPlugins = await pluginService.getAllPlugins();
|
|
12385
13574
|
stats = await pluginService.getPluginStats();
|
|
12386
13575
|
} catch (error) {
|
|
12387
13576
|
console.error("Error loading plugins:", error);
|
|
12388
13577
|
}
|
|
12389
|
-
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) => ({
|
|
12390
13581
|
id: p.id,
|
|
12391
13582
|
name: p.name,
|
|
12392
13583
|
displayName: p.display_name,
|
|
@@ -12403,8 +13594,28 @@ adminPluginRoutes.get("/", async (c) => {
|
|
|
12403
13594
|
permissions: p.permissions,
|
|
12404
13595
|
isCore: p.is_core
|
|
12405
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;
|
|
12406
13617
|
const pageData = {
|
|
12407
|
-
plugins:
|
|
13618
|
+
plugins: allPlugins,
|
|
12408
13619
|
stats,
|
|
12409
13620
|
user: {
|
|
12410
13621
|
name: user?.email || "User",
|
|
@@ -12541,7 +13752,7 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12541
13752
|
id: "demo-login-prefill",
|
|
12542
13753
|
name: "demo-login-plugin",
|
|
12543
13754
|
display_name: "Demo Login Prefill",
|
|
12544
|
-
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",
|
|
12545
13756
|
version: "1.0.0-beta.1",
|
|
12546
13757
|
author: "SonicJS",
|
|
12547
13758
|
category: "demo",
|
|
@@ -12551,7 +13762,7 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12551
13762
|
settings: {
|
|
12552
13763
|
enableNotice: true,
|
|
12553
13764
|
demoEmail: "admin@sonicjs.com",
|
|
12554
|
-
demoPassword: "
|
|
13765
|
+
demoPassword: "sonicjs!"
|
|
12555
13766
|
}
|
|
12556
13767
|
});
|
|
12557
13768
|
return c.json({ success: true, plugin: demoPlugin });
|
|
@@ -12650,6 +13861,50 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
12650
13861
|
});
|
|
12651
13862
|
return c.json({ success: true, plugin: seedDataPlugin });
|
|
12652
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
|
+
}
|
|
12653
13908
|
return c.json({ error: "Plugin not found in registry" }, 404);
|
|
12654
13909
|
} catch (error) {
|
|
12655
13910
|
console.error("Error installing plugin:", error);
|
|
@@ -13725,751 +14980,146 @@ adminLogsRoutes.get("/export", async (c) => {
|
|
|
13725
14980
|
return c.json({ error: "Failed to export logs" }, 500);
|
|
13726
14981
|
}
|
|
13727
14982
|
});
|
|
13728
|
-
adminLogsRoutes.post("/cleanup", async (c) => {
|
|
13729
|
-
try {
|
|
13730
|
-
const user = c.get("user");
|
|
13731
|
-
if (!user || user.role !== "admin") {
|
|
13732
|
-
return c.json({
|
|
13733
|
-
success: false,
|
|
13734
|
-
error: "Unauthorized. Admin access required."
|
|
13735
|
-
}, 403);
|
|
13736
|
-
}
|
|
13737
|
-
const logger = getLogger(c.env.DB);
|
|
13738
|
-
await logger.cleanupByRetention();
|
|
13739
|
-
return c.html(html`
|
|
13740
|
-
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
|
13741
|
-
Log cleanup completed successfully!
|
|
13742
|
-
</div>
|
|
13743
|
-
`);
|
|
13744
|
-
} catch (error) {
|
|
13745
|
-
console.error("Error cleaning up logs:", error);
|
|
13746
|
-
return c.html(html`
|
|
13747
|
-
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
13748
|
-
Failed to clean up logs. Please try again.
|
|
13749
|
-
</div>
|
|
13750
|
-
`);
|
|
13751
|
-
}
|
|
13752
|
-
});
|
|
13753
|
-
adminLogsRoutes.post("/search", async (c) => {
|
|
13754
|
-
try {
|
|
13755
|
-
const formData = await c.req.formData();
|
|
13756
|
-
const search = formData.get("search");
|
|
13757
|
-
const level = formData.get("level");
|
|
13758
|
-
const category = formData.get("category");
|
|
13759
|
-
const logger = getLogger(c.env.DB);
|
|
13760
|
-
const filter = {
|
|
13761
|
-
limit: 20,
|
|
13762
|
-
offset: 0,
|
|
13763
|
-
sortBy: "created_at",
|
|
13764
|
-
sortOrder: "desc"
|
|
13765
|
-
};
|
|
13766
|
-
if (search) filter.search = search;
|
|
13767
|
-
if (level) filter.level = [level];
|
|
13768
|
-
if (category) filter.category = [category];
|
|
13769
|
-
const { logs } = await logger.getLogs(filter);
|
|
13770
|
-
const rows = logs.map((log) => {
|
|
13771
|
-
const formattedLog = {
|
|
13772
|
-
...log,
|
|
13773
|
-
formattedDate: new Date(log.createdAt).toLocaleString(),
|
|
13774
|
-
levelClass: getLevelClass(log.level),
|
|
13775
|
-
categoryClass: getCategoryClass(log.category)
|
|
13776
|
-
};
|
|
13777
|
-
return `
|
|
13778
|
-
<tr class="hover:bg-gray-50">
|
|
13779
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
13780
|
-
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.levelClass}">
|
|
13781
|
-
${formattedLog.level}
|
|
13782
|
-
</span>
|
|
13783
|
-
</td>
|
|
13784
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
13785
|
-
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${formattedLog.categoryClass}">
|
|
13786
|
-
${formattedLog.category}
|
|
13787
|
-
</span>
|
|
13788
|
-
</td>
|
|
13789
|
-
<td class="px-6 py-4">
|
|
13790
|
-
<div class="text-sm text-gray-900 max-w-md truncate">${formattedLog.message}</div>
|
|
13791
|
-
</td>
|
|
13792
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.source || "-"}</td>
|
|
13793
|
-
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedLog.formattedDate}</td>
|
|
13794
|
-
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
13795
|
-
<a href="/admin/logs/${formattedLog.id}" class="text-indigo-600 hover:text-indigo-900">View</a>
|
|
13796
|
-
</td>
|
|
13797
|
-
</tr>
|
|
13798
|
-
`;
|
|
13799
|
-
}).join("");
|
|
13800
|
-
return c.html(rows);
|
|
13801
|
-
} catch (error) {
|
|
13802
|
-
console.error("Error searching logs:", error);
|
|
13803
|
-
return c.html(html`<tr><td colspan="6" class="px-6 py-4 text-center text-red-500">Error searching logs</td></tr>`);
|
|
13804
|
-
}
|
|
13805
|
-
});
|
|
13806
|
-
function getLevelClass(level) {
|
|
13807
|
-
switch (level) {
|
|
13808
|
-
case "debug":
|
|
13809
|
-
return "bg-gray-100 text-gray-800";
|
|
13810
|
-
case "info":
|
|
13811
|
-
return "bg-blue-100 text-blue-800";
|
|
13812
|
-
case "warn":
|
|
13813
|
-
return "bg-yellow-100 text-yellow-800";
|
|
13814
|
-
case "error":
|
|
13815
|
-
return "bg-red-100 text-red-800";
|
|
13816
|
-
case "fatal":
|
|
13817
|
-
return "bg-purple-100 text-purple-800";
|
|
13818
|
-
default:
|
|
13819
|
-
return "bg-gray-100 text-gray-800";
|
|
13820
|
-
}
|
|
13821
|
-
}
|
|
13822
|
-
function getCategoryClass(category) {
|
|
13823
|
-
switch (category) {
|
|
13824
|
-
case "auth":
|
|
13825
|
-
return "bg-green-100 text-green-800";
|
|
13826
|
-
case "api":
|
|
13827
|
-
return "bg-blue-100 text-blue-800";
|
|
13828
|
-
case "workflow":
|
|
13829
|
-
return "bg-purple-100 text-purple-800";
|
|
13830
|
-
case "plugin":
|
|
13831
|
-
return "bg-indigo-100 text-indigo-800";
|
|
13832
|
-
case "media":
|
|
13833
|
-
return "bg-pink-100 text-pink-800";
|
|
13834
|
-
case "system":
|
|
13835
|
-
return "bg-gray-100 text-gray-800";
|
|
13836
|
-
case "security":
|
|
13837
|
-
return "bg-red-100 text-red-800";
|
|
13838
|
-
case "error":
|
|
13839
|
-
return "bg-red-100 text-red-800";
|
|
13840
|
-
default:
|
|
13841
|
-
return "bg-gray-100 text-gray-800";
|
|
13842
|
-
}
|
|
13843
|
-
}
|
|
13844
|
-
var adminDesignRoutes = new Hono();
|
|
13845
|
-
adminDesignRoutes.get("/", (c) => {
|
|
13846
|
-
const user = c.get("user");
|
|
13847
|
-
const pageData = {
|
|
13848
|
-
user: user ? {
|
|
13849
|
-
name: user.email,
|
|
13850
|
-
email: user.email,
|
|
13851
|
-
role: user.role
|
|
13852
|
-
} : void 0
|
|
13853
|
-
};
|
|
13854
|
-
return c.html(renderDesignPage(pageData));
|
|
13855
|
-
});
|
|
13856
|
-
var adminCheckboxRoutes = new Hono();
|
|
13857
|
-
adminCheckboxRoutes.get("/", (c) => {
|
|
13858
|
-
const user = c.get("user");
|
|
13859
|
-
const pageData = {
|
|
13860
|
-
user: user ? {
|
|
13861
|
-
name: user.email,
|
|
13862
|
-
email: user.email,
|
|
13863
|
-
role: user.role
|
|
13864
|
-
} : void 0
|
|
13865
|
-
};
|
|
13866
|
-
return c.html(renderCheckboxPage(pageData));
|
|
13867
|
-
});
|
|
13868
|
-
|
|
13869
|
-
// src/templates/pages/admin-faq-form.template.ts
|
|
13870
|
-
function renderFAQForm(data) {
|
|
13871
|
-
const { faq, isEdit, errors, message, messageType } = data;
|
|
13872
|
-
const pageTitle = isEdit ? "Edit FAQ" : "New FAQ";
|
|
13873
|
-
const pageContent = `
|
|
13874
|
-
<div class="w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
|
13875
|
-
<!-- Header -->
|
|
13876
|
-
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
13877
|
-
<div>
|
|
13878
|
-
<h1 class="text-2xl font-semibold text-white">${pageTitle}</h1>
|
|
13879
|
-
<p class="mt-2 text-sm text-gray-300">
|
|
13880
|
-
${isEdit ? "Update the FAQ details below" : "Create a new frequently asked question"}
|
|
13881
|
-
</p>
|
|
13882
|
-
</div>
|
|
13883
|
-
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
13884
|
-
<a href="/admin/faq"
|
|
13885
|
-
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">
|
|
13886
|
-
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
13887
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
13888
|
-
</svg>
|
|
13889
|
-
Back to List
|
|
13890
|
-
</a>
|
|
13891
|
-
</div>
|
|
13892
|
-
</div>
|
|
13893
|
-
|
|
13894
|
-
${message ? renderAlert({ type: messageType || "info", message, dismissible: true }) : ""}
|
|
13895
|
-
|
|
13896
|
-
<!-- Form -->
|
|
13897
|
-
<div class="backdrop-blur-xl bg-white/10 rounded-xl border border-white/20 shadow-2xl">
|
|
13898
|
-
<form ${isEdit ? `hx-put="/admin/faq/${faq?.id}"` : 'hx-post="/admin/faq"'}
|
|
13899
|
-
hx-target="body"
|
|
13900
|
-
hx-swap="outerHTML"
|
|
13901
|
-
class="space-y-6 p-6">
|
|
13902
|
-
|
|
13903
|
-
<!-- Question -->
|
|
13904
|
-
<div>
|
|
13905
|
-
<label for="question" class="block text-sm font-medium text-white">
|
|
13906
|
-
Question <span class="text-red-400">*</span>
|
|
13907
|
-
</label>
|
|
13908
|
-
<div class="mt-1">
|
|
13909
|
-
<textarea name="question"
|
|
13910
|
-
id="question"
|
|
13911
|
-
rows="3"
|
|
13912
|
-
required
|
|
13913
|
-
maxlength="500"
|
|
13914
|
-
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"
|
|
13915
|
-
placeholder="Enter the frequently asked question...">${faq?.question || ""}</textarea>
|
|
13916
|
-
<p class="mt-1 text-sm text-gray-300">
|
|
13917
|
-
<span id="question-count">0</span>/500 characters
|
|
13918
|
-
</p>
|
|
13919
|
-
</div>
|
|
13920
|
-
${errors?.question ? `
|
|
13921
|
-
<div class="mt-1">
|
|
13922
|
-
${errors.question.map((error) => `
|
|
13923
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13924
|
-
`).join("")}
|
|
13925
|
-
</div>
|
|
13926
|
-
` : ""}
|
|
13927
|
-
</div>
|
|
13928
|
-
|
|
13929
|
-
<!-- Answer -->
|
|
13930
|
-
<div>
|
|
13931
|
-
<label for="answer" class="block text-sm font-medium text-white">
|
|
13932
|
-
Answer <span class="text-red-400">*</span>
|
|
13933
|
-
</label>
|
|
13934
|
-
<div class="mt-1">
|
|
13935
|
-
<textarea name="answer"
|
|
13936
|
-
id="answer"
|
|
13937
|
-
rows="6"
|
|
13938
|
-
required
|
|
13939
|
-
maxlength="2000"
|
|
13940
|
-
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"
|
|
13941
|
-
placeholder="Enter the detailed answer...">${faq?.answer || ""}</textarea>
|
|
13942
|
-
<p class="mt-1 text-sm text-gray-300">
|
|
13943
|
-
<span id="answer-count">0</span>/2000 characters. You can use basic HTML for formatting.
|
|
13944
|
-
</p>
|
|
13945
|
-
</div>
|
|
13946
|
-
${errors?.answer ? `
|
|
13947
|
-
<div class="mt-1">
|
|
13948
|
-
${errors.answer.map((error) => `
|
|
13949
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13950
|
-
`).join("")}
|
|
13951
|
-
</div>
|
|
13952
|
-
` : ""}
|
|
13953
|
-
</div>
|
|
13954
|
-
|
|
13955
|
-
<!-- Category and Tags Row -->
|
|
13956
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
13957
|
-
<!-- Category -->
|
|
13958
|
-
<div>
|
|
13959
|
-
<label for="category" class="block text-sm font-medium text-white">Category</label>
|
|
13960
|
-
<div class="mt-1">
|
|
13961
|
-
<select name="category"
|
|
13962
|
-
id="category"
|
|
13963
|
-
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">
|
|
13964
|
-
<option value="">Select a category</option>
|
|
13965
|
-
<option value="general" ${faq?.category === "general" ? "selected" : ""}>General</option>
|
|
13966
|
-
<option value="technical" ${faq?.category === "technical" ? "selected" : ""}>Technical</option>
|
|
13967
|
-
<option value="billing" ${faq?.category === "billing" ? "selected" : ""}>Billing</option>
|
|
13968
|
-
<option value="support" ${faq?.category === "support" ? "selected" : ""}>Support</option>
|
|
13969
|
-
<option value="account" ${faq?.category === "account" ? "selected" : ""}>Account</option>
|
|
13970
|
-
<option value="features" ${faq?.category === "features" ? "selected" : ""}>Features</option>
|
|
13971
|
-
</select>
|
|
13972
|
-
</div>
|
|
13973
|
-
${errors?.category ? `
|
|
13974
|
-
<div class="mt-1">
|
|
13975
|
-
${errors.category.map((error) => `
|
|
13976
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13977
|
-
`).join("")}
|
|
13978
|
-
</div>
|
|
13979
|
-
` : ""}
|
|
13980
|
-
</div>
|
|
13981
|
-
|
|
13982
|
-
<!-- Tags -->
|
|
13983
|
-
<div>
|
|
13984
|
-
<label for="tags" class="block text-sm font-medium text-white">Tags</label>
|
|
13985
|
-
<div class="mt-1">
|
|
13986
|
-
<input type="text"
|
|
13987
|
-
name="tags"
|
|
13988
|
-
id="tags"
|
|
13989
|
-
value="${faq?.tags || ""}"
|
|
13990
|
-
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"
|
|
13991
|
-
placeholder="e.g., payment, setup, troubleshooting">
|
|
13992
|
-
<p class="mt-1 text-sm text-gray-300">Separate multiple tags with commas</p>
|
|
13993
|
-
</div>
|
|
13994
|
-
${errors?.tags ? `
|
|
13995
|
-
<div class="mt-1">
|
|
13996
|
-
${errors.tags.map((error) => `
|
|
13997
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
13998
|
-
`).join("")}
|
|
13999
|
-
</div>
|
|
14000
|
-
` : ""}
|
|
14001
|
-
</div>
|
|
14002
|
-
</div>
|
|
14003
|
-
|
|
14004
|
-
<!-- Status and Sort Order Row -->
|
|
14005
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
14006
|
-
<!-- Published Status -->
|
|
14007
|
-
<div>
|
|
14008
|
-
<label class="block text-sm font-medium text-white">Status</label>
|
|
14009
|
-
<div class="mt-2 space-y-2">
|
|
14010
|
-
<div class="flex items-center">
|
|
14011
|
-
<input id="published"
|
|
14012
|
-
name="isPublished"
|
|
14013
|
-
type="radio"
|
|
14014
|
-
value="true"
|
|
14015
|
-
${!faq || faq.isPublished ? "checked" : ""}
|
|
14016
|
-
class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
|
|
14017
|
-
<label for="published" class="ml-2 block text-sm text-white">
|
|
14018
|
-
Published <span class="text-gray-300">(visible to users)</span>
|
|
14019
|
-
</label>
|
|
14020
|
-
</div>
|
|
14021
|
-
<div class="flex items-center">
|
|
14022
|
-
<input id="draft"
|
|
14023
|
-
name="isPublished"
|
|
14024
|
-
type="radio"
|
|
14025
|
-
value="false"
|
|
14026
|
-
${faq && !faq.isPublished ? "checked" : ""}
|
|
14027
|
-
class="h-4 w-4 text-blue-600 focus:ring-blue-600 border-gray-600 bg-gray-700">
|
|
14028
|
-
<label for="draft" class="ml-2 block text-sm text-white">
|
|
14029
|
-
Draft <span class="text-gray-300">(not visible to users)</span>
|
|
14030
|
-
</label>
|
|
14031
|
-
</div>
|
|
14032
|
-
</div>
|
|
14033
|
-
</div>
|
|
14034
|
-
|
|
14035
|
-
<!-- Sort Order -->
|
|
14036
|
-
<div>
|
|
14037
|
-
<label for="sortOrder" class="block text-sm font-medium text-white">Sort Order</label>
|
|
14038
|
-
<div class="mt-1">
|
|
14039
|
-
<input type="number"
|
|
14040
|
-
name="sortOrder"
|
|
14041
|
-
id="sortOrder"
|
|
14042
|
-
value="${faq?.sortOrder || 0}"
|
|
14043
|
-
min="0"
|
|
14044
|
-
step="1"
|
|
14045
|
-
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">
|
|
14046
|
-
<p class="mt-1 text-sm text-gray-300">Lower numbers appear first (0 = highest priority)</p>
|
|
14047
|
-
</div>
|
|
14048
|
-
${errors?.sortOrder ? `
|
|
14049
|
-
<div class="mt-1">
|
|
14050
|
-
${errors.sortOrder.map((error) => `
|
|
14051
|
-
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14052
|
-
`).join("")}
|
|
14053
|
-
</div>
|
|
14054
|
-
` : ""}
|
|
14055
|
-
</div>
|
|
14056
|
-
</div>
|
|
14057
|
-
|
|
14058
|
-
<!-- Form Actions -->
|
|
14059
|
-
<div class="flex items-center justify-end space-x-3 pt-6 border-t border-white/20">
|
|
14060
|
-
<a href="/admin/faq"
|
|
14061
|
-
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">
|
|
14062
|
-
Cancel
|
|
14063
|
-
</a>
|
|
14064
|
-
<button type="submit"
|
|
14065
|
-
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">
|
|
14066
|
-
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
14067
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
14068
|
-
</svg>
|
|
14069
|
-
${isEdit ? "Update FAQ" : "Create FAQ"}
|
|
14070
|
-
</button>
|
|
14071
|
-
</div>
|
|
14072
|
-
</form>
|
|
14073
|
-
</div>
|
|
14074
|
-
</div>
|
|
14075
|
-
|
|
14076
|
-
<script>
|
|
14077
|
-
// Character count for question
|
|
14078
|
-
const questionTextarea = document.getElementById('question');
|
|
14079
|
-
const questionCount = document.getElementById('question-count');
|
|
14080
|
-
|
|
14081
|
-
function updateQuestionCount() {
|
|
14082
|
-
questionCount.textContent = questionTextarea.value.length;
|
|
14083
|
-
}
|
|
14084
|
-
|
|
14085
|
-
questionTextarea.addEventListener('input', updateQuestionCount);
|
|
14086
|
-
updateQuestionCount(); // Initial count
|
|
14087
|
-
|
|
14088
|
-
// Character count for answer
|
|
14089
|
-
const answerTextarea = document.getElementById('answer');
|
|
14090
|
-
const answerCount = document.getElementById('answer-count');
|
|
14091
|
-
|
|
14092
|
-
function updateAnswerCount() {
|
|
14093
|
-
answerCount.textContent = answerTextarea.value.length;
|
|
14094
|
-
}
|
|
14095
|
-
|
|
14096
|
-
answerTextarea.addEventListener('input', updateAnswerCount);
|
|
14097
|
-
updateAnswerCount(); // Initial count
|
|
14098
|
-
</script>
|
|
14099
|
-
`;
|
|
14100
|
-
const layoutData = {
|
|
14101
|
-
title: `${pageTitle} - Admin`,
|
|
14102
|
-
pageTitle,
|
|
14103
|
-
currentPath: isEdit ? `/admin/faq/${faq?.id}` : "/admin/faq/new",
|
|
14104
|
-
user: data.user,
|
|
14105
|
-
content: pageContent
|
|
14106
|
-
};
|
|
14107
|
-
return renderAdminLayout(layoutData);
|
|
14108
|
-
}
|
|
14109
|
-
function escapeHtml4(unsafe) {
|
|
14110
|
-
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14111
|
-
}
|
|
14112
|
-
|
|
14113
|
-
// src/routes/admin-faq.ts
|
|
14114
|
-
var faqSchema = z.object({
|
|
14115
|
-
question: z.string().min(1, "Question is required").max(500, "Question must be under 500 characters"),
|
|
14116
|
-
answer: z.string().min(1, "Answer is required").max(2e3, "Answer must be under 2000 characters"),
|
|
14117
|
-
category: z.string().optional(),
|
|
14118
|
-
tags: z.string().optional(),
|
|
14119
|
-
isPublished: z.string().transform((val) => val === "true"),
|
|
14120
|
-
sortOrder: z.string().transform((val) => parseInt(val, 10)).pipe(z.number().min(0))
|
|
14121
|
-
});
|
|
14122
|
-
var adminFAQRoutes = new Hono();
|
|
14123
|
-
adminFAQRoutes.get("/", async (c) => {
|
|
14124
|
-
try {
|
|
14125
|
-
const user = c.get("user");
|
|
14126
|
-
const { category, published, search, page = "1" } = c.req.query();
|
|
14127
|
-
const currentPage = parseInt(page, 10) || 1;
|
|
14128
|
-
const limit = 20;
|
|
14129
|
-
const offset = (currentPage - 1) * limit;
|
|
14130
|
-
const db = c.env?.DB;
|
|
14131
|
-
if (!db) {
|
|
14132
|
-
return c.html(renderFAQList({
|
|
14133
|
-
faqs: [],
|
|
14134
|
-
totalCount: 0,
|
|
14135
|
-
currentPage: 1,
|
|
14136
|
-
totalPages: 1,
|
|
14137
|
-
user: user ? {
|
|
14138
|
-
name: user.email,
|
|
14139
|
-
email: user.email,
|
|
14140
|
-
role: user.role
|
|
14141
|
-
} : void 0,
|
|
14142
|
-
message: "Database not available",
|
|
14143
|
-
messageType: "error"
|
|
14144
|
-
}));
|
|
14145
|
-
}
|
|
14146
|
-
let whereClause = "WHERE 1=1";
|
|
14147
|
-
const params = [];
|
|
14148
|
-
if (category) {
|
|
14149
|
-
whereClause += " AND category = ?";
|
|
14150
|
-
params.push(category);
|
|
14151
|
-
}
|
|
14152
|
-
if (published !== void 0) {
|
|
14153
|
-
whereClause += " AND isPublished = ?";
|
|
14154
|
-
params.push(published === "true" ? 1 : 0);
|
|
14155
|
-
}
|
|
14156
|
-
if (search) {
|
|
14157
|
-
whereClause += " AND (question LIKE ? OR answer LIKE ? OR tags LIKE ?)";
|
|
14158
|
-
const searchTerm = `%${search}%`;
|
|
14159
|
-
params.push(searchTerm, searchTerm, searchTerm);
|
|
14160
|
-
}
|
|
14161
|
-
const countQuery = `SELECT COUNT(*) as count FROM faqs ${whereClause}`;
|
|
14162
|
-
const { results: countResults } = await db.prepare(countQuery).bind(...params).all();
|
|
14163
|
-
const totalCount = countResults?.[0]?.count || 0;
|
|
14164
|
-
const dataQuery = `
|
|
14165
|
-
SELECT * FROM faqs
|
|
14166
|
-
${whereClause}
|
|
14167
|
-
ORDER BY sortOrder ASC, created_at DESC
|
|
14168
|
-
LIMIT ? OFFSET ?
|
|
14169
|
-
`;
|
|
14170
|
-
const { results: faqs } = await db.prepare(dataQuery).bind(...params, limit, offset).all();
|
|
14171
|
-
const totalPages = Math.ceil(totalCount / limit);
|
|
14172
|
-
return c.html(renderFAQList({
|
|
14173
|
-
faqs: faqs || [],
|
|
14174
|
-
totalCount,
|
|
14175
|
-
currentPage,
|
|
14176
|
-
totalPages,
|
|
14177
|
-
user: user ? {
|
|
14178
|
-
name: user.email,
|
|
14179
|
-
email: user.email,
|
|
14180
|
-
role: user.role
|
|
14181
|
-
} : void 0
|
|
14182
|
-
}));
|
|
14183
|
-
} catch (error) {
|
|
14184
|
-
console.error("Error fetching FAQs:", error);
|
|
14185
|
-
const user = c.get("user");
|
|
14186
|
-
return c.html(renderFAQList({
|
|
14187
|
-
faqs: [],
|
|
14188
|
-
totalCount: 0,
|
|
14189
|
-
currentPage: 1,
|
|
14190
|
-
totalPages: 1,
|
|
14191
|
-
user: user ? {
|
|
14192
|
-
name: user.email,
|
|
14193
|
-
email: user.email,
|
|
14194
|
-
role: user.role
|
|
14195
|
-
} : void 0,
|
|
14196
|
-
message: "Failed to load FAQs",
|
|
14197
|
-
messageType: "error"
|
|
14198
|
-
}));
|
|
14199
|
-
}
|
|
14200
|
-
});
|
|
14201
|
-
adminFAQRoutes.get("/new", async (c) => {
|
|
14202
|
-
const user = c.get("user");
|
|
14203
|
-
return c.html(renderFAQForm({
|
|
14204
|
-
isEdit: false,
|
|
14205
|
-
user: user ? {
|
|
14206
|
-
name: user.email,
|
|
14207
|
-
email: user.email,
|
|
14208
|
-
role: user.role
|
|
14209
|
-
} : void 0
|
|
14210
|
-
}));
|
|
14211
|
-
});
|
|
14212
|
-
adminFAQRoutes.post("/", async (c) => {
|
|
14213
|
-
try {
|
|
14214
|
-
const formData = await c.req.formData();
|
|
14215
|
-
const data = Object.fromEntries(formData.entries());
|
|
14216
|
-
const validatedData = faqSchema.parse(data);
|
|
14217
|
-
const user = c.get("user");
|
|
14218
|
-
const db = c.env?.DB;
|
|
14219
|
-
if (!db) {
|
|
14220
|
-
return c.html(renderFAQForm({
|
|
14221
|
-
isEdit: false,
|
|
14222
|
-
user: user ? {
|
|
14223
|
-
name: user.email,
|
|
14224
|
-
email: user.email,
|
|
14225
|
-
role: user.role
|
|
14226
|
-
} : void 0,
|
|
14227
|
-
message: "Database not available",
|
|
14228
|
-
messageType: "error"
|
|
14229
|
-
}));
|
|
14230
|
-
}
|
|
14231
|
-
const { results } = await db.prepare(`
|
|
14232
|
-
INSERT INTO faqs (question, answer, category, tags, isPublished, sortOrder)
|
|
14233
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
14234
|
-
RETURNING *
|
|
14235
|
-
`).bind(
|
|
14236
|
-
validatedData.question,
|
|
14237
|
-
validatedData.answer,
|
|
14238
|
-
validatedData.category || null,
|
|
14239
|
-
validatedData.tags || null,
|
|
14240
|
-
validatedData.isPublished ? 1 : 0,
|
|
14241
|
-
validatedData.sortOrder
|
|
14242
|
-
).all();
|
|
14243
|
-
if (results && results.length > 0) {
|
|
14244
|
-
return c.redirect("/admin/faq?message=FAQ created successfully");
|
|
14245
|
-
} else {
|
|
14246
|
-
return c.html(renderFAQForm({
|
|
14247
|
-
isEdit: false,
|
|
14248
|
-
user: user ? {
|
|
14249
|
-
name: user.email,
|
|
14250
|
-
email: user.email,
|
|
14251
|
-
role: user.role
|
|
14252
|
-
} : void 0,
|
|
14253
|
-
message: "Failed to create FAQ",
|
|
14254
|
-
messageType: "error"
|
|
14255
|
-
}));
|
|
14256
|
-
}
|
|
14257
|
-
} catch (error) {
|
|
14258
|
-
console.error("Error creating FAQ:", error);
|
|
14259
|
-
const user = c.get("user");
|
|
14260
|
-
if (error instanceof z.ZodError) {
|
|
14261
|
-
const errors = {};
|
|
14262
|
-
error.errors.forEach((err) => {
|
|
14263
|
-
const field = err.path[0];
|
|
14264
|
-
if (!errors[field]) errors[field] = [];
|
|
14265
|
-
errors[field].push(err.message);
|
|
14266
|
-
});
|
|
14267
|
-
return c.html(renderFAQForm({
|
|
14268
|
-
isEdit: false,
|
|
14269
|
-
user: user ? {
|
|
14270
|
-
name: user.email,
|
|
14271
|
-
email: user.email,
|
|
14272
|
-
role: user.role
|
|
14273
|
-
} : void 0,
|
|
14274
|
-
errors,
|
|
14275
|
-
message: "Please correct the errors below",
|
|
14276
|
-
messageType: "error"
|
|
14277
|
-
}));
|
|
14278
|
-
}
|
|
14279
|
-
return c.html(renderFAQForm({
|
|
14280
|
-
isEdit: false,
|
|
14281
|
-
user: user ? {
|
|
14282
|
-
name: user.email,
|
|
14283
|
-
email: user.email,
|
|
14284
|
-
role: user.role
|
|
14285
|
-
} : void 0,
|
|
14286
|
-
message: "Failed to create FAQ",
|
|
14287
|
-
messageType: "error"
|
|
14288
|
-
}));
|
|
14289
|
-
}
|
|
14290
|
-
});
|
|
14291
|
-
adminFAQRoutes.get("/:id", async (c) => {
|
|
14292
|
-
try {
|
|
14293
|
-
const id = parseInt(c.req.param("id"));
|
|
14294
|
-
const user = c.get("user");
|
|
14295
|
-
const db = c.env?.DB;
|
|
14296
|
-
if (!db) {
|
|
14297
|
-
return c.html(renderFAQForm({
|
|
14298
|
-
isEdit: true,
|
|
14299
|
-
user: user ? {
|
|
14300
|
-
name: user.email,
|
|
14301
|
-
email: user.email,
|
|
14302
|
-
role: user.role
|
|
14303
|
-
} : void 0,
|
|
14304
|
-
message: "Database not available",
|
|
14305
|
-
messageType: "error"
|
|
14306
|
-
}));
|
|
14307
|
-
}
|
|
14308
|
-
const { results } = await db.prepare("SELECT * FROM faqs WHERE id = ?").bind(id).all();
|
|
14309
|
-
if (!results || results.length === 0) {
|
|
14310
|
-
return c.redirect("/admin/faq?message=FAQ not found&type=error");
|
|
14311
|
-
}
|
|
14312
|
-
const faq = results[0];
|
|
14313
|
-
return c.html(renderFAQForm({
|
|
14314
|
-
faq: {
|
|
14315
|
-
id: faq.id,
|
|
14316
|
-
question: faq.question,
|
|
14317
|
-
answer: faq.answer,
|
|
14318
|
-
category: faq.category,
|
|
14319
|
-
tags: faq.tags,
|
|
14320
|
-
isPublished: Boolean(faq.isPublished),
|
|
14321
|
-
sortOrder: faq.sortOrder
|
|
14322
|
-
},
|
|
14323
|
-
isEdit: true,
|
|
14324
|
-
user: user ? {
|
|
14325
|
-
name: user.email,
|
|
14326
|
-
email: user.email,
|
|
14327
|
-
role: user.role
|
|
14328
|
-
} : void 0
|
|
14329
|
-
}));
|
|
14330
|
-
} catch (error) {
|
|
14331
|
-
console.error("Error fetching FAQ:", error);
|
|
14332
|
-
const user = c.get("user");
|
|
14333
|
-
return c.html(renderFAQForm({
|
|
14334
|
-
isEdit: true,
|
|
14335
|
-
user: user ? {
|
|
14336
|
-
name: user.email,
|
|
14337
|
-
email: user.email,
|
|
14338
|
-
role: user.role
|
|
14339
|
-
} : void 0,
|
|
14340
|
-
message: "Failed to load FAQ",
|
|
14341
|
-
messageType: "error"
|
|
14342
|
-
}));
|
|
14343
|
-
}
|
|
14344
|
-
});
|
|
14345
|
-
adminFAQRoutes.put("/:id", async (c) => {
|
|
14346
|
-
try {
|
|
14347
|
-
const id = parseInt(c.req.param("id"));
|
|
14348
|
-
const formData = await c.req.formData();
|
|
14349
|
-
const data = Object.fromEntries(formData.entries());
|
|
14350
|
-
const validatedData = faqSchema.parse(data);
|
|
14351
|
-
const user = c.get("user");
|
|
14352
|
-
const db = c.env?.DB;
|
|
14353
|
-
if (!db) {
|
|
14354
|
-
return c.html(renderFAQForm({
|
|
14355
|
-
isEdit: true,
|
|
14356
|
-
user: user ? {
|
|
14357
|
-
name: user.email,
|
|
14358
|
-
email: user.email,
|
|
14359
|
-
role: user.role
|
|
14360
|
-
} : void 0,
|
|
14361
|
-
message: "Database not available",
|
|
14362
|
-
messageType: "error"
|
|
14363
|
-
}));
|
|
14364
|
-
}
|
|
14365
|
-
const { results } = await db.prepare(`
|
|
14366
|
-
UPDATE faqs
|
|
14367
|
-
SET question = ?, answer = ?, category = ?, tags = ?, isPublished = ?, sortOrder = ?
|
|
14368
|
-
WHERE id = ?
|
|
14369
|
-
RETURNING *
|
|
14370
|
-
`).bind(
|
|
14371
|
-
validatedData.question,
|
|
14372
|
-
validatedData.answer,
|
|
14373
|
-
validatedData.category || null,
|
|
14374
|
-
validatedData.tags || null,
|
|
14375
|
-
validatedData.isPublished ? 1 : 0,
|
|
14376
|
-
validatedData.sortOrder,
|
|
14377
|
-
id
|
|
14378
|
-
).all();
|
|
14379
|
-
if (results && results.length > 0) {
|
|
14380
|
-
return c.redirect("/admin/faq?message=FAQ updated successfully");
|
|
14381
|
-
} else {
|
|
14382
|
-
return c.html(renderFAQForm({
|
|
14383
|
-
faq: {
|
|
14384
|
-
id,
|
|
14385
|
-
question: validatedData.question,
|
|
14386
|
-
answer: validatedData.answer,
|
|
14387
|
-
category: validatedData.category,
|
|
14388
|
-
tags: validatedData.tags,
|
|
14389
|
-
isPublished: validatedData.isPublished,
|
|
14390
|
-
sortOrder: validatedData.sortOrder
|
|
14391
|
-
},
|
|
14392
|
-
isEdit: true,
|
|
14393
|
-
user: user ? {
|
|
14394
|
-
name: user.email,
|
|
14395
|
-
email: user.email,
|
|
14396
|
-
role: user.role
|
|
14397
|
-
} : void 0,
|
|
14398
|
-
message: "FAQ not found",
|
|
14399
|
-
messageType: "error"
|
|
14400
|
-
}));
|
|
14401
|
-
}
|
|
14402
|
-
} catch (error) {
|
|
14403
|
-
console.error("Error updating FAQ:", error);
|
|
14404
|
-
const user = c.get("user");
|
|
14405
|
-
const id = parseInt(c.req.param("id"));
|
|
14406
|
-
if (error instanceof z.ZodError) {
|
|
14407
|
-
const errors = {};
|
|
14408
|
-
error.errors.forEach((err) => {
|
|
14409
|
-
const field = err.path[0];
|
|
14410
|
-
if (!errors[field]) errors[field] = [];
|
|
14411
|
-
errors[field].push(err.message);
|
|
14412
|
-
});
|
|
14413
|
-
return c.html(renderFAQForm({
|
|
14414
|
-
faq: {
|
|
14415
|
-
id,
|
|
14416
|
-
question: "",
|
|
14417
|
-
answer: "",
|
|
14418
|
-
category: "",
|
|
14419
|
-
tags: "",
|
|
14420
|
-
isPublished: true,
|
|
14421
|
-
sortOrder: 0
|
|
14422
|
-
},
|
|
14423
|
-
isEdit: true,
|
|
14424
|
-
user: user ? {
|
|
14425
|
-
name: user.email,
|
|
14426
|
-
email: user.email,
|
|
14427
|
-
role: user.role
|
|
14428
|
-
} : void 0,
|
|
14429
|
-
errors,
|
|
14430
|
-
message: "Please correct the errors below",
|
|
14431
|
-
messageType: "error"
|
|
14432
|
-
}));
|
|
14433
|
-
}
|
|
14434
|
-
return c.html(renderFAQForm({
|
|
14435
|
-
faq: {
|
|
14436
|
-
id,
|
|
14437
|
-
question: "",
|
|
14438
|
-
answer: "",
|
|
14439
|
-
category: "",
|
|
14440
|
-
tags: "",
|
|
14441
|
-
isPublished: true,
|
|
14442
|
-
sortOrder: 0
|
|
14443
|
-
},
|
|
14444
|
-
isEdit: true,
|
|
14445
|
-
user: user ? {
|
|
14446
|
-
name: user.email,
|
|
14447
|
-
email: user.email,
|
|
14448
|
-
role: user.role
|
|
14449
|
-
} : void 0,
|
|
14450
|
-
message: "Failed to update FAQ",
|
|
14451
|
-
messageType: "error"
|
|
14452
|
-
}));
|
|
14453
|
-
}
|
|
14454
|
-
});
|
|
14455
|
-
adminFAQRoutes.delete("/:id", async (c) => {
|
|
14983
|
+
adminLogsRoutes.post("/cleanup", async (c) => {
|
|
14456
14984
|
try {
|
|
14457
|
-
const
|
|
14458
|
-
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
if (changes === 0) {
|
|
14464
|
-
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);
|
|
14465
14991
|
}
|
|
14466
|
-
|
|
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);
|
|
14467
15056
|
} catch (error) {
|
|
14468
|
-
console.error("Error
|
|
14469
|
-
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";
|
|
14470
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));
|
|
14471
15122
|
});
|
|
14472
|
-
var admin_faq_default = adminFAQRoutes;
|
|
14473
15123
|
|
|
14474
15124
|
// src/templates/pages/admin-testimonials-form.template.ts
|
|
14475
15125
|
function renderTestimonialsForm(data) {
|
|
@@ -14527,7 +15177,7 @@ function renderTestimonialsForm(data) {
|
|
|
14527
15177
|
${errors?.authorName ? `
|
|
14528
15178
|
<div class="mt-1">
|
|
14529
15179
|
${errors.authorName.map((error) => `
|
|
14530
|
-
<p class="text-sm text-red-400">${
|
|
15180
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14531
15181
|
`).join("")}
|
|
14532
15182
|
</div>
|
|
14533
15183
|
` : ""}
|
|
@@ -14549,7 +15199,7 @@ function renderTestimonialsForm(data) {
|
|
|
14549
15199
|
${errors?.authorTitle ? `
|
|
14550
15200
|
<div class="mt-1">
|
|
14551
15201
|
${errors.authorTitle.map((error) => `
|
|
14552
|
-
<p class="text-sm text-red-400">${
|
|
15202
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14553
15203
|
`).join("")}
|
|
14554
15204
|
</div>
|
|
14555
15205
|
` : ""}
|
|
@@ -14570,7 +15220,7 @@ function renderTestimonialsForm(data) {
|
|
|
14570
15220
|
${errors?.authorCompany ? `
|
|
14571
15221
|
<div class="mt-1">
|
|
14572
15222
|
${errors.authorCompany.map((error) => `
|
|
14573
|
-
<p class="text-sm text-red-400">${
|
|
15223
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14574
15224
|
`).join("")}
|
|
14575
15225
|
</div>
|
|
14576
15226
|
` : ""}
|
|
@@ -14602,7 +15252,7 @@ function renderTestimonialsForm(data) {
|
|
|
14602
15252
|
${errors?.testimonialText ? `
|
|
14603
15253
|
<div class="mt-1">
|
|
14604
15254
|
${errors.testimonialText.map((error) => `
|
|
14605
|
-
<p class="text-sm text-red-400">${
|
|
15255
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14606
15256
|
`).join("")}
|
|
14607
15257
|
</div>
|
|
14608
15258
|
` : ""}
|
|
@@ -14626,7 +15276,7 @@ function renderTestimonialsForm(data) {
|
|
|
14626
15276
|
${errors?.rating ? `
|
|
14627
15277
|
<div class="mt-1">
|
|
14628
15278
|
${errors.rating.map((error) => `
|
|
14629
|
-
<p class="text-sm text-red-400">${
|
|
15279
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14630
15280
|
`).join("")}
|
|
14631
15281
|
</div>
|
|
14632
15282
|
` : ""}
|
|
@@ -14680,7 +15330,7 @@ function renderTestimonialsForm(data) {
|
|
|
14680
15330
|
${errors?.sortOrder ? `
|
|
14681
15331
|
<div class="mt-1">
|
|
14682
15332
|
${errors.sortOrder.map((error) => `
|
|
14683
|
-
<p class="text-sm text-red-400">${
|
|
15333
|
+
<p class="text-sm text-red-400">${escapeHtml4(error)}</p>
|
|
14684
15334
|
`).join("")}
|
|
14685
15335
|
</div>
|
|
14686
15336
|
` : ""}
|
|
@@ -14727,7 +15377,7 @@ function renderTestimonialsForm(data) {
|
|
|
14727
15377
|
};
|
|
14728
15378
|
return renderAdminLayout(layoutData);
|
|
14729
15379
|
}
|
|
14730
|
-
function
|
|
15380
|
+
function escapeHtml4(unsafe) {
|
|
14731
15381
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14732
15382
|
}
|
|
14733
15383
|
|
|
@@ -15155,7 +15805,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15155
15805
|
${errors?.title ? `
|
|
15156
15806
|
<div class="mt-1">
|
|
15157
15807
|
${errors.title.map((error) => `
|
|
15158
|
-
<p class="text-sm text-red-400">${
|
|
15808
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15159
15809
|
`).join("")}
|
|
15160
15810
|
</div>
|
|
15161
15811
|
` : ""}
|
|
@@ -15178,7 +15828,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15178
15828
|
${errors?.description ? `
|
|
15179
15829
|
<div class="mt-1">
|
|
15180
15830
|
${errors.description.map((error) => `
|
|
15181
|
-
<p class="text-sm text-red-400">${
|
|
15831
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15182
15832
|
`).join("")}
|
|
15183
15833
|
</div>
|
|
15184
15834
|
` : ""}
|
|
@@ -15210,7 +15860,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15210
15860
|
${errors?.language ? `
|
|
15211
15861
|
<div class="mt-1">
|
|
15212
15862
|
${errors.language.map((error) => `
|
|
15213
|
-
<p class="text-sm text-red-400">${
|
|
15863
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15214
15864
|
`).join("")}
|
|
15215
15865
|
</div>
|
|
15216
15866
|
` : ""}
|
|
@@ -15231,7 +15881,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15231
15881
|
${errors?.category ? `
|
|
15232
15882
|
<div class="mt-1">
|
|
15233
15883
|
${errors.category.map((error) => `
|
|
15234
|
-
<p class="text-sm text-red-400">${
|
|
15884
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15235
15885
|
`).join("")}
|
|
15236
15886
|
</div>
|
|
15237
15887
|
` : ""}
|
|
@@ -15253,7 +15903,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15253
15903
|
${errors?.tags ? `
|
|
15254
15904
|
<div class="mt-1">
|
|
15255
15905
|
${errors.tags.map((error) => `
|
|
15256
|
-
<p class="text-sm text-red-400">${
|
|
15906
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15257
15907
|
`).join("")}
|
|
15258
15908
|
</div>
|
|
15259
15909
|
` : ""}
|
|
@@ -15284,7 +15934,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15284
15934
|
${errors?.code ? `
|
|
15285
15935
|
<div class="mt-1">
|
|
15286
15936
|
${errors.code.map((error) => `
|
|
15287
|
-
<p class="text-sm text-red-400">${
|
|
15937
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15288
15938
|
`).join("")}
|
|
15289
15939
|
</div>
|
|
15290
15940
|
` : ""}
|
|
@@ -15338,7 +15988,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15338
15988
|
${errors?.sortOrder ? `
|
|
15339
15989
|
<div class="mt-1">
|
|
15340
15990
|
${errors.sortOrder.map((error) => `
|
|
15341
|
-
<p class="text-sm text-red-400">${
|
|
15991
|
+
<p class="text-sm text-red-400">${escapeHtml5(error)}</p>
|
|
15342
15992
|
`).join("")}
|
|
15343
15993
|
</div>
|
|
15344
15994
|
` : ""}
|
|
@@ -15396,7 +16046,7 @@ function renderCodeExamplesForm(data) {
|
|
|
15396
16046
|
};
|
|
15397
16047
|
return renderAdminLayout(layoutData);
|
|
15398
16048
|
}
|
|
15399
|
-
function
|
|
16049
|
+
function escapeHtml5(unsafe) {
|
|
15400
16050
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15401
16051
|
}
|
|
15402
16052
|
|
|
@@ -16730,8 +17380,8 @@ function renderTable2(data) {
|
|
|
16730
17380
|
</td>
|
|
16731
17381
|
` : ""}
|
|
16732
17382
|
${data.columns.map((column, colIndex) => {
|
|
16733
|
-
const
|
|
16734
|
-
const displayValue = column.render ? column.render(
|
|
17383
|
+
const value = row[column.key];
|
|
17384
|
+
const displayValue = column.render ? column.render(value, row) : value;
|
|
16735
17385
|
const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
|
|
16736
17386
|
const isFirst = colIndex === 0 && !data.selectable;
|
|
16737
17387
|
const isLast = colIndex === data.columns.length - 1;
|
|
@@ -17118,10 +17768,41 @@ function renderCollectionsListPage(data) {
|
|
|
17118
17768
|
|
|
17119
17769
|
// src/templates/pages/admin-collections-form.template.ts
|
|
17120
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
|
+
}
|
|
17121
17798
|
function renderCollectionFormPage(data) {
|
|
17122
17799
|
const isEdit = data.isEdit || !!data.id;
|
|
17123
17800
|
const title = isEdit ? "Edit Collection" : "Create New Collection";
|
|
17124
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
|
+
}));
|
|
17125
17806
|
const fields = [
|
|
17126
17807
|
{
|
|
17127
17808
|
name: "displayName",
|
|
@@ -17358,21 +18039,24 @@ function renderCollectionFormPage(data) {
|
|
|
17358
18039
|
|
|
17359
18040
|
<!-- Fields List (Read-Only) -->
|
|
17360
18041
|
<div class="space-y-3">
|
|
17361
|
-
${
|
|
18042
|
+
${fieldsWithData.map((field) => `
|
|
17362
18043
|
<div class="bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4">
|
|
17363
18044
|
<div class="flex items-center justify-between">
|
|
17364
18045
|
<div class="flex items-center gap-x-4">
|
|
17365
18046
|
<div>
|
|
17366
18047
|
<div class="flex items-center gap-x-2">
|
|
17367
18048
|
<span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
|
|
17368
|
-
|
|
17369
|
-
${field.field_type}
|
|
17370
|
-
</span>
|
|
18049
|
+
${getFieldTypeBadge(field.field_type)}
|
|
17371
18050
|
${field.is_required ? `
|
|
17372
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">
|
|
17373
18052
|
Required
|
|
17374
18053
|
</span>
|
|
17375
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
|
+
` : ""}
|
|
17376
18060
|
</div>
|
|
17377
18061
|
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
|
|
17378
18062
|
<code class="px-1.5 py-0.5 rounded bg-zinc-100 dark:bg-zinc-800 font-mono">${field.field_name}</code>
|
|
@@ -17418,8 +18102,10 @@ function renderCollectionFormPage(data) {
|
|
|
17418
18102
|
|
|
17419
18103
|
<!-- Fields List -->
|
|
17420
18104
|
<div id="fields-list" class="space-y-3">
|
|
17421
|
-
${
|
|
17422
|
-
<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}">
|
|
17423
18109
|
<div class="flex items-center justify-between">
|
|
17424
18110
|
<div class="flex items-center gap-x-4">
|
|
17425
18111
|
<div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
|
|
@@ -17430,14 +18116,17 @@ function renderCollectionFormPage(data) {
|
|
|
17430
18116
|
<div>
|
|
17431
18117
|
<div class="flex items-center gap-x-2">
|
|
17432
18118
|
<span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
|
|
17433
|
-
|
|
17434
|
-
${field.field_type}
|
|
17435
|
-
</span>
|
|
18119
|
+
${getFieldTypeBadge(field.field_type)}
|
|
17436
18120
|
${field.is_required ? `
|
|
17437
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">
|
|
17438
18122
|
Required
|
|
17439
18123
|
</span>
|
|
17440
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
|
+
` : ""}
|
|
17441
18130
|
</div>
|
|
17442
18131
|
<div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
|
|
17443
18132
|
Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
|
|
@@ -17548,7 +18237,7 @@ function renderCollectionFormPage(data) {
|
|
|
17548
18237
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
|
|
17549
18238
|
<input
|
|
17550
18239
|
type="text"
|
|
17551
|
-
id="field-name"
|
|
18240
|
+
id="modal-field-name"
|
|
17552
18241
|
name="field_name"
|
|
17553
18242
|
required
|
|
17554
18243
|
pattern="[a-z0-9_]+"
|
|
@@ -17569,13 +18258,14 @@ function renderCollectionFormPage(data) {
|
|
|
17569
18258
|
>
|
|
17570
18259
|
<option value="">Select field type...</option>
|
|
17571
18260
|
<option value="text">Text</option>
|
|
17572
|
-
<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>' : ""}
|
|
17573
18264
|
<option value="number">Number</option>
|
|
17574
18265
|
<option value="boolean">Boolean</option>
|
|
17575
18266
|
<option value="date">Date</option>
|
|
17576
18267
|
<option value="select">Select</option>
|
|
17577
18268
|
<option value="media">Media</option>
|
|
17578
|
-
<option value="guid">GUID (Auto-generated)</option>
|
|
17579
18269
|
</select>
|
|
17580
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">
|
|
17581
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" />
|
|
@@ -17600,6 +18290,7 @@ function renderCollectionFormPage(data) {
|
|
|
17600
18290
|
<div class="flex gap-3">
|
|
17601
18291
|
<div class="flex h-6 shrink-0 items-center">
|
|
17602
18292
|
<div class="group grid size-4 grid-cols-1">
|
|
18293
|
+
<input type="hidden" name="is_required" value="0">
|
|
17603
18294
|
<input
|
|
17604
18295
|
type="checkbox"
|
|
17605
18296
|
id="field-required"
|
|
@@ -17621,6 +18312,7 @@ function renderCollectionFormPage(data) {
|
|
|
17621
18312
|
<div class="flex gap-3">
|
|
17622
18313
|
<div class="flex h-6 shrink-0 items-center">
|
|
17623
18314
|
<div class="group grid size-4 grid-cols-1">
|
|
18315
|
+
<input type="hidden" name="is_searchable" value="0">
|
|
17624
18316
|
<input
|
|
17625
18317
|
type="checkbox"
|
|
17626
18318
|
id="field-searchable"
|
|
@@ -17683,18 +18375,52 @@ function renderCollectionFormPage(data) {
|
|
|
17683
18375
|
document.getElementById('submit-text').textContent = 'Add Field';
|
|
17684
18376
|
document.getElementById('field-form').reset();
|
|
17685
18377
|
document.getElementById('field-id').value = '';
|
|
17686
|
-
document.getElementById('field-name').disabled = false;
|
|
18378
|
+
document.getElementById('modal-field-name').disabled = false;
|
|
17687
18379
|
currentEditingField = null;
|
|
18380
|
+
isEditingField = false; // Allow change handlers for add mode
|
|
17688
18381
|
document.getElementById('field-modal').classList.remove('hidden');
|
|
17689
18382
|
}
|
|
17690
18383
|
|
|
17691
18384
|
function editField(fieldId) {
|
|
17692
18385
|
const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
|
|
17693
|
-
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
|
+
}
|
|
17694
18419
|
|
|
17695
|
-
|
|
17696
|
-
|
|
17697
|
-
|
|
18420
|
+
if (!field) {
|
|
18421
|
+
console.error('Field data not found for id:', fieldId);
|
|
18422
|
+
return;
|
|
18423
|
+
}
|
|
17698
18424
|
|
|
17699
18425
|
// Set up the modal for editing
|
|
17700
18426
|
document.getElementById('modal-title').textContent = 'Edit Field';
|
|
@@ -17702,18 +18428,102 @@ function renderCollectionFormPage(data) {
|
|
|
17702
18428
|
document.getElementById('field-id').value = fieldId;
|
|
17703
18429
|
currentEditingField = fieldId;
|
|
17704
18430
|
|
|
17705
|
-
//
|
|
17706
|
-
document.getElementById('field-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
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
|
+
|
|
17713
18523
|
// Handle field options - serialize object back to JSON string
|
|
17714
18524
|
if (field.field_options) {
|
|
17715
|
-
document.getElementById('field-options').value = typeof field.field_options === 'string'
|
|
17716
|
-
? field.field_options
|
|
18525
|
+
document.getElementById('field-options').value = typeof field.field_options === 'string'
|
|
18526
|
+
? field.field_options
|
|
17717
18527
|
: JSON.stringify(field.field_options, null, 2);
|
|
17718
18528
|
} else {
|
|
17719
18529
|
document.getElementById('field-options').value = '';
|
|
@@ -17724,7 +18534,7 @@ function renderCollectionFormPage(data) {
|
|
|
17724
18534
|
const optionsContainer = document.getElementById('field-options-container');
|
|
17725
18535
|
const helpText = document.getElementById('field-type-help');
|
|
17726
18536
|
|
|
17727
|
-
if (['select', 'media', 'richtext'
|
|
18537
|
+
if (['select', 'media', 'richtext'].includes(fieldType)) {
|
|
17728
18538
|
optionsContainer.classList.remove('hidden');
|
|
17729
18539
|
|
|
17730
18540
|
// Set help text based on type
|
|
@@ -17738,9 +18548,6 @@ function renderCollectionFormPage(data) {
|
|
|
17738
18548
|
case 'richtext':
|
|
17739
18549
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17740
18550
|
break;
|
|
17741
|
-
case 'guid':
|
|
17742
|
-
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17743
|
-
break;
|
|
17744
18551
|
}
|
|
17745
18552
|
} else {
|
|
17746
18553
|
optionsContainer.classList.add('hidden');
|
|
@@ -17764,12 +18571,19 @@ function renderCollectionFormPage(data) {
|
|
|
17764
18571
|
}
|
|
17765
18572
|
}
|
|
17766
18573
|
|
|
17767
|
-
|
|
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
|
|
17768
18581
|
}
|
|
17769
18582
|
|
|
17770
18583
|
function closeFieldModal() {
|
|
17771
18584
|
document.getElementById('field-modal').classList.add('hidden');
|
|
17772
18585
|
currentEditingField = null;
|
|
18586
|
+
isEditingField = false; // Clear the flag when closing
|
|
17773
18587
|
}
|
|
17774
18588
|
|
|
17775
18589
|
let fieldToDelete = null;
|
|
@@ -17843,12 +18657,21 @@ function renderCollectionFormPage(data) {
|
|
|
17843
18657
|
});
|
|
17844
18658
|
});
|
|
17845
18659
|
|
|
18660
|
+
// Flag to prevent change handler during programmatic edits
|
|
18661
|
+
let isEditingField = false;
|
|
18662
|
+
|
|
17846
18663
|
// Field type change handler
|
|
17847
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
|
+
|
|
17848
18671
|
const optionsContainer = document.getElementById('field-options-container');
|
|
17849
18672
|
const fieldOptions = document.getElementById('field-options');
|
|
17850
18673
|
const helpText = document.getElementById('field-type-help');
|
|
17851
|
-
const fieldNameInput = document.getElementById('field-name');
|
|
18674
|
+
const fieldNameInput = document.getElementById('modal-field-name');
|
|
17852
18675
|
|
|
17853
18676
|
// Show/hide options based on field type
|
|
17854
18677
|
if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
|
|
@@ -17868,14 +18691,6 @@ function renderCollectionFormPage(data) {
|
|
|
17868
18691
|
fieldOptions.value = '{"toolbar": "full", "height": 400}';
|
|
17869
18692
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17870
18693
|
break;
|
|
17871
|
-
case 'guid':
|
|
17872
|
-
fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
|
|
17873
|
-
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17874
|
-
// Suggest 'id' as field name for GUID fields
|
|
17875
|
-
if (!fieldNameInput.value || fieldNameInput.value === '') {
|
|
17876
|
-
fieldNameInput.value = 'id';
|
|
17877
|
-
}
|
|
17878
|
-
break;
|
|
17879
18694
|
}
|
|
17880
18695
|
} else {
|
|
17881
18696
|
optionsContainer.classList.add('hidden');
|
|
@@ -18012,8 +18827,19 @@ adminCollectionsRoutes.get("/", async (c) => {
|
|
|
18012
18827
|
return c.html(html`<p>Error loading collections</p>`);
|
|
18013
18828
|
}
|
|
18014
18829
|
});
|
|
18015
|
-
adminCollectionsRoutes.get("/new", (c) => {
|
|
18830
|
+
adminCollectionsRoutes.get("/new", async (c) => {
|
|
18016
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
|
+
});
|
|
18017
18843
|
const formData = {
|
|
18018
18844
|
isEdit: false,
|
|
18019
18845
|
user: user ? {
|
|
@@ -18021,7 +18847,12 @@ adminCollectionsRoutes.get("/new", (c) => {
|
|
|
18021
18847
|
email: user.email,
|
|
18022
18848
|
role: user.role
|
|
18023
18849
|
} : void 0,
|
|
18024
|
-
version: c.get("appVersion")
|
|
18850
|
+
version: c.get("appVersion"),
|
|
18851
|
+
editorPlugins: {
|
|
18852
|
+
tinymce: tinymceActive,
|
|
18853
|
+
quill: quillActive,
|
|
18854
|
+
mdxeditor: mdxeditorActive
|
|
18855
|
+
}
|
|
18025
18856
|
};
|
|
18026
18857
|
return c.html(renderCollectionFormPage(formData));
|
|
18027
18858
|
});
|
|
@@ -18121,16 +18952,16 @@ adminCollectionsRoutes.post("/", async (c) => {
|
|
|
18121
18952
|
if (isHtmx) {
|
|
18122
18953
|
return c.html(html`
|
|
18123
18954
|
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
18124
|
-
Collection created successfully! Redirecting...
|
|
18955
|
+
Collection created successfully! Redirecting to edit mode...
|
|
18125
18956
|
<script>
|
|
18126
18957
|
setTimeout(() => {
|
|
18127
|
-
window.location.href = '/admin/collections';
|
|
18958
|
+
window.location.href = '/admin/collections/${collectionId}';
|
|
18128
18959
|
}, 1500);
|
|
18129
18960
|
</script>
|
|
18130
18961
|
</div>
|
|
18131
18962
|
`);
|
|
18132
18963
|
} else {
|
|
18133
|
-
return c.redirect(
|
|
18964
|
+
return c.redirect(`/admin/collections/${collectionId}`);
|
|
18134
18965
|
}
|
|
18135
18966
|
} catch (error) {
|
|
18136
18967
|
console.error("Error creating collection:", error);
|
|
@@ -18180,7 +19011,7 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18180
19011
|
field_options: fieldConfig,
|
|
18181
19012
|
field_order: fieldOrder++,
|
|
18182
19013
|
is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
|
|
18183
|
-
is_searchable: false
|
|
19014
|
+
is_searchable: fieldConfig.searchable === true || false
|
|
18184
19015
|
}));
|
|
18185
19016
|
}
|
|
18186
19017
|
} catch (e) {
|
|
@@ -18205,6 +19036,11 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18205
19036
|
is_searchable: row.is_searchable === 1
|
|
18206
19037
|
}));
|
|
18207
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
|
+
]);
|
|
18208
19044
|
const formData = {
|
|
18209
19045
|
id: collection.id,
|
|
18210
19046
|
name: collection.name,
|
|
@@ -18218,7 +19054,12 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
18218
19054
|
email: user.email,
|
|
18219
19055
|
role: user.role
|
|
18220
19056
|
} : void 0,
|
|
18221
|
-
version: c.get("appVersion")
|
|
19057
|
+
version: c.get("appVersion"),
|
|
19058
|
+
editorPlugins: {
|
|
19059
|
+
tinymce: tinymceActive,
|
|
19060
|
+
quill: quillActive,
|
|
19061
|
+
mdxeditor: mdxeditorActive
|
|
19062
|
+
}
|
|
18222
19063
|
};
|
|
18223
19064
|
return c.html(renderCollectionFormPage(formData));
|
|
18224
19065
|
} catch (error) {
|
|
@@ -18358,21 +19199,103 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
|
|
|
18358
19199
|
adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
|
|
18359
19200
|
try {
|
|
18360
19201
|
const fieldId = c.req.param("fieldId");
|
|
19202
|
+
const collectionId = c.req.param("collectionId");
|
|
18361
19203
|
const formData = await c.req.formData();
|
|
18362
19204
|
const fieldLabel = formData.get("field_label");
|
|
18363
|
-
const
|
|
18364
|
-
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";
|
|
18365
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
|
+
});
|
|
18366
19219
|
if (!fieldLabel) {
|
|
18367
19220
|
return c.json({ success: false, error: "Field label is required." });
|
|
18368
19221
|
}
|
|
18369
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
|
+
}
|
|
18370
19283
|
const updateStmt = db.prepare(`
|
|
18371
19284
|
UPDATE content_fields
|
|
18372
|
-
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 = ?
|
|
18373
19286
|
WHERE id = ?
|
|
18374
19287
|
`);
|
|
18375
|
-
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);
|
|
18376
19299
|
return c.json({ success: true });
|
|
18377
19300
|
} catch (error) {
|
|
18378
19301
|
console.error("Error updating field:", error);
|
|
@@ -19633,7 +20556,7 @@ function renderMigrationSettings(settings) {
|
|
|
19633
20556
|
btn.innerHTML = 'Running...';
|
|
19634
20557
|
|
|
19635
20558
|
try {
|
|
19636
|
-
const response = await fetch('/admin/api/migrations/run', {
|
|
20559
|
+
const response = await fetch('/admin/settings/api/migrations/run', {
|
|
19637
20560
|
method: 'POST'
|
|
19638
20561
|
});
|
|
19639
20562
|
const result = await response.json();
|
|
@@ -19654,7 +20577,7 @@ function renderMigrationSettings(settings) {
|
|
|
19654
20577
|
|
|
19655
20578
|
window.validateSchema = async function() {
|
|
19656
20579
|
try {
|
|
19657
|
-
const response = await fetch('/admin/api/migrations/validate');
|
|
20580
|
+
const response = await fetch('/admin/settings/api/migrations/validate');
|
|
19658
20581
|
const result = await response.json();
|
|
19659
20582
|
|
|
19660
20583
|
if (result.success) {
|
|
@@ -20282,7 +21205,6 @@ var ROUTES_INFO = {
|
|
|
20282
21205
|
"adminLogsRoutes",
|
|
20283
21206
|
"adminDesignRoutes",
|
|
20284
21207
|
"adminCheckboxRoutes",
|
|
20285
|
-
"adminFAQRoutes",
|
|
20286
21208
|
"adminTestimonialsRoutes",
|
|
20287
21209
|
"adminCodeExamplesRoutes",
|
|
20288
21210
|
"adminDashboardRoutes",
|
|
@@ -20293,6 +21215,6 @@ var ROUTES_INFO = {
|
|
|
20293
21215
|
reference: "https://github.com/sonicjs/sonicjs"
|
|
20294
21216
|
};
|
|
20295
21217
|
|
|
20296
|
-
export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default,
|
|
20297
|
-
//# sourceMappingURL=chunk-
|
|
20298
|
-
//# 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
|