@rmdes/indiekit-endpoint-cv 1.0.6 → 1.0.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/views/cv-dashboard.njk +376 -265
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-cv",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
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",
@@ -8,6 +8,26 @@
8
8
  gap: var(--space-l, 1.5rem);
9
9
  }
10
10
 
11
+ .cv-save-banner {
12
+ position: sticky;
13
+ top: 0;
14
+ z-index: 100;
15
+ background: var(--color-primary-container, #e6f0ff);
16
+ border: 2px solid var(--color-primary, #0066cc);
17
+ border-radius: var(--border-radius-small, 0.25rem);
18
+ padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
19
+ display: flex;
20
+ justify-content: space-between;
21
+ align-items: center;
22
+ gap: var(--space-s, 0.75rem);
23
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
24
+ }
25
+
26
+ .cv-save-banner__text {
27
+ font: var(--font-body, 0.875rem/1.4 sans-serif);
28
+ font-weight: 600;
29
+ }
30
+
11
31
  .cv-accordion {
12
32
  background: var(--color-offset, #f5f5f5);
13
33
  border-radius: var(--border-radius-small, 0.5rem);
@@ -53,6 +73,29 @@
53
73
  margin-block-end: var(--space-s, 0.75rem);
54
74
  }
55
75
 
76
+ .cv-sortable-list {
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+
81
+ .cv-sortable-item {
82
+ transition: opacity 0.15s;
83
+ }
84
+
85
+ .cv-sortable-ghost {
86
+ opacity: 0.3;
87
+ background: var(--color-primary-container, #e6f0ff);
88
+ border-radius: var(--border-radius-small, 0.25rem);
89
+ }
90
+
91
+ .cv-sortable-ghost .cv-edit-details {
92
+ display: none;
93
+ }
94
+
95
+ .cv-sortable-chosen {
96
+ opacity: 0.9;
97
+ }
98
+
56
99
  .cv-item {
57
100
  background: var(--color-background, #fff);
58
101
  border: 1px solid var(--color-outline-variant, #ddd);
@@ -110,39 +153,22 @@
110
153
  flex-wrap: wrap;
111
154
  }
112
155
 
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 {
156
+ .drag-handle {
157
+ cursor: grab;
158
+ color: var(--color-on-offset, #999);
159
+ padding: 0.25rem;
124
160
  display: flex;
125
161
  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;
162
+ flex-shrink: 0;
163
+ touch-action: none;
137
164
  }
138
165
 
139
- .cv-move-btn:hover {
140
- background: var(--color-primary-container, #e6f0ff);
166
+ .drag-handle:hover {
167
+ color: var(--color-primary, #0066cc);
141
168
  }
142
169
 
143
- .cv-move-btn:disabled {
144
- opacity: 0.3;
145
- cursor: default;
170
+ .drag-handle:active {
171
+ cursor: grabbing;
146
172
  }
147
173
 
148
174
  .cv-edit-details {
@@ -269,6 +295,22 @@
269
295
  <p class="cv-accordion__desc">{{ __("cv.lastUpdated") }}: {{ cv.lastUpdated }}</p>
270
296
  {% endif %}
271
297
 
298
+ {# Hidden save form for drag-drop reordering - shown when order changes #}
299
+ <form method="post" action="{{ cvEndpoint }}/save" id="cv-save-form" style="display:none">
300
+ <input type="hidden" name="experience" id="experience-json" value='{{ cv.experience | dump | safe }}'>
301
+ <input type="hidden" name="projects" id="projects-json" value='{{ cv.projects | dump | safe }}'>
302
+ <input type="hidden" name="education" id="education-json" value='{{ cv.education | dump | safe }}'>
303
+ <input type="hidden" name="languages" id="languages-json" value='{{ cv.languages | dump | safe }}'>
304
+ {% for category, items in cv.skills | dictsort %}
305
+ <input type="hidden" name="skills[{{ category }}]" value="{{ items | join(', ') }}">
306
+ {% endfor %}
307
+ <input type="hidden" name="interests" value="{{ cv.interests | join(', ') }}">
308
+ <div class="cv-save-banner">
309
+ <span class="cv-save-banner__text">Order changed — drag more or save now</span>
310
+ <button type="submit" class="button button--primary">Save All Changes</button>
311
+ </div>
312
+ </form>
313
+
272
314
  <div class="cv-dashboard">
273
315
 
274
316
  {# ===== EXPERIENCE ===== #}
@@ -281,92 +323,95 @@
281
323
  <p class="cv-accordion__desc">{{ __("cv.experience.description") }}</p>
282
324
 
283
325
  {% if cv.experience.length %}
326
+ <div class="cv-sortable-list" id="experience-sortable">
284
327
  {% for item in cv.experience %}
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>
290
- <div class="cv-item__info">
291
- <div class="cv-item__title">{{ item.title }}</div>
292
- <div class="cv-item__sub">
293
- {{ item.company }}{% if item.location %} &middot; {{ item.location }}{% endif %}
294
- {% if item.startDate %} &middot; {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% endif %}
295
- {% if item.type %} &middot; {{ item.type }}{% endif %}
296
- </div>
297
- {% if item.description %}
298
- <div class="cv-item__sub" style="margin-top:0.25rem">{{ item.description }}</div>
299
- {% endif %}
300
- {% if item.highlights and item.highlights.length %}
301
- <div class="cv-item__tags">
302
- {% for h in item.highlights %}<span class="cv-tag">{{ h }}</span>{% endfor %}
328
+ <div class="cv-sortable-item" data-index="{{ loop.index0 }}">
329
+ <div class="cv-item cv-item--has-edit">
330
+ <span class="drag-handle" title="Drag to reorder">
331
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path 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"/></svg>
332
+ </span>
333
+ <div class="cv-item__info">
334
+ <div class="cv-item__title">{{ item.title }}</div>
335
+ <div class="cv-item__sub">
336
+ {{ item.company }}{% if item.location %} &middot; {{ item.location }}{% endif %}
337
+ {% if item.startDate %} &middot; {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% endif %}
338
+ {% if item.type %} &middot; {{ item.type }}{% endif %}
339
+ </div>
340
+ {% if item.description %}
341
+ <div class="cv-item__sub" style="margin-top:0.25rem">{{ item.description }}</div>
342
+ {% endif %}
343
+ {% if item.highlights and item.highlights.length %}
344
+ <div class="cv-item__tags">
345
+ {% for h in item.highlights %}<span class="cv-tag">{{ h }}</span>{% endfor %}
346
+ </div>
347
+ {% endif %}
348
+ </div>
349
+ <div class="cv-item__actions">
350
+ <button type="button" class="button button--small" onclick="var d=this.closest('.cv-sortable-item').querySelector('.cv-edit-details');d.open=!d.open">{{ __("cv.experience.edit") }}</button>
351
+ <form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/delete" style="margin:0">
352
+ <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
353
+ </form>
303
354
  </div>
304
- {% endif %}
305
- </div>
306
- <div class="cv-item__actions">
307
- <button type="button" class="button button--small" onclick="var d=this.closest('.cv-item').nextElementSibling;d.open=!d.open">{{ __("cv.experience.edit") }}</button>
308
- <form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/delete" style="margin:0">
309
- <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
310
- </form>
311
355
  </div>
312
- </div>
313
- <details class="cv-edit-details">
314
- <summary></summary>
315
- <div class="cv-form">
316
- <form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/edit">
317
- <div class="field-row">
318
- <div class="field">
319
- <label class="field__label">{{ __("cv.experience.jobTitle") }}</label>
320
- <input class="field__input" type="text" name="title" value="{{ item.title }}" required>
356
+ <details class="cv-edit-details">
357
+ <summary></summary>
358
+ <div class="cv-form">
359
+ <form method="post" action="{{ cvEndpoint }}/experience/{{ loop.index0 }}/edit">
360
+ <div class="field-row">
361
+ <div class="field">
362
+ <label class="field__label">{{ __("cv.experience.jobTitle") }}</label>
363
+ <input class="field__input" type="text" name="title" value="{{ item.title }}" required>
364
+ </div>
365
+ <div class="field">
366
+ <label class="field__label">{{ __("cv.experience.company") }}</label>
367
+ <input class="field__input" type="text" name="company" value="{{ item.company }}" required>
368
+ </div>
321
369
  </div>
322
- <div class="field">
323
- <label class="field__label">{{ __("cv.experience.company") }}</label>
324
- <input class="field__input" type="text" name="company" value="{{ item.company }}" required>
370
+ <div class="field-row">
371
+ <div class="field">
372
+ <label class="field__label">{{ __("cv.experience.location") }}</label>
373
+ <input class="field__input" type="text" name="location" value="{{ item.location }}">
374
+ </div>
375
+ <div class="field">
376
+ <label class="field__label">{{ __("cv.experience.type") }}</label>
377
+ <select class="field__input" name="type">
378
+ <option value="full-time" {% if item.type == "full-time" %}selected{% endif %}>Full-time</option>
379
+ <option value="part-time" {% if item.type == "part-time" %}selected{% endif %}>Part-time</option>
380
+ <option value="contract" {% if item.type == "contract" %}selected{% endif %}>Contract</option>
381
+ <option value="freelance" {% if item.type == "freelance" %}selected{% endif %}>Freelance</option>
382
+ <option value="volunteer" {% if item.type == "volunteer" %}selected{% endif %}>Volunteer</option>
383
+ <option value="internship" {% if item.type == "internship" %}selected{% endif %}>Internship</option>
384
+ </select>
385
+ </div>
325
386
  </div>
326
- </div>
327
- <div class="field-row">
328
- <div class="field">
329
- <label class="field__label">{{ __("cv.experience.location") }}</label>
330
- <input class="field__input" type="text" name="location" value="{{ item.location }}">
387
+ <div class="field-row">
388
+ <div class="field">
389
+ <label class="field__label">{{ __("cv.experience.startDate") }}</label>
390
+ <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
391
+ </div>
392
+ <div class="field">
393
+ <label class="field__label">{{ __("cv.experience.endDate") }}</label>
394
+ <input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
395
+ </div>
331
396
  </div>
332
397
  <div class="field">
333
- <label class="field__label">{{ __("cv.experience.type") }}</label>
334
- <select class="field__input" name="type">
335
- <option value="full-time" {% if item.type == "full-time" %}selected{% endif %}>Full-time</option>
336
- <option value="part-time" {% if item.type == "part-time" %}selected{% endif %}>Part-time</option>
337
- <option value="contract" {% if item.type == "contract" %}selected{% endif %}>Contract</option>
338
- <option value="freelance" {% if item.type == "freelance" %}selected{% endif %}>Freelance</option>
339
- <option value="volunteer" {% if item.type == "volunteer" %}selected{% endif %}>Volunteer</option>
340
- <option value="internship" {% if item.type == "internship" %}selected{% endif %}>Internship</option>
341
- </select>
398
+ <label class="field__label">{{ __("cv.experience.descriptionField") }}</label>
399
+ <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
342
400
  </div>
343
- </div>
344
- <div class="field-row">
345
401
  <div class="field">
346
- <label class="field__label">{{ __("cv.experience.startDate") }}</label>
347
- <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
402
+ <label class="field__label">{{ __("cv.experience.highlights") }}</label>
403
+ <textarea class="field__input" name="highlights" rows="3" placeholder="One highlight per line">{{ item.highlights | join("\n") if item.highlights }}</textarea>
348
404
  </div>
349
- <div class="field">
350
- <label class="field__label">{{ __("cv.experience.endDate") }}</label>
351
- <input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
405
+ <div class="cv-form__buttons">
406
+ <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
407
+ <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
352
408
  </div>
353
- </div>
354
- <div class="field">
355
- <label class="field__label">{{ __("cv.experience.descriptionField") }}</label>
356
- <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
357
- </div>
358
- <div class="field">
359
- <label class="field__label">{{ __("cv.experience.highlights") }}</label>
360
- <textarea class="field__input" name="highlights" rows="3" placeholder="One highlight per line">{{ item.highlights | join("\n") if item.highlights }}</textarea>
361
- </div>
362
- <div class="cv-form__buttons">
363
- <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
364
- <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
365
- </div>
366
- </form>
367
- </div>
368
- </details>
409
+ </form>
410
+ </div>
411
+ </details>
412
+ </div>
369
413
  {% endfor %}
414
+ </div>
370
415
  {% else %}
371
416
  <p class="cv-empty">{{ __("cv.noData") }}</p>
372
417
  {% endif %}
@@ -435,85 +480,88 @@
435
480
  <p class="cv-accordion__desc">{{ __("cv.projects.description") }}</p>
436
481
 
437
482
  {% if cv.projects.length %}
483
+ <div class="cv-sortable-list" id="projects-sortable">
438
484
  {% for item in cv.projects %}
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>
444
- <div class="cv-item__info">
445
- <div class="cv-item__title">
446
- {% if item.url %}<a href="{{ item.url }}">{{ item.name }}</a>{% else %}{{ item.name }}{% endif %}
447
- </div>
448
- <div class="cv-item__sub">
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 %}
451
- {% if item.description %} {{ item.description }}{% endif %}
485
+ <div class="cv-sortable-item" data-index="{{ loop.index0 }}">
486
+ <div class="cv-item cv-item--has-edit">
487
+ <span class="drag-handle" title="Drag to reorder">
488
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path 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"/></svg>
489
+ </span>
490
+ <div class="cv-item__info">
491
+ <div class="cv-item__title">
492
+ {% if item.url %}<a href="{{ item.url }}">{{ item.name }}</a>{% else %}{{ item.name }}{% endif %}
493
+ </div>
494
+ <div class="cv-item__sub">
495
+ {% if item.status %}<span class="cv-tag">{{ item.status }}</span>{% endif %}
496
+ {% if item.startDate %} &middot; {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% endif %}
497
+ {% if item.description %} {{ item.description }}{% endif %}
498
+ </div>
499
+ {% if item.technologies and item.technologies.length %}
500
+ <div class="cv-item__tags">
501
+ {% for t in item.technologies %}<span class="cv-tag">{{ t }}</span>{% endfor %}
502
+ </div>
503
+ {% endif %}
452
504
  </div>
453
- {% if item.technologies and item.technologies.length %}
454
- <div class="cv-item__tags">
455
- {% for t in item.technologies %}<span class="cv-tag">{{ t }}</span>{% endfor %}
505
+ <div class="cv-item__actions">
506
+ <button type="button" class="button button--small" onclick="var d=this.closest('.cv-sortable-item').querySelector('.cv-edit-details');d.open=!d.open">{{ __("cv.projects.edit") }}</button>
507
+ <form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/delete" style="margin:0">
508
+ <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
509
+ </form>
456
510
  </div>
457
- {% endif %}
458
- </div>
459
- <div class="cv-item__actions">
460
- <button type="button" class="button button--small" onclick="var d=this.closest('.cv-item').nextElementSibling;d.open=!d.open">{{ __("cv.projects.edit") }}</button>
461
- <form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/delete" style="margin:0">
462
- <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
463
- </form>
464
511
  </div>
465
- </div>
466
- <details class="cv-edit-details">
467
- <summary></summary>
468
- <div class="cv-form">
469
- <form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/edit">
470
- <div class="field-row">
471
- <div class="field">
472
- <label class="field__label">{{ __("cv.projects.name") }}</label>
473
- <input class="field__input" type="text" name="name" value="{{ item.name }}" required>
512
+ <details class="cv-edit-details">
513
+ <summary></summary>
514
+ <div class="cv-form">
515
+ <form method="post" action="{{ cvEndpoint }}/projects/{{ loop.index0 }}/edit">
516
+ <div class="field-row">
517
+ <div class="field">
518
+ <label class="field__label">{{ __("cv.projects.name") }}</label>
519
+ <input class="field__input" type="text" name="name" value="{{ item.name }}" required>
520
+ </div>
521
+ <div class="field">
522
+ <label class="field__label">{{ __("cv.projects.url") }}</label>
523
+ <input class="field__input" type="url" name="url" value="{{ item.url }}" placeholder="https://...">
524
+ </div>
474
525
  </div>
475
- <div class="field">
476
- <label class="field__label">{{ __("cv.projects.url") }}</label>
477
- <input class="field__input" type="url" name="url" value="{{ item.url }}" placeholder="https://...">
526
+ <div class="field-row">
527
+ <div class="field">
528
+ <label class="field__label">{{ __("cv.experience.startDate") }}</label>
529
+ <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
530
+ </div>
531
+ <div class="field">
532
+ <label class="field__label">{{ __("cv.experience.endDate") }}</label>
533
+ <input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
534
+ </div>
478
535
  </div>
479
- </div>
480
- <div class="field-row">
481
536
  <div class="field">
482
- <label class="field__label">{{ __("cv.experience.startDate") }}</label>
483
- <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
537
+ <label class="field__label">{{ __("cv.projects.descriptionField") }}</label>
538
+ <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
484
539
  </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 }}">
540
+ <div class="field-row">
541
+ <div class="field">
542
+ <label class="field__label">{{ __("cv.projects.tags") }}</label>
543
+ <input class="field__input" type="text" name="tags" value="{{ item.technologies | join(', ') if item.technologies }}" placeholder="Docker, Node.js, Python">
544
+ </div>
545
+ <div class="field">
546
+ <label class="field__label">{{ __("cv.projects.status") }}</label>
547
+ <select class="field__input" name="status">
548
+ <option value="active" {% if item.status == "active" %}selected{% endif %}>Active</option>
549
+ <option value="maintained" {% if item.status == "maintained" %}selected{% endif %}>Maintained</option>
550
+ <option value="archived" {% if item.status == "archived" %}selected{% endif %}>Archived</option>
551
+ <option value="completed" {% if item.status == "completed" %}selected{% endif %}>Completed</option>
552
+ </select>
553
+ </div>
488
554
  </div>
489
- </div>
490
- <div class="field">
491
- <label class="field__label">{{ __("cv.projects.descriptionField") }}</label>
492
- <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
493
- </div>
494
- <div class="field-row">
495
- <div class="field">
496
- <label class="field__label">{{ __("cv.projects.tags") }}</label>
497
- <input class="field__input" type="text" name="tags" value="{{ item.technologies | join(', ') if item.technologies }}" placeholder="Docker, Node.js, Python">
498
- </div>
499
- <div class="field">
500
- <label class="field__label">{{ __("cv.projects.status") }}</label>
501
- <select class="field__input" name="status">
502
- <option value="active" {% if item.status == "active" %}selected{% endif %}>Active</option>
503
- <option value="maintained" {% if item.status == "maintained" %}selected{% endif %}>Maintained</option>
504
- <option value="archived" {% if item.status == "archived" %}selected{% endif %}>Archived</option>
505
- <option value="completed" {% if item.status == "completed" %}selected{% endif %}>Completed</option>
506
- </select>
555
+ <div class="cv-form__buttons">
556
+ <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
557
+ <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
507
558
  </div>
508
- </div>
509
- <div class="cv-form__buttons">
510
- <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
511
- <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
512
- </div>
513
- </form>
514
- </div>
515
- </details>
559
+ </form>
560
+ </div>
561
+ </details>
562
+ </div>
516
563
  {% endfor %}
564
+ </div>
517
565
  {% else %}
518
566
  <p class="cv-empty">{{ __("cv.noData") }}</p>
519
567
  {% endif %}
@@ -622,71 +670,74 @@
622
670
  <p class="cv-accordion__desc">{{ __("cv.education.description") }}</p>
623
671
 
624
672
  {% if cv.education.length %}
673
+ <div class="cv-sortable-list" id="education-sortable">
625
674
  {% for item in cv.education %}
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>
631
- <div class="cv-item__info">
632
- <div class="cv-item__title">{{ item.degree }}</div>
633
- <div class="cv-item__sub">
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 %}
636
- </div>
637
- {% if item.description %}
638
- <div class="cv-item__sub" style="margin-top:0.25rem">{{ item.description }}</div>
639
- {% endif %}
640
- </div>
641
- <div class="cv-item__actions">
642
- <button type="button" class="button button--small" onclick="var d=this.closest('.cv-item').nextElementSibling;d.open=!d.open">{{ __("cv.education.edit") }}</button>
643
- <form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/delete" style="margin:0">
644
- <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
645
- </form>
675
+ <div class="cv-sortable-item" data-index="{{ loop.index0 }}">
676
+ <div class="cv-item cv-item--has-edit">
677
+ <span class="drag-handle" title="Drag to reorder">
678
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path 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"/></svg>
679
+ </span>
680
+ <div class="cv-item__info">
681
+ <div class="cv-item__title">{{ item.degree }}</div>
682
+ <div class="cv-item__sub">
683
+ {{ item.institution }}{% if item.location %} &middot; {{ item.location }}{% endif %}
684
+ {% if item.startDate %} &middot; {{ item.startDate }}{% if item.endDate %} – {{ item.endDate }}{% else %} – Present{% endif %}{% elif item.year %} &middot; {{ item.year }}{% endif %}
685
+ </div>
686
+ {% if item.description %}
687
+ <div class="cv-item__sub" style="margin-top:0.25rem">{{ item.description }}</div>
688
+ {% endif %}
689
+ </div>
690
+ <div class="cv-item__actions">
691
+ <button type="button" class="button button--small" onclick="var d=this.closest('.cv-sortable-item').querySelector('.cv-edit-details');d.open=!d.open">{{ __("cv.education.edit") }}</button>
692
+ <form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/delete" style="margin:0">
693
+ <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
694
+ </form>
695
+ </div>
646
696
  </div>
647
- </div>
648
- <details class="cv-edit-details">
649
- <summary></summary>
650
- <div class="cv-form">
651
- <form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/edit">
652
- <div class="field-row">
653
- <div class="field">
654
- <label class="field__label">{{ __("cv.education.degree") }}</label>
655
- <input class="field__input" type="text" name="degree" value="{{ item.degree }}" required>
697
+ <details class="cv-edit-details">
698
+ <summary></summary>
699
+ <div class="cv-form">
700
+ <form method="post" action="{{ cvEndpoint }}/education/{{ loop.index0 }}/edit">
701
+ <div class="field-row">
702
+ <div class="field">
703
+ <label class="field__label">{{ __("cv.education.degree") }}</label>
704
+ <input class="field__input" type="text" name="degree" value="{{ item.degree }}" required>
705
+ </div>
706
+ <div class="field">
707
+ <label class="field__label">{{ __("cv.education.institution") }}</label>
708
+ <input class="field__input" type="text" name="institution" value="{{ item.institution }}" required>
709
+ </div>
656
710
  </div>
657
- <div class="field">
658
- <label class="field__label">{{ __("cv.education.institution") }}</label>
659
- <input class="field__input" type="text" name="institution" value="{{ item.institution }}" required>
711
+ <div class="field-row">
712
+ <div class="field">
713
+ <label class="field__label">{{ __("cv.education.location") }}</label>
714
+ <input class="field__input" type="text" name="location" value="{{ item.location }}">
715
+ </div>
660
716
  </div>
661
- </div>
662
- <div class="field-row">
663
- <div class="field">
664
- <label class="field__label">{{ __("cv.education.location") }}</label>
665
- <input class="field__input" type="text" name="location" value="{{ item.location }}">
717
+ <div class="field-row">
718
+ <div class="field">
719
+ <label class="field__label">{{ __("cv.experience.startDate") }}</label>
720
+ <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
721
+ </div>
722
+ <div class="field">
723
+ <label class="field__label">{{ __("cv.experience.endDate") }}</label>
724
+ <input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
725
+ </div>
666
726
  </div>
667
- </div>
668
- <div class="field-row">
669
727
  <div class="field">
670
- <label class="field__label">{{ __("cv.experience.startDate") }}</label>
671
- <input class="field__input" type="month" name="startDate" value="{{ item.startDate }}">
728
+ <label class="field__label">{{ __("cv.education.descriptionField") }}</label>
729
+ <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
672
730
  </div>
673
- <div class="field">
674
- <label class="field__label">{{ __("cv.experience.endDate") }}</label>
675
- <input class="field__input" type="month" name="endDate" value="{{ item.endDate }}">
731
+ <div class="cv-form__buttons">
732
+ <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
733
+ <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
676
734
  </div>
677
- </div>
678
- <div class="field">
679
- <label class="field__label">{{ __("cv.education.descriptionField") }}</label>
680
- <textarea class="field__input" name="description" rows="2">{{ item.description }}</textarea>
681
- </div>
682
- <div class="cv-form__buttons">
683
- <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
684
- <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
685
- </div>
686
- </form>
687
- </div>
688
- </details>
735
+ </form>
736
+ </div>
737
+ </details>
738
+ </div>
689
739
  {% endfor %}
740
+ </div>
690
741
  {% else %}
691
742
  <p class="cv-empty">{{ __("cv.noData") }}</p>
692
743
  {% endif %}
@@ -738,51 +789,54 @@
738
789
  <p class="cv-accordion__desc">{{ __("cv.languages.description") }}</p>
739
790
 
740
791
  {% if cv.languages.length %}
792
+ <div class="cv-sortable-list" id="languages-sortable">
741
793
  {% for item in cv.languages %}
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>
747
- <div class="cv-item__info">
748
- <div class="cv-item__title">{{ item.name }}</div>
749
- <div class="cv-item__sub">{{ item.level }}</div>
750
- </div>
751
- <div class="cv-item__actions">
752
- <button type="button" class="button button--small" onclick="var d=this.closest('.cv-item').nextElementSibling;d.open=!d.open">{{ __("cv.languages.edit") }}</button>
753
- <form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/delete" style="margin:0">
754
- <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
755
- </form>
794
+ <div class="cv-sortable-item" data-index="{{ loop.index0 }}">
795
+ <div class="cv-item cv-item--has-edit">
796
+ <span class="drag-handle" title="Drag to reorder">
797
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path 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"/></svg>
798
+ </span>
799
+ <div class="cv-item__info">
800
+ <div class="cv-item__title">{{ item.name }}</div>
801
+ <div class="cv-item__sub">{{ item.level }}</div>
802
+ </div>
803
+ <div class="cv-item__actions">
804
+ <button type="button" class="button button--small" onclick="var d=this.closest('.cv-sortable-item').querySelector('.cv-edit-details');d.open=!d.open">{{ __("cv.languages.edit") }}</button>
805
+ <form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/delete" style="margin:0">
806
+ <button type="submit" class="button button--small button--secondary" onclick="return confirm('Delete this entry?')">Delete</button>
807
+ </form>
808
+ </div>
756
809
  </div>
757
- </div>
758
- <details class="cv-edit-details">
759
- <summary></summary>
760
- <div class="cv-form">
761
- <form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/edit">
762
- <div class="field-row">
763
- <div class="field">
764
- <label class="field__label">{{ __("cv.languages.name") }}</label>
765
- <input class="field__input" type="text" name="name" value="{{ item.name }}" required>
810
+ <details class="cv-edit-details">
811
+ <summary></summary>
812
+ <div class="cv-form">
813
+ <form method="post" action="{{ cvEndpoint }}/languages/{{ loop.index0 }}/edit">
814
+ <div class="field-row">
815
+ <div class="field">
816
+ <label class="field__label">{{ __("cv.languages.name") }}</label>
817
+ <input class="field__input" type="text" name="name" value="{{ item.name }}" required>
818
+ </div>
819
+ <div class="field">
820
+ <label class="field__label">{{ __("cv.languages.level") }}</label>
821
+ <select class="field__input" name="level">
822
+ <option value="native" {% if item.level == "native" %}selected{% endif %}>Native</option>
823
+ <option value="fluent" {% if item.level == "fluent" %}selected{% endif %}>Fluent</option>
824
+ <option value="advanced" {% if item.level == "advanced" %}selected{% endif %}>Advanced</option>
825
+ <option value="intermediate" {% if item.level == "intermediate" %}selected{% endif %}>Intermediate</option>
826
+ <option value="basic" {% if item.level == "basic" %}selected{% endif %}>Basic</option>
827
+ </select>
828
+ </div>
766
829
  </div>
767
- <div class="field">
768
- <label class="field__label">{{ __("cv.languages.level") }}</label>
769
- <select class="field__input" name="level">
770
- <option value="native" {% if item.level == "native" %}selected{% endif %}>Native</option>
771
- <option value="fluent" {% if item.level == "fluent" %}selected{% endif %}>Fluent</option>
772
- <option value="advanced" {% if item.level == "advanced" %}selected{% endif %}>Advanced</option>
773
- <option value="intermediate" {% if item.level == "intermediate" %}selected{% endif %}>Intermediate</option>
774
- <option value="basic" {% if item.level == "basic" %}selected{% endif %}>Basic</option>
775
- </select>
830
+ <div class="cv-form__buttons">
831
+ <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
832
+ <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
776
833
  </div>
777
- </div>
778
- <div class="cv-form__buttons">
779
- <button type="submit" class="button button--primary button--small">{{ __("cv.save") }}</button>
780
- <button type="button" class="button button--small button--secondary" onclick="this.closest('details').open=false">Cancel</button>
781
- </div>
782
- </form>
783
- </div>
784
- </details>
834
+ </form>
835
+ </div>
836
+ </details>
837
+ </div>
785
838
  {% endfor %}
839
+ </div>
786
840
  {% else %}
787
841
  <p class="cv-empty">{{ __("cv.noData") }}</p>
788
842
  {% endif %}
@@ -854,4 +908,61 @@
854
908
  </details>
855
909
 
856
910
  </div>
911
+
912
+ <script>
913
+ (function() {
914
+ var cvData = {
915
+ experience: JSON.parse(document.getElementById('experience-json').value || '[]'),
916
+ projects: JSON.parse(document.getElementById('projects-json').value || '[]'),
917
+ education: JSON.parse(document.getElementById('education-json').value || '[]'),
918
+ languages: JSON.parse(document.getElementById('languages-json').value || '[]')
919
+ };
920
+
921
+ var saveForm = document.getElementById('cv-save-form');
922
+ var dirty = false;
923
+
924
+ function showSaveBanner() {
925
+ if (!dirty) {
926
+ dirty = true;
927
+ saveForm.style.display = '';
928
+ }
929
+ }
930
+
931
+ function syncSection(sectionName) {
932
+ var list = document.getElementById(sectionName + '-sortable');
933
+ if (!list) return;
934
+ var items = list.querySelectorAll('.cv-sortable-item');
935
+ var data = cvData[sectionName];
936
+ var ordered = [];
937
+ items.forEach(function(el) {
938
+ var idx = parseInt(el.dataset.index, 10);
939
+ if (data[idx]) ordered.push(data[idx]);
940
+ });
941
+ cvData[sectionName] = ordered;
942
+ // Reset indices to sequential for next drag
943
+ items.forEach(function(el, i) { el.dataset.index = i; });
944
+ document.getElementById(sectionName + '-json').value = JSON.stringify(ordered);
945
+ showSaveBanner();
946
+ }
947
+
948
+ // Load SortableJS from CDN
949
+ var script = document.createElement('script');
950
+ script.src = 'https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js';
951
+ script.onload = function() {
952
+ ['experience', 'projects', 'education', 'languages'].forEach(function(section) {
953
+ var el = document.getElementById(section + '-sortable');
954
+ if (!el || !el.children.length) return;
955
+ new Sortable(el, {
956
+ handle: '.drag-handle',
957
+ animation: 150,
958
+ ghostClass: 'cv-sortable-ghost',
959
+ chosenClass: 'cv-sortable-chosen',
960
+ draggable: '.cv-sortable-item',
961
+ onEnd: function() { syncSection(section); }
962
+ });
963
+ });
964
+ };
965
+ document.head.appendChild(script);
966
+ })();
967
+ </script>
857
968
  {% endblock %}