kyd-shared-badge 0.3.62 → 0.3.63
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
CHANGED
|
@@ -31,6 +31,7 @@ import AiUsageBody from './components/AiUsageBody';
|
|
|
31
31
|
import SanctionsMatches from './components/SanctionsMatches';
|
|
32
32
|
import AppendixContent from './components/AppendixContent';
|
|
33
33
|
import { ProviderIcon, getProviderDisplayName, getProviderTooltipCopy, getCategoryTooltipCopy, barColor } from './utils/provider';
|
|
34
|
+
import ResumeView from './components/ResumeView';
|
|
34
35
|
type ChatWidgetProps = Partial<{
|
|
35
36
|
api: string;
|
|
36
37
|
title: string;
|
|
@@ -223,6 +224,21 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
223
224
|
|
|
224
225
|
|
|
225
226
|
<div className="pt-8 space-y-8">
|
|
227
|
+
{(() => {
|
|
228
|
+
const resumeJson = (assessmentResult as any)?.resume?.json || null;
|
|
229
|
+
const roleName = (() => {
|
|
230
|
+
try { return (assessmentResult?.enterprise_match?.role?.name) || undefined; } catch { return undefined; }
|
|
231
|
+
})();
|
|
232
|
+
if (!resumeJson) return null;
|
|
233
|
+
return (
|
|
234
|
+
<Reveal headless={isHeadless}>
|
|
235
|
+
<div className={'kyd-avoid-break'}>
|
|
236
|
+
<h3 className={'text-2xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>Resume</h3>
|
|
237
|
+
<ResumeView resume={resumeJson} roleName={roleName} showDownloadButton={false} />
|
|
238
|
+
</div>
|
|
239
|
+
</Reveal>
|
|
240
|
+
);
|
|
241
|
+
})()}
|
|
226
242
|
<Reveal headless={isHeadless} as={'h3'} offsetY={8} className={`text-2xl font-bold ${isHeadless ? 'kyd-break-before' : ''}`} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
|
|
227
243
|
|
|
228
244
|
{/* Risk Graph Insights and Category Bars */}
|
|
@@ -30,6 +30,7 @@ import SanctionsMatches from './components/SanctionsMatches';
|
|
|
30
30
|
import { getCategoryTooltipCopy, barColor } from './utils/provider';
|
|
31
31
|
import { matchLabelToColor } from './colors';
|
|
32
32
|
import { useEffect, useMemo, useState } from 'react';
|
|
33
|
+
import ResumeView from './components/ResumeView';
|
|
33
34
|
type ChatWidgetProps = Partial<{
|
|
34
35
|
api: string;
|
|
35
36
|
title: string;
|
|
@@ -112,6 +113,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
112
113
|
const tabs = useMemo(() => {
|
|
113
114
|
const arr = [
|
|
114
115
|
{ key: 'overview', label: 'Overview' },
|
|
116
|
+
{ key: 'resume', label: 'Resume' },
|
|
115
117
|
{ key: 'role', label: 'Role Fit & Coaching' },
|
|
116
118
|
{ key: 'technical', label: 'KYD Technical' },
|
|
117
119
|
{ key: 'risk', label: 'KYD Risk' },
|
|
@@ -334,6 +336,26 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
334
336
|
);
|
|
335
337
|
};
|
|
336
338
|
|
|
339
|
+
const ResumeSection = () => {
|
|
340
|
+
const resumeJson = (assessmentResult as any)?.resume?.json || null;
|
|
341
|
+
const roleName = (() => {
|
|
342
|
+
try { return (assessmentResult?.enterprise_match?.role?.name) || undefined; } catch { return undefined; }
|
|
343
|
+
})();
|
|
344
|
+
return (
|
|
345
|
+
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
346
|
+
<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)' }}>
|
|
347
|
+
<ResumeView
|
|
348
|
+
resume={resumeJson}
|
|
349
|
+
roleName={roleName}
|
|
350
|
+
badgeId={badgeId}
|
|
351
|
+
isPublic={true}
|
|
352
|
+
showDownloadButton={true}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
337
359
|
const TechnicalSection = () => (
|
|
338
360
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
339
361
|
<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)' }}>
|
|
@@ -542,6 +564,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
542
564
|
<TabNav />
|
|
543
565
|
<div className={'px-2 sm:px-0 pb-2'}>
|
|
544
566
|
{activeTab === 'overview' && OverviewSection()}
|
|
567
|
+
{activeTab === 'resume' && <ResumeSection />}
|
|
545
568
|
{activeTab === 'role' && <RoleFitSection />}
|
|
546
569
|
{activeTab === 'technical' && TechnicalSection()}
|
|
547
570
|
{activeTab === 'risk' && RiskSection()}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
type ResumeJSON = Partial<{
|
|
6
|
+
header: Partial<{ name: string; title: string; contact: Partial<{ email: string; phone: string; location: string; links: string[] }> }>;
|
|
7
|
+
summary: string;
|
|
8
|
+
skills: Partial<{ categories: Array<Partial<{ name: string; items: string[] }>> }>;
|
|
9
|
+
experience: Array<Partial<{ company: string; title: string; startDate: string; endDate: string; location: string; highlights: string[]; technologies: string[] }>>;
|
|
10
|
+
projects: Array<Partial<{ name: string; description: string; impact: string; technologies: string[]; link: string }>>;
|
|
11
|
+
education: Array<Partial<{ institution: string; degree: string; graduationDate: string; location: string }>>;
|
|
12
|
+
certifications: Array<Partial<{ name: string; issuer: string; date: string }>>;
|
|
13
|
+
links: Array<Partial<{ label: string; url: string }>>;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
interface ResumeViewProps {
|
|
17
|
+
resume: ResumeJSON | null | undefined;
|
|
18
|
+
roleName?: string;
|
|
19
|
+
badgeId?: string;
|
|
20
|
+
isPublic?: boolean;
|
|
21
|
+
idToken?: string;
|
|
22
|
+
showDownloadButton?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function asText(value: unknown): string {
|
|
26
|
+
if (value === undefined || value === null) return '';
|
|
27
|
+
if (typeof value === 'string') return value;
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const Row = ({ label, value }: { label: string; value?: string }) => {
|
|
32
|
+
if (!value) return null;
|
|
33
|
+
return (
|
|
34
|
+
<div className={'flex flex-col sm:flex-row sm:items-baseline gap-1'}>
|
|
35
|
+
<div className={'text-sm font-semibold'} style={{ color: 'var(--text-main)' }}>{label}</div>
|
|
36
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{value}</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default function ResumeView({ resume, roleName, badgeId, isPublic, idToken, showDownloadButton }: ResumeViewProps) {
|
|
42
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
43
|
+
|
|
44
|
+
const header = resume?.header || {};
|
|
45
|
+
const contact = header?.contact || {} as any;
|
|
46
|
+
const skills = resume?.skills || {} as any;
|
|
47
|
+
const experience = Array.isArray(resume?.experience) ? (resume?.experience as any[]) : [];
|
|
48
|
+
const projects = Array.isArray(resume?.projects) ? (resume?.projects as any[]) : [];
|
|
49
|
+
const education = Array.isArray(resume?.education) ? (resume?.education as any[]) : [];
|
|
50
|
+
const certifications = Array.isArray(resume?.certifications) ? (resume?.certifications as any[]) : [];
|
|
51
|
+
const links = Array.isArray(resume?.links) ? (resume?.links as any[]) : [];
|
|
52
|
+
|
|
53
|
+
const canDownload = !!(showDownloadButton && badgeId);
|
|
54
|
+
|
|
55
|
+
const handleDownloadResume = async () => {
|
|
56
|
+
if (!badgeId) return;
|
|
57
|
+
try {
|
|
58
|
+
setIsDownloading(true);
|
|
59
|
+
const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL;
|
|
60
|
+
if (!apiGatewayUrl) throw new Error('API not configured');
|
|
61
|
+
const usePublic = !!isPublic;
|
|
62
|
+
const path = usePublic ? `/share/badge/${badgeId}/resume` : `/user/assessment/${badgeId}/resume`;
|
|
63
|
+
const res = await fetch(`${apiGatewayUrl}${path}`, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: (!usePublic && idToken) ? { 'Authorization': `Bearer ${idToken}` } : {}
|
|
66
|
+
});
|
|
67
|
+
const data = await res.json().catch(() => ({}));
|
|
68
|
+
if (!res.ok) throw new Error(data?.error || 'Unable to fetch Resume');
|
|
69
|
+
const url = data?.url;
|
|
70
|
+
if (!url) throw new Error('Missing URL');
|
|
71
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.error('[ResumeView] Failed to download resume', e);
|
|
75
|
+
} finally {
|
|
76
|
+
setIsDownloading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={'space-y-8'}>
|
|
82
|
+
<div className={'flex items-center justify-between'}>
|
|
83
|
+
<div>
|
|
84
|
+
<div className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>{asText(header?.name) || 'Resume'}</div>
|
|
85
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{asText(header?.title)}</div>
|
|
86
|
+
{roleName ? (
|
|
87
|
+
<div className={'text-sm mt-1'} style={{ color: 'var(--text-secondary)' }}>Role: {roleName}</div>
|
|
88
|
+
) : null}
|
|
89
|
+
<div className={'text-sm mt-2'} style={{ color: 'var(--text-secondary)' }}>
|
|
90
|
+
{[asText(contact?.location), asText(contact?.email), asText(contact?.phone)].filter(Boolean).join(' • ')}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{canDownload ? (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={handleDownloadResume}
|
|
97
|
+
disabled={isDownloading}
|
|
98
|
+
className={'px-3 py-2 text-sm rounded-md border'}
|
|
99
|
+
style={{ borderColor: 'var(--icon-button-secondary)', color: 'var(--text-main)', background: 'var(--content-card-background)' }}
|
|
100
|
+
>
|
|
101
|
+
{isDownloading ? 'Preparing…' : 'Download Resume (.docx)'}
|
|
102
|
+
</button>
|
|
103
|
+
) : null}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{asText(resume?.summary) ? (
|
|
107
|
+
<div>
|
|
108
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Summary</div>
|
|
109
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{asText(resume?.summary)}</div>
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
112
|
+
|
|
113
|
+
{Array.isArray(skills?.categories) && skills.categories.length > 0 ? (
|
|
114
|
+
<div>
|
|
115
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Skills</div>
|
|
116
|
+
<div className={'space-y-1'}>
|
|
117
|
+
{skills.categories.map((cat: any, idx: number) => (
|
|
118
|
+
<Row key={idx} label={asText(cat?.name)} value={(Array.isArray(cat?.items) ? cat.items : []).join(', ')} />
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
124
|
+
{experience.length > 0 ? (
|
|
125
|
+
<div>
|
|
126
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Experience</div>
|
|
127
|
+
<div className={'space-y-4'}>
|
|
128
|
+
{experience.map((exp: any, idx: number) => (
|
|
129
|
+
<div key={idx} className={'space-y-1'}>
|
|
130
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>
|
|
131
|
+
{asText(exp?.title)}{asText(exp?.company) ? ` — ${asText(exp?.company)}` : ''}
|
|
132
|
+
</div>
|
|
133
|
+
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>
|
|
134
|
+
{(asText(exp?.startDate) || asText(exp?.endDate)) ? `${asText(exp?.startDate)} – ${asText(exp?.endDate) || 'Present'}` : ''}
|
|
135
|
+
</div>
|
|
136
|
+
{Array.isArray(exp?.highlights) && exp.highlights.length > 0 ? (
|
|
137
|
+
<ul className={'list-disc pl-5 text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
138
|
+
{exp.highlights.map((h: any, i: number) => (
|
|
139
|
+
<li key={i}>{asText(h)}</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
) : null}
|
|
143
|
+
{Array.isArray(exp?.technologies) && exp.technologies.length > 0 ? (
|
|
144
|
+
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>Technologies: {exp.technologies.join(', ')}</div>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
) : null}
|
|
151
|
+
|
|
152
|
+
{projects.length > 0 ? (
|
|
153
|
+
<div>
|
|
154
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Projects</div>
|
|
155
|
+
<div className={'space-y-4'}>
|
|
156
|
+
{projects.map((p: any, idx: number) => (
|
|
157
|
+
<div key={idx} className={'space-y-1'}>
|
|
158
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{asText(p?.name)}</div>
|
|
159
|
+
{asText(p?.description) ? <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{asText(p?.description)}</div> : null}
|
|
160
|
+
{asText(p?.impact) ? <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>Impact: {asText(p?.impact)}</div> : null}
|
|
161
|
+
{Array.isArray(p?.technologies) && p.technologies.length > 0 ? (
|
|
162
|
+
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>Technologies: {p.technologies.join(', ')}</div>
|
|
163
|
+
) : null}
|
|
164
|
+
{asText(p?.link) ? <div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>{asText(p?.link)}</div> : null}
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
|
|
171
|
+
{education.length > 0 ? (
|
|
172
|
+
<div>
|
|
173
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Education</div>
|
|
174
|
+
<div className={'space-y-2'}>
|
|
175
|
+
{education.map((e: any, idx: number) => (
|
|
176
|
+
<div key={idx} className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
177
|
+
{`${asText(e?.degree)} — ${asText(e?.institution)}${asText(e?.graduationDate) ? ` (${asText(e?.graduationDate)})` : ''}`}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
) : null}
|
|
183
|
+
|
|
184
|
+
{certifications.length > 0 ? (
|
|
185
|
+
<div>
|
|
186
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Certifications</div>
|
|
187
|
+
<div className={'space-y-1'}>
|
|
188
|
+
{certifications.map((c: any, idx: number) => (
|
|
189
|
+
<div key={idx} className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
190
|
+
{`${asText(c?.name)} — ${asText(c?.issuer)}${asText(c?.date) ? ` (${asText(c?.date)})` : ''}`}
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
|
|
197
|
+
{links.length > 0 ? (
|
|
198
|
+
<div>
|
|
199
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Links</div>
|
|
200
|
+
<div className={'space-y-1'}>
|
|
201
|
+
{links.map((l: any, idx: number) => (
|
|
202
|
+
<div key={idx} className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
203
|
+
{`${asText(l?.label)}: ${asText(l?.url)}`}
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
) : null}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|