kyd-shared-badge 0.3.87 → 0.3.89
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 +2 -1
- package/src/components/ReportHeader.tsx +17 -19
- package/src/components/SkillsBubble.tsx +94 -45
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.89",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@aws-sdk/lib-dynamodb": "^3.893.0",
|
|
22
22
|
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
|
+
"@knowyourdeveloper/react-bubble-chart": "^1.0.1",
|
|
24
25
|
"@radix-ui/react-slot": "^1.2.3",
|
|
25
26
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
26
27
|
"ai": "5.0.47",
|
|
@@ -73,29 +73,15 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
73
73
|
</div>
|
|
74
74
|
);
|
|
75
75
|
})()}
|
|
76
|
-
<div className="flex flex-col
|
|
77
|
-
{/*
|
|
78
|
-
<div className="w-full
|
|
79
|
-
<div className=
|
|
80
|
-
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
81
|
-
{/* Centered overlay slightly lower on Y axis, responsive and readable */}
|
|
82
|
-
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
83
|
-
<div className="font-extrabold text-black text-3xl " >
|
|
84
|
-
{Math.round(score || 0)}%
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
{/* Right Half: Title, Candidate, Details and Summary section */}
|
|
91
|
-
<div className="w-full md:w-2/3">
|
|
92
|
-
<div className={'space-y-4'}>
|
|
76
|
+
<div className="flex flex-col gap-6">
|
|
77
|
+
{/* Info section: Title, Candidate, Details and Summary */}
|
|
78
|
+
<div className="w-full">
|
|
79
|
+
<div className='space-y-2'>
|
|
93
80
|
<span className='flex gap-2 w-full items-end text-start justify-start'>
|
|
94
81
|
<h2 className={'text-xl font-light'} style={{ color: 'var(--text-main)' }}>KYD Candidate Report:</h2>
|
|
95
82
|
<div className={'text-xl font-bold'} style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</div>
|
|
96
83
|
</span>
|
|
97
|
-
<div className={'text-sm
|
|
98
|
-
|
|
84
|
+
<div className={'text-sm'}>
|
|
99
85
|
{Array.isArray(countries) && countries.length > 0 && (
|
|
100
86
|
(() => {
|
|
101
87
|
const countryNames = countries
|
|
@@ -145,6 +131,18 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
145
131
|
)}
|
|
146
132
|
</div>
|
|
147
133
|
</div>
|
|
134
|
+
|
|
135
|
+
{/* Badge Image with robust centered overlay */}
|
|
136
|
+
<div className="w-full flex items-center justify-center self-stretch">
|
|
137
|
+
<div className="relative w-full max-w-xs select-none">
|
|
138
|
+
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
139
|
+
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
140
|
+
<div className="font-extrabold text-black text-3xl ">
|
|
141
|
+
{Math.round(score || 0)}%
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
148
146
|
</div>
|
|
149
147
|
</div>
|
|
150
148
|
);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { BubbleChart } from '@knowyourdeveloper/react-bubble-chart';
|
|
5
|
+
import '@knowyourdeveloper/react-bubble-chart/style.css';
|
|
5
6
|
|
|
6
7
|
type SkillsRadarPoint = {
|
|
7
8
|
axis: string;
|
|
8
9
|
observed?: number;
|
|
9
10
|
self_reported?: number;
|
|
10
11
|
certified?: number;
|
|
12
|
+
experience?: number; // 0-100 saturation driver
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
type HoverTooltipState = {
|
|
@@ -47,8 +49,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
47
49
|
|
|
48
50
|
export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
49
51
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
50
|
-
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0,
|
|
51
|
-
|
|
52
|
+
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
52
53
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
53
54
|
const legendRef = useRef<HTMLDivElement>(null);
|
|
54
55
|
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
@@ -62,61 +63,98 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
62
63
|
}
|
|
63
64
|
}, []);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// ratio drives size: average of observed/self_reported/certified
|
|
67
|
+
const bubbles = useMemo(() => {
|
|
68
|
+
const seriesAvg = (d: SkillsRadarPoint): number => {
|
|
67
69
|
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
68
70
|
const nonZero = vals.filter((v) => v > 0);
|
|
69
71
|
const base = (nonZero.length > 0 ? nonZero : vals);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const maxValue = Math.max(1, ...skillsRadarLimited.map(seriesAvg));
|
|
76
|
+
|
|
77
|
+
return skillsRadarLimited.map((d) => {
|
|
78
|
+
const value = seriesAvg(d);
|
|
79
|
+
const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
|
|
80
|
+
const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
|
|
81
|
+
// map experience to saturation in HSL; keep hue constant for brand-neutral look
|
|
82
|
+
const saturation = Math.max(20, Math.min(100, experience));
|
|
83
|
+
const color = `hsl(210 ${saturation}% 45%)`;
|
|
84
|
+
return {
|
|
85
|
+
label: d.axis,
|
|
86
|
+
value: size,
|
|
87
|
+
color,
|
|
88
|
+
tooltip: `${d.axis}\nRatio: ${value}\nExperience: ${experience}`,
|
|
89
|
+
data: { ratio: value, experience }
|
|
90
|
+
};
|
|
72
91
|
});
|
|
73
92
|
}, [skillsRadarLimited]);
|
|
74
93
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
const bubbleData = useMemo(() => {
|
|
95
|
+
return bubbles.map((b) => ({
|
|
96
|
+
_id: String(b.label || ''),
|
|
97
|
+
value: Number(b.value || 0),
|
|
98
|
+
displayText: String(b.label || ''),
|
|
99
|
+
colorValue: Number(b.data?.experience || 0)
|
|
100
|
+
}));
|
|
101
|
+
}, [bubbles]);
|
|
102
|
+
|
|
103
|
+
const colorLegend = useMemo(() => {
|
|
104
|
+
const steps = [20, 35, 50, 65, 80, 90, 100];
|
|
105
|
+
return steps.map((s) => `hsl(210 ${s}% 45%)`);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const percentLegend = useMemo(() => {
|
|
109
|
+
const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
|
|
110
|
+
return bubbles
|
|
111
|
+
.map((b) => ({ label: b.label, percent: total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0, experience: b.data?.experience || 0 }))
|
|
112
|
+
.sort((a, b) => b.percent - a.percent);
|
|
113
|
+
}, [bubbles]);
|
|
82
114
|
|
|
83
115
|
if (!hasRadar) return null;
|
|
84
116
|
|
|
85
117
|
return (
|
|
86
118
|
<div className={'kyd-avoid-break'}>
|
|
87
119
|
<div ref={containerRef} style={{ width: '100%', height: 340 }}>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
<BubbleChart
|
|
121
|
+
data={bubbleData}
|
|
122
|
+
legend={false}
|
|
123
|
+
tooltip
|
|
124
|
+
colorLegend={colorLegend}
|
|
125
|
+
fixedDomain={{ min: 0, max: 100 }}
|
|
126
|
+
onClick={(d) => {
|
|
127
|
+
try {
|
|
128
|
+
if (typeof window !== 'undefined') {
|
|
129
|
+
const label = String(d.displayText || d._id || '');
|
|
130
|
+
const anchor = `#appendix-skills-cat-${encodeURIComponent(label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
|
|
131
|
+
const url = `#appendix${anchor}`;
|
|
132
|
+
window.location.hash = url;
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
}}
|
|
136
|
+
tooltipFunc={(node, d) => {
|
|
137
|
+
try {
|
|
138
|
+
node.innerHTML = '';
|
|
139
|
+
const title = document.createElement('div');
|
|
140
|
+
title.style.fontWeight = '600';
|
|
141
|
+
title.textContent = String(d.displayText || d._id || '');
|
|
142
|
+
const body = document.createElement('div');
|
|
143
|
+
body.style.fontSize = '12px';
|
|
144
|
+
body.style.color = 'var(--text-secondary)';
|
|
145
|
+
const ratio = Number(d.value || 0);
|
|
146
|
+
const experience = Number(d.colorValue || 0);
|
|
147
|
+
body.textContent = `${ratio}% ratio • Experience ${experience}`;
|
|
148
|
+
node.appendChild(title);
|
|
149
|
+
node.appendChild(body);
|
|
150
|
+
} catch {}
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
114
153
|
</div>
|
|
115
|
-
{/* Legend */}
|
|
116
154
|
<div className={'mt-3'}>
|
|
117
155
|
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
118
156
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
|
|
119
|
-
{
|
|
157
|
+
{percentLegend.map((item, idx) => (
|
|
120
158
|
<button
|
|
121
159
|
key={idx}
|
|
122
160
|
className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
|
|
@@ -135,7 +173,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
135
173
|
if (!rect) return;
|
|
136
174
|
const x = e.clientX - rect.left + 12;
|
|
137
175
|
const y = e.clientY - rect.top + 12;
|
|
138
|
-
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label}
|
|
176
|
+
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} • ${item.percent}% of ratio • Experience ${item.experience}` });
|
|
139
177
|
}}
|
|
140
178
|
onMouseMove={(e) => {
|
|
141
179
|
if (!legendTooltip || !legendRef.current) return;
|
|
@@ -144,13 +182,24 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
144
182
|
}}
|
|
145
183
|
onMouseLeave={() => setLegendTooltip(null)}
|
|
146
184
|
>
|
|
147
|
-
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor:
|
|
185
|
+
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: `hsl(210 ${Math.max(20, Math.min(100, item.experience))}% 45%)`, flexShrink: 0 }} />
|
|
148
186
|
<span className="truncate">{item.label}</span>
|
|
149
187
|
<span className="ml-auto opacity-80">{item.percent}%</span>
|
|
150
188
|
</button>
|
|
151
189
|
))}
|
|
152
190
|
</div>
|
|
153
191
|
{!headless && <TooltipBox state={legendTooltip} />}
|
|
192
|
+
{/* Legends */}
|
|
193
|
+
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 80% 45%)' }} />
|
|
196
|
+
<span>Bubble size: relative ratio of observed/self-reported/certified</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 20% 45%)' }} />
|
|
200
|
+
<span>Color saturation: experience (darker = more experienced)</span>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
154
203
|
</div>
|
|
155
204
|
</div>
|
|
156
205
|
</div>
|
package/src/types.ts
CHANGED
|
@@ -378,6 +378,8 @@ export interface GraphInsightsPayload {
|
|
|
378
378
|
observed?: number; // 0-100
|
|
379
379
|
self_reported?: number; // 0-100
|
|
380
380
|
certified?: number; // 0-100
|
|
381
|
+
// New: experience metric (0-100) for color saturation
|
|
382
|
+
experience?: number;
|
|
381
383
|
}>;
|
|
382
384
|
// New: Flattened list of business rule selections (for appendix)
|
|
383
385
|
business_rules_all?: Array<{
|