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 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
- let imageUrlPath = options.imageUrlPath || '/uploads';
44
- if (!imageUrlPath.startsWith('/')) imageUrlPath = '/' + imageUrlPath;
45
- if (imageUrlPath.endsWith('/')) imageUrlPath = imageUrlPath.slice(0, -1);
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
- app.use(imageUrlPath, express.static(getPublicDir()));
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
- .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
34
- .section-header h3 { margin: 0; font-size: 0.9rem; }
35
- .lede-indicator { font-size: 0.7rem; color: #004085; }
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>CMS Admin</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="section-header">
279
- <h3 id="sectionTitle">Posts</h3>
280
- <span class="lede-indicator" id="ledeIndicator"></span>
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>Inline Image Captions &amp; Credits</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;">Add captions and credits for images in your content</p>
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' + (hasMetadata ? ' has-metadata' : '');
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seeemess",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A simple CMS framework built on Eleventy with an admin interface for managing blog content.",
5
5
  "type": "module",
6
6
  "main": "index.js",