@intranefr/superbackend 1.4.4 → 1.5.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/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<section class="py-16 bg-gray-50">
|
|
2
|
+
<div class="container mx-auto px-4 max-w-xl">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 text-center mb-8"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<form action="<%= block.props?.action || '/api/forms/submit' %>" method="POST" class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
7
|
+
<% if (block.props?.formId) { %>
|
|
8
|
+
<input type="hidden" name="formId" value="<%= block.props.formId %>">
|
|
9
|
+
<% } %>
|
|
10
|
+
<div class="mb-4">
|
|
11
|
+
<label class="block text-gray-700 font-medium mb-2">Name</label>
|
|
12
|
+
<input type="text" name="name" required class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="mb-4">
|
|
15
|
+
<label class="block text-gray-700 font-medium mb-2">Email</label>
|
|
16
|
+
<input type="email" name="email" required class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="mb-6">
|
|
19
|
+
<label class="block text-gray-700 font-medium mb-2">Message</label>
|
|
20
|
+
<textarea name="message" rows="4" required class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"></textarea>
|
|
21
|
+
</div>
|
|
22
|
+
<button type="submit" class="w-full bg-blue-600 text-white font-semibold py-3 rounded-lg hover:bg-blue-700 transition">
|
|
23
|
+
<%= block.props?.buttonText || 'Send Message' %>
|
|
24
|
+
</button>
|
|
25
|
+
</form>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<section class="py-16 bg-gray-100">
|
|
2
|
+
<div class="container mx-auto px-4 text-center">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 mb-4"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<% if (block.props?.description) { %>
|
|
7
|
+
<p class="text-gray-600 mb-8 max-w-2xl mx-auto"><%= block.props.description %></p>
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (block.props?.buttonText) { %>
|
|
10
|
+
<a
|
|
11
|
+
href="<%= block.props.buttonUrl || '#' %>"
|
|
12
|
+
class="inline-block bg-blue-600 text-white font-semibold px-8 py-3 rounded-lg hover:bg-blue-700 transition"
|
|
13
|
+
>
|
|
14
|
+
<%= block.props.buttonText %>
|
|
15
|
+
</a>
|
|
16
|
+
<% } %>
|
|
17
|
+
</div>
|
|
18
|
+
</section>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<section class="py-16">
|
|
2
|
+
<div class="container mx-auto px-4 max-w-3xl">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<div class="space-y-4">
|
|
7
|
+
<% const items = block.props?.items || []; %>
|
|
8
|
+
<% for (let i = 0; i < items.length; i++) { %>
|
|
9
|
+
<% const item = items[i]; %>
|
|
10
|
+
<details class="bg-white rounded-lg border border-gray-200 p-4 group">
|
|
11
|
+
<summary class="font-semibold text-gray-900 cursor-pointer list-none flex justify-between items-center">
|
|
12
|
+
<%= item.question || 'Question' %>
|
|
13
|
+
<span class="text-gray-500 group-open:rotate-180 transition-transform">▼</span>
|
|
14
|
+
</summary>
|
|
15
|
+
<p class="text-gray-600 mt-4"><%= item.answer || '' %></p>
|
|
16
|
+
</details>
|
|
17
|
+
<% } %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</section>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<section class="py-16">
|
|
2
|
+
<div class="container mx-auto px-4">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
7
|
+
<% const features = block.props?.items || []; %>
|
|
8
|
+
<% for (const feature of features) { %>
|
|
9
|
+
<div class="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
10
|
+
<% if (feature.icon) { %>
|
|
11
|
+
<div class="text-blue-600 text-3xl mb-4"><%= feature.icon %></div>
|
|
12
|
+
<% } %>
|
|
13
|
+
<h3 class="text-xl font-semibold text-gray-900 mb-2"><%= feature.title || 'Feature' %></h3>
|
|
14
|
+
<p class="text-gray-600"><%= feature.description || '' %></p>
|
|
15
|
+
</div>
|
|
16
|
+
<% } %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</section>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<section class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-20">
|
|
2
|
+
<div class="container mx-auto px-4 text-center">
|
|
3
|
+
<h1 class="text-5xl font-bold mb-6"><%= block.props?.title || 'Welcome' %></h1>
|
|
4
|
+
<% if (block.props?.subtitle) { %>
|
|
5
|
+
<p class="text-xl opacity-90 mb-8"><%= block.props.subtitle %></p>
|
|
6
|
+
<% } %>
|
|
7
|
+
<% if (block.props?.ctaText) { %>
|
|
8
|
+
<a href="<%= block.props.ctaUrl || '#' %>" class="inline-block bg-white text-blue-600 font-semibold px-8 py-3 rounded-lg hover:bg-gray-100 transition">
|
|
9
|
+
<%= block.props.ctaText %>
|
|
10
|
+
</a>
|
|
11
|
+
<% } %>
|
|
12
|
+
</div>
|
|
13
|
+
</section>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<section class="py-8">
|
|
2
|
+
<div class="container mx-auto px-4">
|
|
3
|
+
<figure class="<%= block.props?.align === 'center' ? 'text-center' : '' %>">
|
|
4
|
+
<img
|
|
5
|
+
src="<%= block.props?.src || '/placeholder.jpg' %>"
|
|
6
|
+
alt="<%= block.props?.alt || '' %>"
|
|
7
|
+
class="<%= block.props?.fullWidth ? 'w-full' : 'max-w-2xl mx-auto' %> rounded-lg shadow-md"
|
|
8
|
+
>
|
|
9
|
+
<% if (block.props?.caption) { %>
|
|
10
|
+
<figcaption class="text-gray-500 text-sm mt-3"><%= block.props.caption %></figcaption>
|
|
11
|
+
<% } %>
|
|
12
|
+
</figure>
|
|
13
|
+
</div>
|
|
14
|
+
</section>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<section class="py-16 bg-gray-50">
|
|
2
|
+
<div class="container mx-auto px-4">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
7
|
+
<% const testimonials = block.props?.items || []; %>
|
|
8
|
+
<% for (const testimonial of testimonials) { %>
|
|
9
|
+
<div class="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
|
10
|
+
<p class="text-gray-700 italic mb-4">"<%= testimonial.quote || '' %>"</p>
|
|
11
|
+
<div class="flex items-center">
|
|
12
|
+
<% if (testimonial.avatar) { %>
|
|
13
|
+
<img src="<%= testimonial.avatar %>" alt="" class="w-10 h-10 rounded-full mr-3">
|
|
14
|
+
<% } %>
|
|
15
|
+
<div>
|
|
16
|
+
<p class="font-semibold text-gray-900"><%= testimonial.name || 'Anonymous' %></p>
|
|
17
|
+
<% if (testimonial.role) { %>
|
|
18
|
+
<p class="text-gray-500 text-sm"><%= testimonial.role %></p>
|
|
19
|
+
<% } %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<% } %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<section class="py-12">
|
|
2
|
+
<div class="container mx-auto px-4 max-w-3xl">
|
|
3
|
+
<% if (block.props?.title) { %>
|
|
4
|
+
<h2 class="text-3xl font-bold text-gray-900 mb-6"><%= block.props.title %></h2>
|
|
5
|
+
<% } %>
|
|
6
|
+
<div class="prose prose-lg max-w-none text-gray-700">
|
|
7
|
+
<%- block.props?.content || '' %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</section>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= seoMeta.title || page.title || 'Page' %></title>
|
|
7
|
+
<% if (seoMeta.description) { %>
|
|
8
|
+
<meta name="description" content="<%= seoMeta.description %>">
|
|
9
|
+
<% } %>
|
|
10
|
+
<% if (seoMeta.keywords) { %>
|
|
11
|
+
<meta name="keywords" content="<%= seoMeta.keywords %>">
|
|
12
|
+
<% } %>
|
|
13
|
+
<% if (seoMeta.ogImage) { %>
|
|
14
|
+
<meta property="og:image" content="<%= seoMeta.ogImage %>">
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (seoMeta.canonicalUrl) { %>
|
|
17
|
+
<link rel="canonical" href="<%= seoMeta.canonicalUrl %>">
|
|
18
|
+
<% } %>
|
|
19
|
+
<meta property="og:title" content="<%= seoMeta.title || page.title || 'Page' %>">
|
|
20
|
+
<% if (seoMeta.description) { %>
|
|
21
|
+
<meta property="og:description" content="<%= seoMeta.description %>">
|
|
22
|
+
<% } %>
|
|
23
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
24
|
+
<style>
|
|
25
|
+
<% if (customCss) { %>
|
|
26
|
+
<%= customCss %>
|
|
27
|
+
<% } %>
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body class="min-h-screen bg-white">
|
|
31
|
+
<%- include('../partials/header.ejs', { page, pageContext, req }) %>
|
|
32
|
+
|
|
33
|
+
<main>
|
|
34
|
+
<%- include('../templates/' + (templatePath ? templatePath.replace('pages/templates/', '') : 'default.ejs'), {
|
|
35
|
+
page,
|
|
36
|
+
blocks,
|
|
37
|
+
seoMeta,
|
|
38
|
+
pageContext,
|
|
39
|
+
req
|
|
40
|
+
}) %>
|
|
41
|
+
</main>
|
|
42
|
+
|
|
43
|
+
<%- include('../partials/footer.ejs', { page, pageContext, req }) %>
|
|
44
|
+
|
|
45
|
+
<% if (customJs) { %>
|
|
46
|
+
<script>
|
|
47
|
+
<%= customJs %>
|
|
48
|
+
</script>
|
|
49
|
+
<% } %>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= seoMeta.title || page.title || 'Page' %></title>
|
|
7
|
+
<% if (seoMeta.description) { %>
|
|
8
|
+
<meta name="description" content="<%= seoMeta.description %>">
|
|
9
|
+
<% } %>
|
|
10
|
+
<% if (seoMeta.keywords) { %>
|
|
11
|
+
<meta name="keywords" content="<%= seoMeta.keywords %>">
|
|
12
|
+
<% } %>
|
|
13
|
+
<% if (seoMeta.ogImage) { %>
|
|
14
|
+
<meta property="og:image" content="<%= seoMeta.ogImage %>">
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (seoMeta.canonicalUrl) { %>
|
|
17
|
+
<link rel="canonical" href="<%= seoMeta.canonicalUrl %>">
|
|
18
|
+
<% } %>
|
|
19
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
20
|
+
<style>
|
|
21
|
+
<% if (customCss) { %>
|
|
22
|
+
<%= customCss %>
|
|
23
|
+
<% } %>
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="min-h-screen bg-white">
|
|
27
|
+
<main class="container mx-auto px-4 py-8">
|
|
28
|
+
<%- include('../templates/' + (templatePath ? templatePath.replace('pages/templates/', '') : 'default.ejs'), {
|
|
29
|
+
page,
|
|
30
|
+
blocks,
|
|
31
|
+
seoMeta,
|
|
32
|
+
req
|
|
33
|
+
}) %>
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
<% if (customJs) { %>
|
|
37
|
+
<script>
|
|
38
|
+
<%= customJs %>
|
|
39
|
+
</script>
|
|
40
|
+
<% } %>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= seoMeta.title || page.title || 'Page' %></title>
|
|
7
|
+
<% if (seoMeta.description) { %>
|
|
8
|
+
<meta name="description" content="<%= seoMeta.description %>">
|
|
9
|
+
<% } %>
|
|
10
|
+
<% if (seoMeta.keywords) { %>
|
|
11
|
+
<meta name="keywords" content="<%= seoMeta.keywords %>">
|
|
12
|
+
<% } %>
|
|
13
|
+
<% if (seoMeta.ogImage) { %>
|
|
14
|
+
<meta property="og:image" content="<%= seoMeta.ogImage %>">
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (seoMeta.canonicalUrl) { %>
|
|
17
|
+
<link rel="canonical" href="<%= seoMeta.canonicalUrl %>">
|
|
18
|
+
<% } %>
|
|
19
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
20
|
+
<style>
|
|
21
|
+
<% if (customCss) { %>
|
|
22
|
+
<%= customCss %>
|
|
23
|
+
<% } %>
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="min-h-screen bg-gray-50">
|
|
27
|
+
<%- include('../partials/header.ejs', { page, req }) %>
|
|
28
|
+
|
|
29
|
+
<div class="container mx-auto px-4 py-8">
|
|
30
|
+
<div class="flex flex-col lg:flex-row gap-8">
|
|
31
|
+
<aside class="lg:w-64 flex-shrink-0">
|
|
32
|
+
<%- include('../partials/sidebar.ejs', { page, req }) %>
|
|
33
|
+
</aside>
|
|
34
|
+
|
|
35
|
+
<main class="flex-1">
|
|
36
|
+
<%- include('../templates/' + (templatePath ? templatePath.replace('pages/templates/', '') : 'default.ejs'), {
|
|
37
|
+
page,
|
|
38
|
+
blocks,
|
|
39
|
+
seoMeta,
|
|
40
|
+
req
|
|
41
|
+
}) %>
|
|
42
|
+
</main>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<%- include('../partials/footer.ejs', { page, req }) %>
|
|
47
|
+
|
|
48
|
+
<% if (customJs) { %>
|
|
49
|
+
<script>
|
|
50
|
+
<%= customJs %>
|
|
51
|
+
</script>
|
|
52
|
+
<% } %>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<footer class="bg-gray-900 text-white py-8 mt-auto">
|
|
2
|
+
<div class="container mx-auto px-4">
|
|
3
|
+
<div class="flex flex-col md:flex-row justify-between items-center">
|
|
4
|
+
<p class="text-gray-400 text-sm">
|
|
5
|
+
© <%= new Date().getFullYear() %> All rights reserved.
|
|
6
|
+
</p>
|
|
7
|
+
<div class="flex space-x-6 mt-4 md:mt-0">
|
|
8
|
+
<a href="/privacy" class="text-gray-400 hover:text-white text-sm">Privacy</a>
|
|
9
|
+
<a href="/terms" class="text-gray-400 hover:text-white text-sm">Terms</a>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</footer>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<header class="bg-white shadow-sm border-b border-gray-200">
|
|
2
|
+
<div class="container mx-auto px-4">
|
|
3
|
+
<nav class="flex items-center justify-between h-16">
|
|
4
|
+
<a href="/" class="text-xl font-bold text-gray-900">
|
|
5
|
+
Site
|
|
6
|
+
</a>
|
|
7
|
+
<div class="flex items-center space-x-6">
|
|
8
|
+
<a href="/" class="text-gray-600 hover:text-gray-900">Home</a>
|
|
9
|
+
</div>
|
|
10
|
+
</nav>
|
|
11
|
+
</div>
|
|
12
|
+
</header>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<nav class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
|
2
|
+
<h3 class="font-semibold text-gray-900 mb-4">Navigation</h3>
|
|
3
|
+
<ul class="space-y-2">
|
|
4
|
+
<li>
|
|
5
|
+
<a href="/" class="block text-gray-600 hover:text-blue-600 py-1">Home</a>
|
|
6
|
+
</li>
|
|
7
|
+
</ul>
|
|
8
|
+
</nav>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<article class="container mx-auto px-4 py-12 max-w-3xl">
|
|
2
|
+
<header class="mb-8">
|
|
3
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4"><%= page.title %></h1>
|
|
4
|
+
<% if (page.publishedAt) { %>
|
|
5
|
+
<p class="text-gray-500 text-sm">
|
|
6
|
+
Published on <%= new Date(page.publishedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %>
|
|
7
|
+
</p>
|
|
8
|
+
<% } %>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<div class="prose prose-lg max-w-none">
|
|
12
|
+
<% if (blocks && blocks.length > 0) { %>
|
|
13
|
+
<% for (const block of blocks) { %>
|
|
14
|
+
<%- include('../blocks/' + block.type + '.ejs', { block, page, pageContext, req }) %>
|
|
15
|
+
<% } %>
|
|
16
|
+
<% } else { %>
|
|
17
|
+
<p class="text-gray-600">This article has no content yet.</p>
|
|
18
|
+
<% } %>
|
|
19
|
+
</div>
|
|
20
|
+
</article>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="page-content">
|
|
2
|
+
<% if (blocks && blocks.length > 0) { %>
|
|
3
|
+
<% for (const block of blocks) { %>
|
|
4
|
+
<%- include('../blocks/' + block.type + '.ejs', { block, page, pageContext, req }) %>
|
|
5
|
+
<% } %>
|
|
6
|
+
<% } else { %>
|
|
7
|
+
<div class="container mx-auto px-4 py-16">
|
|
8
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4"><%= page.title %></h1>
|
|
9
|
+
<p class="text-gray-600">This page has no content blocks yet.</p>
|
|
10
|
+
</div>
|
|
11
|
+
<% } %>
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="landing-page">
|
|
2
|
+
<% if (blocks && blocks.length > 0) { %>
|
|
3
|
+
<% for (const block of blocks) { %>
|
|
4
|
+
<%- include('../blocks/' + block.type + '.ejs', { block, page, req }) %>
|
|
5
|
+
<% } %>
|
|
6
|
+
<% } else { %>
|
|
7
|
+
<section class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-20">
|
|
8
|
+
<div class="container mx-auto px-4 text-center">
|
|
9
|
+
<h1 class="text-5xl font-bold mb-6"><%= page.title %></h1>
|
|
10
|
+
<p class="text-xl opacity-90 mb-8">Add blocks to customize this landing page</p>
|
|
11
|
+
</div>
|
|
12
|
+
</section>
|
|
13
|
+
<% } %>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="container mx-auto px-4 py-12">
|
|
2
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-8"><%= page.title %></h1>
|
|
3
|
+
|
|
4
|
+
<% if (blocks && blocks.length > 0) { %>
|
|
5
|
+
<% for (const block of blocks) { %>
|
|
6
|
+
<%- include('../blocks/' + block.type + '.ejs', { block, page, pageContext, req }) %>
|
|
7
|
+
<% } %>
|
|
8
|
+
<% } else { %>
|
|
9
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
10
|
+
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
|
|
11
|
+
<p class="text-gray-600">Add listing blocks to display items here.</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% } %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<div id="image-upload-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
|
2
|
+
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
|
3
|
+
<div class="flex items-center justify-between px-6 py-4 border-b">
|
|
4
|
+
<h3 class="text-lg font-semibold text-gray-900">Upload image</h3>
|
|
5
|
+
<button type="button" id="image-upload-close" class="text-gray-400 hover:text-gray-600">
|
|
6
|
+
<i class="ti ti-x text-xl"></i>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="p-6 space-y-4">
|
|
11
|
+
<div class="text-sm text-gray-600">
|
|
12
|
+
Paste an image (<span class="font-mono">Ctrl+V</span>), drop a file, or pick a file.
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
id="image-upload-dropzone"
|
|
17
|
+
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-400 transition-colors"
|
|
18
|
+
>
|
|
19
|
+
<div class="text-gray-500">
|
|
20
|
+
<i class="ti ti-photo-plus text-4xl mb-2"></i>
|
|
21
|
+
<div class="font-medium">Drop image here</div>
|
|
22
|
+
<div class="text-sm">or</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="mt-3">
|
|
25
|
+
<input id="image-upload-file" type="file" accept="image/*" class="hidden" />
|
|
26
|
+
<button type="button" id="image-upload-pick" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
|
27
|
+
Pick file
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div id="image-upload-progress" class="hidden">
|
|
33
|
+
<div class="text-sm text-gray-600 mb-2">Uploading...</div>
|
|
34
|
+
<div class="w-full bg-gray-200 rounded h-2">
|
|
35
|
+
<div id="image-upload-progress-bar" class="bg-blue-500 h-2 rounded" style="width: 20%"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div id="image-upload-result" class="hidden">
|
|
40
|
+
<div class="text-sm text-gray-600 mb-2">Uploaded URL</div>
|
|
41
|
+
<div class="flex gap-2">
|
|
42
|
+
<input id="image-upload-url" type="text" class="flex-1 border rounded px-3 py-2 text-sm" readonly />
|
|
43
|
+
<button type="button" id="image-upload-copy" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200">
|
|
44
|
+
Copy
|
|
45
|
+
</button>
|
|
46
|
+
<button type="button" id="image-upload-use" class="bg-green-500 text-white px-3 py-2 rounded hover:bg-green-600">
|
|
47
|
+
Use
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div id="image-upload-error" class="hidden text-sm text-red-600"></div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<script>
|
|
58
|
+
(function() {
|
|
59
|
+
const modal = document.getElementById('image-upload-modal');
|
|
60
|
+
const closeBtn = document.getElementById('image-upload-close');
|
|
61
|
+
const dropzone = document.getElementById('image-upload-dropzone');
|
|
62
|
+
const fileInput = document.getElementById('image-upload-file');
|
|
63
|
+
const pickBtn = document.getElementById('image-upload-pick');
|
|
64
|
+
const progress = document.getElementById('image-upload-progress');
|
|
65
|
+
const progressBar = document.getElementById('image-upload-progress-bar');
|
|
66
|
+
const result = document.getElementById('image-upload-result');
|
|
67
|
+
const urlInput = document.getElementById('image-upload-url');
|
|
68
|
+
const copyBtn = document.getElementById('image-upload-copy');
|
|
69
|
+
const useBtn = document.getElementById('image-upload-use');
|
|
70
|
+
const errorBox = document.getElementById('image-upload-error');
|
|
71
|
+
|
|
72
|
+
let currentOnSelect = null;
|
|
73
|
+
let currentNamespace = 'blog-images';
|
|
74
|
+
let currentVisibility = 'public';
|
|
75
|
+
let lastUrl = '';
|
|
76
|
+
|
|
77
|
+
function show(el) {
|
|
78
|
+
el.classList.remove('hidden');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hide(el) {
|
|
82
|
+
el.classList.add('hidden');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setError(msg) {
|
|
86
|
+
errorBox.textContent = msg || '';
|
|
87
|
+
if (msg) show(errorBox);
|
|
88
|
+
else hide(errorBox);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resetUI() {
|
|
92
|
+
hide(progress);
|
|
93
|
+
hide(result);
|
|
94
|
+
setError('');
|
|
95
|
+
urlInput.value = '';
|
|
96
|
+
lastUrl = '';
|
|
97
|
+
progressBar.style.width = '20%';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function openModal({ onSelect, namespace, visibility } = {}) {
|
|
101
|
+
currentOnSelect = typeof onSelect === 'function' ? onSelect : null;
|
|
102
|
+
currentNamespace = namespace ? String(namespace) : 'blog-images';
|
|
103
|
+
currentVisibility = visibility ? String(visibility) : 'public';
|
|
104
|
+
resetUI();
|
|
105
|
+
modal.classList.remove('hidden');
|
|
106
|
+
modal.classList.add('flex');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function closeModal() {
|
|
110
|
+
modal.classList.add('hidden');
|
|
111
|
+
modal.classList.remove('flex');
|
|
112
|
+
currentOnSelect = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function uploadFile(file) {
|
|
116
|
+
try {
|
|
117
|
+
resetUI();
|
|
118
|
+
show(progress);
|
|
119
|
+
progressBar.style.width = '35%';
|
|
120
|
+
|
|
121
|
+
const form = new FormData();
|
|
122
|
+
form.append('file', file);
|
|
123
|
+
form.append('namespace', currentNamespace);
|
|
124
|
+
form.append('visibility', currentVisibility);
|
|
125
|
+
|
|
126
|
+
const res = await fetch('<%= baseUrl %>/api/admin/assets/upload', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: form,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
progressBar.style.width = '80%';
|
|
132
|
+
|
|
133
|
+
const data = await res.json().catch(() => ({}));
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw new Error(data?.error || 'Upload failed');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const url = data?.asset?.publicUrl || '';
|
|
139
|
+
if (!url) {
|
|
140
|
+
throw new Error('Upload succeeded but no publicUrl was returned');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
lastUrl = url;
|
|
144
|
+
urlInput.value = url;
|
|
145
|
+
hide(progress);
|
|
146
|
+
show(result);
|
|
147
|
+
progressBar.style.width = '100%';
|
|
148
|
+
} catch (e) {
|
|
149
|
+
hide(progress);
|
|
150
|
+
setError(e?.message ? String(e.message) : 'Upload failed');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fileFromClipboardEvent(e) {
|
|
155
|
+
const items = e?.clipboardData?.items;
|
|
156
|
+
if (!items || !items.length) return null;
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
if (item && item.kind === 'file') {
|
|
159
|
+
const f = item.getAsFile();
|
|
160
|
+
if (f && String(f.type || '').startsWith('image/')) return f;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pickBtn.addEventListener('click', () => fileInput.click());
|
|
167
|
+
fileInput.addEventListener('change', () => {
|
|
168
|
+
const file = fileInput.files && fileInput.files[0];
|
|
169
|
+
if (file) uploadFile(file);
|
|
170
|
+
fileInput.value = '';
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
closeBtn.addEventListener('click', closeModal);
|
|
174
|
+
modal.addEventListener('click', (e) => {
|
|
175
|
+
if (e.target === modal) closeModal();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
copyBtn.addEventListener('click', async () => {
|
|
179
|
+
try {
|
|
180
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard API not available');
|
|
181
|
+
await navigator.clipboard.writeText(urlInput.value);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
setError(e?.message ? String(e.message) : 'Failed to copy');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
useBtn.addEventListener('click', () => {
|
|
188
|
+
if (currentOnSelect && lastUrl) {
|
|
189
|
+
currentOnSelect(lastUrl);
|
|
190
|
+
}
|
|
191
|
+
closeModal();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
dropzone.classList.add('border-blue-400');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
dropzone.addEventListener('dragleave', () => {
|
|
200
|
+
dropzone.classList.remove('border-blue-400');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
dropzone.addEventListener('drop', (e) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
dropzone.classList.remove('border-blue-400');
|
|
206
|
+
const file = e.dataTransfer?.files && e.dataTransfer.files[0];
|
|
207
|
+
if (file) uploadFile(file);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
document.addEventListener('paste', (e) => {
|
|
211
|
+
if (modal.classList.contains('hidden')) return;
|
|
212
|
+
const file = fileFromClipboardEvent(e);
|
|
213
|
+
if (file) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
uploadFile(file);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
window.openImageUploadModal = openModal;
|
|
220
|
+
})();
|
|
221
|
+
</script>
|