seeemess 1.0.7 → 1.0.9
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/admin/config.js +7 -3
- package/admin/server.js +8 -3
- package/admin/templates/admin.eta +27 -74
- package/package.json +1 -1
package/admin/config.js
CHANGED
|
@@ -28,6 +28,7 @@ let currentConfig = null;
|
|
|
28
28
|
* @param {string} [options.publicDir] Public/uploads directory path (defaults to cmsRoot/public)
|
|
29
29
|
* @param {string} [options.imageUrlPath] URL path prefix for images in final site (defaults to '/uploads')
|
|
30
30
|
* @param {string} [options.previewTemplate] Path to custom preview template (relative to cmsRoot)
|
|
31
|
+
* @param {string} [options.title] Title for the admin interface (defaults to 'CMS Admin')
|
|
31
32
|
* @param {number} [options.port] Server port (defaults to 3000 or PORT env var)
|
|
32
33
|
* @param {Array} options.sections Array of section definitions: { id, name, folder, tag, customFields? }
|
|
33
34
|
* customFields is an optional array of: { id, label, type, placeholder? }
|
|
@@ -40,9 +41,10 @@ export function createConfig(options = {}) {
|
|
|
40
41
|
const cmsRoot = options.cmsRoot || process.cwd();
|
|
41
42
|
|
|
42
43
|
// Normalize imageUrlPath - ensure it starts with / and doesn't end with /
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (imageUrlPath.
|
|
44
|
+
// Use 'imageUrlPath' in options check to allow empty string as valid value
|
|
45
|
+
let imageUrlPath = 'imageUrlPath' in options ? options.imageUrlPath : '/uploads';
|
|
46
|
+
if (imageUrlPath && !imageUrlPath.startsWith('/')) imageUrlPath = '/' + imageUrlPath;
|
|
47
|
+
if (imageUrlPath && imageUrlPath.endsWith('/')) imageUrlPath = imageUrlPath.slice(0, -1);
|
|
46
48
|
|
|
47
49
|
const config = {
|
|
48
50
|
CMS_ROOT: cmsRoot,
|
|
@@ -50,6 +52,7 @@ export function createConfig(options = {}) {
|
|
|
50
52
|
PUBLIC_DIR: options.publicDir || join(cmsRoot, 'public'),
|
|
51
53
|
IMAGE_URL_PATH: imageUrlPath,
|
|
52
54
|
PREVIEW_TEMPLATE: options.previewTemplate ? join(cmsRoot, options.previewTemplate) : null,
|
|
55
|
+
TITLE: options.title || 'CMS Admin',
|
|
53
56
|
PORT: options.port || process.env.PORT || 3000,
|
|
54
57
|
SECTIONS: options.sections || [],
|
|
55
58
|
IMAGE_SIZES: options.imageSizes || DEFAULT_IMAGE_SIZES,
|
|
@@ -80,6 +83,7 @@ export const getContentDir = () => getConfig().CONTENT_DIR;
|
|
|
80
83
|
export const getPublicDir = () => getConfig().PUBLIC_DIR;
|
|
81
84
|
export const getImageUrlPath = () => getConfig().IMAGE_URL_PATH;
|
|
82
85
|
export const getPreviewTemplate = () => getConfig().PREVIEW_TEMPLATE;
|
|
86
|
+
export const getTitle = () => getConfig().TITLE;
|
|
83
87
|
export const getPort = () => getConfig().PORT;
|
|
84
88
|
export const getSections = () => getConfig().SECTIONS;
|
|
85
89
|
export const getImageSizes = () => getConfig().IMAGE_SIZES;
|
package/admin/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { Eta } from 'eta';
|
|
5
5
|
|
|
6
6
|
// Config
|
|
7
|
-
import { createConfig, getConfig, getCmsRoot, getPublicDir, getImageUrlPath, getPreviewTemplate, getPort, getSections } from './config.js';
|
|
7
|
+
import { createConfig, getConfig, getCmsRoot, getPublicDir, getImageUrlPath, getPreviewTemplate, getTitle, getPort, getSections } from './config.js';
|
|
8
8
|
|
|
9
9
|
// Routes
|
|
10
10
|
import postsRouter from './routes/posts.js';
|
|
@@ -44,12 +44,17 @@ export function startAdminServer(options = {}) {
|
|
|
44
44
|
|
|
45
45
|
// Static files - serve images at configured URL path
|
|
46
46
|
const imageUrlPath = getImageUrlPath();
|
|
47
|
-
|
|
47
|
+
// Use '/' as mount path if imageUrlPath is empty string
|
|
48
|
+
app.use(imageUrlPath || '/', express.static(getPublicDir()));
|
|
49
|
+
// Also serve at /uploads for backward compatibility in admin previews
|
|
50
|
+
if (imageUrlPath !== '/uploads') {
|
|
51
|
+
app.use('/uploads', express.static(getPublicDir()));
|
|
52
|
+
}
|
|
48
53
|
app.use('/css', express.static(join(getCmsRoot(), 'css')));
|
|
49
54
|
|
|
50
55
|
// Admin interface
|
|
51
56
|
app.get('/', (req, res) => {
|
|
52
|
-
res.send(eta.render('admin', { imageUrlPath }));
|
|
57
|
+
res.send(eta.render('admin', { imageUrlPath, title: getTitle() }));
|
|
53
58
|
});
|
|
54
59
|
|
|
55
60
|
// API Routes
|
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
.section-tab { flex: 1; padding: 0.6rem 0.5rem; text-align: center; background: #f0f0f0; border: none; cursor: pointer; font-size: 0.8rem; font-weight: 600; border-radius: 4px 4px 0 0; }
|
|
31
31
|
.section-tab:hover { background: #e0e0e0; }
|
|
32
32
|
.section-tab.active { background: #333; color: #fff; }
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
.
|
|
33
|
+
.search-box { position: relative; margin-bottom: 0.75rem; }
|
|
34
|
+
.search-box input { width: 100%; padding: 0.5rem 0.5rem 0.5rem 2rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.85rem; }
|
|
35
|
+
.search-box input:focus { outline: none; border-color: #333; }
|
|
36
|
+
.search-icon { position: absolute; left: 0.5rem; top: 50%; transform: translateY(-50%); color: #666; font-size: 0.9rem; pointer-events: none; }
|
|
36
37
|
.btn-lede { font-size: 0.65rem; padding: 2px 6px; background: #17a2b8; color: #fff; border: none; border-radius: 3px; cursor: pointer; margin-left: auto; }
|
|
37
38
|
.btn-lede:hover { background: #138496; }
|
|
38
39
|
.btn-lede.is-lede { background: #28a745; }
|
|
@@ -268,16 +269,16 @@
|
|
|
268
269
|
</head>
|
|
269
270
|
<body>
|
|
270
271
|
<div class="header-row">
|
|
271
|
-
<h1
|
|
272
|
+
<h1><%= it.title || "CMS Admin" %></h1>
|
|
272
273
|
<button class="btn-publish-global" id="publishGlobalBtn" disabled onclick="publishChanges()">Publish Changes</button>
|
|
273
274
|
</div>
|
|
274
275
|
<div class="container">
|
|
275
276
|
<div class="sidebar">
|
|
276
277
|
<button class="btn btn-primary" style="width:100%; margin-bottom:1rem;" onclick="newPost()">+ New Post</button>
|
|
277
278
|
<div class="section-tabs" id="sectionTabs"></div>
|
|
278
|
-
<div class="
|
|
279
|
-
<
|
|
280
|
-
<
|
|
279
|
+
<div class="search-box">
|
|
280
|
+
<span class="search-icon">🔍</span>
|
|
281
|
+
<input type="text" id="searchInput" placeholder="Search posts..." oninput="filterPosts()">
|
|
281
282
|
</div>
|
|
282
283
|
<ul class="post-list" id="postList"></ul>
|
|
283
284
|
</div>
|
|
@@ -386,15 +387,16 @@
|
|
|
386
387
|
|
|
387
388
|
<div>
|
|
388
389
|
<label>Content (Markdown)</label>
|
|
390
|
+
<p style="font-size: 0.85rem; color: #666; margin: 0.25rem 0 0.5rem;">[Photo](photo.jpg){.float-left .w-1/3 .mt-4 .mb-4 .mr-4} or [Photo](photo.jpg){.float-right .w-1/3 .mt-4 .mb-4 .ml-4}</p>
|
|
389
391
|
<textarea id="body" required></textarea>
|
|
390
392
|
</div>
|
|
391
393
|
|
|
392
394
|
<div class="image-section" id="inlineImagesSection" style="display: none;">
|
|
393
395
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
394
|
-
<label>
|
|
396
|
+
<label>Images in Content</label>
|
|
395
397
|
<button type="button" class="btn btn-secondary" onclick="syncInlineImages()" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Refresh</button>
|
|
396
398
|
</div>
|
|
397
|
-
<p style="font-size: 0.85rem; color: #666; margin: 0.25rem 0 0.5rem;">
|
|
399
|
+
<p style="font-size: 0.85rem; color: #666; margin: 0.25rem 0 0.5rem;">Images found in your markdown content (manage captions/credits in gallery)</p>
|
|
398
400
|
<div id="inlineImagesList" class="gallery-thumbnails"></div>
|
|
399
401
|
</div>
|
|
400
402
|
|
|
@@ -495,7 +497,6 @@
|
|
|
495
497
|
let modalResolve = null;
|
|
496
498
|
let formDirty = false;
|
|
497
499
|
let editingGalleryIndex = null;
|
|
498
|
-
let editingImageIndex = null;
|
|
499
500
|
let currentSection = 'news';
|
|
500
501
|
let pendingChanges = false;
|
|
501
502
|
|
|
@@ -601,10 +602,25 @@
|
|
|
601
602
|
document.querySelectorAll('.section-tab').forEach((tab, idx) => {
|
|
602
603
|
tab.classList.toggle('active', sections[idx].id === sectionId);
|
|
603
604
|
});
|
|
605
|
+
document.getElementById('searchInput').value = '';
|
|
604
606
|
renderPostList();
|
|
605
607
|
renderCustomFields();
|
|
606
608
|
}
|
|
607
609
|
|
|
610
|
+
function filterPosts() {
|
|
611
|
+
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
612
|
+
const listItems = document.querySelectorAll('.post-list li');
|
|
613
|
+
|
|
614
|
+
listItems.forEach(li => {
|
|
615
|
+
const title = li.querySelector('.post-title').textContent.toLowerCase();
|
|
616
|
+
if (title.includes(searchTerm)) {
|
|
617
|
+
li.style.display = '';
|
|
618
|
+
} else {
|
|
619
|
+
li.style.display = 'none';
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
608
624
|
function renderCustomFields() {
|
|
609
625
|
const container = document.getElementById('customFieldsContainer');
|
|
610
626
|
const section = sections.find(s => s.id === currentSection);
|
|
@@ -714,19 +730,6 @@
|
|
|
714
730
|
// Filter posts by current section
|
|
715
731
|
const sectionPosts = posts.filter(p => p.section === currentSection);
|
|
716
732
|
|
|
717
|
-
// Find current lede
|
|
718
|
-
const ledePost = sectionPosts.find(p => p.lede === true || p.lede === 'true');
|
|
719
|
-
const ledeIndicator = document.getElementById('ledeIndicator');
|
|
720
|
-
if (ledePost) {
|
|
721
|
-
ledeIndicator.textContent = 'Lead: ' + (ledePost.title || '').substring(0, 20) + (ledePost.title && ledePost.title.length > 20 ? '...' : '');
|
|
722
|
-
} else {
|
|
723
|
-
ledeIndicator.textContent = 'Lead: Most recent';
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Update section title
|
|
727
|
-
const section = sections.find(s => s.id === currentSection);
|
|
728
|
-
document.getElementById('sectionTitle').textContent = section ? section.name : 'Posts';
|
|
729
|
-
|
|
730
733
|
sectionPosts.forEach(p => {
|
|
731
734
|
const li = document.createElement('li');
|
|
732
735
|
li.dataset.path = p.path;
|
|
@@ -1430,60 +1433,17 @@
|
|
|
1430
1433
|
container.innerHTML = '';
|
|
1431
1434
|
|
|
1432
1435
|
images.forEach((item, idx) => {
|
|
1433
|
-
const hasMetadata = item.caption || item.credit;
|
|
1434
1436
|
const thumb = document.createElement('div');
|
|
1435
|
-
thumb.className = 'gallery-thumb'
|
|
1437
|
+
thumb.className = 'gallery-thumb';
|
|
1436
1438
|
|
|
1437
1439
|
thumb.innerHTML =
|
|
1438
1440
|
'<img src="' + imageUrlPath + '/' + item.src.replace(/(\.[^.]+)$/, '-thumb$1') + '" alt="" onerror="this.onerror=null; this.src=\'' + imageUrlPath + '/' + item.src + '\'">' +
|
|
1439
|
-
'<div class="gallery-thumb-actions">' +
|
|
1440
|
-
'<button type="button" class="gallery-thumb-btn edit" onclick="openInlineImageEditModal(' + idx + ')" title="Edit caption/credit">✎</button>' +
|
|
1441
|
-
'</div>' +
|
|
1442
1441
|
'<div class="gallery-thumb-name">' + item.src + '</div>';
|
|
1443
1442
|
|
|
1444
1443
|
container.appendChild(thumb);
|
|
1445
1444
|
});
|
|
1446
1445
|
}
|
|
1447
1446
|
|
|
1448
|
-
function openInlineImageEditModal(idx) {
|
|
1449
|
-
editingImageIndex = idx;
|
|
1450
|
-
const item = images[idx];
|
|
1451
|
-
|
|
1452
|
-
document.getElementById('galleryCaption').value = item.caption || '';
|
|
1453
|
-
document.getElementById('galleryCredit').value = item.credit || '';
|
|
1454
|
-
document.getElementById('captionCount').textContent = (item.caption || '').length;
|
|
1455
|
-
document.getElementById('creditCount').textContent = (item.credit || '').length;
|
|
1456
|
-
|
|
1457
|
-
// Update modal title to indicate inline image
|
|
1458
|
-
document.querySelector('#galleryEditModal .modal-title').textContent = 'Edit Inline Image Details';
|
|
1459
|
-
document.getElementById('galleryEditModal').classList.add('active');
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function closeInlineImageEditModal() {
|
|
1463
|
-
document.getElementById('galleryEditModal').classList.remove('active');
|
|
1464
|
-
editingImageIndex = null;
|
|
1465
|
-
// Restore modal title
|
|
1466
|
-
document.querySelector('#galleryEditModal .modal-title').textContent = 'Edit Photo Details';
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
function saveInlineImageEdit() {
|
|
1470
|
-
if (editingImageIndex === null) return;
|
|
1471
|
-
|
|
1472
|
-
const caption = document.getElementById('galleryCaption').value.trim();
|
|
1473
|
-
const credit = document.getElementById('galleryCredit').value.trim();
|
|
1474
|
-
|
|
1475
|
-
images[editingImageIndex] = {
|
|
1476
|
-
src: images[editingImageIndex].src,
|
|
1477
|
-
caption,
|
|
1478
|
-
credit
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
closeInlineImageEditModal();
|
|
1482
|
-
renderInlineImages();
|
|
1483
|
-
markFormDirty();
|
|
1484
|
-
showStatus('Image details saved', 'success');
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
1447
|
// Listen for body changes to sync inline images
|
|
1488
1448
|
document.getElementById('body').addEventListener('blur', syncInlineImages);
|
|
1489
1449
|
|
|
@@ -1546,18 +1506,11 @@
|
|
|
1546
1506
|
function closeGalleryEditModal() {
|
|
1547
1507
|
document.getElementById('galleryEditModal').classList.remove('active');
|
|
1548
1508
|
editingGalleryIndex = null;
|
|
1549
|
-
editingImageIndex = null;
|
|
1550
1509
|
// Restore modal title
|
|
1551
1510
|
document.querySelector('#galleryEditModal .modal-title').textContent = 'Edit Photo Details';
|
|
1552
1511
|
}
|
|
1553
1512
|
|
|
1554
1513
|
function saveGalleryEdit() {
|
|
1555
|
-
// Check if editing inline image or gallery image
|
|
1556
|
-
if (editingImageIndex !== null) {
|
|
1557
|
-
saveInlineImageEdit();
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
1514
|
if (editingGalleryIndex === null) return;
|
|
1562
1515
|
|
|
1563
1516
|
const caption = document.getElementById('galleryCaption').value.trim();
|