@rmdes/indiekit-endpoint-homepage 1.0.6 → 1.0.8
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 +81 -0
- package/lib/controllers/dashboard.js +69 -0
- package/locales/en.json +8 -0
- package/package.json +1 -1
- package/views/homepage-dashboard.njk +108 -0
package/index.js
CHANGED
|
@@ -48,6 +48,81 @@ export default class HomepageEndpoint {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Layout presets — quick-start configurations for common homepage styles
|
|
53
|
+
*/
|
|
54
|
+
get layoutPresets() {
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: "blog",
|
|
58
|
+
label: "Blog",
|
|
59
|
+
description: "Recent posts front and center",
|
|
60
|
+
icon: "newspaper",
|
|
61
|
+
layout: "two-column",
|
|
62
|
+
hero: { enabled: true, showSocial: true },
|
|
63
|
+
sections: [
|
|
64
|
+
{ type: "hero", config: {} },
|
|
65
|
+
{ type: "recent-posts", config: { maxItems: 15 } },
|
|
66
|
+
],
|
|
67
|
+
sidebar: [
|
|
68
|
+
{ type: "search", config: {} },
|
|
69
|
+
{ type: "author-card", config: {} },
|
|
70
|
+
{ type: "social-activity", config: {} },
|
|
71
|
+
{ type: "recent-posts", config: { maxItems: 5 } },
|
|
72
|
+
],
|
|
73
|
+
footer: [],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "cv",
|
|
77
|
+
label: "CV / Portfolio",
|
|
78
|
+
description: "Professional profile with experience and projects",
|
|
79
|
+
icon: "briefcase",
|
|
80
|
+
layout: "full-width-hero",
|
|
81
|
+
hero: { enabled: true, showSocial: true },
|
|
82
|
+
sections: [
|
|
83
|
+
{ type: "hero", config: {} },
|
|
84
|
+
{ type: "cv-experience", config: {} },
|
|
85
|
+
{ type: "cv-skills", config: {} },
|
|
86
|
+
{ type: "cv-projects", config: {} },
|
|
87
|
+
{ type: "cv-education", config: {} },
|
|
88
|
+
{ type: "cv-interests", config: {} },
|
|
89
|
+
],
|
|
90
|
+
sidebar: [
|
|
91
|
+
{ type: "search", config: {} },
|
|
92
|
+
{ type: "social-activity", config: {} },
|
|
93
|
+
{ type: "github-repos", config: {} },
|
|
94
|
+
{ type: "blogroll", config: {} },
|
|
95
|
+
{ type: "recent-posts", config: {} },
|
|
96
|
+
{ type: "funkwhale", config: {} },
|
|
97
|
+
{ type: "author-card", config: {} },
|
|
98
|
+
],
|
|
99
|
+
footer: [],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "hybrid",
|
|
103
|
+
label: "Hybrid",
|
|
104
|
+
description: "Blog posts with CV highlights",
|
|
105
|
+
icon: "layout",
|
|
106
|
+
layout: "two-column",
|
|
107
|
+
hero: { enabled: true, showSocial: true },
|
|
108
|
+
sections: [
|
|
109
|
+
{ type: "hero", config: {} },
|
|
110
|
+
{ type: "cv-experience", config: { maxItems: 3 } },
|
|
111
|
+
{ type: "recent-posts", config: { maxItems: 10 } },
|
|
112
|
+
{ type: "cv-projects", config: { maxItems: 3 } },
|
|
113
|
+
],
|
|
114
|
+
sidebar: [
|
|
115
|
+
{ type: "search", config: {} },
|
|
116
|
+
{ type: "author-card", config: {} },
|
|
117
|
+
{ type: "social-activity", config: {} },
|
|
118
|
+
{ type: "github-repos", config: {} },
|
|
119
|
+
{ type: "blogroll", config: {} },
|
|
120
|
+
],
|
|
121
|
+
footer: [],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
51
126
|
/**
|
|
52
127
|
* Built-in section types (always available)
|
|
53
128
|
*/
|
|
@@ -197,6 +272,9 @@ export default class HomepageEndpoint {
|
|
|
197
272
|
// Save configuration
|
|
198
273
|
protectedRouter.post("/save", dashboardController.save);
|
|
199
274
|
|
|
275
|
+
// Apply a layout preset
|
|
276
|
+
protectedRouter.post("/apply-preset", dashboardController.applyPreset);
|
|
277
|
+
|
|
200
278
|
// Get available sections (for section picker)
|
|
201
279
|
protectedRouter.get("/api/sections", apiController.listSections);
|
|
202
280
|
|
|
@@ -229,6 +307,9 @@ export default class HomepageEndpoint {
|
|
|
229
307
|
Indiekit.config.application.homepageConfig = this.options;
|
|
230
308
|
Indiekit.config.application.homepageEndpoint = this.mountPath;
|
|
231
309
|
|
|
310
|
+
// Store layout presets for dashboard access
|
|
311
|
+
Indiekit.config.application.layoutPresets = this.layoutPresets;
|
|
312
|
+
|
|
232
313
|
// Store content directory path
|
|
233
314
|
Indiekit.config.application.contentDir =
|
|
234
315
|
this.options.contentDir ||
|
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
import { getConfig, saveConfig, getDefaultConfig } from "../storage/config.js";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Detect which preset matches the current config (if any)
|
|
10
|
+
*/
|
|
11
|
+
function detectActivePreset(config, presets) {
|
|
12
|
+
for (const preset of presets) {
|
|
13
|
+
if (config.layout !== preset.layout) continue;
|
|
14
|
+
|
|
15
|
+
const configTypes = (config.sections || []).map((s) => s.type).join(",");
|
|
16
|
+
const presetTypes = preset.sections.map((s) => s.type).join(",");
|
|
17
|
+
if (configTypes !== presetTypes) continue;
|
|
18
|
+
|
|
19
|
+
const configWidgets = (config.sidebar || []).map((w) => w.type).join(",");
|
|
20
|
+
const presetWidgets = preset.sidebar.map((w) => w.type).join(",");
|
|
21
|
+
if (configWidgets !== presetWidgets) continue;
|
|
22
|
+
|
|
23
|
+
return preset.id;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
export const dashboardController = {
|
|
9
29
|
/**
|
|
10
30
|
* GET / - Main dashboard
|
|
@@ -22,6 +42,7 @@ export const dashboardController = {
|
|
|
22
42
|
// Get discovered sections and widgets
|
|
23
43
|
const sections = application.discoveredSections || [];
|
|
24
44
|
const widgets = application.discoveredWidgets || [];
|
|
45
|
+
const presets = application.layoutPresets || [];
|
|
25
46
|
|
|
26
47
|
// Group sections by source plugin
|
|
27
48
|
const sectionsByPlugin = {};
|
|
@@ -33,11 +54,16 @@ export const dashboardController = {
|
|
|
33
54
|
sectionsByPlugin[source].push(section);
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
// Detect which preset is active (if any)
|
|
58
|
+
const activePresetId = detectActivePreset(config, presets);
|
|
59
|
+
|
|
36
60
|
response.render("homepage-dashboard", {
|
|
37
61
|
title: "Homepage Builder",
|
|
38
62
|
config,
|
|
39
63
|
sections,
|
|
40
64
|
widgets,
|
|
65
|
+
presets,
|
|
66
|
+
activePresetId,
|
|
41
67
|
sectionsByPlugin,
|
|
42
68
|
homepageEndpoint: application.homepageEndpoint,
|
|
43
69
|
layouts: [
|
|
@@ -97,4 +123,47 @@ export const dashboardController = {
|
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
125
|
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* POST /apply-preset - Apply a layout preset
|
|
129
|
+
*/
|
|
130
|
+
async applyPreset(request, response) {
|
|
131
|
+
const { application } = request.app.locals;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const { presetId } = request.body;
|
|
135
|
+
const presets = application.layoutPresets || [];
|
|
136
|
+
const preset = presets.find((p) => p.id === presetId);
|
|
137
|
+
|
|
138
|
+
if (!preset) {
|
|
139
|
+
return response.status(400).redirect(
|
|
140
|
+
application.homepageEndpoint + "?error=unknown-preset"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get current config to preserve footer (webrings etc.)
|
|
145
|
+
const currentConfig = await getConfig(application);
|
|
146
|
+
const existingFooter = currentConfig?.footer || [];
|
|
147
|
+
|
|
148
|
+
const config = {
|
|
149
|
+
layout: preset.layout,
|
|
150
|
+
hero: { ...preset.hero },
|
|
151
|
+
sections: preset.sections.map((s) => ({ ...s })),
|
|
152
|
+
sidebar: preset.sidebar.map((w) => ({ ...w })),
|
|
153
|
+
footer: existingFooter,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await saveConfig(application, config);
|
|
157
|
+
|
|
158
|
+
console.log(`[Homepage] Applied preset: ${preset.label}`);
|
|
159
|
+
response.redirect(application.homepageEndpoint + "?saved=1");
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("[Homepage] Apply preset error:", error);
|
|
162
|
+
response.status(500).render("error", {
|
|
163
|
+
title: "Error",
|
|
164
|
+
message: "Failed to apply preset",
|
|
165
|
+
error: error.message,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
},
|
|
100
169
|
};
|
package/locales/en.json
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
"homepage": {
|
|
3
3
|
"title": "Homepage Builder",
|
|
4
4
|
"description": "Configure your homepage layout, sections, sidebar widgets, and footer.",
|
|
5
|
+
"presets": {
|
|
6
|
+
"title": "Quick Start",
|
|
7
|
+
"description": "Choose a preset to quickly configure your homepage. You can customize it further below.",
|
|
8
|
+
"apply": "Apply",
|
|
9
|
+
"active": "Active",
|
|
10
|
+
"custom": "Custom",
|
|
11
|
+
"customDescription": "Your own customized layout"
|
|
12
|
+
},
|
|
5
13
|
"saved": "Configuration saved successfully. Refresh your homepage to see changes.",
|
|
6
14
|
"save": "Save Configuration",
|
|
7
15
|
"layout": {
|
package/package.json
CHANGED
|
@@ -239,6 +239,61 @@
|
|
|
239
239
|
padding: var(--space-s, 0.75rem);
|
|
240
240
|
margin-block-end: var(--space-m, 1rem);
|
|
241
241
|
}
|
|
242
|
+
|
|
243
|
+
.hp-preset-grid {
|
|
244
|
+
display: grid;
|
|
245
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
246
|
+
gap: var(--space-s, 0.75rem);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.hp-preset-card {
|
|
250
|
+
background: var(--color-background, #fff);
|
|
251
|
+
border: 2px solid var(--color-outline-variant, #ddd);
|
|
252
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
253
|
+
padding: var(--space-m, 1.25rem);
|
|
254
|
+
text-align: center;
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: var(--space-2xs, 0.375rem);
|
|
259
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.hp-preset-card:hover {
|
|
263
|
+
border-color: var(--color-primary, #0066cc);
|
|
264
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.hp-preset-card--active {
|
|
268
|
+
border-color: var(--color-primary, #0066cc);
|
|
269
|
+
background: var(--color-primary-container, #e6f0ff);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.hp-preset-card__icon {
|
|
273
|
+
color: var(--color-primary, #0066cc);
|
|
274
|
+
margin-block-end: var(--space-2xs, 0.25rem);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.hp-preset-card__label {
|
|
278
|
+
font-weight: 700;
|
|
279
|
+
font-size: 1rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.hp-preset-card__desc {
|
|
283
|
+
color: var(--color-on-offset, #666);
|
|
284
|
+
font: var(--font-caption, 0.8rem/1.4 sans-serif);
|
|
285
|
+
margin-block-end: var(--space-xs, 0.5rem);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.hp-preset-card__badge {
|
|
289
|
+
display: inline-block;
|
|
290
|
+
padding: 0.25rem 0.75rem;
|
|
291
|
+
font: var(--font-caption, 0.75rem/1.4 sans-serif);
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
background: var(--color-primary, #0066cc);
|
|
294
|
+
color: #fff;
|
|
295
|
+
border-radius: 1rem;
|
|
296
|
+
}
|
|
242
297
|
</style>
|
|
243
298
|
|
|
244
299
|
<header class="page-header">
|
|
@@ -252,6 +307,59 @@
|
|
|
252
307
|
</div>
|
|
253
308
|
{% endif %}
|
|
254
309
|
|
|
310
|
+
{# Quick Start Presets #}
|
|
311
|
+
{% if presets and presets.length %}
|
|
312
|
+
<section class="hp-section" style="margin-block-end: var(--space-xl, 2rem);">
|
|
313
|
+
<h2>{{ __("homepage.presets.title") }}</h2>
|
|
314
|
+
<p class="hp-section__desc">{{ __("homepage.presets.description") }}</p>
|
|
315
|
+
|
|
316
|
+
<div class="hp-preset-grid">
|
|
317
|
+
{% for preset in presets %}
|
|
318
|
+
<form method="post" action="{{ homepageEndpoint }}/apply-preset" class="hp-preset-card {% if activePresetId == preset.id %}hp-preset-card--active{% endif %}">
|
|
319
|
+
<input type="hidden" name="presetId" value="{{ preset.id }}">
|
|
320
|
+
<div class="hp-preset-card__icon">
|
|
321
|
+
{% if preset.icon == "newspaper" %}
|
|
322
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
323
|
+
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
|
|
324
|
+
<path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6z"/>
|
|
325
|
+
</svg>
|
|
326
|
+
{% elif preset.icon == "briefcase" %}
|
|
327
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
328
|
+
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
|
329
|
+
</svg>
|
|
330
|
+
{% elif preset.icon == "layout" %}
|
|
331
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
332
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>
|
|
333
|
+
</svg>
|
|
334
|
+
{% endif %}
|
|
335
|
+
</div>
|
|
336
|
+
<div class="hp-preset-card__label">{{ preset.label }}</div>
|
|
337
|
+
<div class="hp-preset-card__desc">{{ preset.description }}</div>
|
|
338
|
+
{% if activePresetId == preset.id %}
|
|
339
|
+
<div class="hp-preset-card__badge">{{ __("homepage.presets.active") }}</div>
|
|
340
|
+
{% else %}
|
|
341
|
+
<button type="submit" class="button button--small">{{ __("homepage.presets.apply") }}</button>
|
|
342
|
+
{% endif %}
|
|
343
|
+
</form>
|
|
344
|
+
{% endfor %}
|
|
345
|
+
{# Custom card — shown as active when config doesn't match any preset #}
|
|
346
|
+
<div class="hp-preset-card {% if not activePresetId %}hp-preset-card--active{% endif %}">
|
|
347
|
+
<div class="hp-preset-card__icon">
|
|
348
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
349
|
+
<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/>
|
|
350
|
+
<line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>
|
|
351
|
+
</svg>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="hp-preset-card__label">{{ __("homepage.presets.custom") }}</div>
|
|
354
|
+
<div class="hp-preset-card__desc">{{ __("homepage.presets.customDescription") }}</div>
|
|
355
|
+
{% if not activePresetId %}
|
|
356
|
+
<div class="hp-preset-card__badge">{{ __("homepage.presets.active") }}</div>
|
|
357
|
+
{% endif %}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</section>
|
|
361
|
+
{% endif %}
|
|
362
|
+
|
|
255
363
|
<form method="post" action="{{ homepageEndpoint }}/save" class="hp-dashboard" id="hp-form">
|
|
256
364
|
{# Layout Selection #}
|
|
257
365
|
<section class="hp-section">
|