kyd-shared-badge 0.3.85 → 0.3.87
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/PrintableBadgeDisplay.tsx +3 -2
- package/src/SharedBadgeDisplay.tsx +90 -22
- package/src/components/ReportHeader.tsx +1 -11
- package/src/components/Skills.tsx +25 -60
- package/src/components/SkillsBubble.tsx +160 -0
- package/src/components/SkillsValidation.tsx +37 -0
- package/src/components/SummaryCards.tsx +3 -13
- package/src/connect/ConnectAccounts.tsx +255 -215
package/package.json
CHANGED
|
@@ -17,7 +17,8 @@ import GaugeCard from './components/GaugeCard';
|
|
|
17
17
|
import RiskCard from './components/RiskCard';
|
|
18
18
|
|
|
19
19
|
import { yellow, green } from './colors';
|
|
20
|
-
import
|
|
20
|
+
import SkillsValidation from './components/SkillsValidation';
|
|
21
|
+
import SkillsBubble from './components/SkillsBubble';
|
|
21
22
|
import CategoryBars from './components/CategoryBars';
|
|
22
23
|
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
23
24
|
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
@@ -212,7 +213,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
212
213
|
<div className="pt-8 border-t kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
213
214
|
<h3 className={'text-xl font-bold mb-3 kyd-keep-with-next'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
214
215
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
215
|
-
<
|
|
216
|
+
<SkillsValidation skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
216
217
|
</div>
|
|
217
218
|
</div>
|
|
218
219
|
</Reveal>
|
|
@@ -15,7 +15,8 @@ import CodeIcon from './components/icons/code';
|
|
|
15
15
|
import RiskIcon from './components/icons/risk';
|
|
16
16
|
|
|
17
17
|
import { yellow } from './colors';
|
|
18
|
-
import
|
|
18
|
+
import SkillsBubble from './components/SkillsBubble';
|
|
19
|
+
import SkillsValidation from './components/SkillsValidation';
|
|
19
20
|
import CategoryBars from './components/CategoryBars';
|
|
20
21
|
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
21
22
|
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
@@ -24,6 +25,8 @@ import { formatLocalDateTime } from './utils/date';
|
|
|
24
25
|
import ChatWidget from './chat/ChatWidget';
|
|
25
26
|
import UseCases from './components/UseCases';
|
|
26
27
|
import SummaryCards from './components/SummaryCards';
|
|
28
|
+
import GaugeCard from './components/GaugeCard';
|
|
29
|
+
import RiskCard from './components/RiskCard';
|
|
27
30
|
import TopContributingFactors from './components/TopContributingFactors';
|
|
28
31
|
import AiUsageBody from './components/AiUsageBody';
|
|
29
32
|
import SanctionsMatches from './components/SanctionsMatches';
|
|
@@ -204,28 +207,93 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
204
207
|
const OverviewSection = () => (
|
|
205
208
|
<div className={`${wrapperMaxWidth} mx-auto mt-6`}>
|
|
206
209
|
<Reveal headless={isHeadless} offsetY={8} durationMs={500}>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
210
|
+
<div className={'rounded-xl shadow-xl p-6 sm:p-8 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
211
|
+
{/* Quadrant layout */}
|
|
212
|
+
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
213
|
+
{/* Top-left: Name, countries, badge image handled by ReportHeader */}
|
|
214
|
+
<div>
|
|
215
|
+
<ReportHeader
|
|
216
|
+
badgeId={badgeId}
|
|
217
|
+
developerName={badgeData.developerName}
|
|
218
|
+
updatedAt={updatedAt}
|
|
219
|
+
score={overallFinalPercent || 0}
|
|
220
|
+
isPublic={true}
|
|
221
|
+
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
222
|
+
summary={undefined}
|
|
223
|
+
enterpriseMatch={null}
|
|
224
|
+
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
225
|
+
accountAuthenticity={assessmentResult?.account_authenticity}
|
|
226
|
+
companyName={badgeData.companyName}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
{/* Top-right: Role match section */}
|
|
230
|
+
<div className={'rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
231
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Role Alignment</div>
|
|
232
|
+
{(() => {
|
|
233
|
+
const em = assessmentResult?.enterprise_match;
|
|
234
|
+
if (!em) return <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>No role match available.</div>;
|
|
235
|
+
const role = em.role || {};
|
|
236
|
+
return (
|
|
237
|
+
<div className={'space-y-2'}>
|
|
238
|
+
<div className={'flex items-center gap-2'}>
|
|
239
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{role?.name || 'Role'}</div>
|
|
240
|
+
<span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
|
|
241
|
+
</div>
|
|
242
|
+
{em.description ? (
|
|
243
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
|
|
244
|
+
) : null}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
})()}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Bottom-left: Technical score */}
|
|
251
|
+
<div>
|
|
252
|
+
{(() => {
|
|
253
|
+
const uiTech = graphInsights?.uiSummary?.technical || {};
|
|
254
|
+
const techPct = Math.round(Number(uiTech?.percent ?? 0));
|
|
255
|
+
const techLabel = uiTech?.label || 'EVIDENCE';
|
|
256
|
+
return (
|
|
257
|
+
<GaugeCard
|
|
258
|
+
title={'KYD Technical'}
|
|
259
|
+
description={'Composite of technical evidence; more right indicates stronger capability'}
|
|
260
|
+
percent={techPct}
|
|
261
|
+
label={techLabel}
|
|
262
|
+
topMovers={[]}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
})()}
|
|
266
|
+
</div>
|
|
267
|
+
{/* Bottom-right: Risk score */}
|
|
268
|
+
<div>
|
|
269
|
+
{(() => {
|
|
270
|
+
const uiRisk = graphInsights?.uiSummary?.risk || {};
|
|
271
|
+
const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
|
|
272
|
+
const riskLabel = uiRisk?.label || 'RISK';
|
|
273
|
+
return (
|
|
274
|
+
<RiskCard
|
|
275
|
+
title={'KYD Risk'}
|
|
276
|
+
description={'Lower bar height indicates lower risk exposure'}
|
|
277
|
+
percentGood={riskPctGood}
|
|
278
|
+
label={riskLabel}
|
|
279
|
+
topMovers={[]}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
})()}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
224
286
|
</Reveal>
|
|
287
|
+
|
|
288
|
+
{/* Full-width Skills bubble chart below quadrant */}
|
|
225
289
|
<div className={'rounded-xl shadow-xl p-6 sm:p-8 mt-6 border'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
226
|
-
<Reveal headless={isHeadless}
|
|
227
|
-
|
|
228
|
-
|
|
290
|
+
<Reveal headless={isHeadless}>
|
|
291
|
+
<div className={'kyd-avoid-break'}>
|
|
292
|
+
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
|
|
293
|
+
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
294
|
+
</div>
|
|
295
|
+
</Reveal>
|
|
296
|
+
<div className={'pt-6 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
229
297
|
Report Completed: {formatLocalDateTime(updatedAt)}
|
|
230
298
|
</div>
|
|
231
299
|
</div>
|
|
@@ -392,7 +460,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
392
460
|
<div className="pt-8 border-t kyd-avoid-break" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
393
461
|
<h3 className={'text-xl font-bold mb-3 kyd-keep-with-next'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
394
462
|
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
395
|
-
<
|
|
463
|
+
<SkillsValidation skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
396
464
|
</div>
|
|
397
465
|
</div>
|
|
398
466
|
</Reveal>
|
|
@@ -49,12 +49,6 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
49
49
|
const tint = hexToRgba(pickTint(score || 0), 0.06);
|
|
50
50
|
const matchLabel = enterpriseMatch?.label;
|
|
51
51
|
|
|
52
|
-
const formattedDate = updatedAt ? formatLocalDate(updatedAt, {
|
|
53
|
-
year: 'numeric',
|
|
54
|
-
month: 'long',
|
|
55
|
-
day: 'numeric',
|
|
56
|
-
}) : 'N/A';
|
|
57
|
-
|
|
58
52
|
return (
|
|
59
53
|
<div
|
|
60
54
|
className={'mb-8 p-6 rounded-xl shadow-lg border'}
|
|
@@ -101,11 +95,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
101
95
|
<div className={'text-xl font-bold'} style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</div>
|
|
102
96
|
</span>
|
|
103
97
|
<div className={'text-sm space-y-2'}>
|
|
104
|
-
|
|
105
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Requested By:</span> <span style={{ color: 'var(--text-main)' }}>{developerName || 'N/A'}</span></p>
|
|
106
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Organization:</span> <span style={{ color: 'var(--text-main)' }}>{companyName || 'Unaffiliated'}</span></p>
|
|
107
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Date Generated:</span> <span style={{ color: 'var(--text-main)' }}>{formattedDate}</span></p>
|
|
108
|
-
<p><span className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Report ID:</span> <span style={{ color: 'var(--text-main)' }}>{badgeId || 'N/A'}</span></p>
|
|
98
|
+
|
|
109
99
|
{Array.isArray(countries) && countries.length > 0 && (
|
|
110
100
|
(() => {
|
|
111
101
|
const countryNames = countries
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
YAxis,
|
|
12
12
|
CartesianGrid,
|
|
13
13
|
Tooltip,
|
|
14
|
+
ScatterChart,
|
|
15
|
+
Scatter,
|
|
16
|
+
ZAxis,
|
|
14
17
|
} from 'recharts';
|
|
15
18
|
|
|
16
19
|
|
|
@@ -265,69 +268,31 @@ const Skills = ({ skillsCategoryRadar, headless }: { skillsMatrix?: SkillsMatrix
|
|
|
265
268
|
}}
|
|
266
269
|
>
|
|
267
270
|
{(() => {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
const data = combinedBubbleData.map((d, idx) => ({ x: (idx % 4) + 1, y: Math.floor(idx / 4) + 1, value: d.value, label: d.label }));
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
+
const BubbleTooltip = ({ active, payload }: any) => {
|
|
274
|
+
if (!active || !payload || !payload.length) return null;
|
|
275
|
+
const p = payload[0]?.payload;
|
|
276
|
+
return (
|
|
277
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
278
|
+
<div style={{ fontWeight: 600 }}>{p?.label}</div>
|
|
279
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>Score: {p?.value}</div>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
275
283
|
return (
|
|
276
|
-
<
|
|
277
|
-
<
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<g
|
|
287
|
-
key={idx}
|
|
288
|
-
transform={`translate(${b.x},${b.y})`}
|
|
289
|
-
onMouseEnter={(e) => {
|
|
290
|
-
const rect = containerRef.current?.getBoundingClientRect();
|
|
291
|
-
if (!rect) return;
|
|
292
|
-
setFootprintChartTooltip({
|
|
293
|
-
visible: true,
|
|
294
|
-
x: e.clientX - rect.left + 12,
|
|
295
|
-
y: e.clientY - rect.top + 12,
|
|
296
|
-
title: b.label,
|
|
297
|
-
body: `Score: ${b.value}`,
|
|
298
|
-
});
|
|
299
|
-
}}
|
|
300
|
-
onMouseMove={(e) => {
|
|
301
|
-
if (!footprintChartTooltip || !containerRef.current) return;
|
|
302
|
-
const rect = containerRef.current.getBoundingClientRect();
|
|
303
|
-
setFootprintChartTooltip({ ...footprintChartTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
304
|
-
}}
|
|
305
|
-
onMouseLeave={() => setFootprintChartTooltip(null)}
|
|
306
|
-
>
|
|
307
|
-
<defs>
|
|
308
|
-
<clipPath id={clipId}>
|
|
309
|
-
<circle r={b.r} />
|
|
310
|
-
</clipPath>
|
|
311
|
-
</defs>
|
|
312
|
-
<circle r={b.r} fill={'var(--bubble-foreground)'} stroke={'var(--icon-button-secondary)'} />
|
|
313
|
-
<title>{`${b.label}: ${b.value}`}</title>
|
|
314
|
-
{canShowText && labelSpec.lines.length > 0 ? (
|
|
315
|
-
<g clipPath={`url(#${clipId})`} style={{ pointerEvents: 'none' }}>
|
|
316
|
-
{labelSpec.lines.map((line, li) => (
|
|
317
|
-
<text key={li} y={startY + li * (labelSpec.fontSize + lineGap)} fontSize={labelSpec.fontSize} textAnchor="middle" fill={'var(--bubble-background)'}>
|
|
318
|
-
{line}
|
|
319
|
-
</text>
|
|
320
|
-
))}
|
|
321
|
-
</g>
|
|
322
|
-
) : null}
|
|
323
|
-
</g>
|
|
324
|
-
);
|
|
325
|
-
})}
|
|
326
|
-
</g>
|
|
327
|
-
</svg>
|
|
284
|
+
<ResponsiveContainer>
|
|
285
|
+
<ScatterChart margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
|
|
286
|
+
<CartesianGrid strokeDasharray="3 3" stroke={'var(--icon-button-secondary)'} />
|
|
287
|
+
<XAxis type="number" dataKey="x" tick={false} axisLine={false} domain={[0, 5]} />
|
|
288
|
+
<YAxis type="number" dataKey="y" tick={false} axisLine={false} domain={[0, Math.ceil(data.length / 4) + 1]} />
|
|
289
|
+
<ZAxis dataKey="value" range={[80, 360]} />
|
|
290
|
+
<Tooltip content={<BubbleTooltip />} cursor={{ stroke: 'var(--icon-button-secondary)' }} />
|
|
291
|
+
<Scatter data={data} fill={'var(--bubble-foreground)'} />
|
|
292
|
+
</ScatterChart>
|
|
293
|
+
</ResponsiveContainer>
|
|
328
294
|
);
|
|
329
295
|
})()}
|
|
330
|
-
{!headless && <TooltipBox state={footprintChartTooltip} />}
|
|
331
296
|
</div>
|
|
332
297
|
{/* Legend */}
|
|
333
298
|
<div className={'mt-3'}>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
|
4
|
+
import { ResponsiveContainer, ScatterChart, Scatter, ZAxis, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
|
|
5
|
+
|
|
6
|
+
type SkillsRadarPoint = {
|
|
7
|
+
axis: string;
|
|
8
|
+
observed?: number;
|
|
9
|
+
self_reported?: number;
|
|
10
|
+
certified?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type HoverTooltipState = {
|
|
14
|
+
visible: boolean;
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
title: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
} | null;
|
|
20
|
+
|
|
21
|
+
const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
22
|
+
if (!state || !state.visible) return null;
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
left: state.x,
|
|
28
|
+
top: state.y,
|
|
29
|
+
pointerEvents: 'none',
|
|
30
|
+
background: 'var(--content-card-background)',
|
|
31
|
+
border: '1px solid var(--icon-button-secondary)',
|
|
32
|
+
color: 'var(--text-main)',
|
|
33
|
+
padding: 10,
|
|
34
|
+
borderRadius: 6,
|
|
35
|
+
minWidth: 250,
|
|
36
|
+
maxWidth: 320,
|
|
37
|
+
zIndex: 10,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<div style={{ fontWeight: 600 }}>{state.title}</div>
|
|
41
|
+
{state.body ? (
|
|
42
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{state.body}</div>
|
|
43
|
+
) : null}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
49
|
+
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
50
|
+
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
|
|
51
|
+
|
|
52
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const legendRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window !== 'undefined') {
|
|
58
|
+
const id = window.setTimeout(() => {
|
|
59
|
+
try { window.dispatchEvent(new Event('resize')); } catch {}
|
|
60
|
+
}, 0);
|
|
61
|
+
return () => window.clearTimeout(id);
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const combinedBubbleData = useMemo(() => {
|
|
66
|
+
return skillsRadarLimited.map((d) => {
|
|
67
|
+
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
68
|
+
const nonZero = vals.filter((v) => v > 0);
|
|
69
|
+
const base = (nonZero.length > 0 ? nonZero : vals);
|
|
70
|
+
const avg = Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1));
|
|
71
|
+
return { label: d.axis, value: avg };
|
|
72
|
+
});
|
|
73
|
+
}, [skillsRadarLimited]);
|
|
74
|
+
|
|
75
|
+
const legendData = useMemo(() => {
|
|
76
|
+
const total = combinedBubbleData.reduce((sum, d) => sum + d.value, 0);
|
|
77
|
+
return combinedBubbleData
|
|
78
|
+
.slice()
|
|
79
|
+
.sort((a, b) => b.value - a.value)
|
|
80
|
+
.map((d) => ({ label: d.label, percent: total > 0 ? Math.round((d.value / total) * 100) : 0 }));
|
|
81
|
+
}, [combinedBubbleData]);
|
|
82
|
+
|
|
83
|
+
if (!hasRadar) return null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={'kyd-avoid-break'}>
|
|
87
|
+
<div ref={containerRef} style={{ width: '100%', height: 340 }}>
|
|
88
|
+
{(() => {
|
|
89
|
+
const data = combinedBubbleData.map((d, idx) => ({ x: (idx % 4) + 1, y: Math.floor(idx / 4) + 1, value: d.value, label: d.label }));
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
const BubbleTooltip = ({ active, payload }: any) => {
|
|
92
|
+
if (!active || !payload || !payload.length) return null;
|
|
93
|
+
const p = payload[0]?.payload;
|
|
94
|
+
return (
|
|
95
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
96
|
+
<div style={{ fontWeight: 600 }}>{p?.label}</div>
|
|
97
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>Score: {p?.value}</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
return (
|
|
102
|
+
<ResponsiveContainer>
|
|
103
|
+
<ScatterChart margin={{ top: 8, right: 8, bottom: 8, left: 8 }}>
|
|
104
|
+
<CartesianGrid strokeDasharray={'3 3'} stroke={'var(--icon-button-secondary)'} />
|
|
105
|
+
<XAxis type={'number'} dataKey={'x'} tick={false} axisLine={false} domain={[0, 5]} />
|
|
106
|
+
<YAxis type={'number'} dataKey={'y'} tick={false} axisLine={false} domain={[0, Math.ceil(data.length / 4) + 1]} />
|
|
107
|
+
<ZAxis dataKey={'value'} range={[80, 360]} />
|
|
108
|
+
<Tooltip content={<BubbleTooltip />} cursor={{ stroke: 'var(--icon-button-secondary)' }} />
|
|
109
|
+
<Scatter data={data} fill={'var(--bubble-foreground)'} />
|
|
110
|
+
</ScatterChart>
|
|
111
|
+
</ResponsiveContainer>
|
|
112
|
+
);
|
|
113
|
+
})()}
|
|
114
|
+
</div>
|
|
115
|
+
{/* Legend */}
|
|
116
|
+
<div className={'mt-3'}>
|
|
117
|
+
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
118
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
|
|
119
|
+
{legendData.map((item, idx) => (
|
|
120
|
+
<button
|
|
121
|
+
key={idx}
|
|
122
|
+
className="flex items-center gap-2 text-xs text-left hover:underline underline-offset-2"
|
|
123
|
+
style={{ color: 'var(--text-secondary)', background: 'transparent' }}
|
|
124
|
+
onClick={() => {
|
|
125
|
+
try {
|
|
126
|
+
if (typeof window !== 'undefined') {
|
|
127
|
+
const anchor = `#appendix-skills-cat-${encodeURIComponent(item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
|
|
128
|
+
const url = `#appendix${anchor}`;
|
|
129
|
+
window.location.hash = url;
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
}}
|
|
133
|
+
onMouseEnter={(e) => {
|
|
134
|
+
const rect = legendRef.current?.getBoundingClientRect();
|
|
135
|
+
if (!rect) return;
|
|
136
|
+
const x = e.clientX - rect.left + 12;
|
|
137
|
+
const y = e.clientY - rect.top + 12;
|
|
138
|
+
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} represents ${item.percent}% of the Developer’s observable technical skills.` });
|
|
139
|
+
}}
|
|
140
|
+
onMouseMove={(e) => {
|
|
141
|
+
if (!legendTooltip || !legendRef.current) return;
|
|
142
|
+
const rect = legendRef.current.getBoundingClientRect();
|
|
143
|
+
setLegendTooltip({ ...legendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
144
|
+
}}
|
|
145
|
+
onMouseLeave={() => setLegendTooltip(null)}
|
|
146
|
+
>
|
|
147
|
+
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: 'var(--text-secondary)', flexShrink: 0 }} />
|
|
148
|
+
<span className="truncate">{item.label}</span>
|
|
149
|
+
<span className="ml-auto opacity-80">{item.percent}%</span>
|
|
150
|
+
</button>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
{!headless && <TooltipBox state={legendTooltip} />}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
|
|
5
|
+
|
|
6
|
+
type SkillsRadarPoint = {
|
|
7
|
+
axis: string;
|
|
8
|
+
observed?: number;
|
|
9
|
+
self_reported?: number;
|
|
10
|
+
certified?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function SkillsValidation({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
14
|
+
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 8);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={'rounded-lg p-4 border kyd-avoid-break'} style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
18
|
+
<h4 className={'font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills by Validation Type</h4>
|
|
19
|
+
<p className={'text-sm mb-4'} style={{ color: 'var(--text-secondary)' }}>The bar chart shows how each skill is supported by self-attested claims, observed practice, or certified evidence.</p>
|
|
20
|
+
<div style={{ height: 360 }}>
|
|
21
|
+
<ResponsiveContainer>
|
|
22
|
+
<BarChart data={skillsRadarLimited} margin={{ top: 8, right: 8, left: 8, bottom: 36 }}>
|
|
23
|
+
<CartesianGrid strokeDasharray="3 3" stroke={'var(--icon-button-secondary)'} />
|
|
24
|
+
<XAxis dataKey="axis" tick={{ fill: 'var(--text-secondary)', fontSize: 12 }} interval={0} angle={-20} textAnchor="end" height={50} />
|
|
25
|
+
<YAxis domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} />
|
|
26
|
+
<Tooltip contentStyle={{ background: 'var(--content-card-background)', border: `1px solid var(--icon-button-secondary)`, color: 'var(--text-main)' }} />
|
|
27
|
+
<Bar dataKey="observed" name="Observed" fill={'var(--bar-observed)'} isAnimationActive={!headless} />
|
|
28
|
+
<Bar dataKey="self_reported" name="Self-reported" fill={'var(--bar-self-reported)'} isAnimationActive={!headless} />
|
|
29
|
+
<Bar dataKey="certified" name="Certified" fill={'var(--bar-certified)'} isAnimationActive={!headless} />
|
|
30
|
+
</BarChart>
|
|
31
|
+
</ResponsiveContainer>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
@@ -20,12 +20,10 @@ export default function SummaryCards({ graphInsights, assessmentResult, topBusin
|
|
|
20
20
|
const riskLabel = uiRisk?.label || 'RISK';
|
|
21
21
|
const riskTop = uiRisk?.top_movers && uiRisk.top_movers.length > 0 ? uiRisk.top_movers : topBusinessForGenre('Risk');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const aiLabel = 'AI Transparency';
|
|
25
|
-
const aiTopMovers = ai?.key_findings || [];
|
|
23
|
+
// AI section removed per requirements
|
|
26
24
|
|
|
27
25
|
return (
|
|
28
|
-
<div className="grid grid-cols-1 sm:grid-cols-
|
|
26
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 *:min-h-full">
|
|
29
27
|
<GaugeCard
|
|
30
28
|
key={'technical-card'}
|
|
31
29
|
title={'KYD Technical'}
|
|
@@ -44,15 +42,7 @@ export default function SummaryCards({ graphInsights, assessmentResult, topBusin
|
|
|
44
42
|
topMoversTitle={'Top Score Movers'}
|
|
45
43
|
tooltipText={'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.'}
|
|
46
44
|
/>
|
|
47
|
-
|
|
48
|
-
key={'ai-card'}
|
|
49
|
-
title={'KYD AI (Beta)'}
|
|
50
|
-
description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
|
|
51
|
-
percent={ai?.transparency_score}
|
|
52
|
-
label={aiLabel}
|
|
53
|
-
topMovers={(aiTopMovers).map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
54
|
-
topMoversTitle={'Key Findings'}
|
|
55
|
-
/>
|
|
45
|
+
{/* AI card removed */}
|
|
56
46
|
</div>
|
|
57
47
|
);
|
|
58
48
|
}
|
|
@@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation';
|
|
|
3
3
|
import { ProviderIcon } from '../utils/provider';
|
|
4
4
|
import { normalizeLinkedInInput } from './linkedin';
|
|
5
5
|
import type { ConnectAccountsProps } from './types';
|
|
6
|
-
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings } from 'lucide-react';
|
|
6
|
+
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings, Shield, InfoIcon } from 'lucide-react';
|
|
7
7
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
8
|
import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
|
|
9
9
|
import Link from 'next/link';
|
|
@@ -38,7 +38,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
38
38
|
headerDescription,
|
|
39
39
|
requiredProviders,
|
|
40
40
|
companyName,
|
|
41
|
-
initialProviderId,
|
|
41
|
+
initialProviderId, // = process.env.NEXT_PUBLIC_STAGE === 'dev' ? 'githubapp' : undefined,
|
|
42
42
|
githubAppSlugId,
|
|
43
43
|
userId,
|
|
44
44
|
inviteId,
|
|
@@ -50,6 +50,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
50
50
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
51
51
|
const [isDisconnecting, setIsDisconnecting] = useState<string | null>(null);
|
|
52
52
|
const [showGithubManage, setShowGithubManage] = useState(false);
|
|
53
|
+
const [showDataHandling, setShowDataHandling] = useState(false);
|
|
53
54
|
|
|
54
55
|
const apiBase = apiGatewayUrl || (typeof process !== 'undefined' ? (process.env.NEXT_PUBLIC_API_GATEWAY_URL as string) : '');
|
|
55
56
|
const connectedIds = useMemo(
|
|
@@ -197,6 +198,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
197
198
|
setSelectedProviderIdAndCallback(null);
|
|
198
199
|
setLinkUrl('');
|
|
199
200
|
setShowGithubManage(false);
|
|
201
|
+
setShowDataHandling(false);
|
|
200
202
|
};
|
|
201
203
|
|
|
202
204
|
const cardVariants = {
|
|
@@ -204,6 +206,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
204
206
|
animate: { opacity: 1, y: 0 },
|
|
205
207
|
exit: { opacity: 0, y: -20 },
|
|
206
208
|
};
|
|
209
|
+
const fadeOnly = {
|
|
210
|
+
initial: { opacity: 0 },
|
|
211
|
+
animate: { opacity: 1 },
|
|
212
|
+
exit: { opacity: 0 },
|
|
213
|
+
};
|
|
207
214
|
|
|
208
215
|
// GitHub status helpers
|
|
209
216
|
const githubConnectedAccount = useMemo(() => {
|
|
@@ -216,14 +223,189 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
216
223
|
}, [githubConnectedAccount]);
|
|
217
224
|
|
|
218
225
|
return (
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
<
|
|
226
|
+
<AnimatePresence initial={false} mode="wait">
|
|
227
|
+
{showDataHandling ? (
|
|
228
|
+
<motion.div
|
|
229
|
+
key="data-handling-card"
|
|
230
|
+
className="rounded-xl border max-w-xl w-full"
|
|
231
|
+
initial="initial" animate="animate" exit="exit" variants={fadeOnly}
|
|
232
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
233
|
+
>
|
|
234
|
+
<div className="sm:p-6 p-4">
|
|
235
|
+
<button onClick={() => setShowDataHandling(false)} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
236
|
+
<ArrowLeft className="w-4 h-4" />
|
|
237
|
+
Back
|
|
238
|
+
</button>
|
|
239
|
+
<div className="text-center">
|
|
240
|
+
<div className="flex justify-center mb-4">
|
|
241
|
+
<InfoIcon className="w-10 h-10 text-[var(--text-main)]" />
|
|
242
|
+
</div>
|
|
243
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>How your data is handled</h3>
|
|
244
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-2 leading-relaxed">
|
|
245
|
+
Hey there! 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 - we only have read access. 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
|
+
</p>
|
|
247
|
+
<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{' '}
|
|
249
|
+
<Link href="https://www.knowyourdeveloper.ai/privacy-policy" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>
|
|
250
|
+
Privacy Policy <ExternalLink className="size-3 inline-block ml-1" />
|
|
251
|
+
</Link>.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</motion.div>
|
|
256
|
+
) : selectedProvider && selectedProvider.id !== 'githubapp' ? (
|
|
257
|
+
<motion.div
|
|
258
|
+
key="connect-card"
|
|
259
|
+
initial="initial" animate="animate" exit="exit" variants={fadeOnly}
|
|
260
|
+
className="rounded-xl border max-w-xl w-full"
|
|
261
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
262
|
+
>
|
|
263
|
+
<div className="sm:p-6 p-4">
|
|
264
|
+
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
265
|
+
<ArrowLeft className="w-4 h-4" />
|
|
266
|
+
Back
|
|
267
|
+
</button>
|
|
268
|
+
<div className="text-center">
|
|
269
|
+
<div className="flex justify-center mb-4">
|
|
270
|
+
<ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
|
|
271
|
+
</div>
|
|
272
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
273
|
+
{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
|
|
274
|
+
? `Use Public ${selectedProvider.name} Profile`
|
|
275
|
+
: `Connect ${selectedProvider.name}`}
|
|
276
|
+
</h3>
|
|
277
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
278
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
279
|
+
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
280
|
+
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
|
|
285
|
+
<motion.form
|
|
286
|
+
onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
|
|
287
|
+
className="mt-6 space-y-4"
|
|
288
|
+
initial="initial" animate="animate" exit="exit" variants={cardVariants}
|
|
289
|
+
>
|
|
290
|
+
{selectedProvider.id === 'linkedin' && (
|
|
291
|
+
<p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
|
|
292
|
+
<Link
|
|
293
|
+
href="https://www.linkedin.com/public-profile/settings"
|
|
294
|
+
target="_blank"
|
|
295
|
+
rel="noopener noreferrer"
|
|
296
|
+
className="underline"
|
|
297
|
+
style={{ color: 'var(--icon-accent)' }}
|
|
298
|
+
>
|
|
299
|
+
LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
|
|
300
|
+
</Link>
|
|
301
|
+
. This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
|
|
302
|
+
</p>
|
|
303
|
+
)}
|
|
304
|
+
<div className="relative">
|
|
305
|
+
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
306
|
+
<Input
|
|
307
|
+
type="url"
|
|
308
|
+
value={linkUrl}
|
|
309
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
310
|
+
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
311
|
+
required
|
|
312
|
+
className="w-full border bg-transparent p-2 pl-9"
|
|
313
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
314
|
+
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
315
|
+
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
319
|
+
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
320
|
+
{isSubmitting ? (
|
|
321
|
+
<div className="flex items-center justify-center">
|
|
322
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
323
|
+
Connecting...
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
'Connect'
|
|
327
|
+
)}
|
|
328
|
+
</Button>
|
|
329
|
+
</motion.div>
|
|
330
|
+
</motion.form>
|
|
331
|
+
) : (
|
|
332
|
+
<div className="mt-6">
|
|
333
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
334
|
+
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
335
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
336
|
+
Connect with {selectedProvider.name}
|
|
337
|
+
</Button>
|
|
338
|
+
</motion.div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</motion.div>
|
|
343
|
+
) : selectedProvider && selectedProvider.id === 'githubapp' ? (
|
|
344
|
+
(!showGithubManage && initialProviderId === 'githubapp') ? (
|
|
345
|
+
<div
|
|
346
|
+
key="github-card"
|
|
347
|
+
className="rounded-xl border max-w-xl w-full"
|
|
348
|
+
style={{
|
|
349
|
+
backgroundColor: 'var(--content-card-background)',
|
|
350
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<motion.div className="p-6 flex flex-col items-center" initial="initial" animate="animate" exit="exit" variants={fadeOnly}>
|
|
354
|
+
<div className="w-full flex items-center gap-3 mb-2 justify-center">
|
|
355
|
+
<ProviderIcon name="github" className="w-8 h-8 inline-block" />
|
|
356
|
+
<span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
|
|
357
|
+
</div>
|
|
358
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
|
|
359
|
+
You've successfully linked your GitHub account!
|
|
360
|
+
<br />
|
|
361
|
+
To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you'd like to highlight private work or share additional contributions for verification.
|
|
362
|
+
<br /><br />
|
|
363
|
+
<span className="text-[var(--text-main)] font-medium">
|
|
364
|
+
Would you like to connect your private repositories?
|
|
365
|
+
</span>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={() => setShowDataHandling(true)}
|
|
369
|
+
className="sm:text-sm text-xs underline text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)]"
|
|
370
|
+
>
|
|
371
|
+
See how your data is handled
|
|
372
|
+
</button>
|
|
373
|
+
</p>
|
|
374
|
+
<div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
|
|
375
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
376
|
+
<Button
|
|
377
|
+
className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
|
|
378
|
+
variant="destructive"
|
|
379
|
+
onClick={() => {
|
|
380
|
+
handleConnectBack();
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
No, don't connect
|
|
384
|
+
</Button>
|
|
385
|
+
</motion.div>
|
|
386
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
387
|
+
<Button
|
|
388
|
+
className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
|
|
389
|
+
onClick={onGithubAppInstall}
|
|
390
|
+
>
|
|
391
|
+
<span className="flex items-center justify-center">
|
|
392
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
393
|
+
Yes, connect my private repos
|
|
394
|
+
</span>
|
|
395
|
+
</Button>
|
|
396
|
+
</motion.div>
|
|
397
|
+
</div>
|
|
398
|
+
</motion.div>
|
|
399
|
+
</div>
|
|
400
|
+
) : (
|
|
222
401
|
<motion.div
|
|
223
|
-
key="
|
|
402
|
+
key="github-manage"
|
|
224
403
|
className="rounded-xl border max-w-xl w-full"
|
|
225
404
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
226
|
-
|
|
405
|
+
variants={cardVariants}
|
|
406
|
+
initial="initial"
|
|
407
|
+
animate="animate"
|
|
408
|
+
exit="exit"
|
|
227
409
|
>
|
|
228
410
|
<div className="sm:p-6 p-4">
|
|
229
411
|
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
@@ -232,227 +414,85 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
232
414
|
</button>
|
|
233
415
|
<div className="text-center">
|
|
234
416
|
<div className="flex justify-center mb-4">
|
|
235
|
-
<ProviderIcon name=
|
|
417
|
+
<ProviderIcon name="github" className="w-10 h-10" />
|
|
236
418
|
</div>
|
|
237
|
-
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
: `Connect ${selectedProvider.name}`}
|
|
241
|
-
</h3>
|
|
242
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
243
|
-
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
244
|
-
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
245
|
-
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
419
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
|
|
420
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
|
|
421
|
+
Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
|
|
246
422
|
</p>
|
|
247
423
|
</div>
|
|
248
424
|
|
|
249
|
-
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)}
|
|
269
|
-
<div className="relative">
|
|
270
|
-
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
271
|
-
<Input
|
|
272
|
-
type="url"
|
|
273
|
-
value={linkUrl}
|
|
274
|
-
onChange={(e) => setLinkUrl(e.target.value)}
|
|
275
|
-
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
276
|
-
required
|
|
277
|
-
className="w-full border bg-transparent p-2 pl-9"
|
|
278
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
279
|
-
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
280
|
-
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
284
|
-
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
285
|
-
{isSubmitting ? (
|
|
286
|
-
<div className="flex items-center justify-center">
|
|
287
|
-
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
288
|
-
Connecting...
|
|
289
|
-
</div>
|
|
425
|
+
<div className="mt-6 space-y-4">
|
|
426
|
+
<div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
427
|
+
<div className="flex items-center justify-between">
|
|
428
|
+
<div>
|
|
429
|
+
<div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub OAuth</div>
|
|
430
|
+
<div className="sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>{isGithubConnected ? 'Connected' : 'Not connected'}</div>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex items-center gap-2">
|
|
433
|
+
{isGithubConnected ? (
|
|
434
|
+
<Button
|
|
435
|
+
onClick={() => onDisconnect('github')}
|
|
436
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
437
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
438
|
+
variant="destructive"
|
|
439
|
+
disabled={isDisconnecting === 'github'}
|
|
440
|
+
>
|
|
441
|
+
{isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
|
|
442
|
+
<span>Disconnect</span>
|
|
443
|
+
</Button>
|
|
290
444
|
) : (
|
|
291
|
-
|
|
445
|
+
<Button
|
|
446
|
+
onClick={() => onOAuth('github')}
|
|
447
|
+
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
448
|
+
>
|
|
449
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
450
|
+
Connect
|
|
451
|
+
</Button>
|
|
292
452
|
)}
|
|
293
|
-
</
|
|
294
|
-
</motion.div>
|
|
295
|
-
</motion.form>
|
|
296
|
-
) : (
|
|
297
|
-
<div className="mt-6">
|
|
298
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
299
|
-
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
300
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
301
|
-
Connect with {selectedProvider.name}
|
|
302
|
-
</Button>
|
|
303
|
-
</motion.div>
|
|
304
|
-
</div>
|
|
305
|
-
)}
|
|
306
|
-
</div>
|
|
307
|
-
</motion.div>
|
|
308
|
-
</AnimatePresence>
|
|
309
|
-
) : selectedProvider && selectedProvider.id === 'githubapp' ? (
|
|
310
|
-
<AnimatePresence>
|
|
311
|
-
{(!showGithubManage && initialProviderId === 'githubapp') ? (
|
|
312
|
-
<motion.div
|
|
313
|
-
key="github-card"
|
|
314
|
-
className="rounded-xl border max-w-xl w-full"
|
|
315
|
-
style={{
|
|
316
|
-
backgroundColor: 'var(--content-card-background)',
|
|
317
|
-
borderColor: 'var(--icon-button-secondary)',
|
|
318
|
-
}}
|
|
319
|
-
variants={cardVariants}
|
|
320
|
-
initial="initial"
|
|
321
|
-
animate="animate"
|
|
322
|
-
exit="exit"
|
|
323
|
-
transition={{ duration: 0.3 }}
|
|
324
|
-
>
|
|
325
|
-
<div className="p-6 flex flex-col items-center">
|
|
326
|
-
<div className="w-full flex items-center gap-3 mb-2 justify-center">
|
|
327
|
-
<ProviderIcon name="github" className="w-8 h-8 inline-block" />
|
|
328
|
-
<span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
|
|
329
|
-
</div>
|
|
330
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
|
|
331
|
-
You’ve successfully linked your GitHub account!
|
|
332
|
-
<br />
|
|
333
|
-
To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you’d like to highlight private work or share additional contributions for verification.
|
|
334
|
-
<br /><br />
|
|
335
|
-
<span className="text-[var(--text-main)] font-medium">
|
|
336
|
-
Would you like to connect your private repositories?
|
|
337
|
-
</span>
|
|
338
|
-
</p>
|
|
339
|
-
<div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
|
|
340
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
341
|
-
<Button
|
|
342
|
-
className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
|
|
343
|
-
variant="destructive"
|
|
344
|
-
onClick={() => {
|
|
345
|
-
handleConnectBack();
|
|
346
|
-
}}
|
|
347
|
-
>
|
|
348
|
-
No, don't connect
|
|
349
|
-
</Button>
|
|
350
|
-
</motion.div>
|
|
351
|
-
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
352
|
-
<Button
|
|
353
|
-
className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
|
|
354
|
-
onClick={onGithubAppInstall}
|
|
355
|
-
>
|
|
356
|
-
<span className="flex items-center justify-center">
|
|
357
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
358
|
-
Yes, connect my private repos
|
|
359
|
-
</span>
|
|
360
|
-
</Button>
|
|
361
|
-
</motion.div>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
</motion.div>
|
|
365
|
-
) : (
|
|
366
|
-
<motion.div
|
|
367
|
-
key="github-manage"
|
|
368
|
-
className="rounded-xl border max-w-xl w-full"
|
|
369
|
-
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
370
|
-
variants={cardVariants}
|
|
371
|
-
initial="initial"
|
|
372
|
-
animate="animate"
|
|
373
|
-
exit="exit"
|
|
374
|
-
>
|
|
375
|
-
<div className="sm:p-6 p-4">
|
|
376
|
-
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
377
|
-
<ArrowLeft className="w-4 h-4" />
|
|
378
|
-
Back
|
|
379
|
-
</button>
|
|
380
|
-
<div className="text-center">
|
|
381
|
-
<div className="flex justify-center mb-4">
|
|
382
|
-
<ProviderIcon name="github" className="w-10 h-10" />
|
|
453
|
+
</div>
|
|
383
454
|
</div>
|
|
384
|
-
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
|
|
385
|
-
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
|
|
386
|
-
Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
|
|
387
|
-
</p>
|
|
388
455
|
</div>
|
|
389
456
|
|
|
390
|
-
<div className="
|
|
391
|
-
<div className="
|
|
392
|
-
<div
|
|
393
|
-
<div>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
402
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
403
|
-
variant="destructive"
|
|
404
|
-
disabled={isDisconnecting === 'github'}
|
|
405
|
-
>
|
|
406
|
-
{isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
|
|
407
|
-
<span>Disconnect</span>
|
|
408
|
-
</Button>
|
|
409
|
-
) : (
|
|
410
|
-
<Button
|
|
411
|
-
onClick={() => onOAuth('github')}
|
|
412
|
-
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
413
|
-
>
|
|
414
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
415
|
-
Connect
|
|
416
|
-
</Button>
|
|
417
|
-
)}
|
|
418
|
-
</div>
|
|
457
|
+
<div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
458
|
+
<div className="flex items-center justify-between">
|
|
459
|
+
<div>
|
|
460
|
+
<div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub App (Private Repos)</div>
|
|
461
|
+
<Button
|
|
462
|
+
className="inline-flex items-center text-xs font-medium text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)] transition-colors underline px-0 py-0 h-auto"
|
|
463
|
+
onClick={() => setShowDataHandling(true)}
|
|
464
|
+
variant="link"
|
|
465
|
+
>
|
|
466
|
+
How is my data handled?
|
|
467
|
+
</Button>
|
|
419
468
|
</div>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<Button
|
|
441
|
-
onClick={onGithubAppInstall}
|
|
442
|
-
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
443
|
-
>
|
|
444
|
-
<ExternalLink className="w-4 h-4 mr-2" />
|
|
445
|
-
Install
|
|
446
|
-
</Button>
|
|
447
|
-
)}
|
|
448
|
-
</div>
|
|
469
|
+
<div className="flex items-center gap-2">
|
|
470
|
+
{isGithubAppInstalled ? (
|
|
471
|
+
<Button
|
|
472
|
+
onClick={() => { window.location.href = 'https://github.com/settings/installations'; }}
|
|
473
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
474
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
475
|
+
variant="destructive"
|
|
476
|
+
>
|
|
477
|
+
<Unlink className="size-3 sm:size-4" />
|
|
478
|
+
<span>Uninstall</span>
|
|
479
|
+
</Button>
|
|
480
|
+
) : (
|
|
481
|
+
<Button
|
|
482
|
+
onClick={onGithubAppInstall}
|
|
483
|
+
className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
|
|
484
|
+
>
|
|
485
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
486
|
+
Install
|
|
487
|
+
</Button>
|
|
488
|
+
)}
|
|
449
489
|
</div>
|
|
450
490
|
</div>
|
|
451
491
|
</div>
|
|
452
492
|
</div>
|
|
453
|
-
</
|
|
454
|
-
|
|
455
|
-
|
|
493
|
+
</div>
|
|
494
|
+
</motion.div>
|
|
495
|
+
)
|
|
456
496
|
) : (
|
|
457
497
|
<Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
|
|
458
498
|
<AnimatePresence mode="wait">
|
|
@@ -647,7 +687,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
647
687
|
</AnimatePresence>
|
|
648
688
|
</Card>
|
|
649
689
|
)}
|
|
650
|
-
|
|
690
|
+
</AnimatePresence>
|
|
651
691
|
);
|
|
652
692
|
}
|
|
653
693
|
|