@sonicjs-cms/core 2.0.0-beta.3 → 2.0.1
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-4MZPFGLT.cjs → chunk-5FDDDD4J.cjs} +4385 -352
- package/dist/chunk-5FDDDD4J.cjs.map +1 -0
- package/dist/chunk-5XTB4FE5.js +1030 -0
- package/dist/chunk-5XTB4FE5.js.map +1 -0
- package/dist/{chunk-ET5I4GBD.cjs → chunk-ALOS2CBJ.cjs} +194 -4
- package/dist/chunk-ALOS2CBJ.cjs.map +1 -0
- package/dist/{chunk-7N3HK7ZK.js → chunk-CDBVZEWR.js} +7 -904
- package/dist/chunk-CDBVZEWR.js.map +1 -0
- package/dist/chunk-EGFHFM4N.cjs +76 -0
- package/dist/chunk-EGFHFM4N.cjs.map +1 -0
- package/dist/chunk-KM4AJFXI.cjs +101 -0
- package/dist/chunk-KM4AJFXI.cjs.map +1 -0
- package/dist/{chunk-RNR4HA23.cjs → chunk-LEG4KNFP.cjs} +6 -945
- package/dist/chunk-LEG4KNFP.cjs.map +1 -0
- package/dist/{chunk-P3VS4DV3.js → chunk-O46XKBFM.js} +193 -5
- package/dist/chunk-O46XKBFM.js.map +1 -0
- package/dist/chunk-P2PTTBO5.js +74 -0
- package/dist/chunk-P2PTTBO5.js.map +1 -0
- package/dist/{chunk-Q3KCKPHE.js → chunk-QSF34IYQ.js} +4244 -214
- package/dist/chunk-QSF34IYQ.js.map +1 -0
- package/dist/chunk-SRCY43RN.cjs +1076 -0
- package/dist/chunk-SRCY43RN.cjs.map +1 -0
- package/dist/chunk-TY3NHEBN.js +80 -0
- package/dist/chunk-TY3NHEBN.js.map +1 -0
- package/dist/index.cjs +196 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +22 -22
- package/dist/middleware.js +2 -2
- package/dist/routes.cjs +34 -22
- package/dist/routes.js +5 -5
- package/dist/services.cjs +28 -28
- package/dist/services.js +2 -2
- package/dist/templates.cjs +24 -24
- package/dist/templates.js +2 -2
- package/package.json +2 -16
- package/dist/chunk-3MNMOLSA.js +0 -133
- package/dist/chunk-3MNMOLSA.js.map +0 -1
- package/dist/chunk-4MZPFGLT.cjs.map +0 -1
- package/dist/chunk-4XI3YBKU.cjs +0 -266
- package/dist/chunk-4XI3YBKU.cjs.map +0 -1
- package/dist/chunk-7N3HK7ZK.js.map +0 -1
- package/dist/chunk-AGOE25LF.cjs +0 -137
- package/dist/chunk-AGOE25LF.cjs.map +0 -1
- package/dist/chunk-BUKT6HP5.cjs +0 -776
- package/dist/chunk-BUKT6HP5.cjs.map +0 -1
- package/dist/chunk-ET5I4GBD.cjs.map +0 -1
- package/dist/chunk-LU6J53IX.js +0 -262
- package/dist/chunk-LU6J53IX.js.map +0 -1
- package/dist/chunk-P3VS4DV3.js.map +0 -1
- package/dist/chunk-Q3KCKPHE.js.map +0 -1
- package/dist/chunk-RNR4HA23.cjs.map +0 -1
- package/dist/chunk-WESS2U3K.js +0 -755
- package/dist/chunk-WESS2U3K.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { getCacheService, CACHE_CONFIGS } from './chunk-
|
|
2
|
-
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-
|
|
3
|
-
import { PluginService,
|
|
4
|
-
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2 } from './chunk-
|
|
5
|
-
import { QueryFilterBuilder, sanitizeInput, escapeHtml } from './chunk-OL2OE3VJ.js';
|
|
1
|
+
import { getCacheService, CACHE_CONFIGS, getLogger } from './chunk-5XTB4FE5.js';
|
|
2
|
+
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-TY3NHEBN.js';
|
|
3
|
+
import { PluginService, MigrationService } from './chunk-CDBVZEWR.js';
|
|
4
|
+
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-O46XKBFM.js';
|
|
5
|
+
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-OL2OE3VJ.js';
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
7
|
import { cors } from 'hono/cors';
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -2030,209 +2030,9 @@ function renderRegisterPage(data) {
|
|
|
2030
2030
|
</html>
|
|
2031
2031
|
`;
|
|
2032
2032
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
cacheExpiry = 0;
|
|
2037
|
-
CACHE_TTL = 5 * 60 * 1e3;
|
|
2038
|
-
// 5 minutes
|
|
2039
|
-
static getInstance() {
|
|
2040
|
-
if (!_AuthValidationService.instance) {
|
|
2041
|
-
_AuthValidationService.instance = new _AuthValidationService();
|
|
2042
|
-
}
|
|
2043
|
-
return _AuthValidationService.instance;
|
|
2044
|
-
}
|
|
2045
|
-
/**
|
|
2046
|
-
* Get authentication settings from core-auth plugin
|
|
2047
|
-
*/
|
|
2048
|
-
async getAuthSettings(db) {
|
|
2049
|
-
if (this.cachedSettings && Date.now() < this.cacheExpiry) {
|
|
2050
|
-
return this.cachedSettings;
|
|
2051
|
-
}
|
|
2052
|
-
try {
|
|
2053
|
-
const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ? AND status = ?").bind("core-auth", "active").first();
|
|
2054
|
-
if (!plugin || !plugin.settings) {
|
|
2055
|
-
console.warn("[AuthValidation] Core-auth plugin not found or not active, using defaults");
|
|
2056
|
-
return this.getDefaultSettings();
|
|
2057
|
-
}
|
|
2058
|
-
const settings = typeof plugin.settings === "string" ? JSON.parse(plugin.settings) : plugin.settings;
|
|
2059
|
-
this.cachedSettings = settings;
|
|
2060
|
-
this.cacheExpiry = Date.now() + this.CACHE_TTL;
|
|
2061
|
-
return settings;
|
|
2062
|
-
} catch (error) {
|
|
2063
|
-
console.error("[AuthValidation] Error loading auth settings:", error);
|
|
2064
|
-
return this.getDefaultSettings();
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
/**
|
|
2068
|
-
* Get default authentication settings
|
|
2069
|
-
*/
|
|
2070
|
-
getDefaultSettings() {
|
|
2071
|
-
return {
|
|
2072
|
-
requiredFields: {
|
|
2073
|
-
email: { required: true, minLength: 5, label: "Email", type: "email" },
|
|
2074
|
-
password: { required: true, minLength: 8, label: "Password", type: "password" },
|
|
2075
|
-
username: { required: true, minLength: 3, label: "Username", type: "text" },
|
|
2076
|
-
firstName: { required: true, minLength: 1, label: "First Name", type: "text" },
|
|
2077
|
-
lastName: { required: true, minLength: 1, label: "Last Name", type: "text" }
|
|
2078
|
-
},
|
|
2079
|
-
validation: {
|
|
2080
|
-
emailFormat: true,
|
|
2081
|
-
allowDuplicateUsernames: false,
|
|
2082
|
-
passwordRequirements: {
|
|
2083
|
-
requireUppercase: false,
|
|
2084
|
-
requireLowercase: false,
|
|
2085
|
-
requireNumbers: false,
|
|
2086
|
-
requireSpecialChars: false
|
|
2087
|
-
}
|
|
2088
|
-
},
|
|
2089
|
-
registration: {
|
|
2090
|
-
enabled: true,
|
|
2091
|
-
requireEmailVerification: false,
|
|
2092
|
-
defaultRole: "viewer"
|
|
2093
|
-
}
|
|
2094
|
-
};
|
|
2095
|
-
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Build dynamic Zod schema based on settings
|
|
2098
|
-
*/
|
|
2099
|
-
async buildRegistrationSchema(db) {
|
|
2100
|
-
const settings = await this.getAuthSettings(db);
|
|
2101
|
-
const fields = settings.requiredFields;
|
|
2102
|
-
const validation = settings.validation;
|
|
2103
|
-
const schemaFields = {};
|
|
2104
|
-
if (fields.email.required) {
|
|
2105
|
-
let emailSchema = z.string();
|
|
2106
|
-
if (validation.emailFormat) {
|
|
2107
|
-
emailSchema = emailSchema.email("Valid email is required");
|
|
2108
|
-
}
|
|
2109
|
-
if (fields.email.minLength > 0) {
|
|
2110
|
-
emailSchema = emailSchema.min(
|
|
2111
|
-
fields.email.minLength,
|
|
2112
|
-
`Email must be at least ${fields.email.minLength} characters`
|
|
2113
|
-
);
|
|
2114
|
-
}
|
|
2115
|
-
schemaFields.email = emailSchema;
|
|
2116
|
-
} else {
|
|
2117
|
-
schemaFields.email = z.string().email().optional();
|
|
2118
|
-
}
|
|
2119
|
-
if (fields.password.required) {
|
|
2120
|
-
let passwordSchema = z.string().min(
|
|
2121
|
-
fields.password.minLength,
|
|
2122
|
-
`Password must be at least ${fields.password.minLength} characters`
|
|
2123
|
-
);
|
|
2124
|
-
if (validation.passwordRequirements.requireUppercase) {
|
|
2125
|
-
passwordSchema = passwordSchema.regex(
|
|
2126
|
-
/[A-Z]/,
|
|
2127
|
-
"Password must contain at least one uppercase letter"
|
|
2128
|
-
);
|
|
2129
|
-
}
|
|
2130
|
-
if (validation.passwordRequirements.requireLowercase) {
|
|
2131
|
-
passwordSchema = passwordSchema.regex(
|
|
2132
|
-
/[a-z]/,
|
|
2133
|
-
"Password must contain at least one lowercase letter"
|
|
2134
|
-
);
|
|
2135
|
-
}
|
|
2136
|
-
if (validation.passwordRequirements.requireNumbers) {
|
|
2137
|
-
passwordSchema = passwordSchema.regex(
|
|
2138
|
-
/[0-9]/,
|
|
2139
|
-
"Password must contain at least one number"
|
|
2140
|
-
);
|
|
2141
|
-
}
|
|
2142
|
-
if (validation.passwordRequirements.requireSpecialChars) {
|
|
2143
|
-
passwordSchema = passwordSchema.regex(
|
|
2144
|
-
/[!@#$%^&*(),.?":{}|<>]/,
|
|
2145
|
-
"Password must contain at least one special character"
|
|
2146
|
-
);
|
|
2147
|
-
}
|
|
2148
|
-
schemaFields.password = passwordSchema;
|
|
2149
|
-
} else {
|
|
2150
|
-
schemaFields.password = z.string().min(fields.password.minLength).optional();
|
|
2151
|
-
}
|
|
2152
|
-
if (fields.username.required) {
|
|
2153
|
-
schemaFields.username = z.string().min(
|
|
2154
|
-
fields.username.minLength,
|
|
2155
|
-
`Username must be at least ${fields.username.minLength} characters`
|
|
2156
|
-
);
|
|
2157
|
-
} else {
|
|
2158
|
-
schemaFields.username = z.string().min(fields.username.minLength).optional();
|
|
2159
|
-
}
|
|
2160
|
-
if (fields.firstName.required) {
|
|
2161
|
-
schemaFields.firstName = z.string().min(
|
|
2162
|
-
fields.firstName.minLength,
|
|
2163
|
-
`First name must be at least ${fields.firstName.minLength} characters`
|
|
2164
|
-
);
|
|
2165
|
-
} else {
|
|
2166
|
-
schemaFields.firstName = z.string().optional();
|
|
2167
|
-
}
|
|
2168
|
-
if (fields.lastName.required) {
|
|
2169
|
-
schemaFields.lastName = z.string().min(
|
|
2170
|
-
fields.lastName.minLength,
|
|
2171
|
-
`Last name must be at least ${fields.lastName.minLength} characters`
|
|
2172
|
-
);
|
|
2173
|
-
} else {
|
|
2174
|
-
schemaFields.lastName = z.string().optional();
|
|
2175
|
-
}
|
|
2176
|
-
return z.object(schemaFields);
|
|
2177
|
-
}
|
|
2178
|
-
/**
|
|
2179
|
-
* Validate registration data against settings
|
|
2180
|
-
*/
|
|
2181
|
-
async validateRegistration(db, data) {
|
|
2182
|
-
try {
|
|
2183
|
-
const schema = await this.buildRegistrationSchema(db);
|
|
2184
|
-
await schema.parseAsync(data);
|
|
2185
|
-
return { valid: true, errors: [] };
|
|
2186
|
-
} catch (error) {
|
|
2187
|
-
if (error instanceof z.ZodError) {
|
|
2188
|
-
return {
|
|
2189
|
-
valid: false,
|
|
2190
|
-
errors: error.errors.map((e) => e.message)
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
|
-
return {
|
|
2194
|
-
valid: false,
|
|
2195
|
-
errors: ["Validation failed"]
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
/**
|
|
2200
|
-
* Clear cached settings (call after updating plugin settings)
|
|
2201
|
-
*/
|
|
2202
|
-
clearCache() {
|
|
2203
|
-
this.cachedSettings = null;
|
|
2204
|
-
this.cacheExpiry = 0;
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* Get required field names for database insertion
|
|
2208
|
-
*/
|
|
2209
|
-
async getRequiredFieldNames(db) {
|
|
2210
|
-
const settings = await this.getAuthSettings(db);
|
|
2211
|
-
const requiredFields = [];
|
|
2212
|
-
Object.entries(settings.requiredFields).forEach(([key, config]) => {
|
|
2213
|
-
if (config.required) {
|
|
2214
|
-
requiredFields.push(key);
|
|
2215
|
-
}
|
|
2216
|
-
});
|
|
2217
|
-
return requiredFields;
|
|
2218
|
-
}
|
|
2219
|
-
/**
|
|
2220
|
-
* Generate auto-fill values for optional fields
|
|
2221
|
-
*/
|
|
2222
|
-
generateDefaultValue(fieldName, data) {
|
|
2223
|
-
switch (fieldName) {
|
|
2224
|
-
case "username":
|
|
2225
|
-
return data.email ? data.email.split("@")[0] : `user_${Date.now()}`;
|
|
2226
|
-
case "firstName":
|
|
2227
|
-
return data.firstName || "User";
|
|
2228
|
-
case "lastName":
|
|
2229
|
-
return data.lastName || "";
|
|
2230
|
-
default:
|
|
2231
|
-
return "";
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
};
|
|
2235
|
-
var authValidationService = AuthValidationService.getInstance();
|
|
2033
|
+
|
|
2034
|
+
// src/services/auth-validation.ts
|
|
2035
|
+
var authValidationService = {};
|
|
2236
2036
|
|
|
2237
2037
|
// src/routes/auth.ts
|
|
2238
2038
|
var authRoutes = new Hono();
|
|
@@ -4899,6 +4699,9 @@ function escapeHtml3(text) {
|
|
|
4899
4699
|
})[char] || char);
|
|
4900
4700
|
}
|
|
4901
4701
|
|
|
4702
|
+
// src/middleware/plugin-middleware.ts
|
|
4703
|
+
var isPluginActive2 = () => false;
|
|
4704
|
+
|
|
4902
4705
|
// src/routes/admin-content.ts
|
|
4903
4706
|
var adminContentRoutes = new Hono();
|
|
4904
4707
|
async function getCollectionFields(db, collectionId) {
|
|
@@ -5147,7 +4950,7 @@ adminContentRoutes.get("/new", async (c) => {
|
|
|
5147
4950
|
return c.html(renderContentFormPage(formData2));
|
|
5148
4951
|
}
|
|
5149
4952
|
const fields = await getCollectionFields(db, collectionId);
|
|
5150
|
-
const workflowEnabled = await
|
|
4953
|
+
const workflowEnabled = await isPluginActive2(db, "workflow");
|
|
5151
4954
|
const formData = {
|
|
5152
4955
|
collection,
|
|
5153
4956
|
fields,
|
|
@@ -5219,7 +5022,7 @@ adminContentRoutes.get("/:id/edit", async (c) => {
|
|
|
5219
5022
|
};
|
|
5220
5023
|
const fields = await getCollectionFields(db, content.collection_id);
|
|
5221
5024
|
const contentData = content.data ? JSON.parse(content.data) : {};
|
|
5222
|
-
const workflowEnabled = await
|
|
5025
|
+
const workflowEnabled = await isPluginActive2(db, "workflow");
|
|
5223
5026
|
const formData = {
|
|
5224
5027
|
id: content.id,
|
|
5225
5028
|
title: content.title,
|
|
@@ -15753,6 +15556,4230 @@ adminCodeExamplesRoutes.delete("/:id", async (c) => {
|
|
|
15753
15556
|
});
|
|
15754
15557
|
var admin_code_examples_default = adminCodeExamplesRoutes;
|
|
15755
15558
|
|
|
15559
|
+
// src/templates/pages/admin-dashboard.template.ts
|
|
15560
|
+
function renderDashboardPage(data) {
|
|
15561
|
+
const pageContent = `
|
|
15562
|
+
<div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
15563
|
+
<div>
|
|
15564
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Dashboard</h1>
|
|
15565
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Welcome to your SonicJS AI admin dashboard</p>
|
|
15566
|
+
</div>
|
|
15567
|
+
<div class="mt-4 sm:mt-0 flex items-center gap-x-3">
|
|
15568
|
+
<a href="/docs/getting-started" target="_blank" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-lime-600 dark:bg-lime-700 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-lime-700 dark:hover:bg-lime-600 transition-colors shadow-sm">
|
|
15569
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
15570
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5"/>
|
|
15571
|
+
</svg>
|
|
15572
|
+
Developer Docs
|
|
15573
|
+
</a>
|
|
15574
|
+
<a href="/admin/api-reference" class="inline-flex items-center justify-center gap-x-1.5 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">
|
|
15575
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
15576
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"/>
|
|
15577
|
+
</svg>
|
|
15578
|
+
API Docs
|
|
15579
|
+
</a>
|
|
15580
|
+
<a href="/api" target="_blank" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
15581
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
15582
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>
|
|
15583
|
+
</svg>
|
|
15584
|
+
OpenAPI
|
|
15585
|
+
</a>
|
|
15586
|
+
</div>
|
|
15587
|
+
</div>
|
|
15588
|
+
|
|
15589
|
+
<!-- Stats Cards -->
|
|
15590
|
+
<div
|
|
15591
|
+
id="stats-container"
|
|
15592
|
+
class="mb-8"
|
|
15593
|
+
hx-get="/admin/dashboard/stats"
|
|
15594
|
+
hx-trigger="load"
|
|
15595
|
+
hx-swap="innerHTML"
|
|
15596
|
+
>
|
|
15597
|
+
${renderStatsCardsSkeleton()}
|
|
15598
|
+
</div>
|
|
15599
|
+
|
|
15600
|
+
<!-- Dashboard Grid -->
|
|
15601
|
+
<div class="grid grid-cols-1 gap-6 xl:grid-cols-3 mb-8">
|
|
15602
|
+
<!-- Analytics Chart -->
|
|
15603
|
+
<div class="xl:col-span-2">
|
|
15604
|
+
${renderAnalyticsChart()}
|
|
15605
|
+
</div>
|
|
15606
|
+
|
|
15607
|
+
<!-- Recent Activity -->
|
|
15608
|
+
<div
|
|
15609
|
+
class="xl:col-span-1"
|
|
15610
|
+
id="recent-activity-container"
|
|
15611
|
+
hx-get="/admin/dashboard/recent-activity"
|
|
15612
|
+
hx-trigger="load"
|
|
15613
|
+
hx-swap="innerHTML"
|
|
15614
|
+
>
|
|
15615
|
+
${renderRecentActivitySkeleton()}
|
|
15616
|
+
</div>
|
|
15617
|
+
</div>
|
|
15618
|
+
|
|
15619
|
+
<!-- Secondary Grid -->
|
|
15620
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
15621
|
+
<!-- Quick Actions -->
|
|
15622
|
+
${renderQuickActions()}
|
|
15623
|
+
|
|
15624
|
+
<!-- System Status -->
|
|
15625
|
+
${renderSystemStatus()}
|
|
15626
|
+
|
|
15627
|
+
<!-- Storage Usage -->
|
|
15628
|
+
<div id="storage-usage-container" hx-get="/admin/dashboard/storage" hx-trigger="load" hx-swap="innerHTML">
|
|
15629
|
+
${renderStorageUsage()}
|
|
15630
|
+
</div>
|
|
15631
|
+
</div>
|
|
15632
|
+
|
|
15633
|
+
<script>
|
|
15634
|
+
function refreshDashboard() {
|
|
15635
|
+
htmx.trigger('#stats-container', 'htmx:load');
|
|
15636
|
+
showNotification('Dashboard refreshed', 'success');
|
|
15637
|
+
}
|
|
15638
|
+
</script>
|
|
15639
|
+
`;
|
|
15640
|
+
const layoutData = {
|
|
15641
|
+
title: "Dashboard",
|
|
15642
|
+
pageTitle: "Dashboard",
|
|
15643
|
+
currentPath: "/admin",
|
|
15644
|
+
user: data.user,
|
|
15645
|
+
version: data.version,
|
|
15646
|
+
content: pageContent
|
|
15647
|
+
};
|
|
15648
|
+
return renderAdminLayout(layoutData);
|
|
15649
|
+
}
|
|
15650
|
+
function renderStatsCards(stats) {
|
|
15651
|
+
const cards = [
|
|
15652
|
+
{
|
|
15653
|
+
title: "Total Collections",
|
|
15654
|
+
value: stats.collections.toString(),
|
|
15655
|
+
change: "12.5",
|
|
15656
|
+
isPositive: true
|
|
15657
|
+
},
|
|
15658
|
+
{
|
|
15659
|
+
title: "Content Items",
|
|
15660
|
+
value: stats.contentItems.toString(),
|
|
15661
|
+
change: "8.2",
|
|
15662
|
+
isPositive: true
|
|
15663
|
+
},
|
|
15664
|
+
{
|
|
15665
|
+
title: "Media Files",
|
|
15666
|
+
value: stats.mediaFiles.toString(),
|
|
15667
|
+
change: "15.3",
|
|
15668
|
+
isPositive: true
|
|
15669
|
+
},
|
|
15670
|
+
{
|
|
15671
|
+
title: "Active Users",
|
|
15672
|
+
value: stats.users.toString(),
|
|
15673
|
+
change: "2.4",
|
|
15674
|
+
isPositive: false
|
|
15675
|
+
}
|
|
15676
|
+
];
|
|
15677
|
+
const cardColors = ["text-cyan-400", "text-lime-400", "text-pink-400", "text-purple-400"];
|
|
15678
|
+
return `
|
|
15679
|
+
<div>
|
|
15680
|
+
<h3 class="text-base font-semibold text-zinc-950 dark:text-white">Last 30 days</h3>
|
|
15681
|
+
<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-4 md:divide-x md:divide-y-0">
|
|
15682
|
+
${cards.map((card, index) => `
|
|
15683
|
+
<div class="px-4 py-5 sm:p-6">
|
|
15684
|
+
<dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">${card.title}</dt>
|
|
15685
|
+
<dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
|
|
15686
|
+
<div class="flex items-baseline text-2xl font-semibold ${cardColors[index]}">
|
|
15687
|
+
${card.value}
|
|
15688
|
+
</div>
|
|
15689
|
+
<div class="inline-flex items-baseline rounded-full ${card.isPositive ? "bg-lime-400/10 text-lime-600 dark:text-lime-400" : "bg-pink-400/10 text-pink-600 dark:text-pink-400"} px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
|
|
15690
|
+
<svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
|
|
15691
|
+
${card.isPositive ? '<path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />' : '<path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />'}
|
|
15692
|
+
</svg>
|
|
15693
|
+
<span class="sr-only">${card.isPositive ? "Increased" : "Decreased"} by</span>
|
|
15694
|
+
${card.change}%
|
|
15695
|
+
</div>
|
|
15696
|
+
</dd>
|
|
15697
|
+
</div>
|
|
15698
|
+
`).join("")}
|
|
15699
|
+
</dl>
|
|
15700
|
+
</div>
|
|
15701
|
+
`;
|
|
15702
|
+
}
|
|
15703
|
+
function renderStatsCardsSkeleton() {
|
|
15704
|
+
return `
|
|
15705
|
+
<div>
|
|
15706
|
+
<div class="h-6 w-32 bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse mb-5"></div>
|
|
15707
|
+
<div class="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-4 md:divide-x md:divide-y-0">
|
|
15708
|
+
${Array(4).fill(0).map(
|
|
15709
|
+
() => `
|
|
15710
|
+
<div class="px-4 py-5 sm:p-6 animate-pulse">
|
|
15711
|
+
<div class="h-4 w-24 bg-zinc-200 dark:bg-zinc-700 rounded mb-3"></div>
|
|
15712
|
+
<div class="h-8 w-16 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
15713
|
+
</div>
|
|
15714
|
+
`
|
|
15715
|
+
).join("")}
|
|
15716
|
+
</div>
|
|
15717
|
+
</div>
|
|
15718
|
+
`;
|
|
15719
|
+
}
|
|
15720
|
+
function renderAnalyticsChart() {
|
|
15721
|
+
return `
|
|
15722
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
15723
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
15724
|
+
<div class="flex flex-wrap items-start justify-between gap-3 sm:flex-nowrap">
|
|
15725
|
+
<div>
|
|
15726
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Real-Time Analytics</h3>
|
|
15727
|
+
<p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Requests per second (live)</p>
|
|
15728
|
+
</div>
|
|
15729
|
+
<div class="flex items-center gap-2">
|
|
15730
|
+
<div class="h-2 w-2 rounded-full bg-lime-500 animate-pulse"></div>
|
|
15731
|
+
<span class="text-xs text-zinc-500 dark:text-zinc-400">Live</span>
|
|
15732
|
+
</div>
|
|
15733
|
+
</div>
|
|
15734
|
+
<div class="mt-4 flex items-baseline gap-2">
|
|
15735
|
+
<span id="current-rps" class="text-4xl font-bold text-cyan-500 dark:text-cyan-400">0</span>
|
|
15736
|
+
<span class="text-sm text-zinc-500 dark:text-zinc-400">req/s</span>
|
|
15737
|
+
</div>
|
|
15738
|
+
</div>
|
|
15739
|
+
|
|
15740
|
+
<div class="px-6 py-6">
|
|
15741
|
+
<canvas id="requestsChart" class="w-full" style="height: 300px;"></canvas>
|
|
15742
|
+
</div>
|
|
15743
|
+
|
|
15744
|
+
<!-- Hidden div to trigger HTMX polling -->
|
|
15745
|
+
<div
|
|
15746
|
+
hx-get="/admin/api/metrics"
|
|
15747
|
+
hx-trigger="every 1s"
|
|
15748
|
+
hx-swap="none"
|
|
15749
|
+
style="display: none;"
|
|
15750
|
+
></div>
|
|
15751
|
+
</div>
|
|
15752
|
+
|
|
15753
|
+
<script>
|
|
15754
|
+
// Initialize Chart.js for Real-time Requests
|
|
15755
|
+
(function() {
|
|
15756
|
+
const ctx = document.getElementById('requestsChart');
|
|
15757
|
+
if (!ctx) return;
|
|
15758
|
+
|
|
15759
|
+
// Initialize with last 60 seconds of data (1 data point per second)
|
|
15760
|
+
const maxDataPoints = 60;
|
|
15761
|
+
const labels = [];
|
|
15762
|
+
const data = [];
|
|
15763
|
+
|
|
15764
|
+
for (let i = maxDataPoints - 1; i >= 0; i--) {
|
|
15765
|
+
labels.push(\`-\${i}s\`);
|
|
15766
|
+
data.push(0);
|
|
15767
|
+
}
|
|
15768
|
+
|
|
15769
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
15770
|
+
|
|
15771
|
+
const chart = new Chart(ctx, {
|
|
15772
|
+
type: 'line',
|
|
15773
|
+
data: {
|
|
15774
|
+
labels: labels,
|
|
15775
|
+
datasets: [{
|
|
15776
|
+
label: 'Requests/sec',
|
|
15777
|
+
data: data,
|
|
15778
|
+
borderColor: isDark ? 'rgb(34, 211, 238)' : 'rgb(6, 182, 212)',
|
|
15779
|
+
backgroundColor: isDark ? 'rgba(34, 211, 238, 0.1)' : 'rgba(6, 182, 212, 0.1)',
|
|
15780
|
+
borderWidth: 2,
|
|
15781
|
+
fill: true,
|
|
15782
|
+
tension: 0.4,
|
|
15783
|
+
pointRadius: 0,
|
|
15784
|
+
pointHoverRadius: 4,
|
|
15785
|
+
pointBackgroundColor: isDark ? 'rgb(34, 211, 238)' : 'rgb(6, 182, 212)',
|
|
15786
|
+
pointBorderColor: isDark ? 'rgb(17, 24, 39)' : 'rgb(255, 255, 255)',
|
|
15787
|
+
pointBorderWidth: 2
|
|
15788
|
+
}]
|
|
15789
|
+
},
|
|
15790
|
+
options: {
|
|
15791
|
+
responsive: true,
|
|
15792
|
+
maintainAspectRatio: false,
|
|
15793
|
+
plugins: {
|
|
15794
|
+
legend: {
|
|
15795
|
+
display: false
|
|
15796
|
+
},
|
|
15797
|
+
tooltip: {
|
|
15798
|
+
backgroundColor: isDark ? 'rgb(39, 39, 42)' : 'rgb(255, 255, 255)',
|
|
15799
|
+
titleColor: isDark ? 'rgb(255, 255, 255)' : 'rgb(9, 9, 11)',
|
|
15800
|
+
bodyColor: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
|
|
15801
|
+
borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(9, 9, 11, 0.05)',
|
|
15802
|
+
borderWidth: 1,
|
|
15803
|
+
padding: 12,
|
|
15804
|
+
displayColors: false,
|
|
15805
|
+
callbacks: {
|
|
15806
|
+
label: function(context) {
|
|
15807
|
+
return 'Requests/sec: ' + context.parsed.y.toFixed(2);
|
|
15808
|
+
}
|
|
15809
|
+
}
|
|
15810
|
+
}
|
|
15811
|
+
},
|
|
15812
|
+
scales: {
|
|
15813
|
+
y: {
|
|
15814
|
+
beginAtZero: true,
|
|
15815
|
+
border: {
|
|
15816
|
+
display: false
|
|
15817
|
+
},
|
|
15818
|
+
grid: {
|
|
15819
|
+
color: isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
|
|
15820
|
+
drawBorder: false
|
|
15821
|
+
},
|
|
15822
|
+
ticks: {
|
|
15823
|
+
color: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
|
|
15824
|
+
padding: 8,
|
|
15825
|
+
callback: function(value) {
|
|
15826
|
+
return value.toFixed(1);
|
|
15827
|
+
}
|
|
15828
|
+
}
|
|
15829
|
+
},
|
|
15830
|
+
x: {
|
|
15831
|
+
border: {
|
|
15832
|
+
display: false
|
|
15833
|
+
},
|
|
15834
|
+
grid: {
|
|
15835
|
+
display: false
|
|
15836
|
+
},
|
|
15837
|
+
ticks: {
|
|
15838
|
+
color: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
|
|
15839
|
+
padding: 8,
|
|
15840
|
+
maxTicksLimit: 6
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
}
|
|
15844
|
+
}
|
|
15845
|
+
});
|
|
15846
|
+
|
|
15847
|
+
// Listen for metrics updates from HTMX
|
|
15848
|
+
window.addEventListener('htmx:afterRequest', function(event) {
|
|
15849
|
+
if (event.detail.pathInfo.requestPath === '/admin/api/metrics') {
|
|
15850
|
+
try {
|
|
15851
|
+
const metrics = JSON.parse(event.detail.xhr.responseText);
|
|
15852
|
+
|
|
15853
|
+
// Update current RPS display
|
|
15854
|
+
const rpsElement = document.getElementById('current-rps');
|
|
15855
|
+
if (rpsElement) {
|
|
15856
|
+
rpsElement.textContent = metrics.requestsPerSecond.toFixed(2);
|
|
15857
|
+
}
|
|
15858
|
+
|
|
15859
|
+
// Add new data point to chart
|
|
15860
|
+
chart.data.datasets[0].data.shift();
|
|
15861
|
+
chart.data.datasets[0].data.push(metrics.requestsPerSecond);
|
|
15862
|
+
|
|
15863
|
+
// Regenerate labels to maintain -60s to now format
|
|
15864
|
+
const newLabels = [];
|
|
15865
|
+
for (let i = maxDataPoints - 1; i >= 1; i--) {
|
|
15866
|
+
newLabels.push(\`-\${i}s\`);
|
|
15867
|
+
}
|
|
15868
|
+
newLabels.push('now');
|
|
15869
|
+
chart.data.labels = newLabels;
|
|
15870
|
+
|
|
15871
|
+
chart.update('none'); // Update without animation for smoother real-time updates
|
|
15872
|
+
} catch (e) {
|
|
15873
|
+
console.error('Error updating metrics:', e);
|
|
15874
|
+
}
|
|
15875
|
+
}
|
|
15876
|
+
});
|
|
15877
|
+
})();
|
|
15878
|
+
</script>
|
|
15879
|
+
`;
|
|
15880
|
+
}
|
|
15881
|
+
function renderRecentActivitySkeleton() {
|
|
15882
|
+
return `
|
|
15883
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 animate-pulse">
|
|
15884
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
15885
|
+
<div class="h-5 w-32 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
15886
|
+
</div>
|
|
15887
|
+
<div class="px-6 py-6">
|
|
15888
|
+
<div class="space-y-6">
|
|
15889
|
+
${Array(3).fill(0).map(() => `
|
|
15890
|
+
<div class="flex gap-x-4">
|
|
15891
|
+
<div class="h-10 w-10 rounded-full bg-zinc-200 dark:bg-zinc-700"></div>
|
|
15892
|
+
<div class="flex-auto space-y-2">
|
|
15893
|
+
<div class="h-4 w-48 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
15894
|
+
<div class="h-3 w-32 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
15895
|
+
</div>
|
|
15896
|
+
</div>
|
|
15897
|
+
`).join("")}
|
|
15898
|
+
</div>
|
|
15899
|
+
</div>
|
|
15900
|
+
</div>
|
|
15901
|
+
`;
|
|
15902
|
+
}
|
|
15903
|
+
function renderRecentActivity(activities) {
|
|
15904
|
+
const getInitials = (user) => {
|
|
15905
|
+
const parts = user.split(" ").filter((p) => p.length > 0);
|
|
15906
|
+
if (parts.length >= 2) {
|
|
15907
|
+
const first = parts[0]?.[0] || "";
|
|
15908
|
+
const second = parts[1]?.[0] || "";
|
|
15909
|
+
return (first + second).toUpperCase();
|
|
15910
|
+
}
|
|
15911
|
+
return user.substring(0, 2).toUpperCase();
|
|
15912
|
+
};
|
|
15913
|
+
const getRelativeTime = (timestamp) => {
|
|
15914
|
+
const date = new Date(timestamp);
|
|
15915
|
+
const now = /* @__PURE__ */ new Date();
|
|
15916
|
+
const diffMs = now.getTime() - date.getTime();
|
|
15917
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
15918
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
15919
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
15920
|
+
if (diffMins < 1) return "just now";
|
|
15921
|
+
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
|
|
15922
|
+
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
|
|
15923
|
+
return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
|
|
15924
|
+
};
|
|
15925
|
+
const getColorClasses = (type) => {
|
|
15926
|
+
switch (type) {
|
|
15927
|
+
case "content":
|
|
15928
|
+
return {
|
|
15929
|
+
bgColor: "bg-lime-500/10 dark:bg-lime-400/10",
|
|
15930
|
+
textColor: "text-lime-700 dark:text-lime-300"
|
|
15931
|
+
};
|
|
15932
|
+
case "media":
|
|
15933
|
+
return {
|
|
15934
|
+
bgColor: "bg-cyan-500/10 dark:bg-cyan-400/10",
|
|
15935
|
+
textColor: "text-cyan-700 dark:text-cyan-300"
|
|
15936
|
+
};
|
|
15937
|
+
case "user":
|
|
15938
|
+
return {
|
|
15939
|
+
bgColor: "bg-pink-500/10 dark:bg-pink-400/10",
|
|
15940
|
+
textColor: "text-pink-700 dark:text-pink-300"
|
|
15941
|
+
};
|
|
15942
|
+
case "collection":
|
|
15943
|
+
return {
|
|
15944
|
+
bgColor: "bg-purple-500/10 dark:bg-purple-400/10",
|
|
15945
|
+
textColor: "text-purple-700 dark:text-purple-300"
|
|
15946
|
+
};
|
|
15947
|
+
default:
|
|
15948
|
+
return {
|
|
15949
|
+
bgColor: "bg-gray-500/10 dark:bg-gray-400/10",
|
|
15950
|
+
textColor: "text-gray-700 dark:text-gray-300"
|
|
15951
|
+
};
|
|
15952
|
+
}
|
|
15953
|
+
};
|
|
15954
|
+
const formattedActivities = (activities || []).map((activity) => {
|
|
15955
|
+
const colors = getColorClasses(activity.type);
|
|
15956
|
+
return {
|
|
15957
|
+
...activity,
|
|
15958
|
+
initials: getInitials(activity.user),
|
|
15959
|
+
time: getRelativeTime(activity.timestamp),
|
|
15960
|
+
...colors
|
|
15961
|
+
};
|
|
15962
|
+
});
|
|
15963
|
+
if (formattedActivities.length === 0) {
|
|
15964
|
+
formattedActivities.push({
|
|
15965
|
+
type: "content",
|
|
15966
|
+
description: "No recent activity",
|
|
15967
|
+
user: "System",
|
|
15968
|
+
time: "",
|
|
15969
|
+
initials: "SY",
|
|
15970
|
+
bgColor: "bg-gray-500/10 dark:bg-gray-400/10",
|
|
15971
|
+
textColor: "text-gray-700 dark:text-gray-300",
|
|
15972
|
+
id: "0",
|
|
15973
|
+
action: "",
|
|
15974
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15975
|
+
});
|
|
15976
|
+
}
|
|
15977
|
+
return `
|
|
15978
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
15979
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
15980
|
+
<div class="flex items-center justify-between">
|
|
15981
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Recent Activity</h3>
|
|
15982
|
+
<button class="text-xs/5 font-medium text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 transition-colors">
|
|
15983
|
+
View all
|
|
15984
|
+
</button>
|
|
15985
|
+
</div>
|
|
15986
|
+
</div>
|
|
15987
|
+
|
|
15988
|
+
<div class="px-6 py-6">
|
|
15989
|
+
<ul role="list" class="space-y-6">
|
|
15990
|
+
${formattedActivities.map(
|
|
15991
|
+
(activity) => `
|
|
15992
|
+
<li class="relative flex gap-x-4">
|
|
15993
|
+
<div class="flex h-10 w-10 flex-none items-center justify-center rounded-full ${activity.bgColor}">
|
|
15994
|
+
<span class="text-xs font-semibold ${activity.textColor}">${activity.initials}</span>
|
|
15995
|
+
</div>
|
|
15996
|
+
<div class="flex-auto">
|
|
15997
|
+
<p class="text-sm/6 font-medium text-zinc-950 dark:text-white">${activity.description}</p>
|
|
15998
|
+
<p class="mt-1 text-xs/5 text-zinc-500 dark:text-zinc-400">
|
|
15999
|
+
<span class="font-medium text-zinc-950 dark:text-white">${activity.user}</span>
|
|
16000
|
+
<span class="text-zinc-400 dark:text-zinc-500"> \xB7 </span>
|
|
16001
|
+
${activity.time}
|
|
16002
|
+
</p>
|
|
16003
|
+
</div>
|
|
16004
|
+
</li>
|
|
16005
|
+
`
|
|
16006
|
+
).join("")}
|
|
16007
|
+
</ul>
|
|
16008
|
+
</div>
|
|
16009
|
+
</div>
|
|
16010
|
+
`;
|
|
16011
|
+
}
|
|
16012
|
+
function renderQuickActions() {
|
|
16013
|
+
const actions = [
|
|
16014
|
+
{
|
|
16015
|
+
title: "Create Content",
|
|
16016
|
+
description: "Add new blog post or page",
|
|
16017
|
+
href: "/admin/content/new",
|
|
16018
|
+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16019
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
|
16020
|
+
</svg>`
|
|
16021
|
+
},
|
|
16022
|
+
{
|
|
16023
|
+
title: "Upload Media",
|
|
16024
|
+
description: "Add images and files",
|
|
16025
|
+
href: "/admin/media",
|
|
16026
|
+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16027
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
|
16028
|
+
</svg>`
|
|
16029
|
+
},
|
|
16030
|
+
{
|
|
16031
|
+
title: "Manage Users",
|
|
16032
|
+
description: "Add or edit user accounts",
|
|
16033
|
+
href: "/admin/users",
|
|
16034
|
+
icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16035
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
16036
|
+
</svg>`
|
|
16037
|
+
}
|
|
16038
|
+
];
|
|
16039
|
+
return `
|
|
16040
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
16041
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
16042
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Quick Actions</h3>
|
|
16043
|
+
</div>
|
|
16044
|
+
|
|
16045
|
+
<div class="p-6">
|
|
16046
|
+
<div class="space-y-2">
|
|
16047
|
+
${actions.map(
|
|
16048
|
+
(action) => `
|
|
16049
|
+
<a href="${action.href}" class="group flex items-center gap-x-3 rounded-lg px-3 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors">
|
|
16050
|
+
<div class="flex h-10 w-10 flex-none items-center justify-center text-zinc-400 dark:text-zinc-500 group-hover:text-zinc-600 dark:group-hover:text-zinc-400">
|
|
16051
|
+
${action.icon}
|
|
16052
|
+
</div>
|
|
16053
|
+
<div class="flex-auto">
|
|
16054
|
+
<p class="text-sm/6 font-medium text-zinc-950 dark:text-white">${action.title}</p>
|
|
16055
|
+
<p class="text-xs/5 text-zinc-500 dark:text-zinc-400">${action.description}</p>
|
|
16056
|
+
</div>
|
|
16057
|
+
<svg class="h-5 w-5 flex-none text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16058
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
|
|
16059
|
+
</svg>
|
|
16060
|
+
</a>
|
|
16061
|
+
`
|
|
16062
|
+
).join("")}
|
|
16063
|
+
</div>
|
|
16064
|
+
</div>
|
|
16065
|
+
</div>
|
|
16066
|
+
`;
|
|
16067
|
+
}
|
|
16068
|
+
function renderSystemStatus() {
|
|
16069
|
+
return `
|
|
16070
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
|
|
16071
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
16072
|
+
<div class="flex items-center justify-between">
|
|
16073
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">System Status</h3>
|
|
16074
|
+
<div class="flex items-center gap-2">
|
|
16075
|
+
<div class="h-2 w-2 rounded-full bg-lime-500 animate-pulse"></div>
|
|
16076
|
+
<span class="text-xs text-zinc-500 dark:text-zinc-400">Live</span>
|
|
16077
|
+
</div>
|
|
16078
|
+
</div>
|
|
16079
|
+
</div>
|
|
16080
|
+
|
|
16081
|
+
<div
|
|
16082
|
+
id="system-status-container"
|
|
16083
|
+
class="p-6"
|
|
16084
|
+
hx-get="/admin/dashboard/system-status"
|
|
16085
|
+
hx-trigger="load, every 30s"
|
|
16086
|
+
hx-swap="innerHTML"
|
|
16087
|
+
>
|
|
16088
|
+
<!-- Loading skeleton with gradient -->
|
|
16089
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
16090
|
+
${[
|
|
16091
|
+
{ color: "from-blue-500/20 to-cyan-500/20", darkColor: "dark:from-blue-500/10 dark:to-cyan-500/10" },
|
|
16092
|
+
{ color: "from-purple-500/20 to-pink-500/20", darkColor: "dark:from-purple-500/10 dark:to-pink-500/10" },
|
|
16093
|
+
{ color: "from-amber-500/20 to-orange-500/20", darkColor: "dark:from-amber-500/10 dark:to-orange-500/10" },
|
|
16094
|
+
{ color: "from-lime-500/20 to-emerald-500/20", darkColor: "dark:from-lime-500/10 dark:to-emerald-500/10" }
|
|
16095
|
+
].map((gradient, i) => `
|
|
16096
|
+
<div class="relative group">
|
|
16097
|
+
<div class="absolute inset-0 bg-gradient-to-br ${gradient.color} ${gradient.darkColor} rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
16098
|
+
<div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50 animate-pulse">
|
|
16099
|
+
<div class="flex items-center justify-between mb-3">
|
|
16100
|
+
<div class="h-4 w-24 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
16101
|
+
<div class="h-6 w-6 bg-zinc-200 dark:bg-zinc-700 rounded-full"></div>
|
|
16102
|
+
</div>
|
|
16103
|
+
<div class="h-3 w-20 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
|
|
16104
|
+
</div>
|
|
16105
|
+
</div>
|
|
16106
|
+
`).join("")}
|
|
16107
|
+
</div>
|
|
16108
|
+
</div>
|
|
16109
|
+
</div>
|
|
16110
|
+
|
|
16111
|
+
<style>
|
|
16112
|
+
@keyframes ping-slow {
|
|
16113
|
+
0%, 100% { opacity: 1; }
|
|
16114
|
+
50% { opacity: 0.5; }
|
|
16115
|
+
}
|
|
16116
|
+
.animate-ping-slow {
|
|
16117
|
+
animation: ping-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
16118
|
+
}
|
|
16119
|
+
</style>
|
|
16120
|
+
`;
|
|
16121
|
+
}
|
|
16122
|
+
function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
|
|
16123
|
+
const formatBytes = (bytes) => {
|
|
16124
|
+
if (bytes === 0) return "0 B";
|
|
16125
|
+
const k = 1024;
|
|
16126
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
16127
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
16128
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
16129
|
+
};
|
|
16130
|
+
const dbSizeGB = databaseSizeBytes ? databaseSizeBytes / 1024 ** 3 : 0;
|
|
16131
|
+
const dbMaxGB = 10;
|
|
16132
|
+
const dbPercentageRaw = dbSizeGB / dbMaxGB * 100;
|
|
16133
|
+
const dbPercentage = Math.min(Math.max(dbPercentageRaw, 0.5), 100);
|
|
16134
|
+
const dbUsedFormatted = databaseSizeBytes ? formatBytes(databaseSizeBytes) : "Unknown";
|
|
16135
|
+
const mediaUsedFormatted = mediaSizeBytes ? formatBytes(mediaSizeBytes) : "0 B";
|
|
16136
|
+
const storageItems = [
|
|
16137
|
+
{
|
|
16138
|
+
label: "Database",
|
|
16139
|
+
used: dbUsedFormatted,
|
|
16140
|
+
total: "10 GB",
|
|
16141
|
+
percentage: dbPercentage,
|
|
16142
|
+
color: dbPercentage > 80 ? "bg-red-500 dark:bg-red-400" : dbPercentage > 60 ? "bg-amber-500 dark:bg-amber-400" : "bg-cyan-500 dark:bg-cyan-400"
|
|
16143
|
+
},
|
|
16144
|
+
{
|
|
16145
|
+
label: "Media Files",
|
|
16146
|
+
used: mediaUsedFormatted,
|
|
16147
|
+
total: "\u221E",
|
|
16148
|
+
percentage: 0,
|
|
16149
|
+
color: "bg-lime-500 dark:bg-lime-400",
|
|
16150
|
+
note: "Stored in R2"
|
|
16151
|
+
},
|
|
16152
|
+
{
|
|
16153
|
+
label: "Cache (KV)",
|
|
16154
|
+
used: "N/A",
|
|
16155
|
+
total: "\u221E",
|
|
16156
|
+
percentage: 0,
|
|
16157
|
+
color: "bg-purple-500 dark:bg-purple-400",
|
|
16158
|
+
note: "Unlimited"
|
|
16159
|
+
}
|
|
16160
|
+
];
|
|
16161
|
+
return `
|
|
16162
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
16163
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
16164
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Storage Usage</h3>
|
|
16165
|
+
</div>
|
|
16166
|
+
|
|
16167
|
+
<div class="px-6 py-6">
|
|
16168
|
+
<dl class="space-y-6">
|
|
16169
|
+
${storageItems.map(
|
|
16170
|
+
(item) => `
|
|
16171
|
+
<div>
|
|
16172
|
+
<div class="flex items-center justify-between mb-2">
|
|
16173
|
+
<dt class="text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
16174
|
+
${item.label}
|
|
16175
|
+
${item.note ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">(${item.note})</span>` : ""}
|
|
16176
|
+
</dt>
|
|
16177
|
+
<dd class="text-sm/6 font-medium text-zinc-950 dark:text-white">${item.used} / ${item.total}</dd>
|
|
16178
|
+
</div>
|
|
16179
|
+
<div class="w-full bg-zinc-100 dark:bg-zinc-800 rounded-full h-1.5 overflow-hidden">
|
|
16180
|
+
<div class="${item.color} h-full rounded-full transition-all duration-300" style="width: ${item.percentage}%"></div>
|
|
16181
|
+
</div>
|
|
16182
|
+
</div>
|
|
16183
|
+
`
|
|
16184
|
+
).join("")}
|
|
16185
|
+
</dl>
|
|
16186
|
+
</div>
|
|
16187
|
+
</div>
|
|
16188
|
+
`;
|
|
16189
|
+
}
|
|
16190
|
+
|
|
16191
|
+
// src/routes/admin-dashboard.ts
|
|
16192
|
+
var VERSION = getCoreVersion();
|
|
16193
|
+
var router = new Hono();
|
|
16194
|
+
router.use("*", requireAuth());
|
|
16195
|
+
router.get("/", async (c) => {
|
|
16196
|
+
const user = c.get("user");
|
|
16197
|
+
try {
|
|
16198
|
+
const pageData = {
|
|
16199
|
+
user: {
|
|
16200
|
+
name: user.email.split("@")[0] || user.email,
|
|
16201
|
+
email: user.email,
|
|
16202
|
+
role: user.role
|
|
16203
|
+
},
|
|
16204
|
+
version: VERSION
|
|
16205
|
+
};
|
|
16206
|
+
return c.html(renderDashboardPage(pageData));
|
|
16207
|
+
} catch (error) {
|
|
16208
|
+
console.error("Dashboard error:", error);
|
|
16209
|
+
const pageData = {
|
|
16210
|
+
user: {
|
|
16211
|
+
name: user.email,
|
|
16212
|
+
email: user.email,
|
|
16213
|
+
role: user.role
|
|
16214
|
+
},
|
|
16215
|
+
version: VERSION
|
|
16216
|
+
};
|
|
16217
|
+
return c.html(renderDashboardPage(pageData));
|
|
16218
|
+
}
|
|
16219
|
+
});
|
|
16220
|
+
router.get("/dashboard/stats", async (c) => {
|
|
16221
|
+
try {
|
|
16222
|
+
const db = c.env.DB;
|
|
16223
|
+
let collectionsCount = 0;
|
|
16224
|
+
try {
|
|
16225
|
+
const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1");
|
|
16226
|
+
const collectionsResult = await collectionsStmt.first();
|
|
16227
|
+
collectionsCount = collectionsResult?.count || 0;
|
|
16228
|
+
} catch (error) {
|
|
16229
|
+
console.error("Error fetching collections count:", error);
|
|
16230
|
+
}
|
|
16231
|
+
let contentCount = 0;
|
|
16232
|
+
try {
|
|
16233
|
+
const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE deleted_at IS NULL");
|
|
16234
|
+
const contentResult = await contentStmt.first();
|
|
16235
|
+
contentCount = contentResult?.count || 0;
|
|
16236
|
+
} catch (error) {
|
|
16237
|
+
console.error("Error fetching content count:", error);
|
|
16238
|
+
}
|
|
16239
|
+
let mediaCount = 0;
|
|
16240
|
+
let mediaSize = 0;
|
|
16241
|
+
try {
|
|
16242
|
+
const mediaStmt = db.prepare("SELECT COUNT(*) as count, COALESCE(SUM(size), 0) as total_size FROM media WHERE deleted_at IS NULL");
|
|
16243
|
+
const mediaResult = await mediaStmt.first();
|
|
16244
|
+
mediaCount = mediaResult?.count || 0;
|
|
16245
|
+
mediaSize = mediaResult?.total_size || 0;
|
|
16246
|
+
} catch (error) {
|
|
16247
|
+
console.error("Error fetching media count:", error);
|
|
16248
|
+
}
|
|
16249
|
+
let usersCount = 0;
|
|
16250
|
+
try {
|
|
16251
|
+
const usersStmt = db.prepare("SELECT COUNT(*) as count FROM users WHERE is_active = 1");
|
|
16252
|
+
const usersResult = await usersStmt.first();
|
|
16253
|
+
usersCount = usersResult?.count || 0;
|
|
16254
|
+
} catch (error) {
|
|
16255
|
+
console.error("Error fetching users count:", error);
|
|
16256
|
+
}
|
|
16257
|
+
const html9 = renderStatsCards({
|
|
16258
|
+
collections: collectionsCount,
|
|
16259
|
+
contentItems: contentCount,
|
|
16260
|
+
mediaFiles: mediaCount,
|
|
16261
|
+
users: usersCount,
|
|
16262
|
+
mediaSize
|
|
16263
|
+
});
|
|
16264
|
+
return c.html(html9);
|
|
16265
|
+
} catch (error) {
|
|
16266
|
+
console.error("Error fetching stats:", error);
|
|
16267
|
+
return c.html('<div class="text-red-500">Failed to load statistics</div>');
|
|
16268
|
+
}
|
|
16269
|
+
});
|
|
16270
|
+
router.get("/dashboard/storage", async (c) => {
|
|
16271
|
+
try {
|
|
16272
|
+
const db = c.env.DB;
|
|
16273
|
+
let databaseSize = 0;
|
|
16274
|
+
try {
|
|
16275
|
+
const result = await db.prepare("SELECT 1").run();
|
|
16276
|
+
databaseSize = result?.meta?.size_after || 0;
|
|
16277
|
+
} catch (error) {
|
|
16278
|
+
console.error("Error fetching database size:", error);
|
|
16279
|
+
}
|
|
16280
|
+
let mediaSize = 0;
|
|
16281
|
+
try {
|
|
16282
|
+
const mediaStmt = db.prepare("SELECT COALESCE(SUM(size), 0) as total_size FROM media WHERE deleted_at IS NULL");
|
|
16283
|
+
const mediaResult = await mediaStmt.first();
|
|
16284
|
+
mediaSize = mediaResult?.total_size || 0;
|
|
16285
|
+
} catch (error) {
|
|
16286
|
+
console.error("Error fetching media size:", error);
|
|
16287
|
+
}
|
|
16288
|
+
const html9 = renderStorageUsage(databaseSize, mediaSize);
|
|
16289
|
+
return c.html(html9);
|
|
16290
|
+
} catch (error) {
|
|
16291
|
+
console.error("Error fetching storage usage:", error);
|
|
16292
|
+
return c.html('<div class="text-red-500">Failed to load storage information</div>');
|
|
16293
|
+
}
|
|
16294
|
+
});
|
|
16295
|
+
router.get("/dashboard/recent-activity", async (c) => {
|
|
16296
|
+
try {
|
|
16297
|
+
const db = c.env.DB;
|
|
16298
|
+
const limit = parseInt(c.req.query("limit") || "5");
|
|
16299
|
+
const activityStmt = db.prepare(`
|
|
16300
|
+
SELECT
|
|
16301
|
+
a.id,
|
|
16302
|
+
a.action,
|
|
16303
|
+
a.resource_type,
|
|
16304
|
+
a.resource_id,
|
|
16305
|
+
a.details,
|
|
16306
|
+
a.created_at,
|
|
16307
|
+
u.email,
|
|
16308
|
+
u.first_name,
|
|
16309
|
+
u.last_name
|
|
16310
|
+
FROM activity_logs a
|
|
16311
|
+
LEFT JOIN users u ON a.user_id = u.id
|
|
16312
|
+
WHERE a.resource_type IN ('content', 'collections', 'users', 'media')
|
|
16313
|
+
ORDER BY a.created_at DESC
|
|
16314
|
+
LIMIT ?
|
|
16315
|
+
`);
|
|
16316
|
+
const { results } = await activityStmt.bind(limit).all();
|
|
16317
|
+
const activities = (results || []).map((row) => {
|
|
16318
|
+
const userName = row.first_name && row.last_name ? `${row.first_name} ${row.last_name}` : row.email || "System";
|
|
16319
|
+
let description = "";
|
|
16320
|
+
if (row.action === "create") {
|
|
16321
|
+
description = `Created new ${row.resource_type}`;
|
|
16322
|
+
} else if (row.action === "update") {
|
|
16323
|
+
description = `Updated ${row.resource_type}`;
|
|
16324
|
+
} else if (row.action === "delete") {
|
|
16325
|
+
description = `Deleted ${row.resource_type}`;
|
|
16326
|
+
} else {
|
|
16327
|
+
description = `${row.action} ${row.resource_type}`;
|
|
16328
|
+
}
|
|
16329
|
+
return {
|
|
16330
|
+
id: row.id,
|
|
16331
|
+
type: row.resource_type,
|
|
16332
|
+
action: row.action,
|
|
16333
|
+
description,
|
|
16334
|
+
timestamp: new Date(Number(row.created_at)).toISOString(),
|
|
16335
|
+
user: userName
|
|
16336
|
+
};
|
|
16337
|
+
});
|
|
16338
|
+
const html9 = renderRecentActivity(activities);
|
|
16339
|
+
return c.html(html9);
|
|
16340
|
+
} catch (error) {
|
|
16341
|
+
console.error("Error fetching recent activity:", error);
|
|
16342
|
+
const html9 = renderRecentActivity([]);
|
|
16343
|
+
return c.html(html9);
|
|
16344
|
+
}
|
|
16345
|
+
});
|
|
16346
|
+
router.get("/api/metrics", async (c) => {
|
|
16347
|
+
try {
|
|
16348
|
+
const { metricsTracker } = await import('../utils/metrics-tracker');
|
|
16349
|
+
return c.json({
|
|
16350
|
+
requestsPerSecond: metricsTracker.getRequestsPerSecond(),
|
|
16351
|
+
totalRequests: metricsTracker.getTotalRequests(),
|
|
16352
|
+
averageRPS: metricsTracker.getAverageRPS(),
|
|
16353
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
16354
|
+
});
|
|
16355
|
+
} catch (error) {
|
|
16356
|
+
console.error("Error fetching metrics:", error);
|
|
16357
|
+
return c.json({
|
|
16358
|
+
requestsPerSecond: 0,
|
|
16359
|
+
totalRequests: 0,
|
|
16360
|
+
averageRPS: 0,
|
|
16361
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
16362
|
+
});
|
|
16363
|
+
}
|
|
16364
|
+
});
|
|
16365
|
+
router.get("/dashboard/system-status", async (c) => {
|
|
16366
|
+
try {
|
|
16367
|
+
const html9 = `
|
|
16368
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
16369
|
+
<div class="relative group">
|
|
16370
|
+
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 dark:from-blue-500/10 dark:to-cyan-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
16371
|
+
<div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
|
|
16372
|
+
<div class="flex items-center justify-between mb-3">
|
|
16373
|
+
<span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">API Status</span>
|
|
16374
|
+
<svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
|
|
16375
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
16376
|
+
</svg>
|
|
16377
|
+
</div>
|
|
16378
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400">Operational</p>
|
|
16379
|
+
</div>
|
|
16380
|
+
</div>
|
|
16381
|
+
|
|
16382
|
+
<div class="relative group">
|
|
16383
|
+
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-pink-500/20 dark:from-purple-500/10 dark:to-pink-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
16384
|
+
<div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
|
|
16385
|
+
<div class="flex items-center justify-between mb-3">
|
|
16386
|
+
<span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Database</span>
|
|
16387
|
+
<svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
|
|
16388
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
16389
|
+
</svg>
|
|
16390
|
+
</div>
|
|
16391
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400">Connected</p>
|
|
16392
|
+
</div>
|
|
16393
|
+
</div>
|
|
16394
|
+
|
|
16395
|
+
<div class="relative group">
|
|
16396
|
+
<div class="absolute inset-0 bg-gradient-to-br from-amber-500/20 to-orange-500/20 dark:from-amber-500/10 dark:to-orange-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
16397
|
+
<div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
|
|
16398
|
+
<div class="flex items-center justify-between mb-3">
|
|
16399
|
+
<span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">R2 Storage</span>
|
|
16400
|
+
<svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
|
|
16401
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
16402
|
+
</svg>
|
|
16403
|
+
</div>
|
|
16404
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400">Available</p>
|
|
16405
|
+
</div>
|
|
16406
|
+
</div>
|
|
16407
|
+
|
|
16408
|
+
<div class="relative group">
|
|
16409
|
+
<div class="absolute inset-0 bg-gradient-to-br from-lime-500/20 to-emerald-500/20 dark:from-lime-500/10 dark:to-emerald-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
16410
|
+
<div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
|
|
16411
|
+
<div class="flex items-center justify-between mb-3">
|
|
16412
|
+
<span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">KV Cache</span>
|
|
16413
|
+
<svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
|
|
16414
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
16415
|
+
</svg>
|
|
16416
|
+
</div>
|
|
16417
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400">Ready</p>
|
|
16418
|
+
</div>
|
|
16419
|
+
</div>
|
|
16420
|
+
</div>
|
|
16421
|
+
`;
|
|
16422
|
+
return c.html(html9);
|
|
16423
|
+
} catch (error) {
|
|
16424
|
+
console.error("Error fetching system status:", error);
|
|
16425
|
+
return c.html('<div class="text-red-500">Failed to load system status</div>');
|
|
16426
|
+
}
|
|
16427
|
+
});
|
|
16428
|
+
|
|
16429
|
+
// src/templates/pages/admin-collections-list.template.ts
|
|
16430
|
+
init_admin_layout_catalyst_template();
|
|
16431
|
+
|
|
16432
|
+
// src/templates/components/table.template.ts
|
|
16433
|
+
function renderTable2(data) {
|
|
16434
|
+
const tableId = data.tableId || `table-${Math.random().toString(36).substr(2, 9)}`;
|
|
16435
|
+
if (data.rows.length === 0) {
|
|
16436
|
+
return `
|
|
16437
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-8 text-center">
|
|
16438
|
+
<div class="text-zinc-500 dark:text-zinc-400">
|
|
16439
|
+
<svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
16440
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
16441
|
+
</svg>
|
|
16442
|
+
<p class="mt-2 text-sm text-zinc-500 dark:text-zinc-400">${data.emptyMessage || "No data available"}</p>
|
|
16443
|
+
</div>
|
|
16444
|
+
</div>
|
|
16445
|
+
`;
|
|
16446
|
+
}
|
|
16447
|
+
return `
|
|
16448
|
+
<div class="${data.className || ""}" id="${tableId}">
|
|
16449
|
+
${data.title ? `
|
|
16450
|
+
<div class="px-4 sm:px-0 mb-4">
|
|
16451
|
+
<h3 class="text-base font-semibold text-zinc-950 dark:text-white">${data.title}</h3>
|
|
16452
|
+
</div>
|
|
16453
|
+
` : ""}
|
|
16454
|
+
<div class="overflow-x-auto">
|
|
16455
|
+
<table class="min-w-full sortable-table">
|
|
16456
|
+
<thead>
|
|
16457
|
+
<tr>
|
|
16458
|
+
${data.selectable ? `
|
|
16459
|
+
<th class="px-4 py-3.5 text-center sm:pl-0">
|
|
16460
|
+
<div class="flex items-center justify-center">
|
|
16461
|
+
<div class="group grid size-4 grid-cols-1">
|
|
16462
|
+
<input type="checkbox" id="select-all-${tableId}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
|
|
16463
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
|
|
16464
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
16465
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
16466
|
+
</svg>
|
|
16467
|
+
</div>
|
|
16468
|
+
</div>
|
|
16469
|
+
</th>
|
|
16470
|
+
` : ""}
|
|
16471
|
+
${data.columns.map((column, index) => {
|
|
16472
|
+
const isFirst = index === 0 && !data.selectable;
|
|
16473
|
+
const isLast = index === data.columns.length - 1;
|
|
16474
|
+
return `
|
|
16475
|
+
<th class="px-4 py-3.5 text-left text-sm font-semibold text-zinc-950 dark:text-white ${isFirst ? "sm:pl-0" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}">
|
|
16476
|
+
${column.sortable ? `
|
|
16477
|
+
<button
|
|
16478
|
+
class="flex items-center gap-x-2 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors sort-btn text-left"
|
|
16479
|
+
data-column="${column.key}"
|
|
16480
|
+
data-sort-type="${column.sortType || "string"}"
|
|
16481
|
+
data-sort-direction="none"
|
|
16482
|
+
onclick="sortTable('${tableId}', '${column.key}', '${column.sortType || "string"}')"
|
|
16483
|
+
>
|
|
16484
|
+
<span>${column.label}</span>
|
|
16485
|
+
<div class="sort-icons flex flex-col">
|
|
16486
|
+
<svg class="w-3 h-3 sort-up opacity-30" fill="currentColor" viewBox="0 0 20 20">
|
|
16487
|
+
<path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" />
|
|
16488
|
+
</svg>
|
|
16489
|
+
<svg class="w-3 h-3 sort-down opacity-30 -mt-1" fill="currentColor" viewBox="0 0 20 20">
|
|
16490
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
16491
|
+
</svg>
|
|
16492
|
+
</div>
|
|
16493
|
+
</button>
|
|
16494
|
+
` : column.label}
|
|
16495
|
+
</th>
|
|
16496
|
+
`;
|
|
16497
|
+
}).join("")}
|
|
16498
|
+
</tr>
|
|
16499
|
+
</thead>
|
|
16500
|
+
<tbody>
|
|
16501
|
+
${data.rows.map((row, rowIndex) => {
|
|
16502
|
+
if (!row) return "";
|
|
16503
|
+
const clickableClass = data.rowClickable ? "cursor-pointer" : "";
|
|
16504
|
+
const clickHandler = data.rowClickable && data.rowClickUrl ? `onclick="window.location.href='${data.rowClickUrl(row)}'"` : "";
|
|
16505
|
+
return `
|
|
16506
|
+
<tr class="group border-t border-zinc-950/5 dark:border-white/5 hover:bg-gradient-to-r hover:from-cyan-50/50 hover:via-blue-50/30 hover:to-purple-50/50 dark:hover:from-cyan-900/20 dark:hover:via-blue-900/10 dark:hover:to-purple-900/20 hover:shadow-sm hover:shadow-cyan-500/5 dark:hover:shadow-cyan-400/5 transition-all duration-300 ${clickableClass}" ${clickHandler}>
|
|
16507
|
+
${data.selectable ? `
|
|
16508
|
+
<td class="px-4 py-4 sm:pl-0" onclick="event.stopPropagation()">
|
|
16509
|
+
<div class="flex items-center justify-center">
|
|
16510
|
+
<div class="group grid size-4 grid-cols-1">
|
|
16511
|
+
<input type="checkbox" value="${row.id || ""}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
|
|
16512
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
|
|
16513
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
16514
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
16515
|
+
</svg>
|
|
16516
|
+
</div>
|
|
16517
|
+
</div>
|
|
16518
|
+
</td>
|
|
16519
|
+
` : ""}
|
|
16520
|
+
${data.columns.map((column, colIndex) => {
|
|
16521
|
+
const value = row[column.key];
|
|
16522
|
+
const displayValue = column.render ? column.render(value, row) : value;
|
|
16523
|
+
const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
|
|
16524
|
+
const isFirst = colIndex === 0 && !data.selectable;
|
|
16525
|
+
const isLast = colIndex === data.columns.length - 1;
|
|
16526
|
+
return `
|
|
16527
|
+
<td class="px-4 py-4 text-sm text-zinc-500 dark:text-zinc-400 ${isFirst ? "sm:pl-0 font-medium text-zinc-950 dark:text-white" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}" ${stopPropagation}>
|
|
16528
|
+
${displayValue || ""}
|
|
16529
|
+
</td>
|
|
16530
|
+
`;
|
|
16531
|
+
}).join("")}
|
|
16532
|
+
</tr>
|
|
16533
|
+
`;
|
|
16534
|
+
}).join("")}
|
|
16535
|
+
</tbody>
|
|
16536
|
+
</table>
|
|
16537
|
+
</div>
|
|
16538
|
+
|
|
16539
|
+
<script>
|
|
16540
|
+
// Table sorting functionality
|
|
16541
|
+
window.sortTable = function(tableId, column, sortType) {
|
|
16542
|
+
const tableContainer = document.getElementById(tableId);
|
|
16543
|
+
const table = tableContainer.querySelector('.sortable-table');
|
|
16544
|
+
const tbody = table.querySelector('tbody');
|
|
16545
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
16546
|
+
const headerBtn = table.querySelector(\`[data-column="\${column}"]\`);
|
|
16547
|
+
|
|
16548
|
+
// Get current sort direction
|
|
16549
|
+
let direction = headerBtn.getAttribute('data-sort-direction');
|
|
16550
|
+
|
|
16551
|
+
// Reset all sort indicators
|
|
16552
|
+
table.querySelectorAll('.sort-btn').forEach(btn => {
|
|
16553
|
+
btn.setAttribute('data-sort-direction', 'none');
|
|
16554
|
+
btn.querySelectorAll('.sort-up, .sort-down').forEach(icon => {
|
|
16555
|
+
icon.classList.add('opacity-30');
|
|
16556
|
+
icon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
|
|
16557
|
+
});
|
|
16558
|
+
});
|
|
16559
|
+
|
|
16560
|
+
// Determine new direction
|
|
16561
|
+
if (direction === 'none' || direction === 'desc') {
|
|
16562
|
+
direction = 'asc';
|
|
16563
|
+
} else {
|
|
16564
|
+
direction = 'desc';
|
|
16565
|
+
}
|
|
16566
|
+
|
|
16567
|
+
// Update current header
|
|
16568
|
+
headerBtn.setAttribute('data-sort-direction', direction);
|
|
16569
|
+
const upIcon = headerBtn.querySelector('.sort-up');
|
|
16570
|
+
const downIcon = headerBtn.querySelector('.sort-down');
|
|
16571
|
+
|
|
16572
|
+
if (direction === 'asc') {
|
|
16573
|
+
upIcon.classList.remove('opacity-30');
|
|
16574
|
+
upIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
|
|
16575
|
+
downIcon.classList.add('opacity-30');
|
|
16576
|
+
downIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
|
|
16577
|
+
} else {
|
|
16578
|
+
downIcon.classList.remove('opacity-30');
|
|
16579
|
+
downIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
|
|
16580
|
+
upIcon.classList.add('opacity-30');
|
|
16581
|
+
upIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
|
|
16582
|
+
}
|
|
16583
|
+
|
|
16584
|
+
// Find column index (accounting for potential select column)
|
|
16585
|
+
const headers = Array.from(table.querySelectorAll('th'));
|
|
16586
|
+
const selectableOffset = table.querySelector('input[id^="select-all"]') ? 1 : 0;
|
|
16587
|
+
const columnIndex = headers.findIndex(th => th.querySelector(\`[data-column="\${column}"]\`)) - selectableOffset;
|
|
16588
|
+
|
|
16589
|
+
// Sort rows
|
|
16590
|
+
rows.sort((a, b) => {
|
|
16591
|
+
const aCell = a.children[columnIndex + selectableOffset];
|
|
16592
|
+
const bCell = b.children[columnIndex + selectableOffset];
|
|
16593
|
+
|
|
16594
|
+
if (!aCell || !bCell) return 0;
|
|
16595
|
+
|
|
16596
|
+
let aValue = aCell.textContent.trim();
|
|
16597
|
+
let bValue = bCell.textContent.trim();
|
|
16598
|
+
|
|
16599
|
+
// Handle different sort types
|
|
16600
|
+
switch (sortType) {
|
|
16601
|
+
case 'number':
|
|
16602
|
+
aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
|
|
16603
|
+
bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
|
|
16604
|
+
break;
|
|
16605
|
+
case 'date':
|
|
16606
|
+
aValue = new Date(aValue).getTime() || 0;
|
|
16607
|
+
bValue = new Date(bValue).getTime() || 0;
|
|
16608
|
+
break;
|
|
16609
|
+
case 'boolean':
|
|
16610
|
+
aValue = aValue.toLowerCase() === 'true' || aValue.toLowerCase() === 'published' || aValue.toLowerCase() === 'active';
|
|
16611
|
+
bValue = bValue.toLowerCase() === 'true' || bValue.toLowerCase() === 'published' || bValue.toLowerCase() === 'active';
|
|
16612
|
+
break;
|
|
16613
|
+
default: // string
|
|
16614
|
+
aValue = aValue.toLowerCase();
|
|
16615
|
+
bValue = bValue.toLowerCase();
|
|
16616
|
+
}
|
|
16617
|
+
|
|
16618
|
+
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
|
|
16619
|
+
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
|
|
16620
|
+
return 0;
|
|
16621
|
+
});
|
|
16622
|
+
|
|
16623
|
+
// Re-append sorted rows
|
|
16624
|
+
rows.forEach(row => tbody.appendChild(row));
|
|
16625
|
+
};
|
|
16626
|
+
|
|
16627
|
+
// Select all functionality
|
|
16628
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
16629
|
+
document.querySelectorAll('[id^="select-all"]').forEach(selectAll => {
|
|
16630
|
+
selectAll.addEventListener('change', function() {
|
|
16631
|
+
const tableId = this.id.replace('select-all-', '');
|
|
16632
|
+
const table = document.getElementById(tableId);
|
|
16633
|
+
if (table) {
|
|
16634
|
+
const checkboxes = table.querySelectorAll('.row-checkbox');
|
|
16635
|
+
checkboxes.forEach(checkbox => {
|
|
16636
|
+
checkbox.checked = this.checked;
|
|
16637
|
+
});
|
|
16638
|
+
}
|
|
16639
|
+
});
|
|
16640
|
+
});
|
|
16641
|
+
});
|
|
16642
|
+
</script>
|
|
16643
|
+
</div>
|
|
16644
|
+
`;
|
|
16645
|
+
}
|
|
16646
|
+
|
|
16647
|
+
// src/templates/pages/admin-collections-list.template.ts
|
|
16648
|
+
function renderCollectionsListPage(data) {
|
|
16649
|
+
const tableData = {
|
|
16650
|
+
tableId: "collections-table",
|
|
16651
|
+
rowClickable: true,
|
|
16652
|
+
rowClickUrl: (collection) => `/admin/collections/${collection.id}`,
|
|
16653
|
+
columns: [
|
|
16654
|
+
{
|
|
16655
|
+
key: "name",
|
|
16656
|
+
label: "Name",
|
|
16657
|
+
sortable: true,
|
|
16658
|
+
sortType: "string",
|
|
16659
|
+
render: (value, collection) => `
|
|
16660
|
+
<div class="flex items-center gap-2 ml-2">
|
|
16661
|
+
<span class="inline-flex items-center rounded-md bg-cyan-50 dark:bg-cyan-500/10 px-2.5 py-1 text-sm font-medium text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-700/10 dark:ring-cyan-400/20">
|
|
16662
|
+
${collection.name}
|
|
16663
|
+
</span>
|
|
16664
|
+
${collection.managed ? `
|
|
16665
|
+
<span class="inline-flex items-center rounded-md bg-purple-50 dark:bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-700/10 dark:ring-purple-400/20" title="Config-managed collection (read-only in UI)">
|
|
16666
|
+
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
16667
|
+
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
|
16668
|
+
</svg>
|
|
16669
|
+
Config
|
|
16670
|
+
</span>
|
|
16671
|
+
` : ""}
|
|
16672
|
+
</div>
|
|
16673
|
+
`
|
|
16674
|
+
},
|
|
16675
|
+
{
|
|
16676
|
+
key: "display_name",
|
|
16677
|
+
label: "Display Name",
|
|
16678
|
+
sortable: true,
|
|
16679
|
+
sortType: "string"
|
|
16680
|
+
},
|
|
16681
|
+
{
|
|
16682
|
+
key: "description",
|
|
16683
|
+
label: "Description",
|
|
16684
|
+
sortable: true,
|
|
16685
|
+
sortType: "string",
|
|
16686
|
+
render: (value, collection) => collection.description || '<span class="text-zinc-500 dark:text-zinc-400">-</span>'
|
|
16687
|
+
},
|
|
16688
|
+
{
|
|
16689
|
+
key: "field_count",
|
|
16690
|
+
label: "Fields",
|
|
16691
|
+
sortable: true,
|
|
16692
|
+
sortType: "number",
|
|
16693
|
+
render: (value, collection) => {
|
|
16694
|
+
const count = collection.field_count || 0;
|
|
16695
|
+
return `
|
|
16696
|
+
<div class="flex items-center">
|
|
16697
|
+
<span class="inline-flex items-center rounded-md bg-pink-50 dark:bg-pink-500/10 px-2.5 py-1 text-sm font-medium text-pink-700 dark:text-pink-300 ring-1 ring-inset ring-pink-700/10 dark:ring-pink-400/20">
|
|
16698
|
+
${count} ${count === 1 ? "field" : "fields"}
|
|
16699
|
+
</span>
|
|
16700
|
+
</div>
|
|
16701
|
+
`;
|
|
16702
|
+
}
|
|
16703
|
+
},
|
|
16704
|
+
{
|
|
16705
|
+
key: "managed",
|
|
16706
|
+
label: "Source",
|
|
16707
|
+
sortable: true,
|
|
16708
|
+
sortType: "string",
|
|
16709
|
+
render: (value, collection) => {
|
|
16710
|
+
if (collection.managed) {
|
|
16711
|
+
return `
|
|
16712
|
+
<div class="flex items-center gap-1.5">
|
|
16713
|
+
<svg class="w-4 h-4 text-purple-600 dark:text-purple-400" fill="currentColor" viewBox="0 0 20 20">
|
|
16714
|
+
<path fill-rule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
16715
|
+
</svg>
|
|
16716
|
+
<span class="text-sm text-zinc-700 dark:text-zinc-300">Code</span>
|
|
16717
|
+
</div>
|
|
16718
|
+
`;
|
|
16719
|
+
} else {
|
|
16720
|
+
return `
|
|
16721
|
+
<div class="flex items-center gap-1.5">
|
|
16722
|
+
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
|
16723
|
+
<path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z"/>
|
|
16724
|
+
<path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z"/>
|
|
16725
|
+
<path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z"/>
|
|
16726
|
+
</svg>
|
|
16727
|
+
<span class="text-sm text-zinc-700 dark:text-zinc-300">Database</span>
|
|
16728
|
+
</div>
|
|
16729
|
+
`;
|
|
16730
|
+
}
|
|
16731
|
+
}
|
|
16732
|
+
},
|
|
16733
|
+
{
|
|
16734
|
+
key: "formattedDate",
|
|
16735
|
+
label: "Created",
|
|
16736
|
+
sortable: true,
|
|
16737
|
+
sortType: "date"
|
|
16738
|
+
},
|
|
16739
|
+
{
|
|
16740
|
+
key: "actions",
|
|
16741
|
+
label: "Content",
|
|
16742
|
+
sortable: false,
|
|
16743
|
+
render: (value, collection) => {
|
|
16744
|
+
if (!collection || !collection.id) return '<span class="text-zinc-500 dark:text-zinc-400">-</span>';
|
|
16745
|
+
return `
|
|
16746
|
+
<div class="flex items-center space-x-2">
|
|
16747
|
+
<a href="/admin/content?model=${collection.name}" class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-lg bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors">
|
|
16748
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16749
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
16750
|
+
</svg>
|
|
16751
|
+
</a>
|
|
16752
|
+
</div>
|
|
16753
|
+
`;
|
|
16754
|
+
}
|
|
16755
|
+
}
|
|
16756
|
+
],
|
|
16757
|
+
rows: data.collections,
|
|
16758
|
+
emptyMessage: "No collections found."
|
|
16759
|
+
};
|
|
16760
|
+
const pageContent = `
|
|
16761
|
+
<div>
|
|
16762
|
+
<!-- Header -->
|
|
16763
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
16764
|
+
<div>
|
|
16765
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Collections</h1>
|
|
16766
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage your content collections and their schemas</p>
|
|
16767
|
+
</div>
|
|
16768
|
+
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
16769
|
+
<a href="/admin/collections/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
16770
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
16771
|
+
<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" />
|
|
16772
|
+
</svg>
|
|
16773
|
+
New Collection
|
|
16774
|
+
</a>
|
|
16775
|
+
</div>
|
|
16776
|
+
</div>
|
|
16777
|
+
|
|
16778
|
+
<!-- Filters -->
|
|
16779
|
+
<div class="relative rounded-xl overflow-hidden mb-6">
|
|
16780
|
+
<!-- Gradient Background -->
|
|
16781
|
+
<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>
|
|
16782
|
+
|
|
16783
|
+
<div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
16784
|
+
<div class="px-6 py-5">
|
|
16785
|
+
<div class="flex items-center justify-between">
|
|
16786
|
+
<div class="flex items-center space-x-4">
|
|
16787
|
+
<form onsubmit="performSearch(event)" class="flex items-center space-x-2">
|
|
16788
|
+
<div class="relative group">
|
|
16789
|
+
<input
|
|
16790
|
+
id="collections-search"
|
|
16791
|
+
type="text"
|
|
16792
|
+
placeholder="Search collections..."
|
|
16793
|
+
value="${data.search || ""}"
|
|
16794
|
+
oninput="toggleClearButton()"
|
|
16795
|
+
class="rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm w-72 text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
|
|
16796
|
+
>
|
|
16797
|
+
<div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
|
|
16798
|
+
<svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
16799
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
16800
|
+
</svg>
|
|
16801
|
+
</div>
|
|
16802
|
+
<button
|
|
16803
|
+
type="button"
|
|
16804
|
+
id="clear-search"
|
|
16805
|
+
onclick="clearSearch()"
|
|
16806
|
+
class="${data.search ? "" : "hidden"} absolute right-3 top-3 flex items-center justify-center w-5 h-5 rounded-full bg-zinc-400/20 dark:bg-zinc-500/20 hover:bg-zinc-400/30 dark:hover:bg-zinc-500/30 transition-colors"
|
|
16807
|
+
>
|
|
16808
|
+
<svg class="h-3 w-3 text-zinc-600 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
16809
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
16810
|
+
</svg>
|
|
16811
|
+
</button>
|
|
16812
|
+
</div>
|
|
16813
|
+
<button
|
|
16814
|
+
type="submit"
|
|
16815
|
+
class="inline-flex items-center gap-x-1.5 px-4 py-2.5 bg-gradient-to-r from-cyan-500 to-blue-500 dark:from-cyan-400 dark:to-blue-400 text-white text-sm font-medium rounded-full hover:from-cyan-600 hover:to-blue-600 dark:hover:from-cyan-500 dark:hover:to-blue-500 shadow-md hover:shadow-lg transition-all duration-200"
|
|
16816
|
+
>
|
|
16817
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
16818
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
16819
|
+
</svg>
|
|
16820
|
+
Search
|
|
16821
|
+
</button>
|
|
16822
|
+
</form>
|
|
16823
|
+
<script>
|
|
16824
|
+
function performSearch(event) {
|
|
16825
|
+
event.preventDefault();
|
|
16826
|
+
const searchInput = document.getElementById('collections-search');
|
|
16827
|
+
const value = searchInput.value.trim();
|
|
16828
|
+
const params = new URLSearchParams(window.location.search);
|
|
16829
|
+
if (value) {
|
|
16830
|
+
params.set('search', value);
|
|
16831
|
+
} else {
|
|
16832
|
+
params.delete('search');
|
|
16833
|
+
}
|
|
16834
|
+
window.location.href = window.location.pathname + '?' + params.toString();
|
|
16835
|
+
}
|
|
16836
|
+
|
|
16837
|
+
function clearSearch() {
|
|
16838
|
+
const params = new URLSearchParams(window.location.search);
|
|
16839
|
+
params.delete('search');
|
|
16840
|
+
window.location.href = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
|
|
16841
|
+
}
|
|
16842
|
+
|
|
16843
|
+
function toggleClearButton() {
|
|
16844
|
+
const searchInput = document.getElementById('collections-search');
|
|
16845
|
+
const clearButton = document.getElementById('clear-search');
|
|
16846
|
+
if (searchInput.value.trim()) {
|
|
16847
|
+
clearButton.classList.remove('hidden');
|
|
16848
|
+
} else {
|
|
16849
|
+
clearButton.classList.add('hidden');
|
|
16850
|
+
}
|
|
16851
|
+
}
|
|
16852
|
+
</script>
|
|
16853
|
+
</div>
|
|
16854
|
+
<div class="flex items-center gap-x-3">
|
|
16855
|
+
<span class="text-sm/6 font-medium text-zinc-700 dark:text-zinc-300 px-3 py-1.5 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-sm">${data.collections.length} ${data.collections.length === 1 ? "collection" : "collections"}</span>
|
|
16856
|
+
<button
|
|
16857
|
+
class="inline-flex items-center gap-x-1.5 px-3 py-1.5 bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm text-zinc-950 dark:text-white text-sm font-medium rounded-full ring-1 ring-inset ring-cyan-200/50 dark:ring-cyan-700/50 hover:bg-gradient-to-r hover:from-cyan-50 hover:to-blue-50 dark:hover:from-cyan-900/30 dark:hover:to-blue-900/30 hover:ring-cyan-300 dark:hover:ring-cyan-600 transition-all duration-200"
|
|
16858
|
+
onclick="location.reload()"
|
|
16859
|
+
>
|
|
16860
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16861
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
16862
|
+
</svg>
|
|
16863
|
+
Refresh
|
|
16864
|
+
</button>
|
|
16865
|
+
</div>
|
|
16866
|
+
</div>
|
|
16867
|
+
</div>
|
|
16868
|
+
</div>
|
|
16869
|
+
</div>
|
|
16870
|
+
|
|
16871
|
+
<!-- Collections List -->
|
|
16872
|
+
<div id="collections-list">
|
|
16873
|
+
${renderTable2(tableData)}
|
|
16874
|
+
</div>
|
|
16875
|
+
|
|
16876
|
+
<!-- Empty State -->
|
|
16877
|
+
${data.collections.length === 0 ? `
|
|
16878
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-12 text-center">
|
|
16879
|
+
<svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
16880
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
16881
|
+
</svg>
|
|
16882
|
+
<h3 class="mt-4 text-base/7 font-semibold text-zinc-950 dark:text-white">No collections found</h3>
|
|
16883
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Get started by creating your first collection</p>
|
|
16884
|
+
<div class="mt-6">
|
|
16885
|
+
<a href="/admin/collections/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
16886
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
16887
|
+
<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" />
|
|
16888
|
+
</svg>
|
|
16889
|
+
New Collection
|
|
16890
|
+
</a>
|
|
16891
|
+
</div>
|
|
16892
|
+
</div>
|
|
16893
|
+
` : ""}
|
|
16894
|
+
</div>
|
|
16895
|
+
`;
|
|
16896
|
+
const layoutData = {
|
|
16897
|
+
title: "Collections",
|
|
16898
|
+
pageTitle: "Collections",
|
|
16899
|
+
currentPath: "/admin/collections",
|
|
16900
|
+
user: data.user,
|
|
16901
|
+
version: data.version,
|
|
16902
|
+
content: pageContent
|
|
16903
|
+
};
|
|
16904
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
16905
|
+
}
|
|
16906
|
+
|
|
16907
|
+
// src/templates/pages/admin-collections-form.template.ts
|
|
16908
|
+
init_admin_layout_catalyst_template();
|
|
16909
|
+
function renderCollectionFormPage(data) {
|
|
16910
|
+
const isEdit = data.isEdit || !!data.id;
|
|
16911
|
+
const title = isEdit ? "Edit Collection" : "Create New Collection";
|
|
16912
|
+
const subtitle = isEdit ? `Update collection: ${data.display_name}` : "Define a new content collection with custom fields and settings.";
|
|
16913
|
+
const fields = [
|
|
16914
|
+
{
|
|
16915
|
+
name: "displayName",
|
|
16916
|
+
label: "Display Name",
|
|
16917
|
+
type: "text",
|
|
16918
|
+
value: data.display_name || "",
|
|
16919
|
+
placeholder: "Blog Posts",
|
|
16920
|
+
required: true,
|
|
16921
|
+
readonly: data.managed,
|
|
16922
|
+
className: data.managed ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
|
|
16923
|
+
},
|
|
16924
|
+
{
|
|
16925
|
+
name: "name",
|
|
16926
|
+
label: "Collection Name",
|
|
16927
|
+
type: "text",
|
|
16928
|
+
value: data.name || "",
|
|
16929
|
+
placeholder: "blog_posts",
|
|
16930
|
+
required: true,
|
|
16931
|
+
readonly: isEdit,
|
|
16932
|
+
helpText: isEdit ? "Collection name cannot be changed" : "Lowercase letters, numbers, and underscores only",
|
|
16933
|
+
className: isEdit ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
|
|
16934
|
+
},
|
|
16935
|
+
{
|
|
16936
|
+
name: "description",
|
|
16937
|
+
label: "Description",
|
|
16938
|
+
type: "textarea",
|
|
16939
|
+
value: data.description || "",
|
|
16940
|
+
placeholder: "Description of this collection...",
|
|
16941
|
+
rows: 3,
|
|
16942
|
+
readonly: data.managed,
|
|
16943
|
+
className: data.managed ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
|
|
16944
|
+
}
|
|
16945
|
+
];
|
|
16946
|
+
const formData = {
|
|
16947
|
+
id: "collection-form",
|
|
16948
|
+
...isEdit ? { hxPut: `/admin/collections/${data.id}`, action: `/admin/collections/${data.id}`, method: "PUT" } : { hxPost: "/admin/collections", action: "/admin/collections" },
|
|
16949
|
+
hxTarget: "#form-messages",
|
|
16950
|
+
fields,
|
|
16951
|
+
submitButtons: data.managed ? [] : [
|
|
16952
|
+
{
|
|
16953
|
+
label: isEdit ? "Update Collection" : "Create Collection",
|
|
16954
|
+
type: "submit",
|
|
16955
|
+
className: "btn-primary"
|
|
16956
|
+
}
|
|
16957
|
+
]
|
|
16958
|
+
};
|
|
16959
|
+
const pageContent = `
|
|
16960
|
+
<div class="space-y-6">
|
|
16961
|
+
<!-- Config-Managed Collection Banner -->
|
|
16962
|
+
${data.managed ? `
|
|
16963
|
+
<div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900/30 p-4">
|
|
16964
|
+
<div class="flex items-start gap-x-3">
|
|
16965
|
+
<svg class="h-5 w-5 text-amber-600 dark:text-amber-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
16966
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
|
16967
|
+
</svg>
|
|
16968
|
+
<div class="flex-1">
|
|
16969
|
+
<h3 class="text-sm/6 font-semibold text-amber-900 dark:text-amber-300">
|
|
16970
|
+
Config-Managed Collection
|
|
16971
|
+
</h3>
|
|
16972
|
+
<div class="text-sm/6 text-amber-800 dark:text-amber-400 mt-1 space-y-1">
|
|
16973
|
+
<p>This collection is managed by a configuration file and cannot be edited through the admin interface.</p>
|
|
16974
|
+
<p class="mt-2">
|
|
16975
|
+
<span class="font-medium">Config file:</span>
|
|
16976
|
+
<code class="ml-2 px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-900/40 text-amber-900 dark:text-amber-300 font-mono text-xs">
|
|
16977
|
+
src/collections/${data.name}.collection.ts
|
|
16978
|
+
</code>
|
|
16979
|
+
</p>
|
|
16980
|
+
<p class="mt-2 text-xs">
|
|
16981
|
+
To modify this collection's schema, edit the configuration file directly in your code editor.
|
|
16982
|
+
</p>
|
|
16983
|
+
</div>
|
|
16984
|
+
</div>
|
|
16985
|
+
</div>
|
|
16986
|
+
</div>
|
|
16987
|
+
` : ""}
|
|
16988
|
+
|
|
16989
|
+
<!-- Header -->
|
|
16990
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
16991
|
+
<div>
|
|
16992
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">${title}</h1>
|
|
16993
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">${subtitle}</p>
|
|
16994
|
+
</div>
|
|
16995
|
+
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
16996
|
+
<a href="/admin/collections" 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">
|
|
16997
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16998
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
16999
|
+
</svg>
|
|
17000
|
+
Back to Collections
|
|
17001
|
+
</a>
|
|
17002
|
+
</div>
|
|
17003
|
+
</div>
|
|
17004
|
+
|
|
17005
|
+
<!-- Form Container -->
|
|
17006
|
+
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
|
|
17007
|
+
<!-- Form Header -->
|
|
17008
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
|
|
17009
|
+
<div class="flex items-center gap-x-3">
|
|
17010
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-50 dark:bg-zinc-800 ring-1 ring-zinc-950/10 dark:ring-white/10">
|
|
17011
|
+
<svg class="h-6 w-6 text-zinc-950 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17012
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
17013
|
+
</svg>
|
|
17014
|
+
</div>
|
|
17015
|
+
<div>
|
|
17016
|
+
<h2 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Collection Details</h2>
|
|
17017
|
+
<p class="text-sm/6 text-zinc-500 dark:text-zinc-400">Configure your collection settings below</p>
|
|
17018
|
+
</div>
|
|
17019
|
+
</div>
|
|
17020
|
+
</div>
|
|
17021
|
+
|
|
17022
|
+
<!-- Form Content -->
|
|
17023
|
+
<div class="px-6 py-6">
|
|
17024
|
+
<div id="form-messages"></div>
|
|
17025
|
+
${data.error ? renderAlert2({ type: "error", message: data.error, dismissible: true }) : ""}
|
|
17026
|
+
${data.success ? renderAlert2({ type: "success", message: data.success, dismissible: true }) : ""}
|
|
17027
|
+
|
|
17028
|
+
<!-- Form Styling -->
|
|
17029
|
+
<style>
|
|
17030
|
+
#collection-form .form-group {
|
|
17031
|
+
margin-bottom: 1.5rem;
|
|
17032
|
+
}
|
|
17033
|
+
|
|
17034
|
+
#collection-form .form-label {
|
|
17035
|
+
display: block;
|
|
17036
|
+
font-size: 0.875rem;
|
|
17037
|
+
font-weight: 500;
|
|
17038
|
+
margin-bottom: 0.5rem;
|
|
17039
|
+
line-height: 1.5rem;
|
|
17040
|
+
}
|
|
17041
|
+
|
|
17042
|
+
.dark #collection-form .form-label {
|
|
17043
|
+
color: white;
|
|
17044
|
+
}
|
|
17045
|
+
|
|
17046
|
+
html:not(.dark) #collection-form .form-label {
|
|
17047
|
+
color: #09090b; /* zinc-950 */
|
|
17048
|
+
}
|
|
17049
|
+
|
|
17050
|
+
#collection-form .form-input,
|
|
17051
|
+
#collection-form .form-textarea {
|
|
17052
|
+
width: 100%;
|
|
17053
|
+
padding: 0.625rem 0.75rem;
|
|
17054
|
+
border-radius: 0.5rem;
|
|
17055
|
+
font-size: 0.875rem;
|
|
17056
|
+
line-height: 1.5rem;
|
|
17057
|
+
transition: all 0.15s;
|
|
17058
|
+
}
|
|
17059
|
+
|
|
17060
|
+
html:not(.dark) #collection-form .form-input,
|
|
17061
|
+
html:not(.dark) #collection-form .form-textarea {
|
|
17062
|
+
background: white;
|
|
17063
|
+
border: 1px solid rgba(9, 9, 11, 0.1); /* zinc-950/10 */
|
|
17064
|
+
color: #09090b; /* zinc-950 */
|
|
17065
|
+
}
|
|
17066
|
+
|
|
17067
|
+
.dark #collection-form .form-input,
|
|
17068
|
+
.dark #collection-form .form-textarea {
|
|
17069
|
+
background: #18181b; /* zinc-900 */
|
|
17070
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
17071
|
+
color: white;
|
|
17072
|
+
}
|
|
17073
|
+
|
|
17074
|
+
#collection-form .form-input:focus,
|
|
17075
|
+
#collection-form .form-textarea:focus {
|
|
17076
|
+
outline: none;
|
|
17077
|
+
box-shadow: 0 0 0 2px #2563eb; /* blue-600 */
|
|
17078
|
+
}
|
|
17079
|
+
|
|
17080
|
+
.dark #collection-form .form-input:focus,
|
|
17081
|
+
.dark #collection-form .form-textarea:focus {
|
|
17082
|
+
box-shadow: 0 0 0 2px #3b82f6; /* blue-500 */
|
|
17083
|
+
}
|
|
17084
|
+
|
|
17085
|
+
html:not(.dark) #collection-form .form-input::placeholder,
|
|
17086
|
+
html:not(.dark) #collection-form .form-textarea::placeholder {
|
|
17087
|
+
color: #71717a; /* zinc-500 */
|
|
17088
|
+
}
|
|
17089
|
+
|
|
17090
|
+
.dark #collection-form .form-input::placeholder,
|
|
17091
|
+
.dark #collection-form .form-textarea::placeholder {
|
|
17092
|
+
color: #71717a; /* zinc-500 */
|
|
17093
|
+
}
|
|
17094
|
+
|
|
17095
|
+
#collection-form .btn {
|
|
17096
|
+
padding: 0.625rem 1rem;
|
|
17097
|
+
font-weight: 600;
|
|
17098
|
+
font-size: 0.875rem;
|
|
17099
|
+
border-radius: 0.5rem;
|
|
17100
|
+
transition: all 0.15s;
|
|
17101
|
+
border: none;
|
|
17102
|
+
cursor: pointer;
|
|
17103
|
+
}
|
|
17104
|
+
|
|
17105
|
+
html:not(.dark) #collection-form .btn-primary {
|
|
17106
|
+
background: #09090b; /* zinc-950 */
|
|
17107
|
+
color: white;
|
|
17108
|
+
}
|
|
17109
|
+
|
|
17110
|
+
html:not(.dark) #collection-form .btn-primary:hover {
|
|
17111
|
+
background: #27272a; /* zinc-800 */
|
|
17112
|
+
}
|
|
17113
|
+
|
|
17114
|
+
.dark #collection-form .btn-primary {
|
|
17115
|
+
background: white;
|
|
17116
|
+
color: #09090b; /* zinc-950 */
|
|
17117
|
+
}
|
|
17118
|
+
|
|
17119
|
+
.dark #collection-form .btn-primary:hover {
|
|
17120
|
+
background: #f4f4f5; /* zinc-100 */
|
|
17121
|
+
}
|
|
17122
|
+
|
|
17123
|
+
#collection-form .form-help-text {
|
|
17124
|
+
font-size: 0.75rem;
|
|
17125
|
+
margin-top: 0.25rem;
|
|
17126
|
+
}
|
|
17127
|
+
|
|
17128
|
+
html:not(.dark) #collection-form .form-help-text {
|
|
17129
|
+
color: #71717a; /* zinc-500 */
|
|
17130
|
+
}
|
|
17131
|
+
|
|
17132
|
+
.dark #collection-form .form-help-text {
|
|
17133
|
+
color: #a1a1aa; /* zinc-400 */
|
|
17134
|
+
}
|
|
17135
|
+
</style>
|
|
17136
|
+
|
|
17137
|
+
${renderForm(formData)}
|
|
17138
|
+
|
|
17139
|
+
${isEdit && !data.managed ? `
|
|
17140
|
+
<!-- Fields Management Section -->
|
|
17141
|
+
<div class="mt-8 pt-8 border-t border-zinc-950/5 dark:border-white/10">
|
|
17142
|
+
<div class="flex items-center justify-between mb-6">
|
|
17143
|
+
<div>
|
|
17144
|
+
<h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Collection Fields</h3>
|
|
17145
|
+
<p class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">Define the fields that content in this collection will have</p>
|
|
17146
|
+
</div>
|
|
17147
|
+
<button
|
|
17148
|
+
type="button"
|
|
17149
|
+
onclick="showAddFieldModal()"
|
|
17150
|
+
class="inline-flex items-center gap-x-1.5 px-3.5 py-2.5 bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 font-semibold text-sm rounded-lg hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
|
|
17151
|
+
>
|
|
17152
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
17153
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
|
|
17154
|
+
</svg>
|
|
17155
|
+
Add Field
|
|
17156
|
+
</button>
|
|
17157
|
+
</div>
|
|
17158
|
+
|
|
17159
|
+
<!-- Fields List -->
|
|
17160
|
+
<div id="fields-list" class="space-y-3">
|
|
17161
|
+
${(data.fields || []).map((field) => `
|
|
17162
|
+
<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" data-field-id="${field.id}">
|
|
17163
|
+
<div class="flex items-center justify-between">
|
|
17164
|
+
<div class="flex items-center gap-x-4">
|
|
17165
|
+
<div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
|
|
17166
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
17167
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
|
|
17168
|
+
</svg>
|
|
17169
|
+
</div>
|
|
17170
|
+
<div>
|
|
17171
|
+
<div class="flex items-center gap-x-2">
|
|
17172
|
+
<span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
|
|
17173
|
+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-500/20 dark:ring-cyan-400/20">
|
|
17174
|
+
${field.field_type}
|
|
17175
|
+
</span>
|
|
17176
|
+
${field.is_required ? `
|
|
17177
|
+
<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">
|
|
17178
|
+
Required
|
|
17179
|
+
</span>
|
|
17180
|
+
` : ""}
|
|
17181
|
+
</div>
|
|
17182
|
+
<div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
|
|
17183
|
+
Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
|
|
17184
|
+
</div>
|
|
17185
|
+
</div>
|
|
17186
|
+
</div>
|
|
17187
|
+
<div class="flex items-center gap-x-2">
|
|
17188
|
+
<button
|
|
17189
|
+
type="button"
|
|
17190
|
+
onclick="editField('${field.id}')"
|
|
17191
|
+
class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-sm font-medium text-zinc-950 dark:text-white hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-lg transition-colors"
|
|
17192
|
+
>
|
|
17193
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17194
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>
|
|
17195
|
+
</svg>
|
|
17196
|
+
Edit
|
|
17197
|
+
</button>
|
|
17198
|
+
<button
|
|
17199
|
+
type="button"
|
|
17200
|
+
onclick="deleteField('${field.id}')"
|
|
17201
|
+
class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-sm font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors"
|
|
17202
|
+
>
|
|
17203
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17204
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
|
|
17205
|
+
</svg>
|
|
17206
|
+
Delete
|
|
17207
|
+
</button>
|
|
17208
|
+
</div>
|
|
17209
|
+
</div>
|
|
17210
|
+
</div>
|
|
17211
|
+
`).join("")}
|
|
17212
|
+
|
|
17213
|
+
${(data.fields || []).length === 0 ? `
|
|
17214
|
+
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
|
17215
|
+
<svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17216
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/>
|
|
17217
|
+
</svg>
|
|
17218
|
+
<p class="mt-4 text-base/7 font-semibold text-zinc-950 dark:text-white">No fields defined</p>
|
|
17219
|
+
<p class="mt-2 text-sm/6">Add your first field to get started</p>
|
|
17220
|
+
</div>
|
|
17221
|
+
` : ""}
|
|
17222
|
+
</div>
|
|
17223
|
+
</div>
|
|
17224
|
+
` : `
|
|
17225
|
+
<div class="mt-6 rounded-lg bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-100 dark:border-cyan-900/30 p-4">
|
|
17226
|
+
<div class="flex items-start gap-x-3">
|
|
17227
|
+
<svg class="h-5 w-5 text-cyan-600 dark:text-cyan-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
17228
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
|
17229
|
+
</svg>
|
|
17230
|
+
<div>
|
|
17231
|
+
<h3 class="text-sm/6 font-medium text-cyan-900 dark:text-cyan-300">
|
|
17232
|
+
Create Collection First
|
|
17233
|
+
</h3>
|
|
17234
|
+
<p class="text-sm/6 text-cyan-800 dark:text-cyan-400 mt-1">
|
|
17235
|
+
After creating the collection, you'll be able to add and configure custom fields.
|
|
17236
|
+
</p>
|
|
17237
|
+
</div>
|
|
17238
|
+
</div>
|
|
17239
|
+
</div>
|
|
17240
|
+
`}
|
|
17241
|
+
|
|
17242
|
+
<!-- Action Buttons -->
|
|
17243
|
+
<div class="mt-6 pt-6 border-t border-zinc-950/5 dark:border-white/10 flex items-center justify-between">
|
|
17244
|
+
<a href="/admin/collections" class="inline-flex items-center justify-center gap-x-1.5 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">
|
|
17245
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17246
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
17247
|
+
</svg>
|
|
17248
|
+
${data.managed ? "Back to Collections" : "Cancel"}
|
|
17249
|
+
</a>
|
|
17250
|
+
|
|
17251
|
+
${isEdit && !data.managed ? `
|
|
17252
|
+
<button
|
|
17253
|
+
type="button"
|
|
17254
|
+
hx-delete="/admin/collections/${data.id}"
|
|
17255
|
+
hx-confirm="Are you sure you want to delete this collection? This action cannot be undone."
|
|
17256
|
+
hx-target="body"
|
|
17257
|
+
class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-pink-600 dark:bg-pink-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-pink-700 dark:hover:bg-pink-600 transition-colors shadow-sm"
|
|
17258
|
+
>
|
|
17259
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
17260
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
|
|
17261
|
+
</svg>
|
|
17262
|
+
Delete Collection
|
|
17263
|
+
</button>
|
|
17264
|
+
` : ""}
|
|
17265
|
+
</div>
|
|
17266
|
+
</div>
|
|
17267
|
+
</div>
|
|
17268
|
+
</div>
|
|
17269
|
+
|
|
17270
|
+
<!-- Add/Edit Field Modal -->
|
|
17271
|
+
<div id="field-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
|
17272
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 w-full max-w-lg mx-4">
|
|
17273
|
+
<div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
|
|
17274
|
+
<div class="flex items-center justify-between">
|
|
17275
|
+
<h3 id="modal-title" class="text-lg font-semibold text-zinc-950 dark:text-white">Add Field</h3>
|
|
17276
|
+
<button onclick="closeFieldModal()" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-950 dark:hover:text-white transition-colors">
|
|
17277
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
17278
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
17279
|
+
</svg>
|
|
17280
|
+
</button>
|
|
17281
|
+
</div>
|
|
17282
|
+
</div>
|
|
17283
|
+
|
|
17284
|
+
<form id="field-form" class="p-6 space-y-4">
|
|
17285
|
+
<input type="hidden" id="field-id" name="field_id">
|
|
17286
|
+
|
|
17287
|
+
<div>
|
|
17288
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
|
|
17289
|
+
<input
|
|
17290
|
+
type="text"
|
|
17291
|
+
id="field-name"
|
|
17292
|
+
name="field_name"
|
|
17293
|
+
required
|
|
17294
|
+
pattern="[a-z0-9_]+"
|
|
17295
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors"
|
|
17296
|
+
placeholder="field_name"
|
|
17297
|
+
>
|
|
17298
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Lowercase letters, numbers, and underscores only</p>
|
|
17299
|
+
</div>
|
|
17300
|
+
|
|
17301
|
+
<div>
|
|
17302
|
+
<label for="field-type" class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Field Type</label>
|
|
17303
|
+
<div class="mt-2 grid grid-cols-1">
|
|
17304
|
+
<select
|
|
17305
|
+
id="field-type"
|
|
17306
|
+
name="field_type"
|
|
17307
|
+
required
|
|
17308
|
+
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-blue-500/30 dark:outline-blue-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-500 dark:focus-visible:outline-blue-400 sm:text-sm/6"
|
|
17309
|
+
>
|
|
17310
|
+
<option value="">Select field type...</option>
|
|
17311
|
+
<option value="text">Text</option>
|
|
17312
|
+
<option value="richtext">Rich Text</option>
|
|
17313
|
+
<option value="number">Number</option>
|
|
17314
|
+
<option value="boolean">Boolean</option>
|
|
17315
|
+
<option value="date">Date</option>
|
|
17316
|
+
<option value="select">Select</option>
|
|
17317
|
+
<option value="media">Media</option>
|
|
17318
|
+
<option value="guid">GUID (Auto-generated)</option>
|
|
17319
|
+
</select>
|
|
17320
|
+
<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">
|
|
17321
|
+
<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" />
|
|
17322
|
+
</svg>
|
|
17323
|
+
</div>
|
|
17324
|
+
<p id="field-type-help" class="text-xs text-zinc-500 dark:text-zinc-400 mt-1"></p>
|
|
17325
|
+
</div>
|
|
17326
|
+
|
|
17327
|
+
<div>
|
|
17328
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Label</label>
|
|
17329
|
+
<input
|
|
17330
|
+
type="text"
|
|
17331
|
+
id="field-label"
|
|
17332
|
+
name="field_label"
|
|
17333
|
+
required
|
|
17334
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors"
|
|
17335
|
+
placeholder="Field Label"
|
|
17336
|
+
>
|
|
17337
|
+
</div>
|
|
17338
|
+
|
|
17339
|
+
<div class="flex items-center space-x-6">
|
|
17340
|
+
<div class="flex gap-3">
|
|
17341
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
17342
|
+
<div class="group grid size-4 grid-cols-1">
|
|
17343
|
+
<input
|
|
17344
|
+
type="checkbox"
|
|
17345
|
+
id="field-required"
|
|
17346
|
+
name="is_required"
|
|
17347
|
+
value="1"
|
|
17348
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
17349
|
+
/>
|
|
17350
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
17351
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
17352
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
17353
|
+
</svg>
|
|
17354
|
+
</div>
|
|
17355
|
+
</div>
|
|
17356
|
+
<div class="text-sm/6">
|
|
17357
|
+
<label for="field-required" class="font-medium text-zinc-950 dark:text-white">Required</label>
|
|
17358
|
+
</div>
|
|
17359
|
+
</div>
|
|
17360
|
+
|
|
17361
|
+
<div class="flex gap-3">
|
|
17362
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
17363
|
+
<div class="group grid size-4 grid-cols-1">
|
|
17364
|
+
<input
|
|
17365
|
+
type="checkbox"
|
|
17366
|
+
id="field-searchable"
|
|
17367
|
+
name="is_searchable"
|
|
17368
|
+
value="1"
|
|
17369
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
17370
|
+
/>
|
|
17371
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
17372
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
17373
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
17374
|
+
</svg>
|
|
17375
|
+
</div>
|
|
17376
|
+
</div>
|
|
17377
|
+
<div class="text-sm/6">
|
|
17378
|
+
<label for="field-searchable" class="font-medium text-zinc-950 dark:text-white">Searchable</label>
|
|
17379
|
+
</div>
|
|
17380
|
+
</div>
|
|
17381
|
+
</div>
|
|
17382
|
+
|
|
17383
|
+
<div id="field-options-container" class="hidden">
|
|
17384
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Options (JSON)</label>
|
|
17385
|
+
<textarea
|
|
17386
|
+
id="field-options"
|
|
17387
|
+
name="field_options"
|
|
17388
|
+
rows="3"
|
|
17389
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors font-mono"
|
|
17390
|
+
placeholder='{"maxLength": 200, "placeholder": "Enter text..."}'
|
|
17391
|
+
></textarea>
|
|
17392
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">JSON configuration for field-specific options</p>
|
|
17393
|
+
</div>
|
|
17394
|
+
|
|
17395
|
+
<div class="flex justify-end space-x-3 pt-4 border-t border-zinc-950/5 dark:border-white/10">
|
|
17396
|
+
<button
|
|
17397
|
+
type="button"
|
|
17398
|
+
onclick="closeFieldModal()"
|
|
17399
|
+
class="rounded-lg bg-white dark:bg-zinc-800 px-4 py-2 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"
|
|
17400
|
+
>
|
|
17401
|
+
Cancel
|
|
17402
|
+
</button>
|
|
17403
|
+
<button
|
|
17404
|
+
type="submit"
|
|
17405
|
+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 transition-colors"
|
|
17406
|
+
>
|
|
17407
|
+
<span id="submit-text">Add Field</span>
|
|
17408
|
+
</button>
|
|
17409
|
+
</div>
|
|
17410
|
+
</form>
|
|
17411
|
+
</div>
|
|
17412
|
+
</div>
|
|
17413
|
+
|
|
17414
|
+
<script>
|
|
17415
|
+
const collectionId = '${data.id || ""}';
|
|
17416
|
+
|
|
17417
|
+
|
|
17418
|
+
let currentEditingField = null;
|
|
17419
|
+
|
|
17420
|
+
// Field modal functions
|
|
17421
|
+
function showAddFieldModal() {
|
|
17422
|
+
document.getElementById('modal-title').textContent = 'Add Field';
|
|
17423
|
+
document.getElementById('submit-text').textContent = 'Add Field';
|
|
17424
|
+
document.getElementById('field-form').reset();
|
|
17425
|
+
document.getElementById('field-id').value = '';
|
|
17426
|
+
document.getElementById('field-name').disabled = false;
|
|
17427
|
+
currentEditingField = null;
|
|
17428
|
+
document.getElementById('field-modal').classList.remove('hidden');
|
|
17429
|
+
}
|
|
17430
|
+
|
|
17431
|
+
function editField(fieldId) {
|
|
17432
|
+
const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
|
|
17433
|
+
if (!fieldItem) return;
|
|
17434
|
+
|
|
17435
|
+
// Find the field data from the collection's fields array
|
|
17436
|
+
const field = ${JSON.stringify(data.fields || [])}.find(f => f.id === fieldId);
|
|
17437
|
+
if (!field) return;
|
|
17438
|
+
|
|
17439
|
+
// Set up the modal for editing
|
|
17440
|
+
document.getElementById('modal-title').textContent = 'Edit Field';
|
|
17441
|
+
document.getElementById('submit-text').textContent = 'Update Field';
|
|
17442
|
+
document.getElementById('field-id').value = fieldId;
|
|
17443
|
+
currentEditingField = fieldId;
|
|
17444
|
+
|
|
17445
|
+
// Populate form with existing field data
|
|
17446
|
+
document.getElementById('field-name').value = field.field_name || '';
|
|
17447
|
+
document.getElementById('field-name').disabled = true;
|
|
17448
|
+
document.getElementById('field-label').value = field.field_label || '';
|
|
17449
|
+
document.getElementById('field-type').value = field.field_type || '';
|
|
17450
|
+
document.getElementById('field-required').checked = Boolean(field.is_required);
|
|
17451
|
+
document.getElementById('field-searchable').checked = Boolean(field.is_searchable);
|
|
17452
|
+
|
|
17453
|
+
// Handle field options - serialize object back to JSON string
|
|
17454
|
+
if (field.field_options) {
|
|
17455
|
+
document.getElementById('field-options').value = typeof field.field_options === 'string'
|
|
17456
|
+
? field.field_options
|
|
17457
|
+
: JSON.stringify(field.field_options, null, 2);
|
|
17458
|
+
} else {
|
|
17459
|
+
document.getElementById('field-options').value = '';
|
|
17460
|
+
}
|
|
17461
|
+
|
|
17462
|
+
// Show/hide options container based on field type
|
|
17463
|
+
const fieldType = field.field_type;
|
|
17464
|
+
const optionsContainer = document.getElementById('field-options-container');
|
|
17465
|
+
const helpText = document.getElementById('field-type-help');
|
|
17466
|
+
|
|
17467
|
+
if (['select', 'media', 'richtext', 'guid'].includes(fieldType)) {
|
|
17468
|
+
optionsContainer.classList.remove('hidden');
|
|
17469
|
+
|
|
17470
|
+
// Set help text based on type
|
|
17471
|
+
switch (fieldType) {
|
|
17472
|
+
case 'select':
|
|
17473
|
+
helpText.textContent = 'Create a dropdown select field with custom options';
|
|
17474
|
+
break;
|
|
17475
|
+
case 'media':
|
|
17476
|
+
helpText.textContent = 'Upload and manage media files (images, videos, documents)';
|
|
17477
|
+
break;
|
|
17478
|
+
case 'richtext':
|
|
17479
|
+
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17480
|
+
break;
|
|
17481
|
+
case 'guid':
|
|
17482
|
+
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17483
|
+
break;
|
|
17484
|
+
}
|
|
17485
|
+
} else {
|
|
17486
|
+
optionsContainer.classList.add('hidden');
|
|
17487
|
+
|
|
17488
|
+
// Set help text for other field types
|
|
17489
|
+
switch (fieldType) {
|
|
17490
|
+
case 'text':
|
|
17491
|
+
helpText.textContent = 'Single-line text input for short content';
|
|
17492
|
+
break;
|
|
17493
|
+
case 'number':
|
|
17494
|
+
helpText.textContent = 'Numeric input field for integers or decimals';
|
|
17495
|
+
break;
|
|
17496
|
+
case 'boolean':
|
|
17497
|
+
helpText.textContent = 'True/false checkbox field';
|
|
17498
|
+
break;
|
|
17499
|
+
case 'date':
|
|
17500
|
+
helpText.textContent = 'Date and time picker field';
|
|
17501
|
+
break;
|
|
17502
|
+
default:
|
|
17503
|
+
helpText.textContent = '';
|
|
17504
|
+
}
|
|
17505
|
+
}
|
|
17506
|
+
|
|
17507
|
+
document.getElementById('field-modal').classList.remove('hidden');
|
|
17508
|
+
}
|
|
17509
|
+
|
|
17510
|
+
function closeFieldModal() {
|
|
17511
|
+
document.getElementById('field-modal').classList.add('hidden');
|
|
17512
|
+
currentEditingField = null;
|
|
17513
|
+
}
|
|
17514
|
+
|
|
17515
|
+
let fieldToDelete = null;
|
|
17516
|
+
|
|
17517
|
+
function deleteField(fieldId) {
|
|
17518
|
+
fieldToDelete = fieldId;
|
|
17519
|
+
showConfirmDialog('delete-field-confirm');
|
|
17520
|
+
}
|
|
17521
|
+
|
|
17522
|
+
function performDeleteField() {
|
|
17523
|
+
if (!fieldToDelete) return;
|
|
17524
|
+
|
|
17525
|
+
fetch(\`/admin/collections/\${collectionId}/fields/\${fieldToDelete}\`, {
|
|
17526
|
+
method: 'DELETE'
|
|
17527
|
+
})
|
|
17528
|
+
.then(response => response.json())
|
|
17529
|
+
.then(data => {
|
|
17530
|
+
if (data.success) {
|
|
17531
|
+
location.reload();
|
|
17532
|
+
} else {
|
|
17533
|
+
alert('Error deleting field: ' + data.error);
|
|
17534
|
+
}
|
|
17535
|
+
})
|
|
17536
|
+
.catch(error => {
|
|
17537
|
+
console.error('Error:', error);
|
|
17538
|
+
alert('Error deleting field');
|
|
17539
|
+
})
|
|
17540
|
+
.finally(() => {
|
|
17541
|
+
fieldToDelete = null;
|
|
17542
|
+
});
|
|
17543
|
+
}
|
|
17544
|
+
|
|
17545
|
+
// Field form submission
|
|
17546
|
+
document.getElementById('field-form').addEventListener('submit', function(e) {
|
|
17547
|
+
e.preventDefault();
|
|
17548
|
+
|
|
17549
|
+
if (!collectionId) {
|
|
17550
|
+
alert('Error: Collection ID is missing. Cannot save field.');
|
|
17551
|
+
return;
|
|
17552
|
+
}
|
|
17553
|
+
|
|
17554
|
+
const formData = new FormData(this);
|
|
17555
|
+
const isEditing = currentEditingField !== null;
|
|
17556
|
+
|
|
17557
|
+
const url = isEditing
|
|
17558
|
+
? \`/admin/collections/\${collectionId}/fields/\${currentEditingField}\`
|
|
17559
|
+
: \`/admin/collections/\${collectionId}/fields\`;
|
|
17560
|
+
|
|
17561
|
+
const method = isEditing ? 'PUT' : 'POST';
|
|
17562
|
+
|
|
17563
|
+
|
|
17564
|
+
fetch(url, {
|
|
17565
|
+
method: method,
|
|
17566
|
+
body: formData
|
|
17567
|
+
})
|
|
17568
|
+
.then(response => {
|
|
17569
|
+
if (!response.ok) {
|
|
17570
|
+
throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
|
|
17571
|
+
}
|
|
17572
|
+
return response.json();
|
|
17573
|
+
})
|
|
17574
|
+
.then(data => {
|
|
17575
|
+
if (data.success) {
|
|
17576
|
+
location.reload();
|
|
17577
|
+
} else {
|
|
17578
|
+
alert('Error saving field: ' + (data.error || 'Unknown error'));
|
|
17579
|
+
}
|
|
17580
|
+
})
|
|
17581
|
+
.catch(error => {
|
|
17582
|
+
alert('Error saving field: ' + error.message);
|
|
17583
|
+
});
|
|
17584
|
+
});
|
|
17585
|
+
|
|
17586
|
+
// Field type change handler
|
|
17587
|
+
document.getElementById('field-type').addEventListener('change', function() {
|
|
17588
|
+
const optionsContainer = document.getElementById('field-options-container');
|
|
17589
|
+
const fieldOptions = document.getElementById('field-options');
|
|
17590
|
+
const helpText = document.getElementById('field-type-help');
|
|
17591
|
+
const fieldNameInput = document.getElementById('field-name');
|
|
17592
|
+
|
|
17593
|
+
// Show/hide options based on field type
|
|
17594
|
+
if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
|
|
17595
|
+
optionsContainer.classList.remove('hidden');
|
|
17596
|
+
|
|
17597
|
+
// Set default options and help text based on type
|
|
17598
|
+
switch (this.value) {
|
|
17599
|
+
case 'select':
|
|
17600
|
+
fieldOptions.value = '{"options": ["Option 1", "Option 2"], "multiple": false}';
|
|
17601
|
+
helpText.textContent = 'Create a dropdown select field with custom options';
|
|
17602
|
+
break;
|
|
17603
|
+
case 'media':
|
|
17604
|
+
fieldOptions.value = '{"accept": "image/*", "maxSize": "10MB"}';
|
|
17605
|
+
helpText.textContent = 'Upload and manage media files (images, videos, documents)';
|
|
17606
|
+
break;
|
|
17607
|
+
case 'richtext':
|
|
17608
|
+
fieldOptions.value = '{"toolbar": "full", "height": 400}';
|
|
17609
|
+
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
17610
|
+
break;
|
|
17611
|
+
case 'guid':
|
|
17612
|
+
fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
|
|
17613
|
+
helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
|
|
17614
|
+
// Suggest 'id' as field name for GUID fields
|
|
17615
|
+
if (!fieldNameInput.value || fieldNameInput.value === '') {
|
|
17616
|
+
fieldNameInput.value = 'id';
|
|
17617
|
+
}
|
|
17618
|
+
break;
|
|
17619
|
+
}
|
|
17620
|
+
} else {
|
|
17621
|
+
optionsContainer.classList.add('hidden');
|
|
17622
|
+
fieldOptions.value = '{}';
|
|
17623
|
+
|
|
17624
|
+
// Set help text for other field types
|
|
17625
|
+
switch (this.value) {
|
|
17626
|
+
case 'text':
|
|
17627
|
+
helpText.textContent = 'Single-line text input for short content';
|
|
17628
|
+
break;
|
|
17629
|
+
case 'number':
|
|
17630
|
+
helpText.textContent = 'Numeric input field for integers or decimals';
|
|
17631
|
+
break;
|
|
17632
|
+
case 'boolean':
|
|
17633
|
+
helpText.textContent = 'True/false checkbox field';
|
|
17634
|
+
break;
|
|
17635
|
+
case 'date':
|
|
17636
|
+
helpText.textContent = 'Date and time picker field';
|
|
17637
|
+
break;
|
|
17638
|
+
default:
|
|
17639
|
+
helpText.textContent = '';
|
|
17640
|
+
}
|
|
17641
|
+
}
|
|
17642
|
+
});
|
|
17643
|
+
|
|
17644
|
+
// Close modal on escape key
|
|
17645
|
+
document.addEventListener('keydown', function(e) {
|
|
17646
|
+
if (e.key === 'Escape' && !document.getElementById('field-modal').classList.contains('hidden')) {
|
|
17647
|
+
closeFieldModal();
|
|
17648
|
+
}
|
|
17649
|
+
});
|
|
17650
|
+
|
|
17651
|
+
// Close modal on backdrop click
|
|
17652
|
+
document.getElementById('field-modal').addEventListener('click', function(e) {
|
|
17653
|
+
if (e.target === this) {
|
|
17654
|
+
closeFieldModal();
|
|
17655
|
+
}
|
|
17656
|
+
});
|
|
17657
|
+
</script>
|
|
17658
|
+
|
|
17659
|
+
<!-- Confirmation Dialogs -->
|
|
17660
|
+
${renderConfirmationDialog2({
|
|
17661
|
+
id: "delete-field-confirm",
|
|
17662
|
+
title: "Delete Field",
|
|
17663
|
+
message: "Are you sure you want to delete this field? This action cannot be undone.",
|
|
17664
|
+
confirmText: "Delete",
|
|
17665
|
+
cancelText: "Cancel",
|
|
17666
|
+
iconColor: "red",
|
|
17667
|
+
confirmClass: "bg-red-500 hover:bg-red-400",
|
|
17668
|
+
onConfirm: "performDeleteField()"
|
|
17669
|
+
})}
|
|
17670
|
+
|
|
17671
|
+
${getConfirmationDialogScript2()}
|
|
17672
|
+
`;
|
|
17673
|
+
const layoutData = {
|
|
17674
|
+
title,
|
|
17675
|
+
pageTitle: "Collections",
|
|
17676
|
+
currentPath: "/admin/collections",
|
|
17677
|
+
user: data.user,
|
|
17678
|
+
version: data.version,
|
|
17679
|
+
content: pageContent
|
|
17680
|
+
};
|
|
17681
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
17682
|
+
}
|
|
17683
|
+
|
|
17684
|
+
// src/routes/admin-collections.ts
|
|
17685
|
+
var adminCollectionsRoutes = new Hono();
|
|
17686
|
+
adminCollectionsRoutes.get("/collections", async (c) => {
|
|
17687
|
+
try {
|
|
17688
|
+
const user = c.get("user");
|
|
17689
|
+
const db = c.env.DB;
|
|
17690
|
+
const url = new URL(c.req.url);
|
|
17691
|
+
const search = url.searchParams.get("search") || "";
|
|
17692
|
+
let stmt;
|
|
17693
|
+
let results;
|
|
17694
|
+
if (search) {
|
|
17695
|
+
stmt = db.prepare(`
|
|
17696
|
+
SELECT id, name, display_name, description, created_at, managed
|
|
17697
|
+
FROM collections
|
|
17698
|
+
WHERE is_active = 1
|
|
17699
|
+
AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)
|
|
17700
|
+
ORDER BY created_at DESC
|
|
17701
|
+
`);
|
|
17702
|
+
const searchParam = `%${search}%`;
|
|
17703
|
+
const queryResults = await stmt.bind(searchParam, searchParam, searchParam).all();
|
|
17704
|
+
results = queryResults.results;
|
|
17705
|
+
} else {
|
|
17706
|
+
stmt = db.prepare("SELECT id, name, display_name, description, created_at, managed FROM collections WHERE is_active = 1 ORDER BY created_at DESC");
|
|
17707
|
+
const queryResults = await stmt.all();
|
|
17708
|
+
results = queryResults.results;
|
|
17709
|
+
}
|
|
17710
|
+
const fieldCountStmt = db.prepare("SELECT collection_id, COUNT(*) as count FROM content_fields GROUP BY collection_id");
|
|
17711
|
+
const { results: fieldCountResults } = await fieldCountStmt.all();
|
|
17712
|
+
const fieldCounts = new Map((fieldCountResults || []).map((row) => [String(row.collection_id), Number(row.count)]));
|
|
17713
|
+
const collections = (results || []).filter((row) => row && row.id).map((row) => {
|
|
17714
|
+
return {
|
|
17715
|
+
id: String(row.id || ""),
|
|
17716
|
+
name: String(row.name || ""),
|
|
17717
|
+
display_name: String(row.display_name || ""),
|
|
17718
|
+
description: row.description ? String(row.description) : void 0,
|
|
17719
|
+
created_at: Number(row.created_at || 0),
|
|
17720
|
+
formattedDate: row.created_at ? new Date(Number(row.created_at)).toLocaleDateString() : "Unknown",
|
|
17721
|
+
field_count: fieldCounts.get(String(row.id)) || 0,
|
|
17722
|
+
managed: row.managed === 1
|
|
17723
|
+
};
|
|
17724
|
+
});
|
|
17725
|
+
const pageData = {
|
|
17726
|
+
collections,
|
|
17727
|
+
search,
|
|
17728
|
+
user: user ? {
|
|
17729
|
+
name: user.email,
|
|
17730
|
+
email: user.email,
|
|
17731
|
+
role: user.role
|
|
17732
|
+
} : void 0,
|
|
17733
|
+
version: c.get("appVersion")
|
|
17734
|
+
};
|
|
17735
|
+
return c.html(renderCollectionsListPage(pageData));
|
|
17736
|
+
} catch (error) {
|
|
17737
|
+
console.error("Error fetching collections:", error);
|
|
17738
|
+
return c.html(html`<p>Error loading collections</p>`);
|
|
17739
|
+
}
|
|
17740
|
+
});
|
|
17741
|
+
adminCollectionsRoutes.get("/collections/new", (c) => {
|
|
17742
|
+
const user = c.get("user");
|
|
17743
|
+
const formData = {
|
|
17744
|
+
isEdit: false,
|
|
17745
|
+
user: user ? {
|
|
17746
|
+
name: user.email,
|
|
17747
|
+
email: user.email,
|
|
17748
|
+
role: user.role
|
|
17749
|
+
} : void 0,
|
|
17750
|
+
version: c.get("appVersion")
|
|
17751
|
+
};
|
|
17752
|
+
return c.html(renderCollectionFormPage(formData));
|
|
17753
|
+
});
|
|
17754
|
+
adminCollectionsRoutes.post("/collections", async (c) => {
|
|
17755
|
+
try {
|
|
17756
|
+
const formData = await c.req.formData();
|
|
17757
|
+
const name = formData.get("name");
|
|
17758
|
+
const displayName = formData.get("displayName");
|
|
17759
|
+
const description = formData.get("description");
|
|
17760
|
+
const isHtmx = c.req.header("HX-Request") === "true";
|
|
17761
|
+
if (!name || !displayName) {
|
|
17762
|
+
const errorMsg = "Name and display name are required.";
|
|
17763
|
+
if (isHtmx) {
|
|
17764
|
+
return c.html(html`
|
|
17765
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17766
|
+
${errorMsg}
|
|
17767
|
+
</div>
|
|
17768
|
+
`);
|
|
17769
|
+
} else {
|
|
17770
|
+
return c.redirect("/admin/collections/new");
|
|
17771
|
+
}
|
|
17772
|
+
}
|
|
17773
|
+
if (!/^[a-z0-9_]+$/.test(name)) {
|
|
17774
|
+
const errorMsg = "Collection name must contain only lowercase letters, numbers, and underscores.";
|
|
17775
|
+
if (isHtmx) {
|
|
17776
|
+
return c.html(html`
|
|
17777
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17778
|
+
${errorMsg}
|
|
17779
|
+
</div>
|
|
17780
|
+
`);
|
|
17781
|
+
} else {
|
|
17782
|
+
return c.redirect("/admin/collections/new");
|
|
17783
|
+
}
|
|
17784
|
+
}
|
|
17785
|
+
const db = c.env.DB;
|
|
17786
|
+
const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
|
|
17787
|
+
const existing = await existingStmt.bind(name).first();
|
|
17788
|
+
if (existing) {
|
|
17789
|
+
const errorMsg = "A collection with this name already exists.";
|
|
17790
|
+
if (isHtmx) {
|
|
17791
|
+
return c.html(html`
|
|
17792
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17793
|
+
${errorMsg}
|
|
17794
|
+
</div>
|
|
17795
|
+
`);
|
|
17796
|
+
} else {
|
|
17797
|
+
return c.redirect("/admin/collections/new");
|
|
17798
|
+
}
|
|
17799
|
+
}
|
|
17800
|
+
const basicSchema = {
|
|
17801
|
+
type: "object",
|
|
17802
|
+
properties: {
|
|
17803
|
+
title: {
|
|
17804
|
+
type: "string",
|
|
17805
|
+
title: "Title",
|
|
17806
|
+
required: true
|
|
17807
|
+
},
|
|
17808
|
+
content: {
|
|
17809
|
+
type: "string",
|
|
17810
|
+
title: "Content",
|
|
17811
|
+
format: "richtext"
|
|
17812
|
+
},
|
|
17813
|
+
status: {
|
|
17814
|
+
type: "string",
|
|
17815
|
+
title: "Status",
|
|
17816
|
+
enum: ["draft", "published", "archived"],
|
|
17817
|
+
default: "draft"
|
|
17818
|
+
}
|
|
17819
|
+
},
|
|
17820
|
+
required: ["title"]
|
|
17821
|
+
};
|
|
17822
|
+
const collectionId = globalThis.crypto.randomUUID();
|
|
17823
|
+
const now = Date.now();
|
|
17824
|
+
const insertStmt = db.prepare(`
|
|
17825
|
+
INSERT INTO collections (id, name, display_name, description, schema, is_active, created_at, updated_at)
|
|
17826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
17827
|
+
`);
|
|
17828
|
+
await insertStmt.bind(
|
|
17829
|
+
collectionId,
|
|
17830
|
+
name,
|
|
17831
|
+
displayName,
|
|
17832
|
+
description || null,
|
|
17833
|
+
JSON.stringify(basicSchema),
|
|
17834
|
+
1,
|
|
17835
|
+
// is_active
|
|
17836
|
+
now,
|
|
17837
|
+
now
|
|
17838
|
+
).run();
|
|
17839
|
+
try {
|
|
17840
|
+
await c.env.CACHE_KV.delete("cache:collections:all");
|
|
17841
|
+
await c.env.CACHE_KV.delete(`cache:collection:${name}`);
|
|
17842
|
+
} catch (e) {
|
|
17843
|
+
console.error("Error clearing cache:", e);
|
|
17844
|
+
}
|
|
17845
|
+
if (isHtmx) {
|
|
17846
|
+
return c.html(html`
|
|
17847
|
+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
17848
|
+
Collection created successfully! Redirecting...
|
|
17849
|
+
<script>
|
|
17850
|
+
setTimeout(() => {
|
|
17851
|
+
window.location.href = '/admin/collections';
|
|
17852
|
+
}, 1500);
|
|
17853
|
+
</script>
|
|
17854
|
+
</div>
|
|
17855
|
+
`);
|
|
17856
|
+
} else {
|
|
17857
|
+
return c.redirect("/admin/collections");
|
|
17858
|
+
}
|
|
17859
|
+
} catch (error) {
|
|
17860
|
+
console.error("Error creating collection:", error);
|
|
17861
|
+
const isHtmx = c.req.header("HX-Request") === "true";
|
|
17862
|
+
if (isHtmx) {
|
|
17863
|
+
return c.html(html`
|
|
17864
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17865
|
+
Failed to create collection. Please try again.
|
|
17866
|
+
</div>
|
|
17867
|
+
`);
|
|
17868
|
+
} else {
|
|
17869
|
+
return c.redirect("/admin/collections/new");
|
|
17870
|
+
}
|
|
17871
|
+
}
|
|
17872
|
+
});
|
|
17873
|
+
adminCollectionsRoutes.get("/collections/:id", async (c) => {
|
|
17874
|
+
try {
|
|
17875
|
+
const id = c.req.param("id");
|
|
17876
|
+
const user = c.get("user");
|
|
17877
|
+
const db = c.env.DB;
|
|
17878
|
+
const stmt = db.prepare("SELECT * FROM collections WHERE id = ?");
|
|
17879
|
+
const collection = await stmt.bind(id).first();
|
|
17880
|
+
if (!collection) {
|
|
17881
|
+
const formData2 = {
|
|
17882
|
+
isEdit: true,
|
|
17883
|
+
error: "Collection not found.",
|
|
17884
|
+
user: user ? {
|
|
17885
|
+
name: user.email,
|
|
17886
|
+
email: user.email,
|
|
17887
|
+
role: user.role
|
|
17888
|
+
} : void 0,
|
|
17889
|
+
version: c.get("appVersion")
|
|
17890
|
+
};
|
|
17891
|
+
return c.html(renderCollectionFormPage(formData2));
|
|
17892
|
+
}
|
|
17893
|
+
const fieldsStmt = db.prepare(`
|
|
17894
|
+
SELECT * FROM content_fields
|
|
17895
|
+
WHERE collection_id = ?
|
|
17896
|
+
ORDER BY field_order ASC
|
|
17897
|
+
`);
|
|
17898
|
+
const { results: fieldsResults } = await fieldsStmt.bind(id).all();
|
|
17899
|
+
const fields = (fieldsResults || []).map((row) => ({
|
|
17900
|
+
id: row.id,
|
|
17901
|
+
field_name: row.field_name,
|
|
17902
|
+
field_type: row.field_type,
|
|
17903
|
+
field_label: row.field_label,
|
|
17904
|
+
field_options: row.field_options ? JSON.parse(row.field_options) : {},
|
|
17905
|
+
field_order: row.field_order,
|
|
17906
|
+
is_required: row.is_required === 1,
|
|
17907
|
+
is_searchable: row.is_searchable === 1
|
|
17908
|
+
}));
|
|
17909
|
+
const formData = {
|
|
17910
|
+
id: collection.id,
|
|
17911
|
+
name: collection.name,
|
|
17912
|
+
display_name: collection.display_name,
|
|
17913
|
+
description: collection.description,
|
|
17914
|
+
fields,
|
|
17915
|
+
managed: collection.managed === 1,
|
|
17916
|
+
isEdit: true,
|
|
17917
|
+
user: user ? {
|
|
17918
|
+
name: user.email,
|
|
17919
|
+
email: user.email,
|
|
17920
|
+
role: user.role
|
|
17921
|
+
} : void 0,
|
|
17922
|
+
version: c.get("appVersion")
|
|
17923
|
+
};
|
|
17924
|
+
return c.html(renderCollectionFormPage(formData));
|
|
17925
|
+
} catch (error) {
|
|
17926
|
+
console.error("Error fetching collection:", error);
|
|
17927
|
+
const user = c.get("user");
|
|
17928
|
+
const formData = {
|
|
17929
|
+
isEdit: true,
|
|
17930
|
+
error: "Failed to load collection.",
|
|
17931
|
+
user: user ? {
|
|
17932
|
+
name: user.email,
|
|
17933
|
+
email: user.email,
|
|
17934
|
+
role: user.role
|
|
17935
|
+
} : void 0,
|
|
17936
|
+
version: c.get("appVersion")
|
|
17937
|
+
};
|
|
17938
|
+
return c.html(renderCollectionFormPage(formData));
|
|
17939
|
+
}
|
|
17940
|
+
});
|
|
17941
|
+
adminCollectionsRoutes.put("/collections/:id", async (c) => {
|
|
17942
|
+
try {
|
|
17943
|
+
const id = c.req.param("id");
|
|
17944
|
+
const formData = await c.req.formData();
|
|
17945
|
+
const displayName = formData.get("displayName");
|
|
17946
|
+
const description = formData.get("description");
|
|
17947
|
+
if (!displayName) {
|
|
17948
|
+
return c.html(html`
|
|
17949
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17950
|
+
Display name is required.
|
|
17951
|
+
</div>
|
|
17952
|
+
`);
|
|
17953
|
+
}
|
|
17954
|
+
const db = c.env.DB;
|
|
17955
|
+
const updateStmt = db.prepare(`
|
|
17956
|
+
UPDATE collections
|
|
17957
|
+
SET display_name = ?, description = ?, updated_at = ?
|
|
17958
|
+
WHERE id = ?
|
|
17959
|
+
`);
|
|
17960
|
+
await updateStmt.bind(displayName, description || null, Date.now(), id).run();
|
|
17961
|
+
return c.html(html`
|
|
17962
|
+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
|
17963
|
+
Collection updated successfully!
|
|
17964
|
+
</div>
|
|
17965
|
+
`);
|
|
17966
|
+
} catch (error) {
|
|
17967
|
+
console.error("Error updating collection:", error);
|
|
17968
|
+
return c.html(html`
|
|
17969
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17970
|
+
Failed to update collection. Please try again.
|
|
17971
|
+
</div>
|
|
17972
|
+
`);
|
|
17973
|
+
}
|
|
17974
|
+
});
|
|
17975
|
+
adminCollectionsRoutes.delete("/collections/:id", async (c) => {
|
|
17976
|
+
try {
|
|
17977
|
+
const id = c.req.param("id");
|
|
17978
|
+
const db = c.env.DB;
|
|
17979
|
+
const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE collection_id = ?");
|
|
17980
|
+
const contentResult = await contentStmt.bind(id).first();
|
|
17981
|
+
if (contentResult && contentResult.count > 0) {
|
|
17982
|
+
return c.html(html`
|
|
17983
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
17984
|
+
Cannot delete collection: it contains ${contentResult.count} content item(s). Delete all content first.
|
|
17985
|
+
</div>
|
|
17986
|
+
`);
|
|
17987
|
+
}
|
|
17988
|
+
const deleteFieldsStmt = db.prepare("DELETE FROM content_fields WHERE collection_id = ?");
|
|
17989
|
+
await deleteFieldsStmt.bind(id).run();
|
|
17990
|
+
const deleteStmt = db.prepare("DELETE FROM collections WHERE id = ?");
|
|
17991
|
+
await deleteStmt.bind(id).run();
|
|
17992
|
+
return c.html(html`
|
|
17993
|
+
<script>
|
|
17994
|
+
window.location.href = '/admin/collections';
|
|
17995
|
+
</script>
|
|
17996
|
+
`);
|
|
17997
|
+
} catch (error) {
|
|
17998
|
+
console.error("Error deleting collection:", error);
|
|
17999
|
+
return c.html(html`
|
|
18000
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
18001
|
+
Failed to delete collection. Please try again.
|
|
18002
|
+
</div>
|
|
18003
|
+
`);
|
|
18004
|
+
}
|
|
18005
|
+
});
|
|
18006
|
+
adminCollectionsRoutes.post("/collections/:id/fields", async (c) => {
|
|
18007
|
+
try {
|
|
18008
|
+
const collectionId = c.req.param("id");
|
|
18009
|
+
const formData = await c.req.formData();
|
|
18010
|
+
const fieldName = formData.get("field_name");
|
|
18011
|
+
const fieldType = formData.get("field_type");
|
|
18012
|
+
const fieldLabel = formData.get("field_label");
|
|
18013
|
+
const isRequired = formData.get("is_required") === "1";
|
|
18014
|
+
const isSearchable = formData.get("is_searchable") === "1";
|
|
18015
|
+
const fieldOptions = formData.get("field_options") || "{}";
|
|
18016
|
+
if (!fieldName || !fieldType || !fieldLabel) {
|
|
18017
|
+
return c.json({ success: false, error: "Field name, type, and label are required." });
|
|
18018
|
+
}
|
|
18019
|
+
if (!/^[a-z0-9_]+$/.test(fieldName)) {
|
|
18020
|
+
return c.json({ success: false, error: "Field name must contain only lowercase letters, numbers, and underscores." });
|
|
18021
|
+
}
|
|
18022
|
+
const db = c.env.DB;
|
|
18023
|
+
const existingStmt = db.prepare("SELECT id FROM content_fields WHERE collection_id = ? AND field_name = ?");
|
|
18024
|
+
const existing = await existingStmt.bind(collectionId, fieldName).first();
|
|
18025
|
+
if (existing) {
|
|
18026
|
+
return c.json({ success: false, error: "A field with this name already exists." });
|
|
18027
|
+
}
|
|
18028
|
+
const orderStmt = db.prepare("SELECT MAX(field_order) as max_order FROM content_fields WHERE collection_id = ?");
|
|
18029
|
+
const orderResult = await orderStmt.bind(collectionId).first();
|
|
18030
|
+
const nextOrder = (orderResult?.max_order || 0) + 1;
|
|
18031
|
+
const fieldId = globalThis.crypto.randomUUID();
|
|
18032
|
+
const now = Date.now();
|
|
18033
|
+
const insertStmt = db.prepare(`
|
|
18034
|
+
INSERT INTO content_fields (
|
|
18035
|
+
id, collection_id, field_name, field_type, field_label,
|
|
18036
|
+
field_options, field_order, is_required, is_searchable,
|
|
18037
|
+
created_at, updated_at
|
|
18038
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
18039
|
+
`);
|
|
18040
|
+
await insertStmt.bind(
|
|
18041
|
+
fieldId,
|
|
18042
|
+
collectionId,
|
|
18043
|
+
fieldName,
|
|
18044
|
+
fieldType,
|
|
18045
|
+
fieldLabel,
|
|
18046
|
+
fieldOptions,
|
|
18047
|
+
nextOrder,
|
|
18048
|
+
isRequired ? 1 : 0,
|
|
18049
|
+
isSearchable ? 1 : 0,
|
|
18050
|
+
now,
|
|
18051
|
+
now
|
|
18052
|
+
).run();
|
|
18053
|
+
return c.json({ success: true, fieldId });
|
|
18054
|
+
} catch (error) {
|
|
18055
|
+
console.error("Error adding field:", error);
|
|
18056
|
+
return c.json({ success: false, error: "Failed to add field." });
|
|
18057
|
+
}
|
|
18058
|
+
});
|
|
18059
|
+
adminCollectionsRoutes.put("/collections/:collectionId/fields/:fieldId", async (c) => {
|
|
18060
|
+
try {
|
|
18061
|
+
const fieldId = c.req.param("fieldId");
|
|
18062
|
+
const formData = await c.req.formData();
|
|
18063
|
+
const fieldLabel = formData.get("field_label");
|
|
18064
|
+
const isRequired = formData.get("is_required") === "1";
|
|
18065
|
+
const isSearchable = formData.get("is_searchable") === "1";
|
|
18066
|
+
const fieldOptions = formData.get("field_options") || "{}";
|
|
18067
|
+
if (!fieldLabel) {
|
|
18068
|
+
return c.json({ success: false, error: "Field label is required." });
|
|
18069
|
+
}
|
|
18070
|
+
const db = c.env.DB;
|
|
18071
|
+
const updateStmt = db.prepare(`
|
|
18072
|
+
UPDATE content_fields
|
|
18073
|
+
SET field_label = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
|
|
18074
|
+
WHERE id = ?
|
|
18075
|
+
`);
|
|
18076
|
+
await updateStmt.bind(fieldLabel, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
|
|
18077
|
+
return c.json({ success: true });
|
|
18078
|
+
} catch (error) {
|
|
18079
|
+
console.error("Error updating field:", error);
|
|
18080
|
+
return c.json({ success: false, error: "Failed to update field." });
|
|
18081
|
+
}
|
|
18082
|
+
});
|
|
18083
|
+
adminCollectionsRoutes.delete("/collections/:collectionId/fields/:fieldId", async (c) => {
|
|
18084
|
+
try {
|
|
18085
|
+
const fieldId = c.req.param("fieldId");
|
|
18086
|
+
const db = c.env.DB;
|
|
18087
|
+
const deleteStmt = db.prepare("DELETE FROM content_fields WHERE id = ?");
|
|
18088
|
+
await deleteStmt.bind(fieldId).run();
|
|
18089
|
+
return c.json({ success: true });
|
|
18090
|
+
} catch (error) {
|
|
18091
|
+
console.error("Error deleting field:", error);
|
|
18092
|
+
return c.json({ success: false, error: "Failed to delete field." });
|
|
18093
|
+
}
|
|
18094
|
+
});
|
|
18095
|
+
adminCollectionsRoutes.post("/collections/:collectionId/fields/reorder", async (c) => {
|
|
18096
|
+
try {
|
|
18097
|
+
const body = await c.req.json();
|
|
18098
|
+
const fieldIds = body.fieldIds;
|
|
18099
|
+
if (!Array.isArray(fieldIds)) {
|
|
18100
|
+
return c.json({ success: false, error: "Invalid field order data." });
|
|
18101
|
+
}
|
|
18102
|
+
const db = c.env.DB;
|
|
18103
|
+
for (let i = 0; i < fieldIds.length; i++) {
|
|
18104
|
+
const updateStmt = db.prepare("UPDATE content_fields SET field_order = ?, updated_at = ? WHERE id = ?");
|
|
18105
|
+
await updateStmt.bind(i + 1, Date.now(), fieldIds[i]).run();
|
|
18106
|
+
}
|
|
18107
|
+
return c.json({ success: true });
|
|
18108
|
+
} catch (error) {
|
|
18109
|
+
console.error("Error reordering fields:", error);
|
|
18110
|
+
return c.json({ success: false, error: "Failed to reorder fields." });
|
|
18111
|
+
}
|
|
18112
|
+
});
|
|
18113
|
+
|
|
18114
|
+
// src/templates/pages/admin-settings.template.ts
|
|
18115
|
+
init_admin_layout_catalyst_template();
|
|
18116
|
+
function renderSettingsPage(data) {
|
|
18117
|
+
const activeTab = data.activeTab || "general";
|
|
18118
|
+
const pageContent = `
|
|
18119
|
+
<div>
|
|
18120
|
+
<!-- Header -->
|
|
18121
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
18122
|
+
<div>
|
|
18123
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Settings</h1>
|
|
18124
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage your application settings and preferences</p>
|
|
18125
|
+
</div>
|
|
18126
|
+
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none flex space-x-3">
|
|
18127
|
+
<button
|
|
18128
|
+
onclick="resetSettings()"
|
|
18129
|
+
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"
|
|
18130
|
+
>
|
|
18131
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18132
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
18133
|
+
</svg>
|
|
18134
|
+
Reset to Defaults
|
|
18135
|
+
</button>
|
|
18136
|
+
<button
|
|
18137
|
+
onclick="saveAllSettings()"
|
|
18138
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
|
|
18139
|
+
>
|
|
18140
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18141
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
18142
|
+
</svg>
|
|
18143
|
+
Save All Changes
|
|
18144
|
+
</button>
|
|
18145
|
+
</div>
|
|
18146
|
+
</div>
|
|
18147
|
+
|
|
18148
|
+
<!-- Settings Navigation Tabs -->
|
|
18149
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 mb-6 overflow-hidden">
|
|
18150
|
+
<div class="border-b border-zinc-950/5 dark:border-white/10">
|
|
18151
|
+
<nav class="flex overflow-x-auto" role="tablist">
|
|
18152
|
+
${renderTabButton("general", "General", "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", activeTab)}
|
|
18153
|
+
${renderTabButton("appearance", "Appearance", "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z", activeTab)}
|
|
18154
|
+
${renderTabButton("security", "Security", "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z", activeTab)}
|
|
18155
|
+
${renderTabButton("notifications", "Notifications", "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9", activeTab)}
|
|
18156
|
+
${renderTabButton("storage", "Storage", "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12", activeTab)}
|
|
18157
|
+
${renderTabButton("migrations", "Migrations", "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4", activeTab)}
|
|
18158
|
+
${renderTabButton("database-tools", "Database Tools", "M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01", activeTab)}
|
|
18159
|
+
</nav>
|
|
18160
|
+
</div>
|
|
18161
|
+
</div>
|
|
18162
|
+
|
|
18163
|
+
<!-- Settings Content -->
|
|
18164
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
18165
|
+
<div id="settings-content" class="p-8">
|
|
18166
|
+
${renderTabContent(activeTab, data.settings)}
|
|
18167
|
+
</div>
|
|
18168
|
+
</div>
|
|
18169
|
+
</div>
|
|
18170
|
+
|
|
18171
|
+
<script>
|
|
18172
|
+
// Initialize tab-specific features on page load
|
|
18173
|
+
const currentTab = '${activeTab}';
|
|
18174
|
+
|
|
18175
|
+
function saveAllSettings() {
|
|
18176
|
+
// Collect all form data
|
|
18177
|
+
const formData = new FormData();
|
|
18178
|
+
|
|
18179
|
+
// Get all form inputs
|
|
18180
|
+
document.querySelectorAll('input, select, textarea').forEach(input => {
|
|
18181
|
+
if (input.type === 'checkbox') {
|
|
18182
|
+
formData.append(input.name, input.checked);
|
|
18183
|
+
} else if (input.name) {
|
|
18184
|
+
formData.append(input.name, input.value);
|
|
18185
|
+
}
|
|
18186
|
+
});
|
|
18187
|
+
|
|
18188
|
+
// Show loading state
|
|
18189
|
+
const saveBtn = document.querySelector('button[onclick="saveAllSettings()"]');
|
|
18190
|
+
const originalText = saveBtn.innerHTML;
|
|
18191
|
+
saveBtn.innerHTML = 'Saving...';
|
|
18192
|
+
saveBtn.disabled = true;
|
|
18193
|
+
|
|
18194
|
+
// Simulate save (replace with actual API call)
|
|
18195
|
+
setTimeout(() => {
|
|
18196
|
+
saveBtn.innerHTML = originalText;
|
|
18197
|
+
saveBtn.disabled = false;
|
|
18198
|
+
showNotification('Settings saved successfully!', 'success');
|
|
18199
|
+
}, 1000);
|
|
18200
|
+
}
|
|
18201
|
+
|
|
18202
|
+
function resetSettings() {
|
|
18203
|
+
showConfirmDialog('reset-settings-confirm');
|
|
18204
|
+
}
|
|
18205
|
+
|
|
18206
|
+
function performResetSettings() {
|
|
18207
|
+
showNotification('Settings reset to defaults', 'info');
|
|
18208
|
+
setTimeout(() => {
|
|
18209
|
+
window.location.reload();
|
|
18210
|
+
}, 1000);
|
|
18211
|
+
}
|
|
18212
|
+
|
|
18213
|
+
// Migration functions
|
|
18214
|
+
window.refreshMigrationStatus = async function() {
|
|
18215
|
+
try {
|
|
18216
|
+
const response = await fetch('/admin/api/migrations/status');
|
|
18217
|
+
const result = await response.json();
|
|
18218
|
+
|
|
18219
|
+
if (result.success) {
|
|
18220
|
+
updateMigrationUI(result.data);
|
|
18221
|
+
} else {
|
|
18222
|
+
console.error('Failed to refresh migration status');
|
|
18223
|
+
}
|
|
18224
|
+
} catch (error) {
|
|
18225
|
+
console.error('Error loading migration status:', error);
|
|
18226
|
+
}
|
|
18227
|
+
};
|
|
18228
|
+
|
|
18229
|
+
window.runPendingMigrations = async function() {
|
|
18230
|
+
const btn = document.getElementById('run-migrations-btn');
|
|
18231
|
+
if (!btn || btn.disabled) return;
|
|
18232
|
+
|
|
18233
|
+
showConfirmDialog('run-migrations-confirm');
|
|
18234
|
+
};
|
|
18235
|
+
|
|
18236
|
+
window.performRunMigrations = async function() {
|
|
18237
|
+
const btn = document.getElementById('run-migrations-btn');
|
|
18238
|
+
if (!btn) return;
|
|
18239
|
+
|
|
18240
|
+
btn.disabled = true;
|
|
18241
|
+
btn.innerHTML = 'Running...';
|
|
18242
|
+
|
|
18243
|
+
try {
|
|
18244
|
+
const response = await fetch('/admin/api/migrations/run', {
|
|
18245
|
+
method: 'POST'
|
|
18246
|
+
});
|
|
18247
|
+
const result = await response.json();
|
|
18248
|
+
|
|
18249
|
+
if (result.success) {
|
|
18250
|
+
alert(result.message);
|
|
18251
|
+
setTimeout(() => window.refreshMigrationStatus(), 1000);
|
|
18252
|
+
} else {
|
|
18253
|
+
alert(result.error || 'Failed to run migrations');
|
|
18254
|
+
}
|
|
18255
|
+
} catch (error) {
|
|
18256
|
+
alert('Error running migrations');
|
|
18257
|
+
} finally {
|
|
18258
|
+
btn.disabled = false;
|
|
18259
|
+
btn.innerHTML = 'Run Pending';
|
|
18260
|
+
}
|
|
18261
|
+
};
|
|
18262
|
+
|
|
18263
|
+
window.validateSchema = async function() {
|
|
18264
|
+
try {
|
|
18265
|
+
const response = await fetch('/admin/api/migrations/validate');
|
|
18266
|
+
const result = await response.json();
|
|
18267
|
+
|
|
18268
|
+
if (result.success) {
|
|
18269
|
+
if (result.data.valid) {
|
|
18270
|
+
alert('Database schema is valid');
|
|
18271
|
+
} else {
|
|
18272
|
+
alert(\`Schema validation failed: \${result.data.issues.join(', ')}\`);
|
|
18273
|
+
}
|
|
18274
|
+
} else {
|
|
18275
|
+
alert('Failed to validate schema');
|
|
18276
|
+
}
|
|
18277
|
+
} catch (error) {
|
|
18278
|
+
alert('Error validating schema');
|
|
18279
|
+
}
|
|
18280
|
+
};
|
|
18281
|
+
|
|
18282
|
+
window.updateMigrationUI = function(data) {
|
|
18283
|
+
const totalEl = document.getElementById('total-migrations');
|
|
18284
|
+
const appliedEl = document.getElementById('applied-migrations');
|
|
18285
|
+
const pendingEl = document.getElementById('pending-migrations');
|
|
18286
|
+
|
|
18287
|
+
if (totalEl) totalEl.textContent = data.totalMigrations;
|
|
18288
|
+
if (appliedEl) appliedEl.textContent = data.appliedMigrations;
|
|
18289
|
+
if (pendingEl) pendingEl.textContent = data.pendingMigrations;
|
|
18290
|
+
|
|
18291
|
+
const runBtn = document.getElementById('run-migrations-btn');
|
|
18292
|
+
if (runBtn) {
|
|
18293
|
+
runBtn.disabled = data.pendingMigrations === 0;
|
|
18294
|
+
}
|
|
18295
|
+
|
|
18296
|
+
// Update migrations list
|
|
18297
|
+
const listContainer = document.getElementById('migrations-list');
|
|
18298
|
+
if (listContainer && data.migrations && data.migrations.length > 0) {
|
|
18299
|
+
listContainer.innerHTML = data.migrations.map(migration => \`
|
|
18300
|
+
<div class="px-6 py-4 flex items-center justify-between">
|
|
18301
|
+
<div class="flex-1">
|
|
18302
|
+
<div class="flex items-center space-x-3">
|
|
18303
|
+
<div class="flex-shrink-0">
|
|
18304
|
+
\${migration.applied
|
|
18305
|
+
? '<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
|
18306
|
+
: '<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
|
18307
|
+
}
|
|
18308
|
+
</div>
|
|
18309
|
+
<div>
|
|
18310
|
+
<h5 class="text-white font-medium">\${migration.name}</h5>
|
|
18311
|
+
<p class="text-sm text-gray-300">\${migration.filename}</p>
|
|
18312
|
+
\${migration.description ? \`<p class="text-xs text-gray-400 mt-1">\${migration.description}</p>\` : ''}
|
|
18313
|
+
</div>
|
|
18314
|
+
</div>
|
|
18315
|
+
</div>
|
|
18316
|
+
|
|
18317
|
+
<div class="flex items-center space-x-4 text-sm">
|
|
18318
|
+
\${migration.size ? \`<span class="text-gray-400">\${(migration.size / 1024).toFixed(1)} KB</span>\` : ''}
|
|
18319
|
+
<span class="px-2 py-1 rounded-full text-xs font-medium \${
|
|
18320
|
+
migration.applied
|
|
18321
|
+
? 'bg-green-100 text-green-800'
|
|
18322
|
+
: 'bg-orange-100 text-orange-800'
|
|
18323
|
+
}">
|
|
18324
|
+
\${migration.applied ? 'Applied' : 'Pending'}
|
|
18325
|
+
</span>
|
|
18326
|
+
\${migration.appliedAt ? \`<span class="text-gray-400">\${new Date(migration.appliedAt).toLocaleDateString()}</span>\` : ''}
|
|
18327
|
+
</div>
|
|
18328
|
+
</div>
|
|
18329
|
+
\`).join('');
|
|
18330
|
+
}
|
|
18331
|
+
};
|
|
18332
|
+
|
|
18333
|
+
// Auto-load migrations when switching to that tab
|
|
18334
|
+
function initializeMigrations() {
|
|
18335
|
+
if (currentTab === 'migrations') {
|
|
18336
|
+
setTimeout(window.refreshMigrationStatus, 500);
|
|
18337
|
+
}
|
|
18338
|
+
}
|
|
18339
|
+
|
|
18340
|
+
// Database Tools functions
|
|
18341
|
+
window.refreshDatabaseStats = async function() {
|
|
18342
|
+
try {
|
|
18343
|
+
const response = await fetch('/admin/database-tools/api/stats');
|
|
18344
|
+
const result = await response.json();
|
|
18345
|
+
|
|
18346
|
+
if (result.success) {
|
|
18347
|
+
updateDatabaseToolsUI(result.data);
|
|
18348
|
+
} else {
|
|
18349
|
+
console.error('Failed to refresh database stats');
|
|
18350
|
+
}
|
|
18351
|
+
} catch (error) {
|
|
18352
|
+
console.error('Error loading database stats:', error);
|
|
18353
|
+
}
|
|
18354
|
+
};
|
|
18355
|
+
|
|
18356
|
+
window.createDatabaseBackup = async function() {
|
|
18357
|
+
const btn = document.getElementById('create-backup-btn');
|
|
18358
|
+
if (!btn) return;
|
|
18359
|
+
|
|
18360
|
+
btn.disabled = true;
|
|
18361
|
+
btn.innerHTML = 'Creating Backup...';
|
|
18362
|
+
|
|
18363
|
+
try {
|
|
18364
|
+
const response = await fetch('/admin/database-tools/api/backup', {
|
|
18365
|
+
method: 'POST'
|
|
18366
|
+
});
|
|
18367
|
+
const result = await response.json();
|
|
18368
|
+
|
|
18369
|
+
if (result.success) {
|
|
18370
|
+
alert(result.message);
|
|
18371
|
+
setTimeout(() => window.refreshDatabaseStats(), 1000);
|
|
18372
|
+
} else {
|
|
18373
|
+
alert(result.error || 'Failed to create backup');
|
|
18374
|
+
}
|
|
18375
|
+
} catch (error) {
|
|
18376
|
+
alert('Error creating backup');
|
|
18377
|
+
} finally {
|
|
18378
|
+
btn.disabled = false;
|
|
18379
|
+
btn.innerHTML = 'Create Backup';
|
|
18380
|
+
}
|
|
18381
|
+
};
|
|
18382
|
+
|
|
18383
|
+
window.truncateDatabase = async function() {
|
|
18384
|
+
// Show dangerous operation warning
|
|
18385
|
+
const confirmText = prompt(
|
|
18386
|
+
'WARNING: This will delete ALL data except your admin account!\\n\\n' +
|
|
18387
|
+
'This action CANNOT be undone!\\n\\n' +
|
|
18388
|
+
'Type "TRUNCATE ALL DATA" to confirm:'
|
|
18389
|
+
);
|
|
18390
|
+
|
|
18391
|
+
if (confirmText !== 'TRUNCATE ALL DATA') {
|
|
18392
|
+
alert('Operation cancelled. Confirmation text did not match.');
|
|
18393
|
+
return;
|
|
18394
|
+
}
|
|
18395
|
+
|
|
18396
|
+
const btn = document.getElementById('truncate-db-btn');
|
|
18397
|
+
if (!btn) return;
|
|
18398
|
+
|
|
18399
|
+
btn.disabled = true;
|
|
18400
|
+
btn.innerHTML = 'Truncating...';
|
|
18401
|
+
|
|
18402
|
+
try {
|
|
18403
|
+
const response = await fetch('/admin/database-tools/api/truncate', {
|
|
18404
|
+
method: 'POST',
|
|
18405
|
+
headers: {
|
|
18406
|
+
'Content-Type': 'application/json'
|
|
18407
|
+
},
|
|
18408
|
+
body: JSON.stringify({
|
|
18409
|
+
confirmText: confirmText
|
|
18410
|
+
})
|
|
18411
|
+
});
|
|
18412
|
+
const result = await response.json();
|
|
18413
|
+
|
|
18414
|
+
if (result.success) {
|
|
18415
|
+
alert(result.message + '\\n\\nTables cleared: ' + result.data.tablesCleared.join(', '));
|
|
18416
|
+
setTimeout(() => {
|
|
18417
|
+
window.refreshDatabaseStats();
|
|
18418
|
+
// Optionally reload page to refresh all data
|
|
18419
|
+
window.location.reload();
|
|
18420
|
+
}, 2000);
|
|
18421
|
+
} else {
|
|
18422
|
+
alert(result.error || 'Failed to truncate database');
|
|
18423
|
+
}
|
|
18424
|
+
} catch (error) {
|
|
18425
|
+
alert('Error truncating database');
|
|
18426
|
+
} finally {
|
|
18427
|
+
btn.disabled = false;
|
|
18428
|
+
btn.innerHTML = 'Truncate All Data';
|
|
18429
|
+
}
|
|
18430
|
+
};
|
|
18431
|
+
|
|
18432
|
+
window.validateDatabase = async function() {
|
|
18433
|
+
try {
|
|
18434
|
+
const response = await fetch('/admin/database-tools/api/validate');
|
|
18435
|
+
const result = await response.json();
|
|
18436
|
+
|
|
18437
|
+
if (result.success) {
|
|
18438
|
+
if (result.data.valid) {
|
|
18439
|
+
alert('Database validation passed. No issues found.');
|
|
18440
|
+
} else {
|
|
18441
|
+
alert('Database validation failed:\\n\\n' + result.data.issues.join('\\n'));
|
|
18442
|
+
}
|
|
18443
|
+
} else {
|
|
18444
|
+
alert('Failed to validate database');
|
|
18445
|
+
}
|
|
18446
|
+
} catch (error) {
|
|
18447
|
+
alert('Error validating database');
|
|
18448
|
+
}
|
|
18449
|
+
};
|
|
18450
|
+
|
|
18451
|
+
window.updateDatabaseToolsUI = function(data) {
|
|
18452
|
+
const totalTablesEl = document.getElementById('total-tables');
|
|
18453
|
+
const totalRowsEl = document.getElementById('total-rows');
|
|
18454
|
+
const tablesListEl = document.getElementById('tables-list');
|
|
18455
|
+
|
|
18456
|
+
if (totalTablesEl) totalTablesEl.textContent = data.tables.length;
|
|
18457
|
+
if (totalRowsEl) totalRowsEl.textContent = data.totalRows.toLocaleString();
|
|
18458
|
+
|
|
18459
|
+
if (tablesListEl && data.tables && data.tables.length > 0) {
|
|
18460
|
+
tablesListEl.innerHTML = data.tables.map(table => \`
|
|
18461
|
+
<a
|
|
18462
|
+
href="/admin/database-tools/tables/\${table.name}"
|
|
18463
|
+
class="flex items-center justify-between py-3 px-4 rounded-lg bg-white dark:bg-white/5 hover:bg-zinc-50 dark:hover:bg-white/10 cursor-pointer transition-colors ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 no-underline"
|
|
18464
|
+
>
|
|
18465
|
+
<div class="flex items-center space-x-3">
|
|
18466
|
+
<svg class="w-5 h-5 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18467
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
18468
|
+
</svg>
|
|
18469
|
+
<span class="text-zinc-950 dark:text-white font-medium">\${table.name}</span>
|
|
18470
|
+
</div>
|
|
18471
|
+
<div class="flex items-center space-x-3">
|
|
18472
|
+
<span class="text-zinc-500 dark:text-zinc-400 text-sm">\${table.rowCount.toLocaleString()} rows</span>
|
|
18473
|
+
<svg class="w-4 h-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18474
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
18475
|
+
</svg>
|
|
18476
|
+
</div>
|
|
18477
|
+
</a>
|
|
18478
|
+
\`).join('');
|
|
18479
|
+
}
|
|
18480
|
+
};
|
|
18481
|
+
|
|
18482
|
+
// Auto-load tab-specific data after all functions are defined
|
|
18483
|
+
if (currentTab === 'migrations') {
|
|
18484
|
+
setTimeout(window.refreshMigrationStatus, 500);
|
|
18485
|
+
}
|
|
18486
|
+
|
|
18487
|
+
if (currentTab === 'database-tools') {
|
|
18488
|
+
setTimeout(window.refreshDatabaseStats, 500);
|
|
18489
|
+
}
|
|
18490
|
+
</script>
|
|
18491
|
+
|
|
18492
|
+
<!-- Confirmation Dialogs -->
|
|
18493
|
+
${renderConfirmationDialog2({
|
|
18494
|
+
id: "reset-settings-confirm",
|
|
18495
|
+
title: "Reset Settings",
|
|
18496
|
+
message: "Are you sure you want to reset all settings to their default values? This action cannot be undone.",
|
|
18497
|
+
confirmText: "Reset",
|
|
18498
|
+
cancelText: "Cancel",
|
|
18499
|
+
iconColor: "yellow",
|
|
18500
|
+
confirmClass: "bg-yellow-500 hover:bg-yellow-400",
|
|
18501
|
+
onConfirm: "performResetSettings()"
|
|
18502
|
+
})}
|
|
18503
|
+
|
|
18504
|
+
${renderConfirmationDialog2({
|
|
18505
|
+
id: "run-migrations-confirm",
|
|
18506
|
+
title: "Run Migrations",
|
|
18507
|
+
message: "Are you sure you want to run pending migrations? This action cannot be undone.",
|
|
18508
|
+
confirmText: "Run Migrations",
|
|
18509
|
+
cancelText: "Cancel",
|
|
18510
|
+
iconColor: "blue",
|
|
18511
|
+
confirmClass: "bg-blue-500 hover:bg-blue-400",
|
|
18512
|
+
onConfirm: "performRunMigrations()"
|
|
18513
|
+
})}
|
|
18514
|
+
|
|
18515
|
+
${getConfirmationDialogScript2()}
|
|
18516
|
+
`;
|
|
18517
|
+
const layoutData = {
|
|
18518
|
+
title: "Settings",
|
|
18519
|
+
pageTitle: "Settings",
|
|
18520
|
+
currentPath: "/admin/settings",
|
|
18521
|
+
user: data.user,
|
|
18522
|
+
version: data.version,
|
|
18523
|
+
content: pageContent
|
|
18524
|
+
};
|
|
18525
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
18526
|
+
}
|
|
18527
|
+
function renderTabButton(tabId, label, iconPath, activeTab) {
|
|
18528
|
+
const isActive = activeTab === tabId;
|
|
18529
|
+
const baseClasses = "flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 whitespace-nowrap no-underline";
|
|
18530
|
+
const activeClasses = isActive ? "border-zinc-950 dark:border-white text-zinc-950 dark:text-white" : "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-700";
|
|
18531
|
+
return `
|
|
18532
|
+
<a
|
|
18533
|
+
href="/admin/settings/${tabId}"
|
|
18534
|
+
data-tab="${tabId}"
|
|
18535
|
+
class="${baseClasses} ${activeClasses}"
|
|
18536
|
+
>
|
|
18537
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18538
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}"/>
|
|
18539
|
+
</svg>
|
|
18540
|
+
<span>${label}</span>
|
|
18541
|
+
</a>
|
|
18542
|
+
`;
|
|
18543
|
+
}
|
|
18544
|
+
function renderTabContent(activeTab, settings) {
|
|
18545
|
+
switch (activeTab) {
|
|
18546
|
+
case "general":
|
|
18547
|
+
return renderGeneralSettings(settings?.general);
|
|
18548
|
+
case "appearance":
|
|
18549
|
+
return renderAppearanceSettings(settings?.appearance);
|
|
18550
|
+
case "security":
|
|
18551
|
+
return renderSecuritySettings(settings?.security);
|
|
18552
|
+
case "notifications":
|
|
18553
|
+
return renderNotificationSettings(settings?.notifications);
|
|
18554
|
+
case "storage":
|
|
18555
|
+
return renderStorageSettings(settings?.storage);
|
|
18556
|
+
case "migrations":
|
|
18557
|
+
return renderMigrationSettings(settings?.migrations);
|
|
18558
|
+
case "database-tools":
|
|
18559
|
+
return renderDatabaseToolsSettings(settings?.databaseTools);
|
|
18560
|
+
default:
|
|
18561
|
+
return renderGeneralSettings(settings?.general);
|
|
18562
|
+
}
|
|
18563
|
+
}
|
|
18564
|
+
function renderGeneralSettings(settings) {
|
|
18565
|
+
return `
|
|
18566
|
+
<div class="space-y-6">
|
|
18567
|
+
<div>
|
|
18568
|
+
<h3 class="text-lg/7 font-semibold text-zinc-950 dark:text-white">General Settings</h3>
|
|
18569
|
+
<p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Configure basic application settings and preferences.</p>
|
|
18570
|
+
</div>
|
|
18571
|
+
|
|
18572
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
18573
|
+
<div class="space-y-4">
|
|
18574
|
+
<div>
|
|
18575
|
+
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Site Name</label>
|
|
18576
|
+
<input
|
|
18577
|
+
type="text"
|
|
18578
|
+
name="siteName"
|
|
18579
|
+
value="${settings?.siteName || "SonicJS AI"}"
|
|
18580
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
|
|
18581
|
+
placeholder="Enter site name"
|
|
18582
|
+
/>
|
|
18583
|
+
</div>
|
|
18584
|
+
|
|
18585
|
+
<div>
|
|
18586
|
+
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Admin Email</label>
|
|
18587
|
+
<input
|
|
18588
|
+
type="email"
|
|
18589
|
+
name="adminEmail"
|
|
18590
|
+
value="${settings?.adminEmail || "admin@example.com"}"
|
|
18591
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
|
|
18592
|
+
placeholder="admin@example.com"
|
|
18593
|
+
/>
|
|
18594
|
+
</div>
|
|
18595
|
+
|
|
18596
|
+
<div>
|
|
18597
|
+
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Timezone</label>
|
|
18598
|
+
<select
|
|
18599
|
+
name="timezone"
|
|
18600
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
|
|
18601
|
+
>
|
|
18602
|
+
<option value="UTC" ${settings?.timezone === "UTC" ? "selected" : ""}>UTC</option>
|
|
18603
|
+
<option value="America/New_York" ${settings?.timezone === "America/New_York" ? "selected" : ""}>Eastern Time</option>
|
|
18604
|
+
<option value="America/Chicago" ${settings?.timezone === "America/Chicago" ? "selected" : ""}>Central Time</option>
|
|
18605
|
+
<option value="America/Denver" ${settings?.timezone === "America/Denver" ? "selected" : ""}>Mountain Time</option>
|
|
18606
|
+
<option value="America/Los_Angeles" ${settings?.timezone === "America/Los_Angeles" ? "selected" : ""}>Pacific Time</option>
|
|
18607
|
+
</select>
|
|
18608
|
+
</div>
|
|
18609
|
+
</div>
|
|
18610
|
+
|
|
18611
|
+
<div class="space-y-4">
|
|
18612
|
+
<div>
|
|
18613
|
+
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Site Description</label>
|
|
18614
|
+
<textarea
|
|
18615
|
+
name="siteDescription"
|
|
18616
|
+
rows="3"
|
|
18617
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
|
|
18618
|
+
placeholder="Describe your site..."
|
|
18619
|
+
>${settings?.siteDescription || ""}</textarea>
|
|
18620
|
+
</div>
|
|
18621
|
+
|
|
18622
|
+
<div>
|
|
18623
|
+
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Language</label>
|
|
18624
|
+
<select
|
|
18625
|
+
name="language"
|
|
18626
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
|
|
18627
|
+
>
|
|
18628
|
+
<option value="en" ${settings?.language === "en" ? "selected" : ""}>English</option>
|
|
18629
|
+
<option value="es" ${settings?.language === "es" ? "selected" : ""}>Spanish</option>
|
|
18630
|
+
<option value="fr" ${settings?.language === "fr" ? "selected" : ""}>French</option>
|
|
18631
|
+
<option value="de" ${settings?.language === "de" ? "selected" : ""}>German</option>
|
|
18632
|
+
</select>
|
|
18633
|
+
</div>
|
|
18634
|
+
|
|
18635
|
+
<div class="flex gap-3">
|
|
18636
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18637
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18638
|
+
<input
|
|
18639
|
+
type="checkbox"
|
|
18640
|
+
id="maintenanceMode"
|
|
18641
|
+
name="maintenanceMode"
|
|
18642
|
+
${settings?.maintenanceMode ? "checked" : ""}
|
|
18643
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18644
|
+
/>
|
|
18645
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18646
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18647
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18648
|
+
</svg>
|
|
18649
|
+
</div>
|
|
18650
|
+
</div>
|
|
18651
|
+
<div class="text-sm/6">
|
|
18652
|
+
<label for="maintenanceMode" class="font-medium text-zinc-950 dark:text-white">
|
|
18653
|
+
Enable maintenance mode
|
|
18654
|
+
</label>
|
|
18655
|
+
</div>
|
|
18656
|
+
</div>
|
|
18657
|
+
</div>
|
|
18658
|
+
</div>
|
|
18659
|
+
</div>
|
|
18660
|
+
`;
|
|
18661
|
+
}
|
|
18662
|
+
function renderAppearanceSettings(settings) {
|
|
18663
|
+
return `
|
|
18664
|
+
<div class="space-y-6">
|
|
18665
|
+
<!-- WIP Notice -->
|
|
18666
|
+
<div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
|
|
18667
|
+
<div class="flex items-start space-x-3">
|
|
18668
|
+
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18669
|
+
<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"/>
|
|
18670
|
+
</svg>
|
|
18671
|
+
<div class="flex-1">
|
|
18672
|
+
<h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
|
|
18673
|
+
<p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
|
|
18674
|
+
This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
|
|
18675
|
+
</p>
|
|
18676
|
+
</div>
|
|
18677
|
+
</div>
|
|
18678
|
+
</div>
|
|
18679
|
+
|
|
18680
|
+
<div>
|
|
18681
|
+
<h3 class="text-lg font-semibold text-white mb-4">Appearance Settings</h3>
|
|
18682
|
+
<p class="text-gray-300 mb-6">Customize the look and feel of your application.</p>
|
|
18683
|
+
</div>
|
|
18684
|
+
|
|
18685
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
18686
|
+
<div class="space-y-4">
|
|
18687
|
+
<div>
|
|
18688
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Theme</label>
|
|
18689
|
+
<div class="grid grid-cols-3 gap-3">
|
|
18690
|
+
<label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
|
|
18691
|
+
<input
|
|
18692
|
+
type="radio"
|
|
18693
|
+
name="theme"
|
|
18694
|
+
value="light"
|
|
18695
|
+
${settings?.theme === "light" ? "checked" : ""}
|
|
18696
|
+
class="text-blue-600"
|
|
18697
|
+
/>
|
|
18698
|
+
<span class="text-sm text-gray-300">Light</span>
|
|
18699
|
+
</label>
|
|
18700
|
+
<label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
|
|
18701
|
+
<input
|
|
18702
|
+
type="radio"
|
|
18703
|
+
name="theme"
|
|
18704
|
+
value="dark"
|
|
18705
|
+
${settings?.theme === "dark" || !settings?.theme ? "checked" : ""}
|
|
18706
|
+
class="text-blue-600"
|
|
18707
|
+
/>
|
|
18708
|
+
<span class="text-sm text-gray-300">Dark</span>
|
|
18709
|
+
</label>
|
|
18710
|
+
<label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
|
|
18711
|
+
<input
|
|
18712
|
+
type="radio"
|
|
18713
|
+
name="theme"
|
|
18714
|
+
value="auto"
|
|
18715
|
+
${settings?.theme === "auto" ? "checked" : ""}
|
|
18716
|
+
class="text-blue-600"
|
|
18717
|
+
/>
|
|
18718
|
+
<span class="text-sm text-gray-300">Auto</span>
|
|
18719
|
+
</label>
|
|
18720
|
+
</div>
|
|
18721
|
+
</div>
|
|
18722
|
+
|
|
18723
|
+
<div>
|
|
18724
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Primary Color</label>
|
|
18725
|
+
<div class="flex items-center space-x-3">
|
|
18726
|
+
<input
|
|
18727
|
+
type="color"
|
|
18728
|
+
name="primaryColor"
|
|
18729
|
+
value="${settings?.primaryColor || "#465FFF"}"
|
|
18730
|
+
class="w-12 h-10 bg-white/10 border border-white/20 rounded-lg cursor-pointer"
|
|
18731
|
+
/>
|
|
18732
|
+
<input
|
|
18733
|
+
type="text"
|
|
18734
|
+
value="${settings?.primaryColor || "#465FFF"}"
|
|
18735
|
+
class="flex-1 px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18736
|
+
placeholder="#465FFF"
|
|
18737
|
+
/>
|
|
18738
|
+
</div>
|
|
18739
|
+
</div>
|
|
18740
|
+
|
|
18741
|
+
<div>
|
|
18742
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Logo URL</label>
|
|
18743
|
+
<input
|
|
18744
|
+
type="url"
|
|
18745
|
+
name="logoUrl"
|
|
18746
|
+
value="${settings?.logoUrl || ""}"
|
|
18747
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18748
|
+
placeholder="https://example.com/logo.png"
|
|
18749
|
+
/>
|
|
18750
|
+
</div>
|
|
18751
|
+
</div>
|
|
18752
|
+
|
|
18753
|
+
<div class="space-y-4">
|
|
18754
|
+
<div>
|
|
18755
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Favicon URL</label>
|
|
18756
|
+
<input
|
|
18757
|
+
type="url"
|
|
18758
|
+
name="favicon"
|
|
18759
|
+
value="${settings?.favicon || ""}"
|
|
18760
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18761
|
+
placeholder="https://example.com/favicon.ico"
|
|
18762
|
+
/>
|
|
18763
|
+
</div>
|
|
18764
|
+
|
|
18765
|
+
<div>
|
|
18766
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Custom CSS</label>
|
|
18767
|
+
<textarea
|
|
18768
|
+
name="customCSS"
|
|
18769
|
+
rows="6"
|
|
18770
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
|
18771
|
+
placeholder="/* Add your custom CSS here */"
|
|
18772
|
+
>${settings?.customCSS || ""}</textarea>
|
|
18773
|
+
</div>
|
|
18774
|
+
</div>
|
|
18775
|
+
</div>
|
|
18776
|
+
</div>
|
|
18777
|
+
`;
|
|
18778
|
+
}
|
|
18779
|
+
function renderSecuritySettings(settings) {
|
|
18780
|
+
return `
|
|
18781
|
+
<div class="space-y-6">
|
|
18782
|
+
<!-- WIP Notice -->
|
|
18783
|
+
<div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
|
|
18784
|
+
<div class="flex items-start space-x-3">
|
|
18785
|
+
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18786
|
+
<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"/>
|
|
18787
|
+
</svg>
|
|
18788
|
+
<div class="flex-1">
|
|
18789
|
+
<h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
|
|
18790
|
+
<p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
|
|
18791
|
+
This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
|
|
18792
|
+
</p>
|
|
18793
|
+
</div>
|
|
18794
|
+
</div>
|
|
18795
|
+
</div>
|
|
18796
|
+
|
|
18797
|
+
<div>
|
|
18798
|
+
<h3 class="text-lg font-semibold text-white mb-4">Security Settings</h3>
|
|
18799
|
+
<p class="text-gray-300 mb-6">Configure security and authentication settings.</p>
|
|
18800
|
+
</div>
|
|
18801
|
+
|
|
18802
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
18803
|
+
<div class="space-y-5">
|
|
18804
|
+
<div class="flex gap-3">
|
|
18805
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18806
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18807
|
+
<input
|
|
18808
|
+
type="checkbox"
|
|
18809
|
+
id="twoFactorEnabled"
|
|
18810
|
+
name="twoFactorEnabled"
|
|
18811
|
+
${settings?.twoFactorEnabled ? "checked" : ""}
|
|
18812
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18813
|
+
/>
|
|
18814
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18815
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18816
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18817
|
+
</svg>
|
|
18818
|
+
</div>
|
|
18819
|
+
</div>
|
|
18820
|
+
<div class="text-sm/6">
|
|
18821
|
+
<label for="twoFactorEnabled" class="font-medium text-zinc-950 dark:text-white">
|
|
18822
|
+
Enable Two-Factor Authentication
|
|
18823
|
+
</label>
|
|
18824
|
+
</div>
|
|
18825
|
+
</div>
|
|
18826
|
+
|
|
18827
|
+
<div>
|
|
18828
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Session Timeout (minutes)</label>
|
|
18829
|
+
<input
|
|
18830
|
+
type="number"
|
|
18831
|
+
name="sessionTimeout"
|
|
18832
|
+
value="${settings?.sessionTimeout || 30}"
|
|
18833
|
+
min="5"
|
|
18834
|
+
max="1440"
|
|
18835
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18836
|
+
/>
|
|
18837
|
+
</div>
|
|
18838
|
+
|
|
18839
|
+
<div>
|
|
18840
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Password Requirements</label>
|
|
18841
|
+
<div class="space-y-3">
|
|
18842
|
+
<div class="flex gap-3">
|
|
18843
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18844
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18845
|
+
<input
|
|
18846
|
+
type="checkbox"
|
|
18847
|
+
id="requireUppercase"
|
|
18848
|
+
name="requireUppercase"
|
|
18849
|
+
${settings?.passwordRequirements?.requireUppercase ? "checked" : ""}
|
|
18850
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18851
|
+
/>
|
|
18852
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18853
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18854
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18855
|
+
</svg>
|
|
18856
|
+
</div>
|
|
18857
|
+
</div>
|
|
18858
|
+
<div class="text-sm/6">
|
|
18859
|
+
<label for="requireUppercase" class="font-medium text-zinc-950 dark:text-white">Require uppercase letters</label>
|
|
18860
|
+
</div>
|
|
18861
|
+
</div>
|
|
18862
|
+
<div class="flex gap-3">
|
|
18863
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18864
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18865
|
+
<input
|
|
18866
|
+
type="checkbox"
|
|
18867
|
+
id="requireNumbers"
|
|
18868
|
+
name="requireNumbers"
|
|
18869
|
+
${settings?.passwordRequirements?.requireNumbers ? "checked" : ""}
|
|
18870
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18871
|
+
/>
|
|
18872
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18873
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18874
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18875
|
+
</svg>
|
|
18876
|
+
</div>
|
|
18877
|
+
</div>
|
|
18878
|
+
<div class="text-sm/6">
|
|
18879
|
+
<label for="requireNumbers" class="font-medium text-zinc-950 dark:text-white">Require numbers</label>
|
|
18880
|
+
</div>
|
|
18881
|
+
</div>
|
|
18882
|
+
<div class="flex gap-3">
|
|
18883
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18884
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18885
|
+
<input
|
|
18886
|
+
type="checkbox"
|
|
18887
|
+
id="requireSymbols"
|
|
18888
|
+
name="requireSymbols"
|
|
18889
|
+
${settings?.passwordRequirements?.requireSymbols ? "checked" : ""}
|
|
18890
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18891
|
+
/>
|
|
18892
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18893
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18894
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18895
|
+
</svg>
|
|
18896
|
+
</div>
|
|
18897
|
+
</div>
|
|
18898
|
+
<div class="text-sm/6">
|
|
18899
|
+
<label for="requireSymbols" class="font-medium text-zinc-950 dark:text-white">Require symbols</label>
|
|
18900
|
+
</div>
|
|
18901
|
+
</div>
|
|
18902
|
+
</div>
|
|
18903
|
+
</div>
|
|
18904
|
+
</div>
|
|
18905
|
+
|
|
18906
|
+
<div class="space-y-4">
|
|
18907
|
+
<div>
|
|
18908
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Minimum Password Length</label>
|
|
18909
|
+
<input
|
|
18910
|
+
type="number"
|
|
18911
|
+
name="minPasswordLength"
|
|
18912
|
+
value="${settings?.passwordRequirements?.minLength || 8}"
|
|
18913
|
+
min="6"
|
|
18914
|
+
max="128"
|
|
18915
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18916
|
+
/>
|
|
18917
|
+
</div>
|
|
18918
|
+
|
|
18919
|
+
<div>
|
|
18920
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">IP Whitelist</label>
|
|
18921
|
+
<textarea
|
|
18922
|
+
name="ipWhitelist"
|
|
18923
|
+
rows="4"
|
|
18924
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
18925
|
+
placeholder="Enter IP addresses (one per line) 192.168.1.1 10.0.0.1"
|
|
18926
|
+
>${settings?.ipWhitelist?.join("\n") || ""}</textarea>
|
|
18927
|
+
<p class="text-xs text-gray-400 mt-1">Leave empty to allow all IPs</p>
|
|
18928
|
+
</div>
|
|
18929
|
+
</div>
|
|
18930
|
+
</div>
|
|
18931
|
+
</div>
|
|
18932
|
+
`;
|
|
18933
|
+
}
|
|
18934
|
+
function renderNotificationSettings(settings) {
|
|
18935
|
+
return `
|
|
18936
|
+
<div class="space-y-6">
|
|
18937
|
+
<!-- WIP Notice -->
|
|
18938
|
+
<div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
|
|
18939
|
+
<div class="flex items-start space-x-3">
|
|
18940
|
+
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18941
|
+
<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"/>
|
|
18942
|
+
</svg>
|
|
18943
|
+
<div class="flex-1">
|
|
18944
|
+
<h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
|
|
18945
|
+
<p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
|
|
18946
|
+
This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
|
|
18947
|
+
</p>
|
|
18948
|
+
</div>
|
|
18949
|
+
</div>
|
|
18950
|
+
</div>
|
|
18951
|
+
|
|
18952
|
+
<div>
|
|
18953
|
+
<h3 class="text-lg font-semibold text-white mb-4">Notification Settings</h3>
|
|
18954
|
+
<p class="text-gray-300 mb-6">Configure how and when you receive notifications.</p>
|
|
18955
|
+
</div>
|
|
18956
|
+
|
|
18957
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
18958
|
+
<div class="space-y-4">
|
|
18959
|
+
<div>
|
|
18960
|
+
<h4 class="text-md font-medium text-white mb-3">Email Notifications</h4>
|
|
18961
|
+
<div class="space-y-5">
|
|
18962
|
+
<div class="flex gap-3">
|
|
18963
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18964
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18965
|
+
<input
|
|
18966
|
+
type="checkbox"
|
|
18967
|
+
id="emailNotifications"
|
|
18968
|
+
name="emailNotifications"
|
|
18969
|
+
${settings?.emailNotifications ? "checked" : ""}
|
|
18970
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18971
|
+
/>
|
|
18972
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18973
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18974
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18975
|
+
</svg>
|
|
18976
|
+
</div>
|
|
18977
|
+
</div>
|
|
18978
|
+
<div class="text-sm/6">
|
|
18979
|
+
<label for="emailNotifications" class="font-medium text-zinc-950 dark:text-white">Enable email notifications</label>
|
|
18980
|
+
</div>
|
|
18981
|
+
</div>
|
|
18982
|
+
|
|
18983
|
+
<div class="flex gap-3">
|
|
18984
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
18985
|
+
<div class="group grid size-4 grid-cols-1">
|
|
18986
|
+
<input
|
|
18987
|
+
type="checkbox"
|
|
18988
|
+
id="contentUpdates"
|
|
18989
|
+
name="contentUpdates"
|
|
18990
|
+
${settings?.contentUpdates ? "checked" : ""}
|
|
18991
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
18992
|
+
/>
|
|
18993
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
18994
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
18995
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
18996
|
+
</svg>
|
|
18997
|
+
</div>
|
|
18998
|
+
</div>
|
|
18999
|
+
<div class="text-sm/6">
|
|
19000
|
+
<label for="contentUpdates" class="font-medium text-zinc-950 dark:text-white">Content updates</label>
|
|
19001
|
+
</div>
|
|
19002
|
+
</div>
|
|
19003
|
+
|
|
19004
|
+
<div class="flex gap-3">
|
|
19005
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
19006
|
+
<div class="group grid size-4 grid-cols-1">
|
|
19007
|
+
<input
|
|
19008
|
+
type="checkbox"
|
|
19009
|
+
id="systemAlerts"
|
|
19010
|
+
name="systemAlerts"
|
|
19011
|
+
${settings?.systemAlerts ? "checked" : ""}
|
|
19012
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
19013
|
+
/>
|
|
19014
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
19015
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
19016
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
19017
|
+
</svg>
|
|
19018
|
+
</div>
|
|
19019
|
+
</div>
|
|
19020
|
+
<div class="text-sm/6">
|
|
19021
|
+
<label for="systemAlerts" class="font-medium text-zinc-950 dark:text-white">System alerts</label>
|
|
19022
|
+
</div>
|
|
19023
|
+
</div>
|
|
19024
|
+
|
|
19025
|
+
<div class="flex gap-3">
|
|
19026
|
+
<div class="flex h-6 shrink-0 items-center">
|
|
19027
|
+
<div class="group grid size-4 grid-cols-1">
|
|
19028
|
+
<input
|
|
19029
|
+
type="checkbox"
|
|
19030
|
+
id="userRegistrations"
|
|
19031
|
+
name="userRegistrations"
|
|
19032
|
+
${settings?.userRegistrations ? "checked" : ""}
|
|
19033
|
+
class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
|
|
19034
|
+
/>
|
|
19035
|
+
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
|
|
19036
|
+
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
|
|
19037
|
+
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
|
|
19038
|
+
</svg>
|
|
19039
|
+
</div>
|
|
19040
|
+
</div>
|
|
19041
|
+
<div class="text-sm/6">
|
|
19042
|
+
<label for="userRegistrations" class="font-medium text-zinc-950 dark:text-white">New user registrations</label>
|
|
19043
|
+
</div>
|
|
19044
|
+
</div>
|
|
19045
|
+
</div>
|
|
19046
|
+
</div>
|
|
19047
|
+
</div>
|
|
19048
|
+
|
|
19049
|
+
<div class="space-y-4">
|
|
19050
|
+
<div>
|
|
19051
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Email Frequency</label>
|
|
19052
|
+
<select
|
|
19053
|
+
name="emailFrequency"
|
|
19054
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19055
|
+
>
|
|
19056
|
+
<option value="immediate" ${settings?.emailFrequency === "immediate" ? "selected" : ""}>Immediate</option>
|
|
19057
|
+
<option value="daily" ${settings?.emailFrequency === "daily" ? "selected" : ""}>Daily Digest</option>
|
|
19058
|
+
<option value="weekly" ${settings?.emailFrequency === "weekly" ? "selected" : ""}>Weekly Digest</option>
|
|
19059
|
+
</select>
|
|
19060
|
+
</div>
|
|
19061
|
+
|
|
19062
|
+
<div class="p-4 bg-blue-500/20 border border-blue-500/30 rounded-lg">
|
|
19063
|
+
<div class="flex items-start space-x-3">
|
|
19064
|
+
<svg class="w-5 h-5 text-blue-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19065
|
+
<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"/>
|
|
19066
|
+
</svg>
|
|
19067
|
+
<div>
|
|
19068
|
+
<h5 class="text-sm font-medium text-blue-300">Notification Preferences</h5>
|
|
19069
|
+
<p class="text-xs text-blue-200 mt-1">
|
|
19070
|
+
Critical system alerts will always be sent immediately regardless of your frequency setting.
|
|
19071
|
+
</p>
|
|
19072
|
+
</div>
|
|
19073
|
+
</div>
|
|
19074
|
+
</div>
|
|
19075
|
+
</div>
|
|
19076
|
+
</div>
|
|
19077
|
+
</div>
|
|
19078
|
+
`;
|
|
19079
|
+
}
|
|
19080
|
+
function renderStorageSettings(settings) {
|
|
19081
|
+
return `
|
|
19082
|
+
<div class="space-y-6">
|
|
19083
|
+
<!-- WIP Notice -->
|
|
19084
|
+
<div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
|
|
19085
|
+
<div class="flex items-start space-x-3">
|
|
19086
|
+
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19087
|
+
<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"/>
|
|
19088
|
+
</svg>
|
|
19089
|
+
<div class="flex-1">
|
|
19090
|
+
<h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
|
|
19091
|
+
<p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
|
|
19092
|
+
This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
|
|
19093
|
+
</p>
|
|
19094
|
+
</div>
|
|
19095
|
+
</div>
|
|
19096
|
+
</div>
|
|
19097
|
+
|
|
19098
|
+
<div>
|
|
19099
|
+
<h3 class="text-lg font-semibold text-white mb-4">Storage Settings</h3>
|
|
19100
|
+
<p class="text-gray-300 mb-6">Configure file storage and backup settings.</p>
|
|
19101
|
+
</div>
|
|
19102
|
+
|
|
19103
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
19104
|
+
<div class="space-y-4">
|
|
19105
|
+
<div>
|
|
19106
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Max File Size (MB)</label>
|
|
19107
|
+
<input
|
|
19108
|
+
type="number"
|
|
19109
|
+
name="maxFileSize"
|
|
19110
|
+
value="${settings?.maxFileSize || 10}"
|
|
19111
|
+
min="1"
|
|
19112
|
+
max="100"
|
|
19113
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19114
|
+
/>
|
|
19115
|
+
</div>
|
|
19116
|
+
|
|
19117
|
+
<div>
|
|
19118
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Storage Provider</label>
|
|
19119
|
+
<select
|
|
19120
|
+
name="storageProvider"
|
|
19121
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19122
|
+
>
|
|
19123
|
+
<option value="local" ${settings?.storageProvider === "local" ? "selected" : ""}>Local Storage</option>
|
|
19124
|
+
<option value="cloudflare" ${settings?.storageProvider === "cloudflare" ? "selected" : ""}>Cloudflare R2</option>
|
|
19125
|
+
<option value="s3" ${settings?.storageProvider === "s3" ? "selected" : ""}>Amazon S3</option>
|
|
19126
|
+
</select>
|
|
19127
|
+
</div>
|
|
19128
|
+
|
|
19129
|
+
<div>
|
|
19130
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Backup Frequency</label>
|
|
19131
|
+
<select
|
|
19132
|
+
name="backupFrequency"
|
|
19133
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19134
|
+
>
|
|
19135
|
+
<option value="daily" ${settings?.backupFrequency === "daily" ? "selected" : ""}>Daily</option>
|
|
19136
|
+
<option value="weekly" ${settings?.backupFrequency === "weekly" ? "selected" : ""}>Weekly</option>
|
|
19137
|
+
<option value="monthly" ${settings?.backupFrequency === "monthly" ? "selected" : ""}>Monthly</option>
|
|
19138
|
+
</select>
|
|
19139
|
+
</div>
|
|
19140
|
+
</div>
|
|
19141
|
+
|
|
19142
|
+
<div class="space-y-4">
|
|
19143
|
+
<div>
|
|
19144
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Allowed File Types</label>
|
|
19145
|
+
<textarea
|
|
19146
|
+
name="allowedFileTypes"
|
|
19147
|
+
rows="3"
|
|
19148
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19149
|
+
placeholder="jpg, jpeg, png, gif, pdf, docx"
|
|
19150
|
+
>${settings?.allowedFileTypes?.join(", ") || "jpg, jpeg, png, gif, pdf, docx"}</textarea>
|
|
19151
|
+
</div>
|
|
19152
|
+
|
|
19153
|
+
<div>
|
|
19154
|
+
<label class="block text-sm font-medium text-gray-300 mb-2">Backup Retention (days)</label>
|
|
19155
|
+
<input
|
|
19156
|
+
type="number"
|
|
19157
|
+
name="retentionPeriod"
|
|
19158
|
+
value="${settings?.retentionPeriod || 30}"
|
|
19159
|
+
min="7"
|
|
19160
|
+
max="365"
|
|
19161
|
+
class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
19162
|
+
/>
|
|
19163
|
+
</div>
|
|
19164
|
+
|
|
19165
|
+
<div class="p-4 bg-green-500/20 border border-green-500/30 rounded-lg">
|
|
19166
|
+
<div class="flex items-start space-x-3">
|
|
19167
|
+
<svg class="w-5 h-5 text-green-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19168
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
19169
|
+
</svg>
|
|
19170
|
+
<div>
|
|
19171
|
+
<h5 class="text-sm font-medium text-green-300">Storage Status</h5>
|
|
19172
|
+
<p class="text-xs text-green-200 mt-1">
|
|
19173
|
+
Current usage: 2.4 GB / 10 GB available
|
|
19174
|
+
</p>
|
|
19175
|
+
</div>
|
|
19176
|
+
</div>
|
|
19177
|
+
</div>
|
|
19178
|
+
</div>
|
|
19179
|
+
</div>
|
|
19180
|
+
</div>
|
|
19181
|
+
`;
|
|
19182
|
+
}
|
|
19183
|
+
function renderMigrationSettings(settings) {
|
|
19184
|
+
return `
|
|
19185
|
+
<div class="space-y-6">
|
|
19186
|
+
<div>
|
|
19187
|
+
<h3 class="text-lg font-semibold text-white mb-4">Database Migrations</h3>
|
|
19188
|
+
<p class="text-gray-300 mb-6">View and manage database migrations to keep your schema up to date.</p>
|
|
19189
|
+
</div>
|
|
19190
|
+
|
|
19191
|
+
<!-- Migration Status Overview -->
|
|
19192
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
19193
|
+
<div class="backdrop-blur-md bg-blue-500/20 rounded-lg border border-blue-500/30 p-4">
|
|
19194
|
+
<div class="flex items-center justify-between">
|
|
19195
|
+
<div>
|
|
19196
|
+
<p class="text-sm text-blue-300">Total Migrations</p>
|
|
19197
|
+
<p id="total-migrations" class="text-2xl font-bold text-white">${settings?.totalMigrations || "0"}</p>
|
|
19198
|
+
</div>
|
|
19199
|
+
<svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19200
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
|
19201
|
+
</svg>
|
|
19202
|
+
</div>
|
|
19203
|
+
</div>
|
|
19204
|
+
|
|
19205
|
+
<div class="backdrop-blur-md bg-green-500/20 rounded-lg border border-green-500/30 p-4">
|
|
19206
|
+
<div class="flex items-center justify-between">
|
|
19207
|
+
<div>
|
|
19208
|
+
<p class="text-sm text-green-300">Applied</p>
|
|
19209
|
+
<p id="applied-migrations" class="text-2xl font-bold text-white">${settings?.appliedMigrations || "0"}</p>
|
|
19210
|
+
</div>
|
|
19211
|
+
<svg class="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19212
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
19213
|
+
</svg>
|
|
19214
|
+
</div>
|
|
19215
|
+
</div>
|
|
19216
|
+
|
|
19217
|
+
<div class="backdrop-blur-md bg-orange-500/20 rounded-lg border border-orange-500/30 p-4">
|
|
19218
|
+
<div class="flex items-center justify-between">
|
|
19219
|
+
<div>
|
|
19220
|
+
<p class="text-sm text-orange-300">Pending</p>
|
|
19221
|
+
<p id="pending-migrations" class="text-2xl font-bold text-white">${settings?.pendingMigrations || "0"}</p>
|
|
19222
|
+
</div>
|
|
19223
|
+
<svg class="w-8 h-8 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19224
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
19225
|
+
</svg>
|
|
19226
|
+
</div>
|
|
19227
|
+
</div>
|
|
19228
|
+
</div>
|
|
19229
|
+
|
|
19230
|
+
<!-- Migration Actions -->
|
|
19231
|
+
<div class="flex items-center space-x-4 mb-6">
|
|
19232
|
+
<button
|
|
19233
|
+
onclick="window.refreshMigrationStatus()"
|
|
19234
|
+
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
|
19235
|
+
>
|
|
19236
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19237
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
19238
|
+
</svg>
|
|
19239
|
+
Refresh Status
|
|
19240
|
+
</button>
|
|
19241
|
+
|
|
19242
|
+
<button
|
|
19243
|
+
onclick="window.runPendingMigrations()"
|
|
19244
|
+
id="run-migrations-btn"
|
|
19245
|
+
class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors"
|
|
19246
|
+
${(settings?.pendingMigrations || 0) === 0 ? "disabled" : ""}
|
|
19247
|
+
>
|
|
19248
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19249
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H15M9 10v4.586a1 1 0 00.293.707l2.414 2.414a1 1 0 00.707.293H15M9 10V9a2 2 0 012-2h2a2 2 0 012 2v1"/>
|
|
19250
|
+
</svg>
|
|
19251
|
+
Run Pending
|
|
19252
|
+
</button>
|
|
19253
|
+
|
|
19254
|
+
<button
|
|
19255
|
+
onclick="window.validateSchema()"
|
|
19256
|
+
class="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors"
|
|
19257
|
+
>
|
|
19258
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19259
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
19260
|
+
</svg>
|
|
19261
|
+
Validate Schema
|
|
19262
|
+
</button>
|
|
19263
|
+
</div>
|
|
19264
|
+
|
|
19265
|
+
<!-- Migrations List -->
|
|
19266
|
+
<div class="backdrop-blur-md bg-white/10 rounded-lg border border-white/20 overflow-hidden">
|
|
19267
|
+
<div class="px-6 py-4 border-b border-white/10">
|
|
19268
|
+
<h4 class="text-lg font-medium text-white">Migration History</h4>
|
|
19269
|
+
<p class="text-sm text-gray-300 mt-1">List of all available database migrations</p>
|
|
19270
|
+
</div>
|
|
19271
|
+
|
|
19272
|
+
<div id="migrations-list" class="divide-y divide-white/10">
|
|
19273
|
+
<div class="px-6 py-8 text-center">
|
|
19274
|
+
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19275
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
|
19276
|
+
</svg>
|
|
19277
|
+
<p class="text-gray-300">Loading migration status...</p>
|
|
19278
|
+
</div>
|
|
19279
|
+
</div>
|
|
19280
|
+
</div>
|
|
19281
|
+
</div>
|
|
19282
|
+
|
|
19283
|
+
<script>
|
|
19284
|
+
// Load migration status when tab becomes active
|
|
19285
|
+
if (typeof refreshMigrationStatus === 'undefined') {
|
|
19286
|
+
window.refreshMigrationStatus = async function() {
|
|
19287
|
+
try {
|
|
19288
|
+
const response = await fetch('/admin/api/migrations/status');
|
|
19289
|
+
const result = await response.json();
|
|
19290
|
+
|
|
19291
|
+
if (result.success) {
|
|
19292
|
+
updateMigrationUI(result.data);
|
|
19293
|
+
} else {
|
|
19294
|
+
console.error('Failed to refresh migration status');
|
|
19295
|
+
}
|
|
19296
|
+
} catch (error) {
|
|
19297
|
+
console.error('Error loading migration status:', error);
|
|
19298
|
+
}
|
|
19299
|
+
};
|
|
19300
|
+
|
|
19301
|
+
window.runPendingMigrations = async function() {
|
|
19302
|
+
const btn = document.getElementById('run-migrations-btn');
|
|
19303
|
+
if (!btn || btn.disabled) return;
|
|
19304
|
+
|
|
19305
|
+
showConfirmDialog('run-migrations-confirm');
|
|
19306
|
+
};
|
|
19307
|
+
|
|
19308
|
+
window.performRunMigrations = async function() {
|
|
19309
|
+
const btn = document.getElementById('run-migrations-btn');
|
|
19310
|
+
if (!btn) return;
|
|
19311
|
+
|
|
19312
|
+
btn.disabled = true;
|
|
19313
|
+
btn.innerHTML = 'Running...';
|
|
19314
|
+
|
|
19315
|
+
try {
|
|
19316
|
+
const response = await fetch('/admin/api/migrations/run', {
|
|
19317
|
+
method: 'POST'
|
|
19318
|
+
});
|
|
19319
|
+
const result = await response.json();
|
|
19320
|
+
|
|
19321
|
+
if (result.success) {
|
|
19322
|
+
alert(result.message);
|
|
19323
|
+
setTimeout(() => window.refreshMigrationStatus(), 1000);
|
|
19324
|
+
} else {
|
|
19325
|
+
alert(result.error || 'Failed to run migrations');
|
|
19326
|
+
}
|
|
19327
|
+
} catch (error) {
|
|
19328
|
+
alert('Error running migrations');
|
|
19329
|
+
} finally {
|
|
19330
|
+
btn.disabled = false;
|
|
19331
|
+
btn.innerHTML = 'Run Pending';
|
|
19332
|
+
}
|
|
19333
|
+
};
|
|
19334
|
+
|
|
19335
|
+
window.validateSchema = async function() {
|
|
19336
|
+
try {
|
|
19337
|
+
const response = await fetch('/admin/api/migrations/validate');
|
|
19338
|
+
const result = await response.json();
|
|
19339
|
+
|
|
19340
|
+
if (result.success) {
|
|
19341
|
+
if (result.data.valid) {
|
|
19342
|
+
alert('Database schema is valid');
|
|
19343
|
+
} else {
|
|
19344
|
+
alert(\`Schema validation failed: \${result.data.issues.join(', ')}\`);
|
|
19345
|
+
}
|
|
19346
|
+
} else {
|
|
19347
|
+
alert('Failed to validate schema');
|
|
19348
|
+
}
|
|
19349
|
+
} catch (error) {
|
|
19350
|
+
alert('Error validating schema');
|
|
19351
|
+
}
|
|
19352
|
+
};
|
|
19353
|
+
|
|
19354
|
+
window.updateMigrationUI = function(data) {
|
|
19355
|
+
const totalEl = document.getElementById('total-migrations');
|
|
19356
|
+
const appliedEl = document.getElementById('applied-migrations');
|
|
19357
|
+
const pendingEl = document.getElementById('pending-migrations');
|
|
19358
|
+
|
|
19359
|
+
if (totalEl) totalEl.textContent = data.totalMigrations;
|
|
19360
|
+
if (appliedEl) appliedEl.textContent = data.appliedMigrations;
|
|
19361
|
+
if (pendingEl) pendingEl.textContent = data.pendingMigrations;
|
|
19362
|
+
|
|
19363
|
+
const runBtn = document.getElementById('run-migrations-btn');
|
|
19364
|
+
if (runBtn) {
|
|
19365
|
+
runBtn.disabled = data.pendingMigrations === 0;
|
|
19366
|
+
}
|
|
19367
|
+
|
|
19368
|
+
// Update migrations list
|
|
19369
|
+
const listContainer = document.getElementById('migrations-list');
|
|
19370
|
+
if (listContainer && data.migrations && data.migrations.length > 0) {
|
|
19371
|
+
listContainer.innerHTML = data.migrations.map(migration => \`
|
|
19372
|
+
<div class="px-6 py-4 flex items-center justify-between">
|
|
19373
|
+
<div class="flex-1">
|
|
19374
|
+
<div class="flex items-center space-x-3">
|
|
19375
|
+
<div class="flex-shrink-0">
|
|
19376
|
+
\${migration.applied
|
|
19377
|
+
? '<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
|
19378
|
+
: '<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
|
19379
|
+
}
|
|
19380
|
+
</div>
|
|
19381
|
+
<div>
|
|
19382
|
+
<h5 class="text-white font-medium">\${migration.name}</h5>
|
|
19383
|
+
<p class="text-sm text-gray-300">\${migration.filename}</p>
|
|
19384
|
+
\${migration.description ? \`<p class="text-xs text-gray-400 mt-1">\${migration.description}</p>\` : ''}
|
|
19385
|
+
</div>
|
|
19386
|
+
</div>
|
|
19387
|
+
</div>
|
|
19388
|
+
|
|
19389
|
+
<div class="flex items-center space-x-4 text-sm">
|
|
19390
|
+
\${migration.size ? \`<span class="text-gray-400">\${(migration.size / 1024).toFixed(1)} KB</span>\` : ''}
|
|
19391
|
+
<span class="px-2 py-1 rounded-full text-xs font-medium \${
|
|
19392
|
+
migration.applied
|
|
19393
|
+
? 'bg-green-100 text-green-800'
|
|
19394
|
+
: 'bg-orange-100 text-orange-800'
|
|
19395
|
+
}">
|
|
19396
|
+
\${migration.applied ? 'Applied' : 'Pending'}
|
|
19397
|
+
</span>
|
|
19398
|
+
\${migration.appliedAt ? \`<span class="text-gray-400">\${new Date(migration.appliedAt).toLocaleDateString()}</span>\` : ''}
|
|
19399
|
+
</div>
|
|
19400
|
+
</div>
|
|
19401
|
+
\`).join('');
|
|
19402
|
+
}
|
|
19403
|
+
};
|
|
19404
|
+
}
|
|
19405
|
+
|
|
19406
|
+
// Auto-load when tab becomes active
|
|
19407
|
+
if (currentTab === 'migrations') {
|
|
19408
|
+
setTimeout(refreshMigrationStatus, 500);
|
|
19409
|
+
}
|
|
19410
|
+
</script>
|
|
19411
|
+
`;
|
|
19412
|
+
}
|
|
19413
|
+
function renderDatabaseToolsSettings(settings) {
|
|
19414
|
+
return `
|
|
19415
|
+
<div class="space-y-6">
|
|
19416
|
+
<div>
|
|
19417
|
+
<h3 class="text-lg/7 font-semibold text-zinc-950 dark:text-white">Database Tools</h3>
|
|
19418
|
+
<p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage database operations including backup, restore, and maintenance.</p>
|
|
19419
|
+
</div>
|
|
19420
|
+
|
|
19421
|
+
<!-- Database Statistics -->
|
|
19422
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
|
19423
|
+
<div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
|
|
19424
|
+
<div class="flex items-center justify-between">
|
|
19425
|
+
<div>
|
|
19426
|
+
<p class="text-sm/6 font-medium text-zinc-500 dark:text-zinc-400">Total Tables</p>
|
|
19427
|
+
<p id="total-tables" class="mt-2 text-3xl/8 font-semibold text-zinc-950 dark:text-white">${settings?.totalTables || "0"}</p>
|
|
19428
|
+
</div>
|
|
19429
|
+
<div class="rounded-lg bg-indigo-500/10 p-3">
|
|
19430
|
+
<svg class="w-8 h-8 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19431
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
19432
|
+
</svg>
|
|
19433
|
+
</div>
|
|
19434
|
+
</div>
|
|
19435
|
+
</div>
|
|
19436
|
+
|
|
19437
|
+
<div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
|
|
19438
|
+
<div class="flex items-center justify-between">
|
|
19439
|
+
<div>
|
|
19440
|
+
<p class="text-sm/6 font-medium text-zinc-500 dark:text-zinc-400">Total Rows</p>
|
|
19441
|
+
<p id="total-rows" class="mt-2 text-3xl/8 font-semibold text-zinc-950 dark:text-white">${settings?.totalRows?.toLocaleString() || "0"}</p>
|
|
19442
|
+
</div>
|
|
19443
|
+
<div class="rounded-lg bg-green-500/10 p-3">
|
|
19444
|
+
<svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19445
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
|
19446
|
+
</svg>
|
|
19447
|
+
</div>
|
|
19448
|
+
</div>
|
|
19449
|
+
</div>
|
|
19450
|
+
</div>
|
|
19451
|
+
|
|
19452
|
+
<!-- Database Operations -->
|
|
19453
|
+
<div class="space-y-4">
|
|
19454
|
+
<!-- Safe Operations -->
|
|
19455
|
+
<div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
|
|
19456
|
+
<h4 class="text-base/7 font-semibold text-zinc-950 dark:text-white mb-4">Safe Operations</h4>
|
|
19457
|
+
<div class="flex flex-wrap gap-3">
|
|
19458
|
+
<button
|
|
19459
|
+
onclick="window.refreshDatabaseStats()"
|
|
19460
|
+
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"
|
|
19461
|
+
>
|
|
19462
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19463
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
19464
|
+
</svg>
|
|
19465
|
+
Refresh Stats
|
|
19466
|
+
</button>
|
|
19467
|
+
|
|
19468
|
+
<button
|
|
19469
|
+
onclick="window.createDatabaseBackup()"
|
|
19470
|
+
id="create-backup-btn"
|
|
19471
|
+
class="inline-flex items-center justify-center rounded-lg bg-indigo-600 dark:bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-indigo-500 dark:hover:bg-indigo-400 transition-colors shadow-sm"
|
|
19472
|
+
>
|
|
19473
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19474
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
19475
|
+
</svg>
|
|
19476
|
+
Create Backup
|
|
19477
|
+
</button>
|
|
19478
|
+
|
|
19479
|
+
<button
|
|
19480
|
+
onclick="window.validateDatabase()"
|
|
19481
|
+
class="inline-flex items-center justify-center rounded-lg bg-green-600 dark:bg-green-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-green-500 dark:hover:bg-green-400 transition-colors shadow-sm"
|
|
19482
|
+
>
|
|
19483
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19484
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
19485
|
+
</svg>
|
|
19486
|
+
Validate Database
|
|
19487
|
+
</button>
|
|
19488
|
+
</div>
|
|
19489
|
+
</div>
|
|
19490
|
+
</div>
|
|
19491
|
+
|
|
19492
|
+
<!-- Tables List -->
|
|
19493
|
+
<div class="rounded-lg bg-white dark:bg-white/5 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 overflow-hidden">
|
|
19494
|
+
<div class="px-6 py-4 border-b border-zinc-950/10 dark:border-white/10">
|
|
19495
|
+
<h4 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Database Tables</h4>
|
|
19496
|
+
<p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Click on a table to view its data</p>
|
|
19497
|
+
</div>
|
|
19498
|
+
|
|
19499
|
+
<div id="tables-list" class="p-6 space-y-2">
|
|
19500
|
+
<div class="text-center py-8">
|
|
19501
|
+
<svg class="w-12 h-12 text-zinc-400 dark:text-zinc-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19502
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
19503
|
+
</svg>
|
|
19504
|
+
<p class="text-zinc-500 dark:text-zinc-400">Loading database statistics...</p>
|
|
19505
|
+
</div>
|
|
19506
|
+
</div>
|
|
19507
|
+
</div>
|
|
19508
|
+
|
|
19509
|
+
<!-- Danger Zone -->
|
|
19510
|
+
<div class="rounded-lg bg-red-50 dark:bg-red-950/20 p-6 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/30">
|
|
19511
|
+
<div class="flex items-start space-x-3">
|
|
19512
|
+
<svg class="w-6 h-6 text-red-600 dark:text-red-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19513
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
|
19514
|
+
</svg>
|
|
19515
|
+
<div class="flex-1">
|
|
19516
|
+
<h4 class="text-base/7 font-semibold text-red-900 dark:text-red-400">Danger Zone</h4>
|
|
19517
|
+
<p class="mt-1 text-sm/6 text-red-700 dark:text-red-300">
|
|
19518
|
+
These operations are destructive and cannot be undone.
|
|
19519
|
+
<strong>Your admin account will be preserved</strong>, but all other data will be permanently deleted.
|
|
19520
|
+
</p>
|
|
19521
|
+
<div class="mt-4">
|
|
19522
|
+
<button
|
|
19523
|
+
onclick="window.truncateDatabase()"
|
|
19524
|
+
id="truncate-db-btn"
|
|
19525
|
+
class="inline-flex items-center justify-center rounded-lg bg-red-600 dark:bg-red-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-red-500 dark:hover:bg-red-400 transition-colors shadow-sm"
|
|
19526
|
+
>
|
|
19527
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
19528
|
+
<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"/>
|
|
19529
|
+
</svg>
|
|
19530
|
+
Truncate All Data
|
|
19531
|
+
</button>
|
|
19532
|
+
</div>
|
|
19533
|
+
</div>
|
|
19534
|
+
</div>
|
|
19535
|
+
</div>
|
|
19536
|
+
</div>
|
|
19537
|
+
`;
|
|
19538
|
+
}
|
|
19539
|
+
|
|
19540
|
+
// src/routes/admin-settings.ts
|
|
19541
|
+
var adminSettingsRoutes = new Hono();
|
|
19542
|
+
function getMockSettings(user) {
|
|
19543
|
+
return {
|
|
19544
|
+
general: {
|
|
19545
|
+
siteName: "SonicJS AI",
|
|
19546
|
+
siteDescription: "A modern headless CMS powered by AI",
|
|
19547
|
+
adminEmail: user?.email || "admin@example.com",
|
|
19548
|
+
timezone: "UTC",
|
|
19549
|
+
language: "en",
|
|
19550
|
+
maintenanceMode: false
|
|
19551
|
+
},
|
|
19552
|
+
appearance: {
|
|
19553
|
+
theme: "dark",
|
|
19554
|
+
primaryColor: "#465FFF",
|
|
19555
|
+
logoUrl: "",
|
|
19556
|
+
favicon: "",
|
|
19557
|
+
customCSS: ""
|
|
19558
|
+
},
|
|
19559
|
+
security: {
|
|
19560
|
+
twoFactorEnabled: false,
|
|
19561
|
+
sessionTimeout: 30,
|
|
19562
|
+
passwordRequirements: {
|
|
19563
|
+
minLength: 8,
|
|
19564
|
+
requireUppercase: true,
|
|
19565
|
+
requireNumbers: true,
|
|
19566
|
+
requireSymbols: false
|
|
19567
|
+
},
|
|
19568
|
+
ipWhitelist: []
|
|
19569
|
+
},
|
|
19570
|
+
notifications: {
|
|
19571
|
+
emailNotifications: true,
|
|
19572
|
+
contentUpdates: true,
|
|
19573
|
+
systemAlerts: true,
|
|
19574
|
+
userRegistrations: false,
|
|
19575
|
+
emailFrequency: "immediate"
|
|
19576
|
+
},
|
|
19577
|
+
storage: {
|
|
19578
|
+
maxFileSize: 10,
|
|
19579
|
+
allowedFileTypes: ["jpg", "jpeg", "png", "gif", "pdf", "docx"],
|
|
19580
|
+
storageProvider: "cloudflare",
|
|
19581
|
+
backupFrequency: "daily",
|
|
19582
|
+
retentionPeriod: 30
|
|
19583
|
+
},
|
|
19584
|
+
migrations: {
|
|
19585
|
+
totalMigrations: 0,
|
|
19586
|
+
appliedMigrations: 0,
|
|
19587
|
+
pendingMigrations: 0,
|
|
19588
|
+
lastApplied: void 0,
|
|
19589
|
+
migrations: []
|
|
19590
|
+
},
|
|
19591
|
+
databaseTools: {
|
|
19592
|
+
totalTables: 0,
|
|
19593
|
+
totalRows: 0,
|
|
19594
|
+
lastBackup: void 0,
|
|
19595
|
+
databaseSize: "0 MB",
|
|
19596
|
+
tables: []
|
|
19597
|
+
}
|
|
19598
|
+
};
|
|
19599
|
+
}
|
|
19600
|
+
adminSettingsRoutes.get("/settings", (c) => {
|
|
19601
|
+
return c.redirect("/admin/settings/general");
|
|
19602
|
+
});
|
|
19603
|
+
adminSettingsRoutes.get("/settings/general", (c) => {
|
|
19604
|
+
const user = c.get("user");
|
|
19605
|
+
const pageData = {
|
|
19606
|
+
user: user ? {
|
|
19607
|
+
name: user.email,
|
|
19608
|
+
email: user.email,
|
|
19609
|
+
role: user.role
|
|
19610
|
+
} : void 0,
|
|
19611
|
+
settings: getMockSettings(user),
|
|
19612
|
+
activeTab: "general",
|
|
19613
|
+
version: c.get("appVersion")
|
|
19614
|
+
};
|
|
19615
|
+
return c.html(renderSettingsPage(pageData));
|
|
19616
|
+
});
|
|
19617
|
+
adminSettingsRoutes.get("/settings/appearance", (c) => {
|
|
19618
|
+
const user = c.get("user");
|
|
19619
|
+
const pageData = {
|
|
19620
|
+
user: user ? {
|
|
19621
|
+
name: user.email,
|
|
19622
|
+
email: user.email,
|
|
19623
|
+
role: user.role
|
|
19624
|
+
} : void 0,
|
|
19625
|
+
settings: getMockSettings(user),
|
|
19626
|
+
activeTab: "appearance",
|
|
19627
|
+
version: c.get("appVersion")
|
|
19628
|
+
};
|
|
19629
|
+
return c.html(renderSettingsPage(pageData));
|
|
19630
|
+
});
|
|
19631
|
+
adminSettingsRoutes.get("/settings/security", (c) => {
|
|
19632
|
+
const user = c.get("user");
|
|
19633
|
+
const pageData = {
|
|
19634
|
+
user: user ? {
|
|
19635
|
+
name: user.email,
|
|
19636
|
+
email: user.email,
|
|
19637
|
+
role: user.role
|
|
19638
|
+
} : void 0,
|
|
19639
|
+
settings: getMockSettings(user),
|
|
19640
|
+
activeTab: "security",
|
|
19641
|
+
version: c.get("appVersion")
|
|
19642
|
+
};
|
|
19643
|
+
return c.html(renderSettingsPage(pageData));
|
|
19644
|
+
});
|
|
19645
|
+
adminSettingsRoutes.get("/settings/notifications", (c) => {
|
|
19646
|
+
const user = c.get("user");
|
|
19647
|
+
const pageData = {
|
|
19648
|
+
user: user ? {
|
|
19649
|
+
name: user.email,
|
|
19650
|
+
email: user.email,
|
|
19651
|
+
role: user.role
|
|
19652
|
+
} : void 0,
|
|
19653
|
+
settings: getMockSettings(user),
|
|
19654
|
+
activeTab: "notifications",
|
|
19655
|
+
version: c.get("appVersion")
|
|
19656
|
+
};
|
|
19657
|
+
return c.html(renderSettingsPage(pageData));
|
|
19658
|
+
});
|
|
19659
|
+
adminSettingsRoutes.get("/settings/storage", (c) => {
|
|
19660
|
+
const user = c.get("user");
|
|
19661
|
+
const pageData = {
|
|
19662
|
+
user: user ? {
|
|
19663
|
+
name: user.email,
|
|
19664
|
+
email: user.email,
|
|
19665
|
+
role: user.role
|
|
19666
|
+
} : void 0,
|
|
19667
|
+
settings: getMockSettings(user),
|
|
19668
|
+
activeTab: "storage",
|
|
19669
|
+
version: c.get("appVersion")
|
|
19670
|
+
};
|
|
19671
|
+
return c.html(renderSettingsPage(pageData));
|
|
19672
|
+
});
|
|
19673
|
+
adminSettingsRoutes.get("/settings/migrations", (c) => {
|
|
19674
|
+
const user = c.get("user");
|
|
19675
|
+
const pageData = {
|
|
19676
|
+
user: user ? {
|
|
19677
|
+
name: user.email,
|
|
19678
|
+
email: user.email,
|
|
19679
|
+
role: user.role
|
|
19680
|
+
} : void 0,
|
|
19681
|
+
settings: getMockSettings(user),
|
|
19682
|
+
activeTab: "migrations",
|
|
19683
|
+
version: c.get("appVersion")
|
|
19684
|
+
};
|
|
19685
|
+
return c.html(renderSettingsPage(pageData));
|
|
19686
|
+
});
|
|
19687
|
+
adminSettingsRoutes.get("/settings/database-tools", (c) => {
|
|
19688
|
+
const user = c.get("user");
|
|
19689
|
+
const pageData = {
|
|
19690
|
+
user: user ? {
|
|
19691
|
+
name: user.email,
|
|
19692
|
+
email: user.email,
|
|
19693
|
+
role: user.role
|
|
19694
|
+
} : void 0,
|
|
19695
|
+
settings: getMockSettings(user),
|
|
19696
|
+
activeTab: "database-tools",
|
|
19697
|
+
version: c.get("appVersion")
|
|
19698
|
+
};
|
|
19699
|
+
return c.html(renderSettingsPage(pageData));
|
|
19700
|
+
});
|
|
19701
|
+
adminSettingsRoutes.get("/api/migrations/status", async (c) => {
|
|
19702
|
+
try {
|
|
19703
|
+
const db = c.env.DB;
|
|
19704
|
+
const migrationService = new MigrationService(db);
|
|
19705
|
+
const status = await migrationService.getMigrationStatus();
|
|
19706
|
+
return c.json({
|
|
19707
|
+
success: true,
|
|
19708
|
+
data: status
|
|
19709
|
+
});
|
|
19710
|
+
} catch (error) {
|
|
19711
|
+
console.error("Error fetching migration status:", error);
|
|
19712
|
+
return c.json({
|
|
19713
|
+
success: false,
|
|
19714
|
+
error: "Failed to fetch migration status"
|
|
19715
|
+
}, 500);
|
|
19716
|
+
}
|
|
19717
|
+
});
|
|
19718
|
+
adminSettingsRoutes.post("/api/migrations/run", async (c) => {
|
|
19719
|
+
try {
|
|
19720
|
+
const user = c.get("user");
|
|
19721
|
+
if (!user || user.role !== "admin") {
|
|
19722
|
+
return c.json({
|
|
19723
|
+
success: false,
|
|
19724
|
+
error: "Unauthorized. Admin access required."
|
|
19725
|
+
}, 403);
|
|
19726
|
+
}
|
|
19727
|
+
const db = c.env.DB;
|
|
19728
|
+
const migrationService = new MigrationService(db);
|
|
19729
|
+
const result = await migrationService.runPendingMigrations();
|
|
19730
|
+
return c.json({
|
|
19731
|
+
success: result.success,
|
|
19732
|
+
message: result.message,
|
|
19733
|
+
applied: result.applied
|
|
19734
|
+
});
|
|
19735
|
+
} catch (error) {
|
|
19736
|
+
console.error("Error running migrations:", error);
|
|
19737
|
+
return c.json({
|
|
19738
|
+
success: false,
|
|
19739
|
+
error: "Failed to run migrations"
|
|
19740
|
+
}, 500);
|
|
19741
|
+
}
|
|
19742
|
+
});
|
|
19743
|
+
adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
|
|
19744
|
+
try {
|
|
19745
|
+
const db = c.env.DB;
|
|
19746
|
+
const migrationService = new MigrationService(db);
|
|
19747
|
+
const validation = await migrationService.validateSchema();
|
|
19748
|
+
return c.json({
|
|
19749
|
+
success: true,
|
|
19750
|
+
data: validation
|
|
19751
|
+
});
|
|
19752
|
+
} catch (error) {
|
|
19753
|
+
console.error("Error validating schema:", error);
|
|
19754
|
+
return c.json({
|
|
19755
|
+
success: false,
|
|
19756
|
+
error: "Failed to validate schema"
|
|
19757
|
+
}, 500);
|
|
19758
|
+
}
|
|
19759
|
+
});
|
|
19760
|
+
adminSettingsRoutes.post("/settings", async (c) => {
|
|
19761
|
+
try {
|
|
19762
|
+
const formData = await c.req.formData();
|
|
19763
|
+
return c.html(html`
|
|
19764
|
+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
|
19765
|
+
Settings saved successfully!
|
|
19766
|
+
<script>
|
|
19767
|
+
setTimeout(() => {
|
|
19768
|
+
showNotification('Settings saved successfully!', 'success');
|
|
19769
|
+
}, 100);
|
|
19770
|
+
</script>
|
|
19771
|
+
</div>
|
|
19772
|
+
`);
|
|
19773
|
+
} catch (error) {
|
|
19774
|
+
console.error("Error saving settings:", error);
|
|
19775
|
+
return c.html(html`
|
|
19776
|
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
19777
|
+
Failed to save settings. Please try again.
|
|
19778
|
+
</div>
|
|
19779
|
+
`);
|
|
19780
|
+
}
|
|
19781
|
+
});
|
|
19782
|
+
|
|
15756
19783
|
// src/routes/index.ts
|
|
15757
19784
|
var ROUTES_INFO = {
|
|
15758
19785
|
message: "Core routes available",
|
|
@@ -15772,12 +19799,15 @@ var ROUTES_INFO = {
|
|
|
15772
19799
|
"adminCheckboxRoutes",
|
|
15773
19800
|
"adminFAQRoutes",
|
|
15774
19801
|
"adminTestimonialsRoutes",
|
|
15775
|
-
"adminCodeExamplesRoutes"
|
|
19802
|
+
"adminCodeExamplesRoutes",
|
|
19803
|
+
"adminDashboardRoutes",
|
|
19804
|
+
"adminCollectionsRoutes",
|
|
19805
|
+
"adminSettingsRoutes"
|
|
15776
19806
|
],
|
|
15777
19807
|
status: "Core package routes ready",
|
|
15778
19808
|
reference: "https://github.com/sonicjs/sonicjs"
|
|
15779
19809
|
};
|
|
15780
19810
|
|
|
15781
|
-
export { ROUTES_INFO, adminCheckboxRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, userRoutes };
|
|
15782
|
-
//# sourceMappingURL=chunk-
|
|
15783
|
-
//# sourceMappingURL=chunk-
|
|
19811
|
+
export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
|
|
19812
|
+
//# sourceMappingURL=chunk-QSF34IYQ.js.map
|
|
19813
|
+
//# sourceMappingURL=chunk-QSF34IYQ.js.map
|