@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,399 @@
|
|
|
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>Blog Posts - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
9
|
+
<style>
|
|
10
|
+
.toast { animation: slideIn 0.3s ease-out; }
|
|
11
|
+
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
12
|
+
.fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
13
|
+
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body class="bg-gray-100">
|
|
17
|
+
<div class="min-h-screen">
|
|
18
|
+
<div class="bg-white shadow">
|
|
19
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
20
|
+
<div class="flex justify-between items-center">
|
|
21
|
+
<div>
|
|
22
|
+
<h1 class="text-2xl font-bold text-gray-900">Blog system</h1>
|
|
23
|
+
<p class="text-sm text-gray-600 mt-1">Posts and automation</p>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="flex items-center gap-4">
|
|
26
|
+
<button id="btn-new-post" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
|
27
|
+
<i class="ti ti-plus mr-2"></i>New Post
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="mt-4 border-b border-gray-200">
|
|
33
|
+
<nav class="flex -mb-px">
|
|
34
|
+
<a id="tab-btn-posts" href="<%= adminPath %>/blog?tab=posts" class="px-6 py-3 border-b-2 font-medium text-sm">
|
|
35
|
+
Posts
|
|
36
|
+
</a>
|
|
37
|
+
<a id="tab-btn-automation" href="<%= adminPath %>/blog?tab=automation" class="px-6 py-3 border-b-2 font-medium text-sm">
|
|
38
|
+
Automation
|
|
39
|
+
</a>
|
|
40
|
+
</nav>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
46
|
+
<div id="tab-posts">
|
|
47
|
+
<!-- Stats Cards -->
|
|
48
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
49
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
50
|
+
<p class="text-sm text-gray-500">Total Posts</p>
|
|
51
|
+
<p id="stat-total" class="text-2xl font-bold text-gray-900">-</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
54
|
+
<p class="text-sm text-gray-500">Published</p>
|
|
55
|
+
<p id="stat-published" class="text-2xl font-bold text-green-600">-</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
58
|
+
<p class="text-sm text-gray-500">Drafts</p>
|
|
59
|
+
<p id="stat-draft" class="text-2xl font-bold text-gray-600">-</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="bg-white rounded-lg shadow p-4">
|
|
62
|
+
<p class="text-sm text-gray-500">Scheduled</p>
|
|
63
|
+
<p id="stat-scheduled" class="text-2xl font-bold text-blue-600">-</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Filters -->
|
|
68
|
+
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
69
|
+
<div class="grid grid-cols-1 md:grid-cols-5 gap-3">
|
|
70
|
+
<div>
|
|
71
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
|
72
|
+
<input id="filter-q" type="text" class="w-full border rounded px-3 py-2" placeholder="title or excerpt">
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
76
|
+
<select id="filter-status" class="w-full border rounded px-3 py-2">
|
|
77
|
+
<option value="">All</option>
|
|
78
|
+
<option value="draft">Draft</option>
|
|
79
|
+
<option value="scheduled">Scheduled</option>
|
|
80
|
+
<option value="published">Published</option>
|
|
81
|
+
<option value="archived">Archived</option>
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
|
86
|
+
<input id="filter-category" type="text" class="w-full border rounded px-3 py-2" placeholder="category">
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Tag</label>
|
|
90
|
+
<input id="filter-tag" type="text" class="w-full border rounded px-3 py-2" placeholder="tag">
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Limit</label>
|
|
94
|
+
<input id="filter-limit" type="number" min="1" max="200" class="w-full border rounded px-3 py-2" value="50">
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex gap-2 mt-4">
|
|
98
|
+
<button id="btn-apply" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Apply</button>
|
|
99
|
+
<button id="btn-reset" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Reset</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Posts List -->
|
|
104
|
+
<div class="bg-white rounded-lg shadow">
|
|
105
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
|
106
|
+
<h2 class="text-lg font-semibold text-gray-900">Posts</h2>
|
|
107
|
+
<p id="posts-subtitle" class="text-sm text-gray-600">-</p>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="overflow-x-auto">
|
|
110
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
111
|
+
<thead class="bg-gray-50">
|
|
112
|
+
<tr>
|
|
113
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
|
|
114
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
115
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
|
116
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Author</th>
|
|
117
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Published</th>
|
|
118
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Updated</th>
|
|
119
|
+
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
120
|
+
</tr>
|
|
121
|
+
</thead>
|
|
122
|
+
<tbody id="posts-tbody" class="bg-white divide-y divide-gray-200">
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
<div id="pagination" class="px-6 py-4 border-t border-gray-200">
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div id="tab-automation" class="hidden">
|
|
132
|
+
<div class="bg-white rounded-lg shadow border overflow-hidden" style="height: calc(100vh - 260px); min-height: 560px;">
|
|
133
|
+
<iframe id="automation-iframe" class="w-full h-full border-none" title="Blog automation"></iframe>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Toast Container -->
|
|
140
|
+
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
|
141
|
+
|
|
142
|
+
<script>
|
|
143
|
+
const API_BASE = '<%= baseUrl %>/api/admin/blog-posts';
|
|
144
|
+
let currentPage = 1;
|
|
145
|
+
let totalPages = 1;
|
|
146
|
+
|
|
147
|
+
function getQueryParam(name) {
|
|
148
|
+
try {
|
|
149
|
+
return new URLSearchParams(window.location.search).get(name);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function activateTab(tabName) {
|
|
156
|
+
const postsBtn = document.getElementById('tab-btn-posts');
|
|
157
|
+
const automationBtn = document.getElementById('tab-btn-automation');
|
|
158
|
+
const postsTab = document.getElementById('tab-posts');
|
|
159
|
+
const automationTab = document.getElementById('tab-automation');
|
|
160
|
+
|
|
161
|
+
const activeBtnClasses = ['border-blue-500', 'text-blue-600'];
|
|
162
|
+
const inactiveBtnClasses = ['border-transparent', 'text-gray-500', 'hover:text-gray-700'];
|
|
163
|
+
|
|
164
|
+
const setBtn = (btn, active) => {
|
|
165
|
+
if (!btn) return;
|
|
166
|
+
btn.classList.remove(...activeBtnClasses, ...inactiveBtnClasses);
|
|
167
|
+
if (active) {
|
|
168
|
+
btn.classList.add(...activeBtnClasses);
|
|
169
|
+
} else {
|
|
170
|
+
btn.classList.add(...inactiveBtnClasses);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const isAutomation = tabName === 'automation';
|
|
175
|
+
setBtn(postsBtn, !isAutomation);
|
|
176
|
+
setBtn(automationBtn, isAutomation);
|
|
177
|
+
|
|
178
|
+
if (isAutomation) {
|
|
179
|
+
postsTab.classList.add('hidden');
|
|
180
|
+
automationTab.classList.remove('hidden');
|
|
181
|
+
const iframe = document.getElementById('automation-iframe');
|
|
182
|
+
if (iframe && !iframe.src) {
|
|
183
|
+
iframe.src = '<%= adminPath %>/blog-automation';
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
automationTab.classList.add('hidden');
|
|
187
|
+
postsTab.classList.remove('hidden');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Utility functions
|
|
192
|
+
function showToast(message, type = 'success') {
|
|
193
|
+
const container = document.getElementById('toast-container');
|
|
194
|
+
const toast = document.createElement('div');
|
|
195
|
+
toast.className = `toast p-4 rounded shadow-lg ${
|
|
196
|
+
type === 'success' ? 'bg-green-500 text-white' :
|
|
197
|
+
type === 'error' ? 'bg-red-500 text-white' :
|
|
198
|
+
'bg-blue-500 text-white'
|
|
199
|
+
}`;
|
|
200
|
+
toast.innerHTML = `
|
|
201
|
+
<div class="flex items-center">
|
|
202
|
+
<i class="ti ti-${type === 'success' ? 'check' : type === 'error' ? 'alert-triangle' : 'info'} mr-2"></i>
|
|
203
|
+
<span>${message}</span>
|
|
204
|
+
</div>
|
|
205
|
+
`;
|
|
206
|
+
container.appendChild(toast);
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
toast.classList.add('fade-out');
|
|
209
|
+
setTimeout(() => toast.remove(), 300);
|
|
210
|
+
}, 3000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatDate(dateString) {
|
|
214
|
+
if (!dateString) return '-';
|
|
215
|
+
const date = new Date(dateString);
|
|
216
|
+
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getStatusBadge(status) {
|
|
220
|
+
const badges = {
|
|
221
|
+
draft: '<span class="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-800 rounded-full">Draft</span>',
|
|
222
|
+
scheduled: '<span class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">Scheduled</span>',
|
|
223
|
+
published: '<span class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 rounded-full">Published</span>',
|
|
224
|
+
archived: '<span class="px-2 py-1 text-xs font-medium bg-red-100 text-red-800 rounded-full">Archived</span>'
|
|
225
|
+
};
|
|
226
|
+
return badges[status] || status;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Load posts
|
|
230
|
+
async function loadPosts(page = 1) {
|
|
231
|
+
try {
|
|
232
|
+
const params = new URLSearchParams({
|
|
233
|
+
page,
|
|
234
|
+
limit: document.getElementById('filter-limit').value,
|
|
235
|
+
q: document.getElementById('filter-q').value,
|
|
236
|
+
status: document.getElementById('filter-status').value,
|
|
237
|
+
category: document.getElementById('filter-category').value,
|
|
238
|
+
tag: document.getElementById('filter-tag').value
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const response = await fetch(`${API_BASE}?${params}`);
|
|
242
|
+
const data = await response.json();
|
|
243
|
+
|
|
244
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load posts');
|
|
245
|
+
|
|
246
|
+
renderPosts(data.items);
|
|
247
|
+
renderPagination(data.pagination);
|
|
248
|
+
updateStats(data.stats || {});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
showToast(error.message, 'error');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function renderPosts(posts) {
|
|
255
|
+
const tbody = document.getElementById('posts-tbody');
|
|
256
|
+
if (!posts || posts.length === 0) {
|
|
257
|
+
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-gray-500">No posts found</td></tr>';
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
tbody.innerHTML = posts.map(post => `
|
|
262
|
+
<tr>
|
|
263
|
+
<td class="px-6 py-4">
|
|
264
|
+
<div class="text-sm font-medium text-gray-900">${post.title || 'Untitled'}</div>
|
|
265
|
+
<div class="text-sm text-gray-500">${post.slug || ''}</div>
|
|
266
|
+
</td>
|
|
267
|
+
<td class="px-6 py-4">${getStatusBadge(post.status)}</td>
|
|
268
|
+
<td class="px-6 py-4 text-sm text-gray-900">${post.category || '-'}</td>
|
|
269
|
+
<td class="px-6 py-4 text-sm text-gray-900">${post.authorName || '-'}</td>
|
|
270
|
+
<td class="px-6 py-4 text-sm text-gray-900">${formatDate(post.publishedAt)}</td>
|
|
271
|
+
<td class="px-6 py-4 text-sm text-gray-900">${formatDate(post.updatedAt)}</td>
|
|
272
|
+
<td class="px-6 py-4 text-right text-sm font-medium">
|
|
273
|
+
<button onclick="editPost('${post._id}')" class="text-blue-600 hover:text-blue-900 mr-3">Edit</button>
|
|
274
|
+
${post.status === 'draft' ? `<button onclick="publishPost('${post._id}')" class="text-green-600 hover:text-green-900 mr-3">Publish</button>` : ''}
|
|
275
|
+
${post.status === 'published' ? `<button onclick="unpublishPost('${post._id}')" class="text-orange-600 hover:text-orange-900 mr-3">Unpublish</button>` : ''}
|
|
276
|
+
${post.status === 'draft' || post.status === 'published' ? `<button onclick="archivePost('${post._id}')" class="text-red-600 hover:text-red-900">Archive</button>` : ''}
|
|
277
|
+
</td>
|
|
278
|
+
</tr>
|
|
279
|
+
`).join('');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderPagination(pagination) {
|
|
283
|
+
const container = document.getElementById('pagination');
|
|
284
|
+
if (!pagination || pagination.pages <= 1) {
|
|
285
|
+
container.innerHTML = '';
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
currentPage = pagination.page;
|
|
290
|
+
totalPages = pagination.pages;
|
|
291
|
+
|
|
292
|
+
let html = '<div class="flex items-center justify-between">';
|
|
293
|
+
html += `<div class="text-sm text-gray-700">Showing ${pagination.page} of ${pagination.pages} pages</div>`;
|
|
294
|
+
html += '<div class="flex gap-2">';
|
|
295
|
+
|
|
296
|
+
if (pagination.page > 1) {
|
|
297
|
+
html += `<button onclick="loadPosts(${pagination.page - 1})" class="px-3 py-1 border rounded hover:bg-gray-50">Previous</button>`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (let i = 1; i <= Math.min(5, pagination.pages); i++) {
|
|
301
|
+
const active = i === pagination.page ? 'bg-blue-500 text-white' : 'border hover:bg-gray-50';
|
|
302
|
+
html += `<button onclick="loadPosts(${i})" class="px-3 py-1 ${active} rounded">${i}</button>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (pagination.page < pagination.pages) {
|
|
306
|
+
html += `<button onclick="loadPosts(${pagination.page + 1})" class="px-3 py-1 border rounded hover:bg-gray-50">Next</button>`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
html += '</div></div>';
|
|
310
|
+
container.innerHTML = html;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function updateStats(stats) {
|
|
314
|
+
document.getElementById('stat-total').textContent = stats.total || 0;
|
|
315
|
+
document.getElementById('stat-published').textContent = stats.published || 0;
|
|
316
|
+
document.getElementById('stat-draft').textContent = stats.draft || 0;
|
|
317
|
+
document.getElementById('stat-scheduled').textContent = stats.scheduled || 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Actions
|
|
321
|
+
function editPost(id) {
|
|
322
|
+
window.location.href = `<%= adminPath %>/blog/edit/${id}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function publishPost(id) {
|
|
326
|
+
if (!confirm('Are you sure you want to publish this post?')) return;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const response = await fetch(`${API_BASE}/${id}/publish`, { method: 'PUT' });
|
|
330
|
+
const data = await response.json();
|
|
331
|
+
|
|
332
|
+
if (!response.ok) throw new Error(data.error || 'Failed to publish');
|
|
333
|
+
|
|
334
|
+
showToast('Post published successfully');
|
|
335
|
+
loadPosts(currentPage);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
showToast(error.message, 'error');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function unpublishPost(id) {
|
|
342
|
+
if (!confirm('Are you sure you want to unpublish this post?')) return;
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const response = await fetch(`${API_BASE}/${id}/unpublish`, { method: 'PUT' });
|
|
346
|
+
const data = await response.json();
|
|
347
|
+
|
|
348
|
+
if (!response.ok) throw new Error(data.error || 'Failed to unpublish');
|
|
349
|
+
|
|
350
|
+
showToast('Post unpublished successfully');
|
|
351
|
+
loadPosts(currentPage);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
showToast(error.message, 'error');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function archivePost(id) {
|
|
358
|
+
if (!confirm('Are you sure you want to archive this post?')) return;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const response = await fetch(`${API_BASE}/${id}/archive`, { method: 'PUT' });
|
|
362
|
+
const data = await response.json();
|
|
363
|
+
|
|
364
|
+
if (!response.ok) throw new Error(data.error || 'Failed to archive');
|
|
365
|
+
|
|
366
|
+
showToast('Post archived successfully');
|
|
367
|
+
loadPosts(currentPage);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
showToast(error.message, 'error');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Event listeners
|
|
374
|
+
document.getElementById('btn-new-post').addEventListener('click', () => {
|
|
375
|
+
window.location.href = `<%= adminPath %>/blog/new`;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
document.getElementById('btn-apply').addEventListener('click', () => {
|
|
379
|
+
loadPosts(1);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
document.getElementById('btn-reset').addEventListener('click', () => {
|
|
383
|
+
document.getElementById('filter-q').value = '';
|
|
384
|
+
document.getElementById('filter-status').value = '';
|
|
385
|
+
document.getElementById('filter-category').value = '';
|
|
386
|
+
document.getElementById('filter-tag').value = '';
|
|
387
|
+
document.getElementById('filter-limit').value = '50';
|
|
388
|
+
loadPosts(1);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Initial load
|
|
392
|
+
const tab = (getQueryParam('tab') || 'posts').toLowerCase();
|
|
393
|
+
activateTab(tab === 'automation' ? 'automation' : 'posts');
|
|
394
|
+
if (tab !== 'automation') {
|
|
395
|
+
loadPosts(1);
|
|
396
|
+
}
|
|
397
|
+
</script>
|
|
398
|
+
</body>
|
|
399
|
+
</html>
|