@rmdes/indiekit-endpoint-cv 1.0.3 → 1.0.5
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 +19 -0
- package/lib/controllers/dashboard.js +27 -6
- package/lib/storage/cv.js +50 -0
- package/package.json +1 -1
- package/views/cv-dashboard.njk +92 -7
package/index.js
CHANGED
|
@@ -158,6 +158,9 @@ export default class CvEndpoint {
|
|
|
158
158
|
protectedRouter.post("/skills/add", dashboardController.addSkillCategory);
|
|
159
159
|
protectedRouter.post("/skills/:category/delete", dashboardController.deleteSkillCategory);
|
|
160
160
|
|
|
161
|
+
// Generic move (reorder) for any array section
|
|
162
|
+
protectedRouter.post("/:section/:index/:direction(up|down)", dashboardController.move);
|
|
163
|
+
|
|
161
164
|
return protectedRouter;
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -183,5 +186,21 @@ export default class CvEndpoint {
|
|
|
183
186
|
|
|
184
187
|
// Store database getter for controller access
|
|
185
188
|
Indiekit.config.application.getCvDb = () => Indiekit.database;
|
|
189
|
+
|
|
190
|
+
// Write CV data file for Eleventy on startup
|
|
191
|
+
// Deferred so database connection is established first
|
|
192
|
+
const app = Indiekit.config.application;
|
|
193
|
+
setTimeout(async () => {
|
|
194
|
+
try {
|
|
195
|
+
const { getCvData, getDefaultCvData, writeCvFile } = await import(
|
|
196
|
+
"./lib/storage/cv.js"
|
|
197
|
+
);
|
|
198
|
+
const data = (await getCvData(app)) || getDefaultCvData();
|
|
199
|
+
writeCvFile(app, data);
|
|
200
|
+
console.log("[CV] Initial data file written for Eleventy");
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.log("[CV] Deferred file write:", error.message);
|
|
203
|
+
}
|
|
204
|
+
}, 2000);
|
|
186
205
|
}
|
|
187
206
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
addToSection,
|
|
11
11
|
updateInSection,
|
|
12
12
|
removeFromSection,
|
|
13
|
+
moveInSection,
|
|
13
14
|
addSkillCategory,
|
|
14
15
|
removeSkillCategory,
|
|
15
16
|
} from "../storage/cv.js";
|
|
@@ -141,13 +142,15 @@ export const dashboardController = {
|
|
|
141
142
|
async addProject(request, response) {
|
|
142
143
|
const { application } = request.app.locals;
|
|
143
144
|
try {
|
|
144
|
-
const { name, url, description, tags, technologies, status } = request.body;
|
|
145
|
+
const { name, url, description, tags, technologies, status, startDate, endDate } = request.body;
|
|
145
146
|
await addToSection(application, "projects", {
|
|
146
147
|
name: name || "",
|
|
147
148
|
url: url || "",
|
|
148
149
|
description: description || "",
|
|
149
150
|
technologies: parseCommaList(tags || technologies),
|
|
150
151
|
status: status || "active",
|
|
152
|
+
startDate: startDate || "",
|
|
153
|
+
endDate: endDate || null,
|
|
151
154
|
});
|
|
152
155
|
response.redirect(application.cvEndpoint + "?saved=1#projects");
|
|
153
156
|
} catch (error) {
|
|
@@ -160,13 +163,15 @@ export const dashboardController = {
|
|
|
160
163
|
const { application } = request.app.locals;
|
|
161
164
|
try {
|
|
162
165
|
const index = Number.parseInt(request.params.index, 10);
|
|
163
|
-
const { name, url, description, tags, status } = request.body;
|
|
166
|
+
const { name, url, description, tags, status, startDate, endDate } = request.body;
|
|
164
167
|
await updateInSection(application, "projects", index, {
|
|
165
168
|
name: name || "",
|
|
166
169
|
url: url || "",
|
|
167
170
|
description: description || "",
|
|
168
171
|
technologies: parseCommaList(tags),
|
|
169
172
|
status: status || "active",
|
|
173
|
+
startDate: startDate || "",
|
|
174
|
+
endDate: endDate || null,
|
|
170
175
|
});
|
|
171
176
|
response.redirect(application.cvEndpoint + "?saved=1#projects");
|
|
172
177
|
} catch (error) {
|
|
@@ -191,12 +196,13 @@ export const dashboardController = {
|
|
|
191
196
|
async addEducation(request, response) {
|
|
192
197
|
const { application } = request.app.locals;
|
|
193
198
|
try {
|
|
194
|
-
const { degree, institution, location,
|
|
199
|
+
const { degree, institution, location, startDate, endDate, description } = request.body;
|
|
195
200
|
await addToSection(application, "education", {
|
|
196
201
|
degree: degree || "",
|
|
197
202
|
institution: institution || "",
|
|
198
203
|
location: location || "",
|
|
199
|
-
|
|
204
|
+
startDate: startDate || "",
|
|
205
|
+
endDate: endDate || null,
|
|
200
206
|
description: description || "",
|
|
201
207
|
});
|
|
202
208
|
response.redirect(application.cvEndpoint + "?saved=1#education");
|
|
@@ -210,12 +216,13 @@ export const dashboardController = {
|
|
|
210
216
|
const { application } = request.app.locals;
|
|
211
217
|
try {
|
|
212
218
|
const index = Number.parseInt(request.params.index, 10);
|
|
213
|
-
const { degree, institution, location,
|
|
219
|
+
const { degree, institution, location, startDate, endDate, description } = request.body;
|
|
214
220
|
await updateInSection(application, "education", index, {
|
|
215
221
|
degree: degree || "",
|
|
216
222
|
institution: institution || "",
|
|
217
223
|
location: location || "",
|
|
218
|
-
|
|
224
|
+
startDate: startDate || "",
|
|
225
|
+
endDate: endDate || null,
|
|
219
226
|
description: description || "",
|
|
220
227
|
});
|
|
221
228
|
response.redirect(application.cvEndpoint + "?saved=1#education");
|
|
@@ -304,6 +311,20 @@ export const dashboardController = {
|
|
|
304
311
|
response.redirect(application.cvEndpoint + "?error=1#skills");
|
|
305
312
|
}
|
|
306
313
|
},
|
|
314
|
+
|
|
315
|
+
// --- Generic move (reorder) ---
|
|
316
|
+
|
|
317
|
+
async move(request, response) {
|
|
318
|
+
const { application } = request.app.locals;
|
|
319
|
+
const { section, index, direction } = request.params;
|
|
320
|
+
try {
|
|
321
|
+
await moveInSection(application, section, Number.parseInt(index, 10), direction);
|
|
322
|
+
response.redirect(application.cvEndpoint + "?saved=1#" + section);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(`[CV] Move ${section} error:`, error);
|
|
325
|
+
response.redirect(application.cvEndpoint + "?error=1#" + section);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
307
328
|
};
|
|
308
329
|
|
|
309
330
|
// --- Helper functions ---
|
package/lib/storage/cv.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* @module storage/cv
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Get collection reference
|
|
10
13
|
* @param {object} application - Application instance
|
|
@@ -47,6 +50,10 @@ export async function saveCvData(application, data) {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
await collection.replaceOne({ _id: "cv" }, document, { upsert: true });
|
|
53
|
+
|
|
54
|
+
// Write JSON file for Eleventy to pick up
|
|
55
|
+
writeCvFile(application, document);
|
|
56
|
+
|
|
50
57
|
return document;
|
|
51
58
|
}
|
|
52
59
|
|
|
@@ -112,6 +119,26 @@ export async function removeFromSection(application, section, index) {
|
|
|
112
119
|
return saveCvData(application, data);
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Move an item up or down in a CV array section
|
|
124
|
+
* @param {object} application - Application instance
|
|
125
|
+
* @param {string} section - Section name
|
|
126
|
+
* @param {number} index - Current index
|
|
127
|
+
* @param {string} direction - "up" or "down"
|
|
128
|
+
*/
|
|
129
|
+
export async function moveInSection(application, section, index, direction) {
|
|
130
|
+
const data = (await getCvData(application)) || getDefaultCvData();
|
|
131
|
+
const arr = data[section];
|
|
132
|
+
if (!Array.isArray(arr)) return data;
|
|
133
|
+
|
|
134
|
+
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
135
|
+
if (targetIndex < 0 || targetIndex >= arr.length) return data;
|
|
136
|
+
|
|
137
|
+
// Swap items
|
|
138
|
+
[arr[index], arr[targetIndex]] = [arr[targetIndex], arr[index]];
|
|
139
|
+
return saveCvData(application, data);
|
|
140
|
+
}
|
|
141
|
+
|
|
115
142
|
/**
|
|
116
143
|
* Add a skill category
|
|
117
144
|
* @param {object} application - Application instance
|
|
@@ -139,3 +166,26 @@ export async function removeSkillCategory(application, category) {
|
|
|
139
166
|
}
|
|
140
167
|
return saveCvData(application, data);
|
|
141
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Write CV data to JSON file in content directory
|
|
172
|
+
* This triggers Eleventy rebuild via file watcher
|
|
173
|
+
* @param {object} application - Application instance
|
|
174
|
+
* @param {object} data - CV data object
|
|
175
|
+
*/
|
|
176
|
+
export function writeCvFile(application, data) {
|
|
177
|
+
const contentDir = application.contentDir || "/app/data/content";
|
|
178
|
+
const configDir = join(contentDir, ".indiekit");
|
|
179
|
+
const filePath = join(configDir, "cv.json");
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(configDir, { recursive: true });
|
|
183
|
+
} catch {
|
|
184
|
+
// Directory may already exist
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Write data (excluding MongoDB-specific fields)
|
|
188
|
+
const { _id, ...fileData } = data;
|
|
189
|
+
writeFileSync(filePath, JSON.stringify(fileData, null, 2));
|
|
190
|
+
console.log(`[CV] Wrote data to ${filePath}`);
|
|
191
|
+
}
|
package/package.json
CHANGED
package/views/cv-dashboard.njk
CHANGED
|
@@ -106,6 +106,43 @@
|
|
|
106
106
|
display: flex;
|
|
107
107
|
gap: 0.25rem;
|
|
108
108
|
flex-shrink: 0;
|
|
109
|
+
align-items: center;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.cv-move-buttons {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 1px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.cv-move-buttons form {
|
|
120
|
+
margin: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.cv-move-btn {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
width: 24px;
|
|
128
|
+
height: 16px;
|
|
129
|
+
padding: 0;
|
|
130
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
131
|
+
background: var(--color-background, #fff);
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
border-radius: 2px;
|
|
134
|
+
color: var(--color-on-surface, #333);
|
|
135
|
+
font-size: 10px;
|
|
136
|
+
line-height: 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cv-move-btn:hover {
|
|
140
|
+
background: var(--color-primary-container, #e6f0ff);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cv-move-btn:disabled {
|
|
144
|
+
opacity: 0.3;
|
|
145
|
+
cursor: default;
|
|
109
146
|
}
|
|
110
147
|
|
|
111
148
|
.cv-edit-details {
|
|
@@ -246,6 +283,10 @@
|
|
|
246
283
|
{% if cv.experience.length %}
|
|
247
284
|
{% for item in cv.experience %}
|
|
248
285
|
<div class="cv-item cv-item--has-edit">
|
|
286
|
+
<div class="cv-move-buttons">
|
|
287
|
+
<form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/up"><button type="submit" class="cv-move-btn" title="Move up" {% if loop.first %}disabled{% endif %}>▲</button></form>
|
|
288
|
+
<form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/down"><button type="submit" class="cv-move-btn" title="Move down" {% if loop.last %}disabled{% endif %}>▼</button></form>
|
|
289
|
+
</div>
|
|
249
290
|
<div class="cv-item__info">
|
|
250
291
|
<div class="cv-item__title">{{ item.title }}</div>
|
|
251
292
|
<div class="cv-item__sub">
|
|
@@ -396,12 +437,17 @@
|
|
|
396
437
|
{% if cv.projects.length %}
|
|
397
438
|
{% for item in cv.projects %}
|
|
398
439
|
<div class="cv-item cv-item--has-edit">
|
|
440
|
+
<div class="cv-move-buttons">
|
|
441
|
+
<form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/up"><button type="submit" class="cv-move-btn" title="Move up" {% if loop.first %}disabled{% endif %}>▲</button></form>
|
|
442
|
+
<form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/down"><button type="submit" class="cv-move-btn" title="Move down" {% if loop.last %}disabled{% endif %}>▼</button></form>
|
|
443
|
+
</div>
|
|
399
444
|
<div class="cv-item__info">
|
|
400
445
|
<div class="cv-item__title">
|
|
401
446
|
{% if item.url %}<a href="{{ item.url }}">{{ item.name }}</a>{% else %}{{ item.name }}{% endif %}
|
|
402
447
|
</div>
|
|
403
448
|
<div class="cv-item__sub">
|
|
404
449
|
{% if item.status %}<span class="cv-tag">{{ item.status }}</span>{% endif %}
|
|
450
|
+
{% if item.startDate %} · {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% endif %}
|
|
405
451
|
{% if item.description %} {{ item.description }}{% endif %}
|
|
406
452
|
</div>
|
|
407
453
|
{% if item.technologies and item.technologies.length %}
|
|
@@ -431,6 +477,16 @@
|
|
|
431
477
|
<input class="field__input" type="url" name="url" value="{{ item.url }}" placeholder="https://...">
|
|
432
478
|
</div>
|
|
433
479
|
</div>
|
|
480
|
+
<div class="field-row">
|
|
481
|
+
<div class="field">
|
|
482
|
+
<label class="field__label">{{ __("cv.experience.startDate") }}</label>
|
|
483
|
+
<input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
|
|
484
|
+
</div>
|
|
485
|
+
<div class="field">
|
|
486
|
+
<label class="field__label">{{ __("cv.experience.endDate") }}</label>
|
|
487
|
+
<input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
434
490
|
<div class="field">
|
|
435
491
|
<label class="field__label">{{ __("cv.projects.descriptionField") }}</label>
|
|
436
492
|
<textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
|
|
@@ -475,6 +531,16 @@
|
|
|
475
531
|
<input class="field__input" type="url" id="proj-url" name="url" placeholder="https://...">
|
|
476
532
|
</div>
|
|
477
533
|
</div>
|
|
534
|
+
<div class="field-row">
|
|
535
|
+
<div class="field">
|
|
536
|
+
<label class="field__label" for="proj-start">{{ __("cv.experience.startDate") }}</label>
|
|
537
|
+
<input class="field__input" type="month" id="proj-start" name="startDate">
|
|
538
|
+
</div>
|
|
539
|
+
<div class="field">
|
|
540
|
+
<label class="field__label" for="proj-end">{{ __("cv.experience.endDate") }}</label>
|
|
541
|
+
<input class="field__input" type="month" id="proj-end" name="endDate">
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
478
544
|
<div class="field">
|
|
479
545
|
<label class="field__label" for="proj-desc">{{ __("cv.projects.descriptionField") }}</label>
|
|
480
546
|
<textarea class="field__input" id="proj-desc" name="description" rows="2"></textarea>
|
|
@@ -558,10 +624,15 @@
|
|
|
558
624
|
{% if cv.education.length %}
|
|
559
625
|
{% for item in cv.education %}
|
|
560
626
|
<div class="cv-item cv-item--has-edit">
|
|
627
|
+
<div class="cv-move-buttons">
|
|
628
|
+
<form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/up"><button type="submit" class="cv-move-btn" title="Move up" {% if loop.first %}disabled{% endif %}>▲</button></form>
|
|
629
|
+
<form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/down"><button type="submit" class="cv-move-btn" title="Move down" {% if loop.last %}disabled{% endif %}>▼</button></form>
|
|
630
|
+
</div>
|
|
561
631
|
<div class="cv-item__info">
|
|
562
632
|
<div class="cv-item__title">{{ item.degree }}</div>
|
|
563
633
|
<div class="cv-item__sub">
|
|
564
|
-
{{ item.institution }}{% if item.location %} · {{ item.location }}{% endif %}
|
|
634
|
+
{{ item.institution }}{% if item.location %} · {{ item.location }}{% endif %}
|
|
635
|
+
{% if item.startDate %} · {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% elif item.year %} · {{ item.year }}{% endif %}
|
|
565
636
|
</div>
|
|
566
637
|
{% if item.description %}
|
|
567
638
|
<div class="cv-item__sub" style="margin-top:0.25rem">{{ item.description }}</div>
|
|
@@ -593,9 +664,15 @@
|
|
|
593
664
|
<label class="field__label">{{ __("cv.education.location") }}</label>
|
|
594
665
|
<input class="field__input" type="text" name="location" value="{{ item.location }}">
|
|
595
666
|
</div>
|
|
667
|
+
</div>
|
|
668
|
+
<div class="field-row">
|
|
669
|
+
<div class="field">
|
|
670
|
+
<label class="field__label">{{ __("cv.experience.startDate") }}</label>
|
|
671
|
+
<input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
|
|
672
|
+
</div>
|
|
596
673
|
<div class="field">
|
|
597
|
-
<label class="field__label">{{ __("cv.
|
|
598
|
-
<input class="field__input" type="
|
|
674
|
+
<label class="field__label">{{ __("cv.experience.endDate") }}</label>
|
|
675
|
+
<input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
|
|
599
676
|
</div>
|
|
600
677
|
</div>
|
|
601
678
|
<div class="field">
|
|
@@ -627,14 +704,18 @@
|
|
|
627
704
|
<input class="field__input" type="text" id="edu-inst" name="institution" required>
|
|
628
705
|
</div>
|
|
629
706
|
</div>
|
|
707
|
+
<div class="field">
|
|
708
|
+
<label class="field__label" for="edu-location">{{ __("cv.education.location") }}</label>
|
|
709
|
+
<input class="field__input" type="text" id="edu-location" name="location">
|
|
710
|
+
</div>
|
|
630
711
|
<div class="field-row">
|
|
631
712
|
<div class="field">
|
|
632
|
-
<label class="field__label" for="edu-
|
|
633
|
-
<input class="field__input" type="
|
|
713
|
+
<label class="field__label" for="edu-start">{{ __("cv.experience.startDate") }}</label>
|
|
714
|
+
<input class="field__input" type="month" id="edu-start" name="startDate">
|
|
634
715
|
</div>
|
|
635
716
|
<div class="field">
|
|
636
|
-
<label class="field__label" for="edu-
|
|
637
|
-
<input class="field__input" type="
|
|
717
|
+
<label class="field__label" for="edu-end">{{ __("cv.experience.endDate") }}</label>
|
|
718
|
+
<input class="field__input" type="month" id="edu-end" name="endDate">
|
|
638
719
|
</div>
|
|
639
720
|
</div>
|
|
640
721
|
<div class="field">
|
|
@@ -659,6 +740,10 @@
|
|
|
659
740
|
{% if cv.languages.length %}
|
|
660
741
|
{% for item in cv.languages %}
|
|
661
742
|
<div class="cv-item cv-item--has-edit">
|
|
743
|
+
<div class="cv-move-buttons">
|
|
744
|
+
<form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/up"><button type="submit" class="cv-move-btn" title="Move up" {% if loop.first %}disabled{% endif %}>▲</button></form>
|
|
745
|
+
<form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/down"><button type="submit" class="cv-move-btn" title="Move down" {% if loop.last %}disabled{% endif %}>▼</button></form>
|
|
746
|
+
</div>
|
|
662
747
|
<div class="cv-item__info">
|
|
663
748
|
<div class="cv-item__title">{{ item.name }}</div>
|
|
664
749
|
<div class="cv-item__sub">{{ item.level }}</div>
|