@commonpub/layer 0.4.11 → 0.4.12
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/package.json +6 -6
- package/pages/settings/profile.vue +52 -59
- package/pages/u/[username]/index.vue +33 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
+
"@commonpub/docs": "0.5.2",
|
|
54
|
+
"@commonpub/config": "0.8.0",
|
|
55
|
+
"@commonpub/explainer": "0.5.3",
|
|
53
56
|
"@commonpub/auth": "0.5.0",
|
|
54
57
|
"@commonpub/editor": "0.5.0",
|
|
55
58
|
"@commonpub/learning": "0.5.0",
|
|
56
|
-
"@commonpub/schema": "0.8.14",
|
|
57
|
-
"@commonpub/config": "0.8.0",
|
|
58
|
-
"@commonpub/explainer": "0.5.3",
|
|
59
|
-
"@commonpub/docs": "0.5.2",
|
|
60
59
|
"@commonpub/ui": "0.8.4",
|
|
61
60
|
"@commonpub/server": "2.23.0",
|
|
62
|
-
"@commonpub/protocol": "0.9.5"
|
|
61
|
+
"@commonpub/protocol": "0.9.5",
|
|
62
|
+
"@commonpub/schema": "0.8.14"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -30,14 +30,14 @@ const form = ref({
|
|
|
30
30
|
bannerUrl: '',
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
const skills = ref<
|
|
33
|
+
const skills = ref<string[]>([]);
|
|
34
34
|
const socialLinks = ref({
|
|
35
35
|
github: '',
|
|
36
36
|
twitter: '',
|
|
37
37
|
linkedin: '',
|
|
38
|
-
website: '',
|
|
39
38
|
});
|
|
40
|
-
const
|
|
39
|
+
const pronouns = ref('');
|
|
40
|
+
const experience = ref<Array<{ title: string; company: string; startDate: string; endDate: string; description: string }>>([]);
|
|
41
41
|
|
|
42
42
|
const emailNotifications = ref<{
|
|
43
43
|
digest: 'daily' | 'weekly' | 'none';
|
|
@@ -75,19 +75,23 @@ if (profile.value) {
|
|
|
75
75
|
form.value.bannerUrl = p.bannerUrl || '';
|
|
76
76
|
|
|
77
77
|
if (Array.isArray(p.skills)) {
|
|
78
|
-
skills.value = p.skills.
|
|
79
|
-
typeof s === 'string' ? { name: s, proficiency: 3 } : s,
|
|
80
|
-
);
|
|
78
|
+
skills.value = p.skills.filter((s): s is string => typeof s === 'string');
|
|
81
79
|
}
|
|
80
|
+
pronouns.value = p.pronouns || '';
|
|
82
81
|
if (p.socialLinks) {
|
|
83
82
|
socialLinks.value.github = p.socialLinks.github || '';
|
|
84
83
|
socialLinks.value.twitter = p.socialLinks.twitter || '';
|
|
85
84
|
socialLinks.value.linkedin = p.socialLinks.linkedin || '';
|
|
86
|
-
socialLinks.value.website = (p.socialLinks as Record<string, string | undefined>).website || '';
|
|
87
85
|
}
|
|
88
86
|
const profileRecord = p as Record<string, unknown>;
|
|
89
87
|
if (Array.isArray(profileRecord.experience)) {
|
|
90
|
-
experience.value = (profileRecord.experience as Array<Record<string, unknown>>).map((e) => ({
|
|
88
|
+
experience.value = (profileRecord.experience as Array<Record<string, unknown>>).map((e) => ({
|
|
89
|
+
title: String(e.title || ''),
|
|
90
|
+
company: String(e.company || ''),
|
|
91
|
+
startDate: String(e.startDate || ''),
|
|
92
|
+
endDate: String(e.endDate || ''),
|
|
93
|
+
description: String(e.description || ''),
|
|
94
|
+
}));
|
|
91
95
|
}
|
|
92
96
|
if (profileRecord.emailNotifications && typeof profileRecord.emailNotifications === 'object') {
|
|
93
97
|
const en = profileRecord.emailNotifications as Record<string, unknown>;
|
|
@@ -104,25 +108,20 @@ if (profile.value) {
|
|
|
104
108
|
// Watch for form changes AFTER initial data is loaded (nextTick avoids false positive)
|
|
105
109
|
onMounted(() => {
|
|
106
110
|
nextTick(() => {
|
|
107
|
-
watch([form, skills, socialLinks, experience, emailNotifications], () => { isDirty.value = true; }, { deep: true });
|
|
111
|
+
watch([form, skills, pronouns, socialLinks, experience, emailNotifications], () => { isDirty.value = true; }, { deep: true });
|
|
108
112
|
});
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
function addSkill(): void {
|
|
112
|
-
skills.value.push(
|
|
116
|
+
skills.value.push('');
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function removeSkill(index: number): void {
|
|
116
120
|
skills.value.splice(index, 1);
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
function generateId(): string {
|
|
120
|
-
return `exp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
123
|
function addExperience(): void {
|
|
124
124
|
experience.value.push({
|
|
125
|
-
id: generateId(),
|
|
126
125
|
title: '',
|
|
127
126
|
company: '',
|
|
128
127
|
startDate: '',
|
|
@@ -170,8 +169,9 @@ async function handleSave(): Promise<void> {
|
|
|
170
169
|
method: 'PUT',
|
|
171
170
|
body: {
|
|
172
171
|
...form.value,
|
|
173
|
-
skills: skills.value.filter((s) => s.
|
|
172
|
+
skills: skills.value.filter((s) => s.trim()),
|
|
174
173
|
experience: experience.value.filter((e) => e.title.trim()),
|
|
174
|
+
pronouns: pronouns.value || undefined,
|
|
175
175
|
socialLinks: socialLinks.value,
|
|
176
176
|
...(emailNotificationsEnabled.value ? { emailNotifications: emailNotifications.value } : {}),
|
|
177
177
|
},
|
|
@@ -330,39 +330,42 @@ async function handleSave(): Promise<void> {
|
|
|
330
330
|
</div>
|
|
331
331
|
</div>
|
|
332
332
|
|
|
333
|
+
<!-- Pronouns -->
|
|
334
|
+
<div class="cpub-form-section">
|
|
335
|
+
<span class="cpub-form-section-label">Identity</span>
|
|
336
|
+
|
|
337
|
+
<div class="cpub-form-group">
|
|
338
|
+
<label for="pronouns" class="cpub-form-label">Pronouns</label>
|
|
339
|
+
<input
|
|
340
|
+
id="pronouns"
|
|
341
|
+
v-model="pronouns"
|
|
342
|
+
type="text"
|
|
343
|
+
class="cpub-input"
|
|
344
|
+
placeholder="e.g., they/them, she/her, he/him"
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
333
349
|
<!-- Skills -->
|
|
334
350
|
<div class="cpub-form-section">
|
|
335
351
|
<span class="cpub-form-section-label">Skills</span>
|
|
336
352
|
|
|
337
353
|
<div
|
|
338
|
-
v-for="(
|
|
354
|
+
v-for="(_skill, index) in skills"
|
|
339
355
|
:key="index"
|
|
340
356
|
class="cpub-skill-row"
|
|
341
357
|
>
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
/>
|
|
350
|
-
</div>
|
|
351
|
-
<div class="cpub-skill-slider">
|
|
352
|
-
<input
|
|
353
|
-
v-model.number="skill.proficiency"
|
|
354
|
-
type="range"
|
|
355
|
-
min="0"
|
|
356
|
-
max="100"
|
|
357
|
-
class="cpub-range"
|
|
358
|
-
:aria-label="`Skill ${index + 1} proficiency`"
|
|
359
|
-
/>
|
|
360
|
-
<span class="cpub-skill-value">{{ skill.proficiency }}%</span>
|
|
361
|
-
</div>
|
|
358
|
+
<input
|
|
359
|
+
v-model="skills[index]"
|
|
360
|
+
type="text"
|
|
361
|
+
class="cpub-input"
|
|
362
|
+
placeholder="Skill name"
|
|
363
|
+
:aria-label="`Skill ${index + 1}`"
|
|
364
|
+
/>
|
|
362
365
|
<button
|
|
363
366
|
type="button"
|
|
364
367
|
class="cpub-btn-icon cpub-btn-danger"
|
|
365
|
-
:aria-label="`Remove skill ${
|
|
368
|
+
:aria-label="`Remove skill ${skills[index] || index + 1}`"
|
|
366
369
|
@click="removeSkill(index)"
|
|
367
370
|
>
|
|
368
371
|
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
|
@@ -416,16 +419,6 @@ async function handleSave(): Promise<void> {
|
|
|
416
419
|
/>
|
|
417
420
|
</div>
|
|
418
421
|
|
|
419
|
-
<div class="cpub-form-group">
|
|
420
|
-
<label for="social-website" class="cpub-form-label">Website URL</label>
|
|
421
|
-
<input
|
|
422
|
-
id="social-website"
|
|
423
|
-
v-model="socialLinks.website"
|
|
424
|
-
type="url"
|
|
425
|
-
class="cpub-input"
|
|
426
|
-
placeholder="https://..."
|
|
427
|
-
/>
|
|
428
|
-
</div>
|
|
429
422
|
</div>
|
|
430
423
|
|
|
431
424
|
<!-- Experience -->
|
|
@@ -434,7 +427,7 @@ async function handleSave(): Promise<void> {
|
|
|
434
427
|
|
|
435
428
|
<div
|
|
436
429
|
v-for="(entry, index) in experience"
|
|
437
|
-
:key="
|
|
430
|
+
:key="index"
|
|
438
431
|
class="cpub-experience-card"
|
|
439
432
|
>
|
|
440
433
|
<div class="cpub-experience-header">
|
|
@@ -451,9 +444,9 @@ async function handleSave(): Promise<void> {
|
|
|
451
444
|
|
|
452
445
|
<div class="cpub-experience-fields">
|
|
453
446
|
<div class="cpub-form-group">
|
|
454
|
-
<label :for="`exp-title-${
|
|
447
|
+
<label :for="`exp-title-${index}`" class="cpub-form-label">Title</label>
|
|
455
448
|
<input
|
|
456
|
-
:id="`exp-title-${
|
|
449
|
+
:id="`exp-title-${index}`"
|
|
457
450
|
v-model="entry.title"
|
|
458
451
|
type="text"
|
|
459
452
|
class="cpub-input"
|
|
@@ -462,9 +455,9 @@ async function handleSave(): Promise<void> {
|
|
|
462
455
|
</div>
|
|
463
456
|
|
|
464
457
|
<div class="cpub-form-group">
|
|
465
|
-
<label :for="`exp-company-${
|
|
458
|
+
<label :for="`exp-company-${index}`" class="cpub-form-label">Company</label>
|
|
466
459
|
<input
|
|
467
|
-
:id="`exp-company-${
|
|
460
|
+
:id="`exp-company-${index}`"
|
|
468
461
|
v-model="entry.company"
|
|
469
462
|
type="text"
|
|
470
463
|
class="cpub-input"
|
|
@@ -474,18 +467,18 @@ async function handleSave(): Promise<void> {
|
|
|
474
467
|
|
|
475
468
|
<div class="cpub-experience-dates">
|
|
476
469
|
<div class="cpub-form-group">
|
|
477
|
-
<label :for="`exp-start-${
|
|
470
|
+
<label :for="`exp-start-${index}`" class="cpub-form-label">Start Date</label>
|
|
478
471
|
<input
|
|
479
|
-
:id="`exp-start-${
|
|
472
|
+
:id="`exp-start-${index}`"
|
|
480
473
|
v-model="entry.startDate"
|
|
481
474
|
type="month"
|
|
482
475
|
class="cpub-input"
|
|
483
476
|
/>
|
|
484
477
|
</div>
|
|
485
478
|
<div class="cpub-form-group">
|
|
486
|
-
<label :for="`exp-end-${
|
|
479
|
+
<label :for="`exp-end-${index}`" class="cpub-form-label">End Date</label>
|
|
487
480
|
<input
|
|
488
|
-
:id="`exp-end-${
|
|
481
|
+
:id="`exp-end-${index}`"
|
|
489
482
|
v-model="entry.endDate"
|
|
490
483
|
type="month"
|
|
491
484
|
class="cpub-input"
|
|
@@ -495,9 +488,9 @@ async function handleSave(): Promise<void> {
|
|
|
495
488
|
</div>
|
|
496
489
|
|
|
497
490
|
<div class="cpub-form-group">
|
|
498
|
-
<label :for="`exp-desc-${
|
|
491
|
+
<label :for="`exp-desc-${index}`" class="cpub-form-label">Description</label>
|
|
499
492
|
<textarea
|
|
500
|
-
:id="`exp-desc-${
|
|
493
|
+
:id="`exp-desc-${index}`"
|
|
501
494
|
v-model="entry.description"
|
|
502
495
|
class="cpub-textarea"
|
|
503
496
|
rows="3"
|
|
@@ -370,6 +370,25 @@ async function handleReport(): Promise<void> {
|
|
|
370
370
|
<p class="cpub-empty-state-title">No skills listed yet</p>
|
|
371
371
|
</div>
|
|
372
372
|
</div>
|
|
373
|
+
|
|
374
|
+
<!-- Experience -->
|
|
375
|
+
<div v-if="(p as Record<string, unknown>).experience && ((p as Record<string, unknown>).experience as Array<Record<string, string>>).length > 0" class="cpub-experience-section">
|
|
376
|
+
<div class="cpub-sec-head">
|
|
377
|
+
<h2><i class="fa-solid fa-briefcase" style="color: var(--purple); margin-right: 6px"></i>Experience</h2>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="cpub-experience-list">
|
|
380
|
+
<div v-for="(exp, idx) in ((p as Record<string, unknown>).experience as Array<Record<string, string>>)" :key="idx" class="cpub-experience-item">
|
|
381
|
+
<div class="cpub-exp-header">
|
|
382
|
+
<strong>{{ exp.title }}</strong>
|
|
383
|
+
<span v-if="exp.company" class="cpub-exp-company">{{ exp.company }}</span>
|
|
384
|
+
</div>
|
|
385
|
+
<div v-if="exp.startDate" class="cpub-exp-dates">
|
|
386
|
+
{{ exp.startDate }}{{ exp.endDate ? ` — ${exp.endDate}` : ' — Present' }}
|
|
387
|
+
</div>
|
|
388
|
+
<p v-if="exp.description" class="cpub-exp-desc">{{ exp.description }}</p>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
373
392
|
</div>
|
|
374
393
|
|
|
375
394
|
<!-- Sidebar -->
|
|
@@ -709,6 +728,20 @@ async function handleReport(): Promise<void> {
|
|
|
709
728
|
margin-bottom: 8px;
|
|
710
729
|
}
|
|
711
730
|
|
|
731
|
+
/* Experience */
|
|
732
|
+
.cpub-experience-section { margin-top: 24px; }
|
|
733
|
+
.cpub-experience-list { display: flex; flex-direction: column; gap: 16px; }
|
|
734
|
+
.cpub-experience-item {
|
|
735
|
+
padding: 12px 16px;
|
|
736
|
+
border: var(--border-width-default) solid var(--border2);
|
|
737
|
+
background: var(--surface);
|
|
738
|
+
}
|
|
739
|
+
.cpub-exp-header { display: flex; flex-direction: column; gap: 2px; }
|
|
740
|
+
.cpub-exp-header strong { font-size: var(--text-sm); }
|
|
741
|
+
.cpub-exp-company { font-size: var(--text-xs); color: var(--text-dim); }
|
|
742
|
+
.cpub-exp-dates { font-size: var(--text-xs); color: var(--text-faint); font-family: var(--font-mono); margin-top: 4px; }
|
|
743
|
+
.cpub-exp-desc { font-size: var(--text-sm); color: var(--text-dim); margin-top: 8px; line-height: var(--leading-normal); }
|
|
744
|
+
|
|
712
745
|
/* About grid */
|
|
713
746
|
.cpub-about-grid {
|
|
714
747
|
display: grid;
|