kyd-shared-badge 0.3.118 → 0.3.120

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.118",
3
+ "version": "0.3.120",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -18,7 +18,9 @@
18
18
  "dependencies": {
19
19
  "@ai-sdk/openai": "^2.0.42",
20
20
  "@aws-sdk/client-s3": "^3.893.0",
21
+ "@aws-sdk/client-ses": "^3.916.0",
21
22
  "@aws-sdk/lib-dynamodb": "^3.893.0",
23
+ "@aws-sdk/s3-request-presigner": "^3.916.0",
22
24
  "@chatscope/chat-ui-kit-react": "^2.1.1",
23
25
  "@chatscope/chat-ui-kit-styles": "^1.4.0",
24
26
  "@knowyourdeveloper/react-bubble-chart": "^1.0.8",
@@ -34,6 +36,7 @@
34
36
  "react-hot-toast": "^2.6.0",
35
37
  "react-icons": "^5.5.0",
36
38
  "recharts": "^2.15.4",
39
+ "stripe": "^19.1.0",
37
40
  "tailwind-merge": "^3.3.1",
38
41
  "ulid": "^3.0.1"
39
42
  },
@@ -15,6 +15,7 @@ import { FaGithub, FaGitlab, FaStackOverflow, FaLinkedin, FaGoogle, FaKaggle } f
15
15
  import { SiCredly, SiFiverr } from 'react-icons/si';
16
16
  import GaugeCard from './components/GaugeCard';
17
17
  import RiskCard from './components/RiskCard';
18
+ import RoleOverviewCard from './components/RoleOverviewCard';
18
19
 
19
20
  import { yellow, green } from './colors';
20
21
  import SkillsValidation from './components/SkillsValidation';
@@ -87,6 +88,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
87
88
  return undefined;
88
89
  }
89
90
  })();
91
+ const hasEnterpriseMatch = !!assessmentResult?.enterprise_match;
90
92
 
91
93
  const topBusinessForGenre = (genre: string) => {
92
94
  const cats: string[] = (genreMapping)?.[genre] || [];
@@ -120,28 +122,86 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
120
122
  }`}
121
123
  </style>
122
124
  )}
123
- {/* Share controls removed; app-level pages render their own actions */}
124
- <Reveal headless={isHeadless} offsetY={8} durationMs={500}>
125
- <ReportHeader
126
- badgeId={badgeId}
127
- developerName={badgeData.developerName}
128
- updatedAt={updatedAt}
129
- score={overallFinalPercent || 0}
130
- isPublic={true}
131
- badgeImageUrl={badgeData.badgeImageUrl || ''}
132
- summary={assessmentResult?.report_summary || ''}
133
- enterpriseMatch={(() => {
134
- const em = assessmentResult?.enterprise_match;
135
- if (!em) return null;
136
- const role = em.role || {};
137
- return { label: em.label, description: em.description, roleName: role.name };
138
- })()}
139
- countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
140
- accountAuthenticity={assessmentResult?.account_authenticity}
141
- companyName={badgeData.companyName}
142
- rightBadgeLayout={!assessmentResult?.enterprise_match}
143
- />
144
- </Reveal>
125
+ {/* Overview quadrant layout (mirrors SharedBadgeDisplay overview) */}
126
+ <div className={'mt-6'}>
127
+ <Reveal headless={isHeadless} offsetY={8} durationMs={500}>
128
+ <div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
129
+ <div className={`${hasEnterpriseMatch ? '' : 'md:col-span-2'}`}>
130
+ <ReportHeader
131
+ badgeId={badgeId}
132
+ developerName={badgeData.developerName}
133
+ updatedAt={updatedAt}
134
+ score={overallFinalPercent || 0}
135
+ isPublic={true}
136
+ badgeImageUrl={badgeData.badgeImageUrl || ''}
137
+ summary={undefined}
138
+ enterpriseMatch={null}
139
+ countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
140
+ accountAuthenticity={assessmentResult?.account_authenticity}
141
+ companyName={badgeData.companyName}
142
+ sourcesProviders={(badgeData?.connectedAccounts || []).map(a => (a?.name || '').toLowerCase())}
143
+ rightBadgeLayout={!hasEnterpriseMatch}
144
+ />
145
+ </div>
146
+ {hasEnterpriseMatch && (
147
+ <div>
148
+ {(() => {
149
+ const em = assessmentResult?.enterprise_match;
150
+ if (!em) return null;
151
+ const role = em.role || {};
152
+ return (
153
+ <RoleOverviewCard
154
+ title={'Role Alignment'}
155
+ matchLabel={em.label}
156
+ roleName={role?.name || 'Role'}
157
+ />
158
+ );
159
+ })()}
160
+ </div>
161
+ )}
162
+ <div>
163
+ {(() => {
164
+ const uiTech = graphInsights?.uiSummary?.technical || {};
165
+ const techPct = Math.round(Number(uiTech?.percent ?? 0));
166
+ const techLabel = uiTech?.label || 'EVIDENCE';
167
+ return (
168
+ <GaugeCard
169
+ title={'KYD Technical'}
170
+ description={'Composite of technical evidence; more right indicates stronger capability'}
171
+ percent={techPct}
172
+ label={techLabel}
173
+ topMovers={[]}
174
+ />
175
+ );
176
+ })()}
177
+ </div>
178
+ <div>
179
+ {(() => {
180
+ const uiRisk = graphInsights?.uiSummary?.risk || {};
181
+ const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
182
+ const riskLabel = uiRisk?.label || 'RISK';
183
+ return (
184
+ <RiskCard
185
+ title={'KYD Risk'}
186
+ description={'Lower bar height indicates lower risk exposure'}
187
+ percentGood={riskPctGood}
188
+ label={riskLabel}
189
+ topMovers={[]}
190
+ />
191
+ );
192
+ })()}
193
+ </div>
194
+ </div>
195
+ </Reveal>
196
+ <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)' }}>
197
+ <Reveal headless={isHeadless}>
198
+ <div className={'kyd-avoid-break'}>
199
+ <h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
200
+ <SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} skillsByCategory={assessmentResult?.graph_insights?.skillsByCategory} skillsMeta={assessmentResult?.graph_insights?.skillsMeta} />
201
+ </div>
202
+ </Reveal>
203
+ </div>
204
+ </div>
145
205
  {/* Coaching / Evidence under header when present */}
146
206
  {(() => {
147
207
  const em = assessmentResult.enterprise_match;
@@ -158,12 +218,6 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
158
218
  style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
159
219
  >
160
220
  <div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
161
- <div className="pt-8 first:pt-0 kyd-avoid-break">
162
- <Reveal headless={isHeadless} as={'h4'} offsetY={8} durationMs={500} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</Reveal>
163
- <Reveal headless={isHeadless} as={'div'} offsetY={8} durationMs={500} className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
164
- <SummaryCards graphInsights={graphInsights} assessmentResult={assessmentResult} topBusinessForGenre={topBusinessForGenre} />
165
- </Reveal>
166
- </div>
167
221
 
168
222
  {/* Enterprise Use Cases (directly under the three summary cards) */}
169
223
  {assessmentResult?.enterprise_use_cases?.items && assessmentResult.enterprise_use_cases.items.length > 0 && (
@@ -5,7 +5,7 @@ import { normalizeLinkedInInput } from './linkedin';
5
5
  import type { ConnectAccountsProps } from './types';
6
6
  import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings, Shield, InfoIcon } from 'lucide-react';
7
7
  import { AnimatePresence, motion } from 'framer-motion';
8
- import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
8
+ import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle, ConnectProgress } from '../ui';
9
9
  import Link from 'next/link';
10
10
  import { Tooltip, TooltipTrigger, TooltipProvider, TooltipContent } from '../ui/';
11
11
 
@@ -222,8 +222,23 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
222
222
  return setupAction === 'install' || setupAction === 'created';
223
223
  }, [githubConnectedAccount]);
224
224
 
225
+ // Progress UI is outsourced to ConnectProgress
226
+
225
227
  return (
226
- <AnimatePresence initial={false} mode="wait">
228
+ <>
229
+ {/* Mobile: relative summary above content */}
230
+ {providers?.length ? (
231
+ <div className="sm:hidden px-4 mb-3">
232
+ <ConnectProgress layout="inline" providers={providers} connectedIds={connectedIds} />
233
+ </div>
234
+ ) : null}
235
+
236
+ {/* Desktop: fixed top-right summary */}
237
+ {providers?.length ? (
238
+ <ConnectProgress providers={providers} connectedIds={connectedIds} />
239
+ ) : null}
240
+
241
+ <AnimatePresence initial={false} mode="wait">
227
242
  {showDataHandling ? (
228
243
  <motion.div
229
244
  key="data-handling-card"
@@ -695,7 +710,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
695
710
  </AnimatePresence>
696
711
  </Card>
697
712
  )}
698
- </AnimatePresence>
713
+ </AnimatePresence>
714
+ </>
699
715
  );
700
716
  }
701
717
 
@@ -86,7 +86,7 @@ export async function createSupportTicketRoute(req: NextRequest, userId: string)
86
86
  const inv = await stripe.invoices.list({ customer: stripeCustomerId, status: 'paid', limit: 100, starting_after: startingAfter })
87
87
  for (const i of inv.data) totalSpentUsd += (i.total || 0)
88
88
  hasMore = inv.has_more
89
- startingAfter = inv.data.at(-1)?.id
89
+ startingAfter = inv.data[inv.data.length - 1]?.id
90
90
  }
91
91
  totalSpentUsd = Math.round(totalSpentUsd) / 100
92
92
  }
@@ -116,7 +116,7 @@ export async function createSupportTicketRoute(req: NextRequest, userId: string)
116
116
  }
117
117
 
118
118
  const product = 'KYD Self-check'
119
- const name = userItem?.fullName || userItem?.email || ''
119
+ const name = userItem?.fullName || userItem?.name || userItem?.email || ''
120
120
  const email = userItem?.email || ''
121
121
 
122
122
  const html = `
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+ import React, { useMemo } from 'react'
3
+ import { Card, CardContent } from './card'
4
+ import { ProgressCircle } from './progress-circle'
5
+
6
+ export type ConnectProgressProps = {
7
+ providers: Array<{ id: string; name: string }>
8
+ connectedIds: Set<string>
9
+ className?: string
10
+ layout?: 'fixed-desktop' | 'inline'
11
+ }
12
+
13
+ export function ConnectProgress(props: ConnectProgressProps) {
14
+ const { providers, connectedIds, className, layout = 'fixed-desktop' } = props
15
+
16
+ const hasGithub = connectedIds.has('github')
17
+ const hasLinkedIn = connectedIds.has('linkedin')
18
+ const otherConnectedCount = useMemo(() => {
19
+ let count = 0
20
+ connectedIds.forEach(id => { if (id !== 'github' && id !== 'linkedin') count += 1 })
21
+ return count
22
+ }, [connectedIds])
23
+
24
+ const progressPercent = useMemo(() => {
25
+ const githubWeight = hasGithub ? 65 : 0
26
+ const linkedinWeight = hasLinkedIn ? 15 : 0
27
+ const othersWeight = Math.min(otherConnectedCount, 2) * 10
28
+ return Math.max(0, Math.min(100, githubWeight + linkedinWeight + othersWeight))
29
+ }, [hasGithub, hasLinkedIn, otherConnectedCount])
30
+
31
+ const progressCopy = useMemo(() => {
32
+ const score = progressPercent
33
+ if (score === 0) {
34
+ return {
35
+ headline: 'Get started — connect GitHub',
36
+ body: 'GitHub contributes ~65% of your profile strength. Add LinkedIn (+15%) and 1–2 other platforms (+10% each). 70%+ is strong; 100% is optional.'
37
+ }
38
+ }
39
+ if (hasGithub && score < 70) {
40
+ return {
41
+ headline: 'Good base — add more signals',
42
+ body: 'Add LinkedIn (+15%) and at least one other platform (+10%) to reach 70%+, which is considered strong. 100% is not mandatory.'
43
+ }
44
+ }
45
+ if (!hasGithub && score < 70) {
46
+ return {
47
+ headline: 'Add GitHub for a big boost',
48
+ body: 'Current signals are light. Connecting GitHub adds ~65% weight and significantly improves your profile.'
49
+ }
50
+ }
51
+ if (score >= 70 && score < 90) {
52
+ return {
53
+ headline: 'Strong — recommended coverage achieved',
54
+ body: '70%+ indicates a strong profile. Adding more platforms can further improve verification. 100% is optional but even better.'
55
+ }
56
+ }
57
+ if (score >= 90) {
58
+ return {
59
+ headline: 'Excellent — high confidence',
60
+ body: 'You are near complete coverage. 100% is not required, but additional platforms can further increase confidence.'
61
+ }
62
+ }
63
+ return {
64
+ headline: 'Keep going',
65
+ body: 'Connect more platforms to strengthen your profile. 70%+ is strong; 100% is optional.'
66
+ }
67
+ }, [progressPercent, hasGithub])
68
+
69
+ const content = (
70
+ <Card className={`border-[var(--icon-button-secondary)] ${className || ''}`} style={{ backgroundColor: 'var(--content-card-background)'}}>
71
+ <CardContent className="py-3 px-3">
72
+ <div className="flex items-center gap-3">
73
+ <ProgressCircle value={progressPercent} size={64} thickness={6} />
74
+ <div className="min-w-0 max-w-xs">
75
+ <div className="text-sm font-semibold truncate" style={{ color: 'var(--text-main)'}}>{progressCopy.headline}</div>
76
+ <div className="text-xs mt-0.5" style={{ color: 'var(--text-secondary)'}}>
77
+ {progressCopy.body}
78
+ </div>
79
+ <div className="text-[10px] mt-1" style={{ color: 'var(--text-secondary)'}}>
80
+ Weighted score • {progressPercent}%
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </CardContent>
85
+ </Card>
86
+ )
87
+
88
+ if (layout === 'inline') return content
89
+
90
+ return (
91
+ <div className="hidden sm:block fixed top-6 right-6 z-50">{content}</div>
92
+ )
93
+ }
94
+
95
+ export default ConnectProgress
96
+
97
+
package/src/ui/index.ts CHANGED
@@ -3,4 +3,6 @@ export * from './card';
3
3
  export * from './input';
4
4
  export * from './textarea';
5
5
  export * from './spinner';
6
- export * from './tooltip';
6
+ export * from './tooltip';
7
+ export * from './progress-circle';
8
+ export * from './connect-progress';
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+ import React from 'react'
3
+
4
+ export type ProgressCircleProps = {
5
+ value: number
6
+ size?: number
7
+ thickness?: number
8
+ className?: string
9
+ showLabel?: boolean
10
+ label?: string
11
+ }
12
+
13
+ export function ProgressCircle(props: ProgressCircleProps) {
14
+ const { value, size = 72, thickness = 6, className, showLabel = true, label } = props
15
+
16
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0))
17
+ const radius = (size - thickness) / 2
18
+ const circumference = 2 * Math.PI * radius
19
+ const dashOffset = circumference * (1 - clamped / 100)
20
+
21
+ return (
22
+ <div className={`relative inline-flex items-center justify-center ${className || ''}`} style={{ width: size, height: size }}>
23
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="block">
24
+ <defs>
25
+ <linearGradient id="kyd-progress-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
26
+ <stop offset="0%" stopColor="var(--icon-accent)" />
27
+ <stop offset="100%" stopColor="var(--icon-accent-hover)" />
28
+ </linearGradient>
29
+ </defs>
30
+ <circle
31
+ cx={size / 2}
32
+ cy={size / 2}
33
+ r={radius}
34
+ fill="none"
35
+ stroke="var(--icon-button-secondary)"
36
+ strokeOpacity={0.3}
37
+ strokeWidth={thickness}
38
+ />
39
+ <circle
40
+ cx={size / 2}
41
+ cy={size / 2}
42
+ r={radius}
43
+ fill="none"
44
+ stroke="url(#kyd-progress-gradient)"
45
+ strokeWidth={thickness}
46
+ strokeLinecap="round"
47
+ strokeDasharray={circumference}
48
+ strokeDashoffset={dashOffset}
49
+ style={{ transition: 'stroke-dashoffset 800ms ease' }}
50
+ transform={`rotate(-90 ${size / 2} ${size / 2})`}
51
+ />
52
+ {/* subtle pulsing halo */}
53
+ <circle
54
+ cx={size / 2}
55
+ cy={size / 2}
56
+ r={radius + thickness / 2}
57
+ fill="none"
58
+ stroke="var(--icon-accent)"
59
+ strokeOpacity={0.15}
60
+ strokeWidth={2}
61
+ className="animate-pulse"
62
+ />
63
+ </svg>
64
+ {showLabel ? (
65
+ <div
66
+ className="absolute inset-0 flex items-center justify-center text-[10px] sm:text-xs font-medium"
67
+ style={{ color: 'var(--text-main)' }}
68
+ >
69
+ {label ? label : `${Math.round(clamped)}%`}
70
+ </div>
71
+ ) : null}
72
+ </div>
73
+ )
74
+ }
75
+
76
+ export default ProgressCircle
77
+
78
+