@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 +18 -2
- package/lib/controllers/api.js +1 -0
- package/lib/controllers/dashboard.js +2 -1
- package/lib/storage/config.js +3 -0
- package/locales/en.json +15 -2
- package/package.json +1 -1
- package/views/homepage-dashboard.njk +456 -103
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
|
-
//
|
|
231
|
-
|
|
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
|
/**
|
package/lib/controllers/api.js
CHANGED
|
@@ -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
|
|
package/lib/storage/config.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
@@ -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
|
|
304
|
+
<div class="hp-section-picker__heading">{{ source }}</div>
|
|
227
305
|
{% for section in items %}
|
|
228
|
-
<div class="hp-section-picker__item"
|
|
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"
|
|
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
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
li.
|
|
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
|
-
|
|
430
|
+
li.appendChild(createDragHandle());
|
|
431
|
+
|
|
432
|
+
var info = document.createElement('div');
|
|
305
433
|
info.className = 'hp-section-item__info';
|
|
306
434
|
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
548
|
+
return container;
|
|
332
549
|
}
|
|
333
550
|
|
|
334
|
-
|
|
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
|
-
|
|
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
|
|
561
|
+
listEl.appendChild(createItemElement(item, labels, removeFn, editFn));
|
|
344
562
|
});
|
|
345
563
|
}
|
|
346
564
|
}
|
|
347
565
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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,
|
|
594
|
+
sections, allLabels, removeSection, editSection,
|
|
365
595
|
'{{ __("homepage.sections.empty") }}'
|
|
366
596
|
);
|
|
597
|
+
initSortable();
|
|
367
598
|
}
|
|
368
599
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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(
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
382
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
399
|
-
document.
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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 %}
|