@rmdes/indiekit-endpoint-homepage 1.0.2 → 1.0.4

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/index.js CHANGED
@@ -170,6 +170,20 @@ export default class HomepageEndpoint {
170
170
  defaultConfig: {},
171
171
  configSchema: {},
172
172
  },
173
+ {
174
+ id: "custom-html",
175
+ label: "Custom Content",
176
+ description: "Freeform HTML or text block",
177
+ icon: "code",
178
+ defaultConfig: {
179
+ title: "",
180
+ content: "",
181
+ },
182
+ configSchema: {
183
+ title: { type: "text", label: "Title (optional)" },
184
+ content: { type: "textarea", label: "Content (HTML)" },
185
+ },
186
+ },
173
187
  ];
174
188
  }
175
189
 
@@ -227,8 +241,10 @@ export default class HomepageEndpoint {
227
241
  // Store reference to Indiekit for plugin discovery
228
242
  Indiekit.config.application.indiekitInstance = Indiekit;
229
243
 
230
- // Discover sections and widgets from other plugins
231
- this._discoverPluginSections(Indiekit);
244
+ // Defer discovery until after all plugins have called init()
245
+ // (process.nextTick runs after the synchronous plugin loading loop)
246
+ const self = this;
247
+ process.nextTick(() => self._discoverPluginSections(Indiekit));
232
248
  }
233
249
 
234
250
  /**
@@ -90,6 +90,7 @@ export const apiController = {
90
90
  hero: config.hero,
91
91
  sections: config.sections,
92
92
  sidebar: config.sidebar,
93
+ footer: config.footer,
93
94
  identity: config.identity,
94
95
  updatedAt: config.updatedAt,
95
96
  });
@@ -63,7 +63,7 @@ export const dashboardController = {
63
63
  const { application } = request.app.locals;
64
64
 
65
65
  try {
66
- const { layout, hero, sections, sidebar, identity } = request.body;
66
+ const { layout, hero, sections, sidebar, footer, identity } = request.body;
67
67
 
68
68
  // Parse JSON strings if needed
69
69
  const config = {
@@ -71,6 +71,7 @@ export const dashboardController = {
71
71
  hero: typeof hero === "string" ? JSON.parse(hero) : hero,
72
72
  sections: typeof sections === "string" ? JSON.parse(sections) : sections,
73
73
  sidebar: typeof sidebar === "string" ? JSON.parse(sidebar) : sidebar,
74
+ footer: typeof footer === "string" ? JSON.parse(footer) : footer,
74
75
  identity: typeof identity === "string" ? JSON.parse(identity) : identity,
75
76
  };
76
77
 
@@ -42,6 +42,7 @@ export async function saveConfig(application, config) {
42
42
  hero: config.hero || { enabled: true, showSocial: true },
43
43
  sections: config.sections || [],
44
44
  sidebar: config.sidebar || [],
45
+ footer: config.footer || [],
45
46
  identity: config.identity || null,
46
47
  updatedAt: now,
47
48
  };
@@ -78,6 +79,7 @@ async function writeConfigFile(application, config) {
78
79
  hero: config.hero,
79
80
  sections: config.sections,
80
81
  sidebar: config.sidebar,
82
+ footer: config.footer,
81
83
  identity: config.identity,
82
84
  updatedAt: config.updatedAt,
83
85
  };
@@ -111,6 +113,7 @@ export function getDefaultConfig() {
111
113
  { type: "recent-posts", config: { maxItems: 5 } },
112
114
  { type: "categories", config: {} },
113
115
  ],
116
+ footer: [],
114
117
  identity: null,
115
118
  };
116
119
  }
package/locales/en.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "homepage": {
3
3
  "title": "Homepage Builder",
4
- "description": "Configure your homepage layout, sections, and sidebar widgets.",
4
+ "description": "Configure your homepage layout, sections, sidebar widgets, and footer.",
5
5
  "saved": "Configuration saved successfully. Refresh your homepage to see changes.",
6
6
  "save": "Save Configuration",
7
7
  "layout": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "sections": {
19
19
  "title": "Content Sections",
20
- "description": "Add and arrange sections that appear on your homepage.",
20
+ "description": "Add and arrange sections that appear in the main content area.",
21
21
  "add": "Add Section",
22
22
  "empty": "No sections configured. Add sections from the picker below."
23
23
  },
@@ -26,6 +26,19 @@
26
26
  "description": "Configure widgets that appear in the sidebar (only visible with two-column layout).",
27
27
  "add": "Add Widget",
28
28
  "empty": "No widgets configured. Add widgets from the picker below."
29
+ },
30
+ "footer": {
31
+ "title": "Footer",
32
+ "description": "Content that appears below the main area — ideal for webrings, links, or custom blocks.",
33
+ "add": "Add Footer Block",
34
+ "empty": "No footer blocks configured."
35
+ },
36
+ "customContent": {
37
+ "editTitle": "Edit Custom Content",
38
+ "titleLabel": "Title (optional)",
39
+ "contentLabel": "Content (HTML or text)",
40
+ "save": "Apply",
41
+ "cancel": "Cancel"
29
42
  }
30
43
  }
31
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-homepage",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Homepage builder endpoint for Indiekit. Configure layout, sections, and sidebar widgets from the admin UI.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -21,6 +21,12 @@
21
21
  border-block-end: 1px solid var(--color-outline-variant, #ddd);
22
22
  }
23
23
 
24
+ .hp-section__desc {
25
+ color: var(--color-on-offset, #666);
26
+ font: var(--font-body, 0.875rem/1.5 sans-serif);
27
+ margin-block-end: var(--space-s, 0.75rem);
28
+ }
29
+
24
30
  .hp-layout-grid {
25
31
  display: grid;
26
32
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -76,6 +82,12 @@
76
82
  gap: var(--space-s, 0.75rem);
77
83
  }
78
84
 
85
+ .hp-section-item--has-edit {
86
+ border-bottom-left-radius: 0;
87
+ border-bottom-right-radius: 0;
88
+ margin-block-end: 0;
89
+ }
90
+
79
91
  .hp-section-item__info {
80
92
  flex: 1;
81
93
  }
@@ -89,9 +101,86 @@
89
101
  font: var(--font-caption, 0.75rem/1.4 sans-serif);
90
102
  }
91
103
 
104
+ .hp-section-item__preview {
105
+ color: var(--color-on-offset, #888);
106
+ font: var(--font-caption, 0.75rem/1.4 sans-serif);
107
+ margin-block-start: 0.125rem;
108
+ max-width: 40ch;
109
+ overflow: hidden;
110
+ text-overflow: ellipsis;
111
+ white-space: nowrap;
112
+ }
113
+
92
114
  .hp-section-item__actions {
93
115
  display: flex;
94
116
  gap: var(--space-2xs, 0.25rem);
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .hp-section-item__drag {
121
+ cursor: grab;
122
+ color: var(--color-on-offset, #999);
123
+ padding: 0 0.25rem;
124
+ display: flex;
125
+ align-items: center;
126
+ }
127
+
128
+ .hp-section-item__drag:active {
129
+ cursor: grabbing;
130
+ }
131
+
132
+ .hp-section-item.sortable-ghost {
133
+ opacity: 0.4;
134
+ background: var(--color-primary-container, #e6f0ff);
135
+ }
136
+
137
+ .hp-section-item.sortable-chosen {
138
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
139
+ }
140
+
141
+ .hp-edit-panel {
142
+ background: var(--color-background, #fff);
143
+ border: 1px solid var(--color-primary, #0066cc);
144
+ border-top: none;
145
+ border-radius: 0 0 var(--border-radius-small, 0.25rem) var(--border-radius-small, 0.25rem);
146
+ padding: var(--space-s, 0.75rem);
147
+ margin-block-end: var(--space-xs, 0.5rem);
148
+ }
149
+
150
+ .hp-edit-panel summary { display: none; }
151
+
152
+ .hp-edit-panel .field {
153
+ margin-block-end: var(--space-xs, 0.5rem);
154
+ }
155
+
156
+ .hp-edit-panel .field__label {
157
+ display: block;
158
+ font: var(--font-caption, 0.75rem/1.4 sans-serif);
159
+ font-weight: 600;
160
+ margin-block-end: 0.25rem;
161
+ }
162
+
163
+ .hp-edit-panel .field__input {
164
+ width: 100%;
165
+ padding: var(--space-2xs, 0.375rem) var(--space-xs, 0.5rem);
166
+ border: 1px solid var(--color-outline-variant, #ccc);
167
+ border-radius: var(--border-radius-small, 0.25rem);
168
+ font: var(--font-body, 0.875rem/1.5 sans-serif);
169
+ background: var(--color-background, #fff);
170
+ color: var(--color-on-surface, inherit);
171
+ }
172
+
173
+ .hp-edit-panel textarea.field__input {
174
+ resize: vertical;
175
+ min-height: 6rem;
176
+ font-family: monospace;
177
+ font-size: 0.8125rem;
178
+ }
179
+
180
+ .hp-edit-panel__buttons {
181
+ display: flex;
182
+ gap: 0.5rem;
183
+ margin-block-start: var(--space-xs, 0.5rem);
95
184
  }
96
185
 
97
186
  .hp-section-picker {
@@ -128,6 +217,14 @@
128
217
  border-color: var(--color-primary, #0066cc);
129
218
  }
130
219
 
220
+ .hp-section-picker__heading {
221
+ grid-column: 1 / -1;
222
+ font-weight: bold;
223
+ margin-top: 0.5rem;
224
+ font: var(--font-caption, 0.8rem/1.4 sans-serif);
225
+ color: var(--color-on-offset, #555);
226
+ }
227
+
131
228
  .hp-empty {
132
229
  color: var(--color-on-offset, #666);
133
230
  font: var(--font-caption, 0.875rem/1.4 sans-serif);
@@ -155,7 +252,7 @@
155
252
  </div>
156
253
  {% endif %}
157
254
 
158
- <form method="post" action="{{ homepageEndpoint }}/save" class="hp-dashboard">
255
+ <form method="post" action="{{ homepageEndpoint }}/save" class="hp-dashboard" id="hp-form">
159
256
  {# Layout Selection #}
160
257
  <section class="hp-section">
161
258
  <h2>{{ __("homepage.layout.title") }}</h2>
@@ -163,7 +260,6 @@
163
260
  {% for layout in layouts %}
164
261
  <label class="hp-layout-option {% if config.layout == layout.id %}selected{% endif %}">
165
262
  <input type="radio" name="layout" value="{{ layout.id }}" {% if config.layout == layout.id %}checked{% endif %}>
166
- {# Simple layout previews #}
167
263
  {% if layout.id == "single-column" %}
168
264
  <svg viewBox="0 0 80 60"><rect x="10" y="5" width="60" height="50" fill="#ddd" stroke="#999"/></svg>
169
265
  {% elif layout.id == "two-column" %}
@@ -194,38 +290,20 @@
194
290
  </div>
195
291
  </section>
196
292
 
197
- {# Sections #}
293
+ {# Content Sections #}
198
294
  <section class="hp-section">
199
295
  <h2>{{ __("homepage.sections.title") }}</h2>
200
296
  <p class="hp-section__desc">{{ __("homepage.sections.description") }}</p>
201
297
 
202
- <ul class="hp-sections-list" id="sections-list">
203
- {% if config.sections and config.sections.length %}
204
- {% for section in config.sections %}
205
- <li class="hp-section-item" data-type="{{ section.type }}">
206
- <div class="hp-section-item__info">
207
- <span class="hp-section-item__name">
208
- {% for s in sections %}{% if s.id == section.type %}{{ s.label }}{% endif %}{% endfor %}
209
- </span>
210
- <span class="hp-section-item__type">{{ section.type }}</span>
211
- </div>
212
- <div class="hp-section-item__actions">
213
- <button type="button" class="button button--small button--secondary" onclick="removeSection(this)">Remove</button>
214
- </div>
215
- </li>
216
- {% endfor %}
217
- {% else %}
218
- <li class="hp-empty">{{ __("homepage.sections.empty") }}</li>
219
- {% endif %}
220
- </ul>
298
+ <ul class="hp-sections-list" id="sections-list"></ul>
221
299
 
222
300
  <div class="hp-section-picker">
223
301
  <h3>{{ __("homepage.sections.add") }}</h3>
224
302
  <div class="hp-section-picker__grid">
225
303
  {% for source, items in sectionsByPlugin %}
226
- <div style="grid-column: 1 / -1; font-weight: bold; margin-top: 0.5rem;">{{ source }}</div>
304
+ <div class="hp-section-picker__heading">{{ source }}</div>
227
305
  {% for section in items %}
228
- <div class="hp-section-picker__item" onclick="addSection('{{ section.id }}', '{{ section.label }}')">
306
+ <div class="hp-section-picker__item" data-add-section="{{ section.id }}">
229
307
  {{ section.label }}
230
308
  </div>
231
309
  {% endfor %}
@@ -233,7 +311,6 @@
233
311
  </div>
234
312
  </div>
235
313
 
236
- {# Hidden input for sections JSON #}
237
314
  <input type="hidden" name="sections" id="sections-json" value='{{ config.sections | dump | safe }}'>
238
315
  </section>
239
316
 
@@ -242,41 +319,47 @@
242
319
  <h2>{{ __("homepage.sidebar.title") }}</h2>
243
320
  <p class="hp-section__desc">{{ __("homepage.sidebar.description") }}</p>
244
321
 
245
- <ul class="hp-sections-list" id="widgets-list">
246
- {% if config.sidebar and config.sidebar.length %}
247
- {% for widget in config.sidebar %}
248
- <li class="hp-section-item" data-type="{{ widget.type }}">
249
- <div class="hp-section-item__info">
250
- <span class="hp-section-item__name">
251
- {% for w in widgets %}{% if w.id == widget.type %}{{ w.label }}{% endif %}{% endfor %}
252
- </span>
253
- <span class="hp-section-item__type">{{ widget.type }}</span>
254
- </div>
255
- <div class="hp-section-item__actions">
256
- <button type="button" class="button button--small button--secondary" onclick="removeWidget(this)">Remove</button>
257
- </div>
258
- </li>
259
- {% endfor %}
260
- {% else %}
261
- <li class="hp-empty">{{ __("homepage.sidebar.empty") }}</li>
262
- {% endif %}
263
- </ul>
322
+ <ul class="hp-sections-list" id="widgets-list"></ul>
264
323
 
265
324
  <div class="hp-section-picker">
266
325
  <h3>{{ __("homepage.sidebar.add") }}</h3>
267
326
  <div class="hp-section-picker__grid">
268
327
  {% for widget in widgets %}
269
- <div class="hp-section-picker__item" onclick="addWidget('{{ widget.id }}', '{{ widget.label }}')">
328
+ <div class="hp-section-picker__item" data-add-widget="{{ widget.id }}">
270
329
  {{ widget.label }}
271
330
  </div>
272
331
  {% endfor %}
273
332
  </div>
274
333
  </div>
275
334
 
276
- {# Hidden input for sidebar JSON #}
277
335
  <input type="hidden" name="sidebar" id="sidebar-json" value='{{ config.sidebar | dump | safe }}'>
278
336
  </section>
279
337
 
338
+ {# Footer #}
339
+ <section class="hp-section">
340
+ <h2>{{ __("homepage.footer.title") }}</h2>
341
+ <p class="hp-section__desc">{{ __("homepage.footer.description") }}</p>
342
+
343
+ <ul class="hp-sections-list" id="footer-list"></ul>
344
+
345
+ <div class="hp-section-picker">
346
+ <h3>{{ __("homepage.footer.add") }}</h3>
347
+ <div class="hp-section-picker__grid">
348
+ {# Footer accepts same section types + custom-html #}
349
+ {% for source, items in sectionsByPlugin %}
350
+ <div class="hp-section-picker__heading">{{ source }}</div>
351
+ {% for section in items %}
352
+ <div class="hp-section-picker__item" data-add-footer="{{ section.id }}">
353
+ {{ section.label }}
354
+ </div>
355
+ {% endfor %}
356
+ {% endfor %}
357
+ </div>
358
+ </div>
359
+
360
+ <input type="hidden" name="footer" id="footer-json" value='{{ config.footer | dump | safe }}'>
361
+ </section>
362
+
280
363
  {# Save Button #}
281
364
  <div class="button-group">
282
365
  <button type="submit" class="button button--primary">{{ __("homepage.save") }}</button>
@@ -284,110 +367,330 @@
284
367
  </form>
285
368
 
286
369
  <script>
287
- // Label lookup maps for rendering
288
- const sectionLabels = {
370
+ // Label lookup maps
371
+ var sectionLabels = {
289
372
  {% for section in sections %}'{{ section.id }}': '{{ section.label }}'{% if not loop.last %}, {% endif %}{% endfor %}
290
373
  };
291
- const widgetLabels = {
374
+ var widgetLabels = {
292
375
  {% for widget in widgets %}'{{ widget.id }}': '{{ widget.label }}'{% if not loop.last %}, {% endif %}{% endfor %}
293
376
  };
377
+ // Merge so all label maps cover all types
378
+ var allLabels = Object.assign({}, sectionLabels, widgetLabels);
379
+
380
+ // Unique key counter for tracking items
381
+ var nextKey = 0;
382
+
383
+ // Parse current data from hidden inputs and assign keys
384
+ var sections = JSON.parse(document.getElementById('sections-json').value || '[]');
385
+ var sidebar = JSON.parse(document.getElementById('sidebar-json').value || '[]');
386
+ var footer = JSON.parse(document.getElementById('footer-json').value || '[]');
387
+
388
+ sections.forEach(function(s) { s._key = nextKey++; });
389
+ sidebar.forEach(function(s) { s._key = nextKey++; });
390
+ footer.forEach(function(s) { s._key = nextKey++; });
391
+
392
+ // Strip _key before form submission
393
+ function stripKeys(items) {
394
+ return items.map(function(item) {
395
+ var copy = {};
396
+ for (var k in item) {
397
+ if (k !== '_key') copy[k] = item[k];
398
+ }
399
+ return copy;
400
+ });
401
+ }
294
402
 
295
- // Parse current sections and sidebar from hidden inputs
296
- let sections = JSON.parse(document.getElementById('sections-json').value || '[]');
297
- let sidebar = JSON.parse(document.getElementById('sidebar-json').value || '[]');
403
+ // Create drag handle SVG
404
+ function createDragHandle() {
405
+ var drag = document.createElement('span');
406
+ drag.className = 'hp-section-item__drag drag-handle';
407
+ drag.title = 'Drag to reorder';
408
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
409
+ svg.setAttribute('width', '16');
410
+ svg.setAttribute('height', '16');
411
+ svg.setAttribute('viewBox', '0 0 24 24');
412
+ svg.setAttribute('fill', 'currentColor');
413
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
414
+ path.setAttribute('d', 'M8 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm8-16a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z');
415
+ svg.appendChild(path);
416
+ drag.appendChild(svg);
417
+ return drag;
418
+ }
419
+
420
+ // Create a list item element for a section/widget/footer item
421
+ function createItemElement(item, labels, removeFn, editFn) {
422
+ var isCustom = (item.type === 'custom-html');
423
+ var container = document.createDocumentFragment();
298
424
 
299
- function createItemElement(type, labels, removeFn) {
300
- const li = document.createElement('li');
301
- li.className = 'hp-section-item';
302
- li.dataset.type = type;
425
+ var li = document.createElement('li');
426
+ li.className = 'hp-section-item' + (isCustom ? ' hp-section-item--has-edit' : '');
427
+ li.dataset.key = item._key;
428
+ li.dataset.type = item.type;
303
429
 
304
- const info = document.createElement('div');
430
+ li.appendChild(createDragHandle());
431
+
432
+ var info = document.createElement('div');
305
433
  info.className = 'hp-section-item__info';
306
434
 
307
- const name = document.createElement('span');
435
+ var name = document.createElement('span');
308
436
  name.className = 'hp-section-item__name';
309
- name.textContent = labels[type] || type;
437
+ name.textContent = labels[item.type] || item.type;
438
+ info.appendChild(name);
310
439
 
311
- const typeSpan = document.createElement('span');
440
+ var typeSpan = document.createElement('span');
312
441
  typeSpan.className = 'hp-section-item__type';
313
- typeSpan.textContent = type;
314
-
315
- info.appendChild(name);
442
+ typeSpan.textContent = ' ' + item.type;
316
443
  info.appendChild(typeSpan);
317
444
 
318
- const actions = document.createElement('div');
445
+ // Show content preview for custom-html
446
+ if (isCustom && item.config && item.config.content) {
447
+ var preview = document.createElement('div');
448
+ preview.className = 'hp-section-item__preview';
449
+ var previewText = item.config.title ? item.config.title + ': ' : '';
450
+ previewText += item.config.content.replace(/<[^>]*>/g, '').substring(0, 60);
451
+ preview.textContent = previewText;
452
+ info.appendChild(preview);
453
+ }
454
+
455
+ li.appendChild(info);
456
+
457
+ var actions = document.createElement('div');
319
458
  actions.className = 'hp-section-item__actions';
320
459
 
321
- const btn = document.createElement('button');
322
- btn.type = 'button';
323
- btn.className = 'button button--small button--secondary';
324
- btn.textContent = 'Remove';
325
- btn.addEventListener('click', function() { removeFn(this); });
460
+ if (isCustom) {
461
+ var editBtn = document.createElement('button');
462
+ editBtn.type = 'button';
463
+ editBtn.className = 'button button--small';
464
+ editBtn.textContent = 'Edit';
465
+ editBtn.addEventListener('click', function() {
466
+ var panel = li.nextElementSibling;
467
+ if (panel && panel.tagName === 'DETAILS') {
468
+ panel.open = !panel.open;
469
+ }
470
+ });
471
+ actions.appendChild(editBtn);
472
+ }
473
+
474
+ var removeBtn = document.createElement('button');
475
+ removeBtn.type = 'button';
476
+ removeBtn.className = 'button button--small button--secondary';
477
+ removeBtn.textContent = 'Remove';
478
+ removeBtn.addEventListener('click', function() { removeFn(item._key); });
479
+ actions.appendChild(removeBtn);
326
480
 
327
- actions.appendChild(btn);
328
- li.appendChild(info);
329
481
  li.appendChild(actions);
482
+ container.appendChild(li);
483
+
484
+ // Add edit panel for custom-html
485
+ if (isCustom) {
486
+ var details = document.createElement('details');
487
+ details.className = 'hp-edit-panel';
488
+
489
+ var summary = document.createElement('summary');
490
+ details.appendChild(summary);
491
+
492
+ var titleField = document.createElement('div');
493
+ titleField.className = 'field';
494
+ var titleLabel = document.createElement('label');
495
+ titleLabel.className = 'field__label';
496
+ titleLabel.textContent = '{{ __("homepage.customContent.titleLabel") }}';
497
+ var titleInput = document.createElement('input');
498
+ titleInput.className = 'field__input';
499
+ titleInput.type = 'text';
500
+ titleInput.placeholder = 'e.g. Webring, Links';
501
+ titleInput.value = (item.config && item.config.title) || '';
502
+ titleField.appendChild(titleLabel);
503
+ titleField.appendChild(titleInput);
504
+ details.appendChild(titleField);
505
+
506
+ var contentField = document.createElement('div');
507
+ contentField.className = 'field';
508
+ var contentLabel = document.createElement('label');
509
+ contentLabel.className = 'field__label';
510
+ contentLabel.textContent = '{{ __("homepage.customContent.contentLabel") }}';
511
+ var contentInput = document.createElement('textarea');
512
+ contentInput.className = 'field__input';
513
+ contentInput.rows = 6;
514
+ contentInput.placeholder = '<p>Your HTML here...</p>';
515
+ contentInput.value = (item.config && item.config.content) || '';
516
+ contentField.appendChild(contentLabel);
517
+ contentField.appendChild(contentInput);
518
+ details.appendChild(contentField);
519
+
520
+ var buttons = document.createElement('div');
521
+ buttons.className = 'hp-edit-panel__buttons';
522
+
523
+ var saveBtn = document.createElement('button');
524
+ saveBtn.type = 'button';
525
+ saveBtn.className = 'button button--primary button--small';
526
+ saveBtn.textContent = '{{ __("homepage.customContent.save") }}';
527
+ saveBtn.addEventListener('click', function() {
528
+ editFn(item._key, {
529
+ title: titleInput.value,
530
+ content: contentInput.value
531
+ });
532
+ details.open = false;
533
+ });
534
+
535
+ var cancelBtn = document.createElement('button');
536
+ cancelBtn.type = 'button';
537
+ cancelBtn.className = 'button button--small button--secondary';
538
+ cancelBtn.textContent = '{{ __("homepage.customContent.cancel") }}';
539
+ cancelBtn.addEventListener('click', function() { details.open = false; });
540
+
541
+ buttons.appendChild(saveBtn);
542
+ buttons.appendChild(cancelBtn);
543
+ details.appendChild(buttons);
544
+
545
+ container.appendChild(details);
546
+ }
330
547
 
331
- return li;
548
+ return container;
332
549
  }
333
550
 
334
- function renderList(listEl, items, labels, removeFn, emptyText) {
551
+ // Render a list of items into a UL element
552
+ function renderList(listEl, items, labels, removeFn, editFn, emptyText) {
335
553
  listEl.textContent = '';
336
554
  if (items.length === 0) {
337
- const li = document.createElement('li');
555
+ var li = document.createElement('li');
338
556
  li.className = 'hp-empty';
339
557
  li.textContent = emptyText;
340
558
  listEl.appendChild(li);
341
559
  } else {
342
560
  items.forEach(function(item) {
343
- listEl.appendChild(createItemElement(item.type, labels, removeFn));
561
+ listEl.appendChild(createItemElement(item, labels, removeFn, editFn));
344
562
  });
345
563
  }
346
564
  }
347
565
 
348
- function addSection(id, label) {
349
- sections.push({ type: id, config: {} });
350
- updateSectionsList();
566
+ // --- Sections ---
567
+ function addSection(id) {
568
+ var item = { type: id, config: {}, _key: nextKey++ };
569
+ sections.push(item);
570
+ updateSections();
571
+ // Auto-open edit panel for custom-html
572
+ if (id === 'custom-html') {
573
+ var list = document.getElementById('sections-list');
574
+ var lastPanel = list.querySelector('details.hp-edit-panel:last-of-type');
575
+ if (lastPanel) lastPanel.open = true;
576
+ }
577
+ }
578
+
579
+ function removeSection(key) {
580
+ sections = sections.filter(function(s) { return s._key !== key; });
581
+ updateSections();
351
582
  }
352
583
 
353
- function removeSection(button) {
354
- const item = button.closest('.hp-section-item');
355
- const type = item.dataset.type;
356
- sections = sections.filter(function(s) { return s.type !== type; });
357
- updateSectionsList();
584
+ function editSection(key, config) {
585
+ var item = sections.find(function(s) { return s._key === key; });
586
+ if (item) item.config = config;
587
+ updateSections();
358
588
  }
359
589
 
360
- function updateSectionsList() {
361
- document.getElementById('sections-json').value = JSON.stringify(sections);
590
+ function updateSections() {
591
+ document.getElementById('sections-json').value = JSON.stringify(stripKeys(sections));
362
592
  renderList(
363
593
  document.getElementById('sections-list'),
364
- sections, sectionLabels, removeSection,
594
+ sections, allLabels, removeSection, editSection,
365
595
  '{{ __("homepage.sections.empty") }}'
366
596
  );
597
+ initSortable();
367
598
  }
368
599
 
369
- function addWidget(id, label) {
370
- sidebar.push({ type: id, config: {} });
371
- updateWidgetsList();
600
+ // --- Sidebar ---
601
+ function addWidget(id) {
602
+ var item = { type: id, config: {}, _key: nextKey++ };
603
+ sidebar.push(item);
604
+ updateWidgets();
605
+ if (id === 'custom-html') {
606
+ var list = document.getElementById('widgets-list');
607
+ var lastPanel = list.querySelector('details.hp-edit-panel:last-of-type');
608
+ if (lastPanel) lastPanel.open = true;
609
+ }
372
610
  }
373
611
 
374
- function removeWidget(button) {
375
- const item = button.closest('.hp-section-item');
376
- const type = item.dataset.type;
377
- sidebar = sidebar.filter(function(w) { return w.type !== type; });
378
- updateWidgetsList();
612
+ function removeWidget(key) {
613
+ sidebar = sidebar.filter(function(w) { return w._key !== key; });
614
+ updateWidgets();
379
615
  }
380
616
 
381
- function updateWidgetsList() {
382
- document.getElementById('sidebar-json').value = JSON.stringify(sidebar);
617
+ function editWidget(key, config) {
618
+ var item = sidebar.find(function(s) { return s._key === key; });
619
+ if (item) item.config = config;
620
+ updateWidgets();
621
+ }
622
+
623
+ function updateWidgets() {
624
+ document.getElementById('sidebar-json').value = JSON.stringify(stripKeys(sidebar));
383
625
  renderList(
384
626
  document.getElementById('widgets-list'),
385
- sidebar, widgetLabels, removeWidget,
627
+ sidebar, allLabels, removeWidget, editWidget,
386
628
  '{{ __("homepage.sidebar.empty") }}'
387
629
  );
630
+ initSortable();
631
+ }
632
+
633
+ // --- Footer ---
634
+ function addFooter(id) {
635
+ var item = { type: id, config: {}, _key: nextKey++ };
636
+ footer.push(item);
637
+ updateFooter();
638
+ if (id === 'custom-html') {
639
+ var list = document.getElementById('footer-list');
640
+ var lastPanel = list.querySelector('details.hp-edit-panel:last-of-type');
641
+ if (lastPanel) lastPanel.open = true;
642
+ }
643
+ }
644
+
645
+ function removeFooter(key) {
646
+ footer = footer.filter(function(f) { return f._key !== key; });
647
+ updateFooter();
648
+ }
649
+
650
+ function editFooter(key, config) {
651
+ var item = footer.find(function(s) { return s._key === key; });
652
+ if (item) item.config = config;
653
+ updateFooter();
654
+ }
655
+
656
+ function updateFooter() {
657
+ document.getElementById('footer-json').value = JSON.stringify(stripKeys(footer));
658
+ renderList(
659
+ document.getElementById('footer-list'),
660
+ footer, allLabels, removeFooter, editFooter,
661
+ '{{ __("homepage.footer.empty") }}'
662
+ );
663
+ initSortable();
664
+ }
665
+
666
+ // --- Sync after drag (preserves config by reordering JS array) ---
667
+ function syncFromDom(listEl, dataArray) {
668
+ var items = listEl.querySelectorAll('.hp-section-item');
669
+ var ordered = [];
670
+ items.forEach(function(el) {
671
+ var key = parseInt(el.dataset.key, 10);
672
+ var item = dataArray.find(function(s) { return s._key === key; });
673
+ if (item) ordered.push(item);
674
+ });
675
+ return ordered;
676
+ }
677
+
678
+ function syncSectionsFromDom() {
679
+ sections = syncFromDom(document.getElementById('sections-list'), sections);
680
+ document.getElementById('sections-json').value = JSON.stringify(stripKeys(sections));
388
681
  }
389
682
 
390
- // Layout selection
683
+ function syncSidebarFromDom() {
684
+ sidebar = syncFromDom(document.getElementById('widgets-list'), sidebar);
685
+ document.getElementById('sidebar-json').value = JSON.stringify(stripKeys(sidebar));
686
+ }
687
+
688
+ function syncFooterFromDom() {
689
+ footer = syncFromDom(document.getElementById('footer-list'), footer);
690
+ document.getElementById('footer-json').value = JSON.stringify(stripKeys(footer));
691
+ }
692
+
693
+ // --- Layout selection ---
391
694
  document.querySelectorAll('.hp-layout-option').forEach(function(option) {
392
695
  option.addEventListener('click', function() {
393
696
  document.querySelectorAll('.hp-layout-option').forEach(function(o) { o.classList.remove('selected'); });
@@ -395,12 +698,23 @@
395
698
  });
396
699
  });
397
700
 
398
- // Hero checkboxes - convert to JSON on submit
399
- document.querySelector('form').addEventListener('submit', function(e) {
400
- const heroEnabled = document.querySelector('input[name="hero[enabled]"]').checked;
401
- const heroShowSocial = document.querySelector('input[name="hero[showSocial]"]').checked;
701
+ // --- Section picker event delegation ---
702
+ document.querySelectorAll('[data-add-section]').forEach(function(el) {
703
+ el.addEventListener('click', function() { addSection(el.dataset.addSection); });
704
+ });
705
+ document.querySelectorAll('[data-add-widget]').forEach(function(el) {
706
+ el.addEventListener('click', function() { addWidget(el.dataset.addWidget); });
707
+ });
708
+ document.querySelectorAll('[data-add-footer]').forEach(function(el) {
709
+ el.addEventListener('click', function() { addFooter(el.dataset.addFooter); });
710
+ });
711
+
712
+ // --- Hero checkboxes → JSON on submit ---
713
+ document.getElementById('hp-form').addEventListener('submit', function(e) {
714
+ var heroEnabled = document.querySelector('input[name="hero[enabled]"]').checked;
715
+ var heroShowSocial = document.querySelector('input[name="hero[showSocial]"]').checked;
402
716
 
403
- const heroInput = document.createElement('input');
717
+ var heroInput = document.createElement('input');
404
718
  heroInput.type = 'hidden';
405
719
  heroInput.name = 'hero';
406
720
  heroInput.value = JSON.stringify({ enabled: heroEnabled, showSocial: heroShowSocial });
@@ -408,5 +722,44 @@
408
722
 
409
723
  document.querySelectorAll('input[name^="hero["]').forEach(function(i) { i.name = ''; });
410
724
  });
725
+
726
+ // --- SortableJS ---
727
+ function initSortable() {
728
+ if (typeof Sortable === 'undefined') return;
729
+
730
+ var lists = [
731
+ { el: 'sections-list', sync: syncSectionsFromDom },
732
+ { el: 'widgets-list', sync: syncSidebarFromDom },
733
+ { el: 'footer-list', sync: syncFooterFromDom }
734
+ ];
735
+
736
+ lists.forEach(function(cfg) {
737
+ var el = document.getElementById(cfg.el);
738
+ if (!el) return;
739
+ // Destroy existing Sortable if any
740
+ if (el._sortable) el._sortable.destroy();
741
+ el._sortable = new Sortable(el, {
742
+ handle: '.drag-handle',
743
+ animation: 150,
744
+ ghostClass: 'sortable-ghost',
745
+ chosenClass: 'sortable-chosen',
746
+ draggable: '.hp-section-item',
747
+ onEnd: cfg.sync
748
+ });
749
+ });
750
+ }
751
+
752
+ // Load SortableJS from CDN
753
+ var sortableScript = document.createElement('script');
754
+ sortableScript.src = 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js';
755
+ sortableScript.onload = function() {
756
+ initSortable();
757
+ };
758
+ document.head.appendChild(sortableScript);
759
+
760
+ // --- Initial render ---
761
+ updateSections();
762
+ updateWidgets();
763
+ updateFooter();
411
764
  </script>
412
765
  {% endblock %}