kyd-shared-badge 0.3.98 → 0.3.100
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 +1 -1
- package/src/SharedBadgeDisplay.tsx +18 -20
- package/src/components/GaugeCard.tsx +40 -9
- package/src/components/ReportHeader.tsx +22 -6
- package/src/components/RiskCard.tsx +12 -0
- package/src/components/RoleOverviewCard.tsx +114 -0
- package/src/components/SkillsBubble.tsx +77 -26
- package/src/connect/ConnectAccounts.tsx +5 -3
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ import UseCases from './components/UseCases';
|
|
|
27
27
|
import SummaryCards from './components/SummaryCards';
|
|
28
28
|
import GaugeCard from './components/GaugeCard';
|
|
29
29
|
import RiskCard from './components/RiskCard';
|
|
30
|
+
import RoleOverviewCard from './components/RoleOverviewCard';
|
|
30
31
|
import TopContributingFactors from './components/TopContributingFactors';
|
|
31
32
|
import AiUsageBody from './components/AiUsageBody';
|
|
32
33
|
import SanctionsMatches from './components/SanctionsMatches';
|
|
@@ -210,7 +211,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
210
211
|
{/* Quadrant layout */}
|
|
211
212
|
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
212
213
|
{/* Top-left: Name, countries, badge image handled by ReportHeader */}
|
|
213
|
-
<div>
|
|
214
|
+
<div className={`${hasEnterpriseMatch ? '' : 'md:col-span-2'}`}>
|
|
214
215
|
<ReportHeader
|
|
215
216
|
badgeId={badgeId}
|
|
216
217
|
developerName={badgeData.developerName}
|
|
@@ -226,25 +227,22 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
226
227
|
/>
|
|
227
228
|
</div>
|
|
228
229
|
{/* Top-right: Role match section */}
|
|
229
|
-
|
|
230
|
-
<div
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
);
|
|
246
|
-
})()}
|
|
247
|
-
</div>
|
|
230
|
+
{hasEnterpriseMatch && (
|
|
231
|
+
<div>
|
|
232
|
+
{(() => {
|
|
233
|
+
const em = assessmentResult?.enterprise_match;
|
|
234
|
+
if (!em) return null;
|
|
235
|
+
const role = em.role || {};
|
|
236
|
+
return (
|
|
237
|
+
<RoleOverviewCard
|
|
238
|
+
title={'Role Alignment'}
|
|
239
|
+
matchLabel={em.label}
|
|
240
|
+
roleName={role?.name || 'Role'}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
})()}
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
248
246
|
|
|
249
247
|
{/* Bottom-left: Technical score */}
|
|
250
248
|
<div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
4
|
import GaugeComponent from '@knowyourdeveloper/react-gauge-component';
|
|
5
5
|
import BusinessRuleLink from './BusinessRuleLink';
|
|
6
6
|
import { FiInfo } from 'react-icons/fi';
|
|
7
|
-
import { hexToRgba, scoreToColorHex,
|
|
7
|
+
import { hexToRgba, scoreToColorHex, clampPercent, red, yellow, green } from '../colors';
|
|
8
8
|
|
|
9
9
|
type TopMover = { label?: string; uid?: string };
|
|
10
10
|
|
|
@@ -29,20 +29,43 @@ export default function GaugeCard({
|
|
|
29
29
|
}) {
|
|
30
30
|
const pct = clampPercent(percent);
|
|
31
31
|
const displayLabel = label || '';
|
|
32
|
-
// Keep card tinting consistent with score
|
|
33
|
-
const progressColor = scoreToCssVar(pct);
|
|
34
32
|
const headerTint = hexToRgba(scoreToColorHex(pct), 0.06);
|
|
35
33
|
|
|
34
|
+
// Technical evidence tiers from backend thresholds
|
|
35
|
+
const tickLabels = useMemo(() => ({
|
|
36
|
+
type: 'outer' as const,
|
|
37
|
+
hideMinMax: false,
|
|
38
|
+
defaultTickLineConfig: {
|
|
39
|
+
length: 7,
|
|
40
|
+
width: 1,
|
|
41
|
+
distanceFromArc: 3,
|
|
42
|
+
color: 'var(--icon-button-secondary)'
|
|
43
|
+
},
|
|
44
|
+
defaultTickValueConfig: {
|
|
45
|
+
style: {
|
|
46
|
+
fontSize: '10px',
|
|
47
|
+
fill: 'var(--text-secondary)'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
ticks: [
|
|
51
|
+
{ value: 0, valueConfig: { formatTextValue: () => 'Very Low' } },
|
|
52
|
+
{ value: 25, valueConfig: { formatTextValue: () => 'Low' } },
|
|
53
|
+
{ value: 50, valueConfig: { formatTextValue: () => 'Moderate' } },
|
|
54
|
+
{ value: 75, valueConfig: { formatTextValue: () => 'High' } },
|
|
55
|
+
{ value: 100, valueConfig: { formatTextValue: () => 'Very High' } },
|
|
56
|
+
]
|
|
57
|
+
}), []);
|
|
58
|
+
|
|
36
59
|
return (
|
|
37
60
|
<div
|
|
38
|
-
className={'rounded-md
|
|
61
|
+
className={'rounded-md border flex flex-col min-h-full'}
|
|
39
62
|
style={{
|
|
40
63
|
backgroundColor: 'var(--content-card-background)',
|
|
41
64
|
borderColor: 'var(--icon-button-secondary)',
|
|
42
65
|
backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
|
|
43
66
|
}}
|
|
44
67
|
>
|
|
45
|
-
<div className="mb-3 flex items-start justify-between gap-2">
|
|
68
|
+
<div className="mb-3 flex items-start justify-between gap-2 pt-5 px-5">
|
|
46
69
|
<div>
|
|
47
70
|
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{title}</div>
|
|
48
71
|
{description ? (
|
|
@@ -63,13 +86,22 @@ export default function GaugeCard({
|
|
|
63
86
|
)}
|
|
64
87
|
</div>
|
|
65
88
|
<div className="flex-grow flex flex-col items-center justify-center" style={{ minHeight: 200 }}>
|
|
66
|
-
<div className="relative group" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth:
|
|
89
|
+
<div className="relative group" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 390 }}>
|
|
67
90
|
<GaugeComponent
|
|
68
91
|
type="semicircle"
|
|
69
|
-
style={{ width: '100%', height: '100%' }}
|
|
92
|
+
style={{ width: 'calc(100% - 16px)', height: '100%', marginLeft: 8, marginRight: 8 }}
|
|
93
|
+
marginInPercent={{ top: 0.08, bottom: 0.0, left: 0.1, right: 0.1 }}
|
|
70
94
|
value={pct}
|
|
71
95
|
minValue={0}
|
|
72
96
|
maxValue={100}
|
|
97
|
+
labels={{
|
|
98
|
+
valueLabel: {
|
|
99
|
+
// Hide center text; show tier labels around the arc instead
|
|
100
|
+
formatTextValue: () => '',
|
|
101
|
+
matchColorWithArc: true,
|
|
102
|
+
},
|
|
103
|
+
tickLabels: tickLabels,
|
|
104
|
+
}}
|
|
73
105
|
arc={{
|
|
74
106
|
padding: 0.02,
|
|
75
107
|
// Explicit subArcs ensure left->right map: red -> yellow -> green
|
|
@@ -91,7 +123,6 @@ export default function GaugeCard({
|
|
|
91
123
|
</div>
|
|
92
124
|
)}
|
|
93
125
|
</div>
|
|
94
|
-
<div className={'mt-3 text-sm font-semibold'} style={{ color: 'var(--text-main)' }}>{displayLabel}</div>
|
|
95
126
|
</div>
|
|
96
127
|
{Array.isArray(topMovers) && topMovers.length > 0 && (
|
|
97
128
|
<div className="mt-4 text-center">
|
|
@@ -51,7 +51,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
53
|
<div
|
|
54
|
-
className={'
|
|
54
|
+
className={'p-6 rounded-md shadow-md border'}
|
|
55
55
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', backgroundImage: `linear-gradient(${tint}, ${tint})` }}
|
|
56
56
|
>
|
|
57
57
|
{(() => {
|
|
@@ -73,14 +73,18 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
73
73
|
</div>
|
|
74
74
|
);
|
|
75
75
|
})()}
|
|
76
|
-
<div className="flex flex-col gap-
|
|
76
|
+
<div className="flex flex-col gap-3">
|
|
77
77
|
{/* Info section: Title, Candidate, Details and Summary */}
|
|
78
78
|
<div className="w-full">
|
|
79
79
|
<div className='space-y-2'>
|
|
80
80
|
<span className='flex gap-2 w-full items-end text-start justify-start'>
|
|
81
|
-
<h2 className={'text-xl font-light'} style={{ color: 'var(--text-main)' }}>KYD Candidate
|
|
81
|
+
<h2 className={'text-xl font-light'} style={{ color: 'var(--text-main)' }}>KYD Candidate:</h2>
|
|
82
82
|
<div className={'text-xl font-bold'} style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</div>
|
|
83
83
|
</span>
|
|
84
|
+
<span className='flex gap-2 w-full items-end text-start justify-start'>
|
|
85
|
+
<h3 className='text-sm font-semibold text-[--text-secondary]'>Date:</h3>
|
|
86
|
+
<span className='text-sm text-[--text-main]'>{formatLocalDate(updatedAt)}</span>
|
|
87
|
+
</span>
|
|
84
88
|
<div className={'text-sm'}>
|
|
85
89
|
{Array.isArray(countries) && countries.length > 0 && (
|
|
86
90
|
(() => {
|
|
@@ -88,9 +92,21 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
88
92
|
.map(code => countriesLib.getName((code || '').toUpperCase(), 'en') || code)
|
|
89
93
|
.filter(Boolean);
|
|
90
94
|
return (
|
|
91
|
-
<p>
|
|
92
|
-
<span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>
|
|
95
|
+
<p className={'flex items-center gap-1'}>
|
|
96
|
+
<span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>
|
|
97
|
+
{countryNames.length > 1 ? 'Countries:' : 'Country:'}
|
|
98
|
+
</span>
|
|
99
|
+
|
|
93
100
|
<span style={{ color: 'var(--text-main)' }}>{countryNames.join(', ')}</span>
|
|
101
|
+
<span className={'relative inline-flex items-center group cursor-help'} style={{ color: 'var(--text-secondary)' }}>
|
|
102
|
+
<FiInfo size={14} />
|
|
103
|
+
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
104
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
105
|
+
<div style={{ fontWeight: 600 }}>Countries</div>
|
|
106
|
+
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>Derived from linked sources and activity; shows where they’re most affiliated.</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</span>
|
|
94
110
|
</p>
|
|
95
111
|
);
|
|
96
112
|
})()
|
|
@@ -135,7 +151,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
135
151
|
{/* Badge Image with robust centered overlay */}
|
|
136
152
|
<div className="w-full flex items-center justify-center self-stretch">
|
|
137
153
|
<div className="relative w-full max-w-xs select-none">
|
|
138
|
-
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={
|
|
154
|
+
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={100} height={100} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
139
155
|
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
140
156
|
<div className="font-extrabold text-black text-3xl ">
|
|
141
157
|
{Math.round(score || 0)}%
|
|
@@ -89,6 +89,18 @@ export default function RiskCard({
|
|
|
89
89
|
}}
|
|
90
90
|
/>
|
|
91
91
|
))}
|
|
92
|
+
<span
|
|
93
|
+
className="pointer-events-none absolute left-[-65px] bottom-0 text-xs"
|
|
94
|
+
style={{ color: 'var(--text-secondary)' }}
|
|
95
|
+
>
|
|
96
|
+
Low risk -
|
|
97
|
+
</span>
|
|
98
|
+
<span
|
|
99
|
+
className="pointer-events-none absolute right-[-68px] top-0 text-xs"
|
|
100
|
+
style={{ color: 'var(--text-secondary)' }}
|
|
101
|
+
>
|
|
102
|
+
- High Risk
|
|
103
|
+
</span>
|
|
92
104
|
{(tooltipText || description) && (
|
|
93
105
|
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
94
106
|
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { FiInfo } from 'react-icons/fi';
|
|
6
|
+
import GaugeComponent from '@knowyourdeveloper/react-gauge-component';
|
|
7
|
+
import { clampPercent, hexToRgba, scoreToColorHex, red, yellow, green } from '../colors';
|
|
8
|
+
|
|
9
|
+
export default function RoleOverviewCard({
|
|
10
|
+
title,
|
|
11
|
+
matchLabel,
|
|
12
|
+
roleName,
|
|
13
|
+
}: {
|
|
14
|
+
title: string;
|
|
15
|
+
matchLabel?: string;
|
|
16
|
+
roleName?: string;
|
|
17
|
+
}) {
|
|
18
|
+
const pct = useMemo(() => {
|
|
19
|
+
const raw = String(matchLabel || '').toLowerCase();
|
|
20
|
+
let mapped = 0;
|
|
21
|
+
if (raw.includes('optimal')) mapped = 100;
|
|
22
|
+
else if (raw.includes('strong')) mapped = 75;
|
|
23
|
+
else if (raw.includes('partial')) mapped = 50;
|
|
24
|
+
else if (raw.includes('weak')) mapped = 25;
|
|
25
|
+
else mapped = 0; // incompatible or unknown
|
|
26
|
+
return clampPercent(mapped);
|
|
27
|
+
}, [matchLabel]);
|
|
28
|
+
|
|
29
|
+
const headerTint = hexToRgba(scoreToColorHex(pct), 0.06);
|
|
30
|
+
const displayLabel = String(matchLabel || '').trim();
|
|
31
|
+
|
|
32
|
+
const tickLabels = useMemo(() => ({
|
|
33
|
+
type: 'outer' as const,
|
|
34
|
+
hideMinMax: false,
|
|
35
|
+
defaultTickLineConfig: {
|
|
36
|
+
length: 7,
|
|
37
|
+
width: 1,
|
|
38
|
+
distanceFromArc: 3,
|
|
39
|
+
color: 'var(--icon-button-secondary)'
|
|
40
|
+
},
|
|
41
|
+
defaultTickValueConfig: {
|
|
42
|
+
style: {
|
|
43
|
+
fontSize: '10px',
|
|
44
|
+
fill: 'var(--text-secondary)'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
ticks: [
|
|
48
|
+
{ value: 0, valueConfig: { formatTextValue: () => 'Incompatible' } },
|
|
49
|
+
{ value: 25, valueConfig: { formatTextValue: () => 'Weak' } },
|
|
50
|
+
{ value: 50, valueConfig: { formatTextValue: () => 'Partial' } },
|
|
51
|
+
{ value: 75, valueConfig: { formatTextValue: () => 'Strong' } },
|
|
52
|
+
{ value: 100, valueConfig: { formatTextValue: () => 'Optimal' } },
|
|
53
|
+
]
|
|
54
|
+
}), []);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={'rounded-md p-5 border flex flex-col h-full'}
|
|
59
|
+
style={{
|
|
60
|
+
backgroundColor: 'var(--content-card-background)',
|
|
61
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
62
|
+
backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<div className="mb-3 flex items-start justify-between gap-2">
|
|
66
|
+
<div>
|
|
67
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{title}</div>
|
|
68
|
+
<div className={'text-xs mt-1'} style={{ color: 'var(--text-secondary)' }}>How well the candidate aligns with the target role based on KYD evidence.</div>
|
|
69
|
+
</div>
|
|
70
|
+
<span className={'relative inline-flex items-center group cursor-help'} style={{ color: 'var(--text-secondary)' }}>
|
|
71
|
+
<FiInfo />
|
|
72
|
+
<div className="hidden group-hover:block absolute z-30 right-0 top-full mt-2 w-80">
|
|
73
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
74
|
+
<div style={{ fontWeight: 600 }}>{title}</div>
|
|
75
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>Alignment reflects how well the candidate matches the target role based on KYD evidence.</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex-grow flex flex-col items-center justify-center" style={{ minHeight: 200 }}>
|
|
81
|
+
<div className="relative" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 360 }}>
|
|
82
|
+
<GaugeComponent
|
|
83
|
+
type="semicircle"
|
|
84
|
+
style={{ width: '100%', height: '100%' }}
|
|
85
|
+
value={pct}
|
|
86
|
+
minValue={0}
|
|
87
|
+
maxValue={100}
|
|
88
|
+
labels={{
|
|
89
|
+
valueLabel: {
|
|
90
|
+
formatTextValue: () => displayLabel,
|
|
91
|
+
matchColorWithArc: true,
|
|
92
|
+
},
|
|
93
|
+
tickLabels: tickLabels,
|
|
94
|
+
}}
|
|
95
|
+
arc={{
|
|
96
|
+
padding: 0.02,
|
|
97
|
+
gradient: true,
|
|
98
|
+
colorArray: [red, yellow, green],
|
|
99
|
+
}}
|
|
100
|
+
pointer={{ type: 'arrow', elastic: true, animationDelay: 0 }}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
{roleName ? (
|
|
104
|
+
<div className="mt-3 text-center">
|
|
105
|
+
<Link href="#role" className={'text-sm font-semibold text-[var(--text-main)] hover:underline'}>{roleName}</Link>
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
@@ -20,7 +20,7 @@ type HoverTooltipState = {
|
|
|
20
20
|
x: number;
|
|
21
21
|
y: number;
|
|
22
22
|
title: string;
|
|
23
|
-
body?:
|
|
23
|
+
body?: React.ReactNode;
|
|
24
24
|
} | null;
|
|
25
25
|
|
|
26
26
|
const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
@@ -50,7 +50,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; years?: number; sources?: string[] }>; headless?: boolean }) {
|
|
53
|
+
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[] }>; headless?: boolean }) {
|
|
54
54
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
55
55
|
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
56
56
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -58,6 +58,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
58
58
|
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
59
59
|
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
60
60
|
const [legendHovered, setLegendHovered] = useState<boolean>(false);
|
|
61
|
+
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
if (typeof window !== 'undefined') {
|
|
63
64
|
const id = window.setTimeout(() => {
|
|
@@ -77,7 +78,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
77
78
|
const y = rect.bottom - hostRect.top + 6;
|
|
78
79
|
return { x, y };
|
|
79
80
|
};
|
|
80
|
-
const showLegendTooltipAt = (target: EventTarget | null, title: string, body?:
|
|
81
|
+
const showLegendTooltipAt = (target: EventTarget | null, title: string, body?: React.ReactNode) => {
|
|
81
82
|
if (!(target instanceof HTMLElement)) return;
|
|
82
83
|
const { x, y } = computeTooltipPosition(target);
|
|
83
84
|
setLegendTooltip({ visible: true, x, y, title, body });
|
|
@@ -144,15 +145,16 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
144
145
|
const enriched = list.map((name) => {
|
|
145
146
|
const meta = skillsMeta?.[name] || {};
|
|
146
147
|
const sources = Array.isArray((meta as any).sources) ? (meta as any).sources as string[] : [];
|
|
147
|
-
|
|
148
|
+
const presenceTypes = Array.isArray((meta as any).presenceTypes) ? ((meta as any).presenceTypes as Array<'certified' | 'observed' | 'self-reported'>) : ((meta as any).presence ? [String((meta as any).presence) as any] : []);
|
|
149
|
+
return { name, years: Number(meta.years || 0), presence: (meta as any).presence as string | undefined, presenceTypes, sources };
|
|
148
150
|
}).sort((a, b) => b.years - a.years || a.name.localeCompare(b.name));
|
|
149
151
|
const items = enriched.slice(0, 10);
|
|
150
152
|
const overflow = list.length - items.length;
|
|
151
|
-
const display: Array<{ label: string; years?: number; presence?: string; sources?: string[] } | ''> = [];
|
|
153
|
+
const display: Array<{ label: string; years?: number; presence?: string; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; sources?: string[] } | ''> = [];
|
|
152
154
|
for (let i = 0; i < Math.min(9, items.length); i++) {
|
|
153
155
|
const it = items[i];
|
|
154
156
|
const label = it.name ? `${it.name}` : '';
|
|
155
|
-
display.push({ label, years: it.years, presence: it.presence, sources: it.sources });
|
|
157
|
+
display.push({ label, years: it.years, presence: it.presence, presenceTypes: it.presenceTypes, sources: it.sources });
|
|
156
158
|
}
|
|
157
159
|
if (list.length > 10) {
|
|
158
160
|
// Aggregate years and sources for remaining datapoints beyond the top 10
|
|
@@ -168,11 +170,11 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
168
170
|
display.push({ label: `Others (${overflow})`, years: aggregatedYears, sources: aggregatedSources });
|
|
169
171
|
} else if (items.length >= 10) {
|
|
170
172
|
const it = items[9];
|
|
171
|
-
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence, sources: it?.sources });
|
|
173
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence, presenceTypes: it?.presenceTypes, sources: it?.sources });
|
|
172
174
|
} else {
|
|
173
175
|
if (items.length > 9) {
|
|
174
176
|
const it = items[9];
|
|
175
|
-
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence, sources: it?.sources });
|
|
177
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence, presenceTypes: it?.presenceTypes, sources: it?.sources });
|
|
176
178
|
}
|
|
177
179
|
}
|
|
178
180
|
while (display.length < 10) display.push('');
|
|
@@ -193,17 +195,41 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
193
195
|
const presenceColor = (presence?: string) => {
|
|
194
196
|
const p = String(presence || '').toLowerCase();
|
|
195
197
|
if (p === 'self-reported') return green5;
|
|
196
|
-
if (p === 'observed') return
|
|
198
|
+
if (p === 'observed') return green2;
|
|
197
199
|
if (p === 'certified') return green1;
|
|
198
200
|
return 'var(--icon-button-secondary)';
|
|
199
201
|
};
|
|
200
202
|
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const presenceLegendTooltip = (): { title: string; body: React.ReactNode } => {
|
|
204
|
+
const Row = ({ color, label }: { color: string; label: string }) => (
|
|
205
|
+
<div className="flex items-center gap-2">
|
|
206
|
+
<span className="inline-block h-2 w-2 rounded-full" style={{ background: color }} />
|
|
207
|
+
<span>{label}</span>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
return {
|
|
211
|
+
title: 'Presence types',
|
|
212
|
+
body: (
|
|
213
|
+
<div className="grid gap-1">
|
|
214
|
+
<Row color={presenceColor('certified')} label="Certified — Verified by credential issuers." />
|
|
215
|
+
<Row color={presenceColor('observed')} label="Observed — Evidence directly from code and repos." />
|
|
216
|
+
<Row color={presenceColor('self-reported')} label="Self-reported — Claims (bios, profiles, resumes)." />
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const experienceLegendTooltip = (): { title: string; body: React.ReactNode } => {
|
|
223
|
+
return {
|
|
224
|
+
title: 'Experience',
|
|
225
|
+
body: (
|
|
226
|
+
<div className="grid gap-1 mt-2">
|
|
227
|
+
<div>
|
|
228
|
+
Calculated as time between first and most recent observed evidence across repositories.
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
};
|
|
207
233
|
};
|
|
208
234
|
|
|
209
235
|
const leftColumnGrid = useMemo(() => (skillsGrid || []).slice(0, 5), [skillsGrid]);
|
|
@@ -211,7 +237,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
211
237
|
|
|
212
238
|
if (!hasRadar) return null;
|
|
213
239
|
|
|
214
|
-
const columnComponent = (entry: { label: string; years?: number; presence?: string; sources?: string[] } | '', idx: number, isLeft: boolean) => {
|
|
240
|
+
const columnComponent = (entry: { label: string; years?: number; presence?: string; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; sources?: string[] } | '', idx: number, isLeft: boolean) => {
|
|
215
241
|
return (
|
|
216
242
|
<div key={idx} className="flex items-stretch justify-between gap-3 min-w-0">
|
|
217
243
|
<div className="flex flex-col min-w-0 justify-center">
|
|
@@ -255,23 +281,48 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
255
281
|
</span>
|
|
256
282
|
</div>
|
|
257
283
|
{entry && typeof entry !== 'string' ? (
|
|
258
|
-
<div
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
284
|
+
<div
|
|
285
|
+
className="flex flex-col items-end leading-tight h-full justify-start text-base"
|
|
286
|
+
>
|
|
287
|
+
<div className="pb-1">
|
|
288
|
+
{entry.years ? (
|
|
263
289
|
<span
|
|
264
|
-
className="whitespace-nowrap text-
|
|
290
|
+
className="whitespace-nowrap text-[var(--text-secondary)] underline decoration-dotted underline-offset-2 cursor-help"
|
|
265
291
|
onMouseEnter={(e) => {
|
|
266
|
-
const copy =
|
|
292
|
+
const copy = experienceLegendTooltip();
|
|
267
293
|
showLegendTooltipAt(e.currentTarget, copy.title, copy.body);
|
|
268
294
|
}}
|
|
269
295
|
onMouseLeave={hideLegendTooltip}
|
|
270
296
|
>
|
|
271
|
-
{entry.
|
|
297
|
+
{`${entry.years} Years`}
|
|
272
298
|
</span>
|
|
273
|
-
|
|
274
|
-
|
|
299
|
+
) : (
|
|
300
|
+
<span className="opacity-0 whitespace-nowrap text-[var(--text-secondary)]">0 Years</span>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
<div
|
|
304
|
+
onMouseEnter={(e) => {
|
|
305
|
+
const copy = presenceLegendTooltip();
|
|
306
|
+
showLegendTooltipAt(e.currentTarget, copy.title, copy.body);
|
|
307
|
+
}}
|
|
308
|
+
onMouseLeave={hideLegendTooltip}
|
|
309
|
+
className="pt-1"
|
|
310
|
+
>
|
|
311
|
+
{(() => {
|
|
312
|
+
const types = Array.isArray(entry.presenceTypes) ? entry.presenceTypes : (entry.presence ? [String(entry.presence) as any] : []);
|
|
313
|
+
const hasAny = types.length > 0;
|
|
314
|
+
return hasAny ? (
|
|
315
|
+
<div
|
|
316
|
+
className="flex items-center gap-1"
|
|
317
|
+
|
|
318
|
+
>
|
|
319
|
+
{types.map((t) => (
|
|
320
|
+
<span key={t} className="inline-block h-2 w-2 rounded-full" style={{ background: presenceColor(t) }} />
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
) : <span className="opacity-0 whitespace-nowrap">.</span>;
|
|
324
|
+
})()}
|
|
325
|
+
</div>
|
|
275
326
|
</div>
|
|
276
327
|
) : null}
|
|
277
328
|
</div>
|
|
@@ -242,10 +242,12 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
242
242
|
</div>
|
|
243
243
|
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>How your data is handled</h3>
|
|
244
244
|
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-2 leading-relaxed">
|
|
245
|
-
|
|
245
|
+
We understand that giving access to your private repositories can be a bit scary. So here's the deal: We install the KYD GitHub App in your account. The app has
|
|
246
|
+
<span className="mx-1"><Link href="https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-contents" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>Contents <ExternalLink className="size-3 inline-block" /></Link></span>
|
|
247
|
+
read access - only to the repositories you select. Then, once you request a badge assessment, we read the repositories and analyze the code, then its deleted, forever. Your code is not accessible to anyone, not even us.
|
|
246
248
|
</p>
|
|
247
249
|
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-3 leading-relaxed">
|
|
248
|
-
For details, see our{' '}
|
|
250
|
+
For other details, see our{' '}
|
|
249
251
|
<Link href="https://www.knowyourdeveloper.ai/privacy-policy" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>
|
|
250
252
|
Privacy Policy <ExternalLink className="size-3 inline-block ml-1" />
|
|
251
253
|
</Link>.
|
|
@@ -368,7 +370,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
368
370
|
onClick={() => setShowDataHandling(true)}
|
|
369
371
|
className="sm:text-sm text-xs underline text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)]"
|
|
370
372
|
>
|
|
371
|
-
|
|
373
|
+
How KYD Handles Your Data
|
|
372
374
|
</button>
|
|
373
375
|
</p>
|
|
374
376
|
<div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
|