kyd-shared-badge 0.3.61 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.61",
3
+ "version": "0.3.63",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -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
+