@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-homepage",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Homepage builder endpoint for Indiekit. Configure layout, sections, and sidebar widgets from the admin UI.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -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">