@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 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, year, description } = request.body;
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
- year: year || "",
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, year, description } = request.body;
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
- year: year || "",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-cv",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "CV/Resume editor endpoint for Indiekit. Manage work experience, projects, skills, education, and interests from the admin UI.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -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 %}>&#9650;</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 %}>&#9660;</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 %}>&#9650;</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 %}>&#9660;</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 %} &middot; {{ 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 %}>&#9650;</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 %}>&#9660;</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 %} &middot; {{ item.location }}{% endif %}{% if item.year %} &middot; {{ item.year }}{% endif %}
634
+ {{ item.institution }}{% if item.location %} &middot; {{ item.location }}{% endif %}
635
+ {% if item.startDate %} &middot; {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% elif item.year %} &middot; {{ 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.education.year") }}</label>
598
- <input class="field__input" type="text" name="year" value="{{ item.year }}" placeholder="2020-2024">
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-location">{{ __("cv.education.location") }}</label>
633
- <input class="field__input" type="text" id="edu-location" name="location">
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-year">{{ __("cv.education.year") }}</label>
637
- <input class="field__input" type="text" id="edu-year" name="year" placeholder="2020-2024">
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 %}>&#9650;</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 %}>&#9660;</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>