kyd-shared-badge 0.1.0

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/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @kyd/shared-badge
2
+
3
+ Shared KYD badge display component for Next.js apps.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm i @kyd/shared-badge
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { SharedBadgeDisplay } from '@kyd/shared-badge'
15
+ import type { PublicBadgeData } from '@kyd/shared-badge'
16
+
17
+ export default function Page({ badgeData }: { badgeData: PublicBadgeData }) {
18
+ return <SharedBadgeDisplay badgeData={badgeData} />
19
+ }
20
+ ```
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "kyd-shared-badge",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "main": "./src/index.ts",
6
+ "module": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "sideEffects": false,
12
+ "scripts": {},
13
+ "peerDependencies": {
14
+ "next": ">=13",
15
+ "react": ">=18",
16
+ "react-dom": ">=18"
17
+ },
18
+ "dependencies": {
19
+ "react-icons": "^4.12.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^19.1.10",
26
+ "@types/react-dom": "^19.1.7",
27
+ "rollup": "^4.46.2"
28
+ }
29
+ }
@@ -0,0 +1,238 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { IconType } from 'react-icons';
5
+ import { FiAlertTriangle, FiCpu, FiShield, FiThumbsUp } from 'react-icons/fi';
6
+ import { PublicBadgeData } from './types';
7
+ import ShareButton from './components/ShareButton';
8
+ import ReportHeader from './components/ReportHeader';
9
+ import AppendixTables from './components/AppendixTables';
10
+ import ProviderInsights from './components/ProviderInsights';
11
+ import IpRiskAnalysisDisplay from './components/IpRiskAnalysisDisplay';
12
+
13
+ const getScoreColor = (score: number) => {
14
+ if (score >= 80) return 'text-[#6A9846]';
15
+ if (score >= 65) return 'text-[#EABE52]';
16
+ return 'text-[#D3452E]';
17
+ };
18
+
19
+ const getRiskText = (risk: number) => {
20
+ if (risk >= 80) return 'Low Risk';
21
+ if (risk >= 65) return 'Medium Risk';
22
+ if (risk >= 40) return 'High Risk';
23
+ return 'Critical Risk';
24
+ };
25
+
26
+ interface ScoreCardProps {
27
+ title: React.ReactNode;
28
+ score: number;
29
+ description: string;
30
+ descriptor?: string;
31
+ icon: IconType;
32
+ scoreType: 'number' | 'risk' | 'descriptor';
33
+ }
34
+
35
+ const ScoreCard = ({ title, score, description, descriptor, icon: Icon, scoreType }: ScoreCardProps) => {
36
+ const scoreColor = scoreType === 'descriptor' ? 'text-neutral-400' : getScoreColor(score);
37
+ let displayScore;
38
+
39
+ if (scoreType === 'risk') {
40
+ displayScore = getRiskText(score);
41
+ } else if (scoreType === 'descriptor') {
42
+ displayScore = descriptor;
43
+ } else {
44
+ displayScore = `${score}/100`;
45
+ }
46
+
47
+ return (
48
+ <div className="p-6 rounded-lg flex flex-col items-center justify-start text-center dark:bg-neutral-800/50 backdrop-blur-sm bg-white/70 shadow-lg border border-neutral-200 dark:border-neutral-700 h-full">
49
+ <div className={`text-3xl mb-4 ${scoreColor}`}>
50
+ <Icon />
51
+ </div>
52
+ <h3 className={`font-semibold text-xl text-neutral-800 dark:text-white mb-2 ${scoreType === 'descriptor' ? 'text-neutral-400' : ''}`}>{title}</h3>
53
+ <p className={`text-4xl font-bold ${scoreColor}`}>{displayScore}</p>
54
+ <p className="text-sm text-neutral-600 dark:text-neutral-400 mt-4">{description}</p>
55
+ </div>
56
+ );
57
+ };
58
+
59
+ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
60
+ const { badgeId, developerName, assessmentResult, updatedAt, connectedPlatforms } = badgeData;
61
+ const { summary_scores, report_summary, developer_trust_explanation, key_skills, screening_sources, industry_considerations } = assessmentResult;
62
+
63
+ const devTrustScore = summary_scores.developer_trust;
64
+ const riskScore = summary_scores.risk_score;
65
+ const aiUsageScore = summary_scores.ai_usage;
66
+
67
+ return (
68
+ <div className='max-w-6xl mx-auto'>
69
+ <div className="flex justify-end items-center mb-4">
70
+ <ShareButton
71
+ badgeId={badgeId}
72
+ shareTitle={`KYD Self-Check™ Report | ${badgeData.developerName}`}
73
+ shareText="Check out my KYD Self-Check™ from Know Your Developer™"
74
+ buttonText="Share"
75
+ className="flex items-center justify-center h-10 w-10 sm:w-auto sm:px-4 sm:py-2 bg-indigo-600 text-white rounded-full sm:rounded-md hover:bg-indigo-700 transition-all duration-300 ease-in-out"
76
+ />
77
+ </div>
78
+ <ReportHeader
79
+ badgeId={badgeId}
80
+ developerName={badgeData.developerName}
81
+ updatedAt={updatedAt}
82
+ score={summary_scores.kyd_self_check.score}
83
+ isPublic={true}
84
+ badgeImageUrl={badgeData.badgeImageUrl || ''}
85
+ />
86
+ <div className="bg-white/80 dark:bg-neutral-900/80 backdrop-blur-sm rounded-lg shadow-xl p-6 sm:p-8 mt-8">
87
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
88
+ <ScoreCard
89
+ title="KYD Technical™"
90
+ score={devTrustScore?.score || 0}
91
+ description={devTrustScore?.description || ''}
92
+ icon={FiThumbsUp}
93
+ scoreType='number'
94
+ />
95
+ <ScoreCard
96
+ title="KYD Risk™"
97
+ score={riskScore?.score || 0}
98
+ description={riskScore?.description || ''}
99
+ icon={FiShield}
100
+ scoreType='risk'
101
+ />
102
+ <ScoreCard
103
+ title={<span>KYD AI™ <span className="text-xs font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded-full align-middle">Beta</span></span>}
104
+ score={0}
105
+ description={aiUsageScore?.description || 'Analysis of AI tool usage and transparency.'}
106
+ descriptor={aiUsageScore?.descriptor}
107
+ icon={FiCpu}
108
+ scoreType='descriptor'
109
+ />
110
+ </div>
111
+
112
+ <div className="space-y-12 divide-y divide-neutral-200/50 dark:divide-neutral-700/50">
113
+ <div className="pt-8 first:pt-0">
114
+ <h3 className="text-2xl font-bold text-neutral-900 dark:text-white mb-4">1. Summary Findings</h3>
115
+ <div className="prose prose-sm dark:prose-invert max-w-none text-neutral-600 dark:text-neutral-300">
116
+ <p>{report_summary}</p>
117
+ </div>
118
+ {key_skills && key_skills.length > 0 && (
119
+ <div className="mt-6">
120
+ <h4 className="text-lg font-semibold text-neutral-900 dark:text-white mb-3">Key Skills Observed</h4>
121
+ <div className="flex flex-wrap gap-2">
122
+ {key_skills.map((skill: string, index: number) => (
123
+ <span key={index} className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-1 rounded-full dark:bg-blue-900 dark:text-blue-300">
124
+ {skill}
125
+ </span>
126
+ ))}
127
+ </div>
128
+ </div>
129
+ )}
130
+ </div>
131
+
132
+ <div className="pt-8">
133
+ <h3 className="text-xl font-bold text-neutral-900 dark:text-white mb-3">2. KYD Technical™ Signals</h3>
134
+ <div className="prose prose-sm dark:prose-invert max-w-none text-neutral-600 dark:text-neutral-300 mb-6">
135
+ <p>{developer_trust_explanation}</p>
136
+ </div>
137
+ <ProviderInsights
138
+ platforms={connectedPlatforms || []}
139
+ insights={assessmentResult.provider_insights}
140
+ variant="public"
141
+ />
142
+ </div>
143
+
144
+ <div className="pt-8">
145
+ <h3 className="text-xl font-bold text-neutral-900 dark:text-white mb-3">3. KYD Risk™ Signals</h3>
146
+ {badgeData.optOutScreening ? (
147
+ <div className="mb-4 p-4 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 rounded-lg">
148
+ <div className="flex items-start">
149
+ <span className="h-5 w-5 text-yellow-500 dark:text-yellow-400 mr-3 mt-0.5 flex-shrink-0">
150
+ <FiAlertTriangle size={20} />
151
+ </span>
152
+ <div>
153
+ <h4 className="font-bold text-yellow-800 dark:text-yellow-200">User Opted Out of Screening</h4>
154
+ <p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
155
+ The user chose not to participate in the automated sanctions and risk screening process. The risk score reflects this decision and is for informational purposes only.
156
+ </p>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ ) : (
161
+ <>
162
+ <div className="prose prose-sm dark:prose-invert max-w-none text-neutral-600 dark:text-neutral-300 space-y-4 mb-6">
163
+ <p>{riskScore?.description || ''}</p>
164
+ </div>
165
+ <IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
166
+ </>
167
+ )}
168
+ </div>
169
+
170
+ <div className="pt-8">
171
+ <h3 className="text-xl font-bold text-neutral-900 dark:text-white mb-3">4. KYD AI™ Signals <span className="text-sm font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded-full align-middle">Beta</span></h3>
172
+ <div className="prose prose-sm dark:prose-invert max-w-none text-neutral-600 dark:text-neutral-300 mb-6 space-y-4">
173
+ <p>{assessmentResult.ai_usage_summary?.explanation}</p>
174
+ {assessmentResult.ai_usage_summary?.key_findings && (
175
+ <ul className="list-disc list-inside">
176
+ {assessmentResult.ai_usage_summary.key_findings.map((finding, index) => (
177
+ <li key={index}>{finding}</li>
178
+ ))}
179
+ </ul>
180
+ )}
181
+ </div>
182
+ </div>
183
+
184
+ <div className="pt-8">
185
+ <h3 className="text-xl font-bold text-neutral-900 dark:text-white mb-3">5. Industry Considerations</h3>
186
+ <div className="prose prose-sm dark:prose-invert max-w-none text-neutral-600 dark:text-neutral-300">
187
+ <p>{industry_considerations}</p>
188
+ </div>
189
+ </div>
190
+
191
+ {!badgeData.optOutScreening && screening_sources && (
192
+ <div className="pt-8">
193
+ <h3 className="text-2xl font-bold text-neutral-900 dark:text-white mb-4">6. Appendix: Data Sources</h3>
194
+ <div className="space-y-8">
195
+ <div>
196
+ <h4 className="text-xl font-bold text-neutral-800 dark:text-white mb-4">Sanctions & Watchlists</h4>
197
+ <AppendixTables
198
+ type="sanctions"
199
+ sources={[...(screening_sources.ofac_lists || []), ...(screening_sources.additional_watchlists || [])]}
200
+ searchedAt={updatedAt}
201
+ developerName={developerName || 'this developer'}
202
+ />
203
+ </div>
204
+ <div>
205
+ <h4 className="text-xl font-bold text-neutral-800 dark:text-white mb-4">Country-specific Entity Affiliations</h4>
206
+ <AppendixTables
207
+ type="domains"
208
+ sources={screening_sources.risk_profile_domains || []}
209
+ searchedAt={updatedAt}
210
+ developerName={developerName || 'this developer'}
211
+ />
212
+ </div>
213
+ </div>
214
+ </div>
215
+ )}
216
+
217
+ <div className="pt-8 text-sm text-neutral-500 dark:text-neutral-400 text-center">
218
+ Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
219
+ year: 'numeric',
220
+ month: 'long',
221
+ day: 'numeric',
222
+ hour: 'numeric',
223
+ minute: '2-digit',
224
+ timeZoneName: 'short',
225
+ })}
226
+ </div>
227
+ </div>
228
+ </div>
229
+ <footer className="mt-12 pt-6 border-t border-neutral-200 dark:border-neutral-700">
230
+ <p className="text-center text-xs text-gray-500 dark:text-gray-400 max-w-4xl mx-auto">
231
+ © 2025 Know Your Developer, LLC. All rights reserved. KYD Self-Check™, and associated marks are trademarks of Know Your Developer, LLC. This document is confidential, proprietary, and intended solely for the individual or entity to whom it is addressed. Unauthorized use, disclosure, copying, or distribution of this document or any of its contents is strictly prohibited and may be unlawful. Know Your Developer, LLC assumes no responsibility or liability for any errors or omissions contained herein. Report validity subject to the terms and conditions stated on the official Know Your Developer website located at https://knowyourdeveloper.ai.
232
+ </p>
233
+ </footer>
234
+ </div>
235
+ );
236
+ };
237
+
238
+ export default SharedBadgeDisplay;
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { DomainCSVRow } from '../lib/types';
5
+
6
+ interface SanctionSource {
7
+ issuingEntity: string;
8
+ listName: string;
9
+ }
10
+
11
+ interface DomainSource {
12
+ country?: string;
13
+ entityType?: string;
14
+ entityName?: string;
15
+ url: string;
16
+ }
17
+
18
+ const PAGE_SIZE = 25;
19
+
20
+ interface AppendixTableProps {
21
+ type: 'sanctions' | 'domains';
22
+ sources: string[] | DomainCSVRow[];
23
+ searchedAt: string;
24
+ developerName: string;
25
+ }
26
+
27
+ const SanctionsRow = ({ source, searchedAt, developerName }: { source: SanctionSource, searchedAt: string, developerName: string }) => (
28
+ <tr className="hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
29
+ <td className="px-4 py-4 whitespace-nowrap text-sm font-medium text-neutral-800 dark:text-neutral-200">
30
+ {source.issuingEntity}
31
+ </td>
32
+ <td className="px-4 py-4 whitespace-normal text-sm text-neutral-600 dark:text-neutral-300">
33
+ {source.listName}
34
+ </td>
35
+ <td className="px-4 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">
36
+ {searchedAt}
37
+ </td>
38
+ <td className="px-4 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">
39
+ <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
40
+ Not Found
41
+ </span>
42
+ </td>
43
+ <td className="px-4 py-4 text-sm text-neutral-600 dark:text-neutral-300 whitespace-normal">
44
+ No exact match for <strong>{developerName}</strong> (based on name and email) was found on this list.
45
+ </td>
46
+ </tr>
47
+ );
48
+
49
+ const DomainRow = ({ source, searchedAt, developerName }: { source: DomainSource, searchedAt: string, developerName: string }) => (
50
+ <tr className="hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
51
+ <td className="px-4 py-4 whitespace-normal text-sm text-neutral-600 dark:text-neutral-300">{source.country || 'N/A'}</td>
52
+ <td className="px-4 py-4 whitespace-normal text-sm text-neutral-600 dark:text-neutral-300">{source.entityType || 'N/A'}</td>
53
+ <td className="px-4 py-4 whitespace-normal text-sm font-medium text-neutral-800 dark:text-neutral-200">{source.entityName || source.url}</td>
54
+ <td className="px-4 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">{searchedAt}</td>
55
+ <td className="px-4 py-4 whitespace-nowrap text-sm text-neutral-600 dark:text-neutral-300">
56
+ <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
57
+ Not Found
58
+ </span>
59
+ </td>
60
+ <td className="px-4 py-4 text-sm text-neutral-600 dark:text-neutral-300 whitespace-normal">
61
+ No profile matching <strong>{developerName}</strong> (based on name and email) was found at this domain.
62
+ </td>
63
+ </tr>
64
+ );
65
+
66
+
67
+ const AppendixTables: React.FC<AppendixTableProps> = ({ type, sources, searchedAt, developerName }) => {
68
+ const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
69
+
70
+ const formattedDate = new Date(searchedAt).toLocaleString(undefined, {
71
+ year: 'numeric', month: 'short', day: 'numeric',
72
+ hour: 'numeric', minute: '2-digit',
73
+ });
74
+
75
+ const headers = type === 'sanctions'
76
+ ? ["Issuing Entity", "List Name", "List Searched On", "Findings", "What the Findings Mean"]
77
+ : ["Country", "Entity Type", "Entity/Domain", "Searched On", "Findings", "What the Findings Mean"];
78
+
79
+ const parsedSources = type === 'sanctions'
80
+ ? (sources as string[]).map(sourceString => {
81
+ const parts = sourceString.split(':');
82
+ return { issuingEntity: parts[0].trim(), listName: parts.slice(1).join(':').trim() };
83
+ })
84
+ : (sources as DomainCSVRow[]).map(s => ({
85
+ country: s.Country,
86
+ entityType: s['Entity Type'],
87
+ entityName: s['Entity Name'],
88
+ url: s.URL,
89
+ }));
90
+
91
+ const visibleParsedSources = parsedSources.slice(0, visibleCount);
92
+
93
+ const handleLoadMore = () => {
94
+ setVisibleCount(currentCount => currentCount + PAGE_SIZE);
95
+ };
96
+
97
+ if (!sources || sources.length === 0) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <div>
103
+ <div className="overflow-x-auto rounded-lg border border-neutral-200 dark:border-neutral-700">
104
+ <table className="min-w-full divide-y divide-neutral-200 dark:divide-neutral-700">
105
+ <thead className="bg-neutral-50 dark:bg-neutral-800">
106
+ <tr>
107
+ {headers.map(header => (
108
+ <th key={header} scope="col" className="px-4 py-3 text-left text-xs font-semibold text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
109
+ {header}
110
+ </th>
111
+ ))}
112
+ </tr>
113
+ </thead>
114
+ <tbody className="bg-white dark:bg-neutral-900 divide-y divide-neutral-200 dark:divide-neutral-700">
115
+ {visibleParsedSources.map((source, index) =>
116
+ type === 'sanctions'
117
+ ? <SanctionsRow key={index} source={source as SanctionSource} searchedAt={formattedDate} developerName={developerName} />
118
+ : <DomainRow key={index} source={source as DomainSource} searchedAt={formattedDate} developerName={developerName} />
119
+ )}
120
+ </tbody>
121
+ </table>
122
+ </div>
123
+ {parsedSources.length > PAGE_SIZE && (
124
+ <div className="mt-4 flex items-center justify-between text-sm text-neutral-600 dark:text-neutral-400">
125
+ <p>
126
+ Showing {Math.min(visibleCount, parsedSources.length)} of {parsedSources.length} entries
127
+ </p>
128
+ {visibleCount < parsedSources.length && (
129
+ <button
130
+ onClick={handleLoadMore}
131
+ className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-400 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded-md"
132
+ >
133
+ Load More
134
+ </button>
135
+ )}
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ };
141
+
142
+ export default AppendixTables;
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { FiInfo } from 'react-icons/fi';
4
+ import { IpRiskAnalysis, IpRiskAnalysisFinding } from '../lib/types';
5
+
6
+ interface IpRiskAnalysisDisplayProps {
7
+ ipRiskAnalysis?: IpRiskAnalysis;
8
+ }
9
+
10
+ const FindingRow = ({ finding }: { finding: IpRiskAnalysisFinding }) => (
11
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-2 py-4 border-b border-neutral-200 dark:border-neutral-700 last:border-b-0">
12
+ <div className="md:col-span-1">
13
+ <p className="font-semibold text-neutral-800 dark:text-white">{finding.label}</p>
14
+ </div>
15
+ <div className="md:col-span-2">
16
+ <p className="text-neutral-700 dark:text-neutral-300">{finding.details}</p>
17
+ <p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1 italic">{finding.implication}</p>
18
+ </div>
19
+ </div>
20
+ );
21
+
22
+
23
+ const IpRiskAnalysisDisplay = ({ ipRiskAnalysis }: IpRiskAnalysisDisplayProps) => {
24
+ if (!ipRiskAnalysis || !ipRiskAnalysis.checked || !ipRiskAnalysis.findings || ipRiskAnalysis.findings.length === 0) {
25
+ return (
26
+ <div className="mt-6 p-4 bg-neutral-50 dark:bg-neutral-800/50 rounded-lg flex items-center">
27
+ <FiInfo className="h-5 w-5 text-neutral-500 mr-3 flex-shrink-0" />
28
+ <p className="text-sm text-neutral-600 dark:text-neutral-400">IP risk analysis was not performed for this assessment.</p>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ const { findings = [] } = ipRiskAnalysis;
34
+
35
+ return (
36
+ <div className="mt-6">
37
+ <div className="divide-y divide-neutral-200 dark:divide-neutral-700">
38
+ {findings.map((finding, index) => (
39
+ <FindingRow key={index} finding={finding} />
40
+ ))}
41
+ </div>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default IpRiskAnalysisDisplay;
@@ -0,0 +1,109 @@
1
+ import { IconType } from 'react-icons';
2
+ import { FaGithub, FaGitlab, FaStackOverflow, FaAtlassian, FaLinkedin, FaGoogle } from 'react-icons/fa';
3
+ import { SiFiverr, SiCredly, SiKaggle } from 'react-icons/si';
4
+ import { ProviderInsight } from '@/lib/types';
5
+
6
+ interface Platform {
7
+ name: string;
8
+ handle?: string | null;
9
+ url?: string | null;
10
+ observedAt?: string;
11
+ }
12
+
13
+ interface ProviderInsightsProps {
14
+ platforms: Platform[];
15
+ insights: { [provider: string]: ProviderInsight };
16
+ variant?: 'public' | 'private';
17
+ }
18
+
19
+ const providerIcons: { [key: string]: IconType } = {
20
+ GitHub: FaGithub,
21
+ GitLab: FaGitlab,
22
+ 'Stack Overflow': FaStackOverflow,
23
+ Atlassian: FaAtlassian,
24
+ Fiverr: SiFiverr,
25
+ LinkedIn: FaLinkedin,
26
+ Credly: SiCredly,
27
+ Kaggle: SiKaggle,
28
+ 'Google Scholar': FaGoogle,
29
+ };
30
+
31
+ export default function ProviderInsights({ platforms, insights, variant = 'private' }: ProviderInsightsProps) {
32
+ if (!platforms || platforms.length === 0) {
33
+ return null;
34
+ }
35
+
36
+ const isPublic = variant === 'public';
37
+
38
+ return (
39
+ <div className="space-y-6">
40
+ {platforms.map(platform => {
41
+ const Icon = platform.name ? providerIcons[platform.name] : null;
42
+ const providerInsight = insights[platform.name];
43
+ const observedDate = platform.observedAt ? new Date(platform.observedAt).toLocaleDateString(undefined, {
44
+ year: 'numeric',
45
+ month: 'short',
46
+ day: 'numeric',
47
+ }) : null;
48
+
49
+ return (
50
+ <div key={platform.name} className="bg-white dark:bg-neutral-800 rounded-lg shadow-sm border border-neutral-200 dark:border-neutral-700">
51
+ {/* Header */}
52
+ <div className="p-4 border-b border-neutral-200 dark:border-neutral-700">
53
+ <div className="flex items-center justify-between">
54
+ <div className="flex items-center space-x-3">
55
+ {Icon && <Icon className="h-6 w-6 text-neutral-600 dark:text-neutral-400" />}
56
+ <h3 className="font-semibold text-lg text-neutral-900 dark:text-white">{platform.name}</h3>
57
+ </div>
58
+ {observedDate && (
59
+ <span className="text-sm text-neutral-500 dark:text-neutral-400">
60
+ {isPublic ? `Accessed: ${observedDate}` : `Observed: ${observedDate}`}
61
+ </span>
62
+ )}
63
+ </div>
64
+ {platform.url && (
65
+ <a
66
+ href={platform.url}
67
+ target="_blank"
68
+ rel="noopener noreferrer"
69
+ className="mt-2 inline-block text-sm text-indigo-600 dark:text-indigo-400 hover:underline"
70
+ >
71
+ {platform.handle || 'View Profile'}
72
+ </a>
73
+ )}
74
+ </div>
75
+
76
+ {/* Insights */}
77
+ {providerInsight && (
78
+ <div className="p-4">
79
+ {/* Summary */}
80
+ <p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
81
+ {providerInsight.summary}
82
+ </p>
83
+
84
+ {/* Data Points */}
85
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
86
+ {providerInsight.data_points.map((point, index) => (
87
+ <div key={index} className="bg-neutral-50 dark:bg-neutral-700/50 rounded-lg p-4">
88
+ <div className="flex justify-between items-baseline mb-2">
89
+ <h4 className="font-medium text-neutral-900 dark:text-white">
90
+ {point.label}
91
+ </h4>
92
+ <span className="font-bold text-neutral-700 dark:text-neutral-200">
93
+ {point.value}
94
+ </span>
95
+ </div>
96
+ <p className="text-sm text-neutral-600 dark:text-neutral-400">
97
+ {point.significance}
98
+ </p>
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </div>
103
+ )}
104
+ </div>
105
+ );
106
+ })}
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+
5
+ const getBadgeImageUrl = (score: number) => {
6
+ if (score >= 75) return '/badgegreen.png';
7
+ if (score >= 50) return '/badgeyellow.png';
8
+ return '/badgered.png';
9
+ };
10
+
11
+ interface ReportHeaderProps {
12
+ badgeId: string | undefined;
13
+ developerName: string | undefined;
14
+ updatedAt: string | undefined;
15
+ score: number | undefined;
16
+ isPublic: boolean;
17
+ badgeImageUrl: string;
18
+ }
19
+
20
+ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, isPublic, badgeImageUrl }: ReportHeaderProps) => {
21
+ // Use the dynamic image if available, otherwise fall back to the score-based one.
22
+ const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score);
23
+
24
+ const formattedDate = updatedAt ? new Date(updatedAt).toLocaleString(undefined, {
25
+ year: 'numeric',
26
+ month: 'long',
27
+ day: 'numeric',
28
+ hour: 'numeric',
29
+ minute: '2-digit',
30
+ }) : 'N/A';
31
+
32
+ return (
33
+ <div className="mb-8 p-6 bg-white/80 dark:bg-neutral-900/80 backdrop-blur-sm rounded-lg shadow-lg flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
34
+ {/* Left Section */}
35
+ <div className="flex items-center text-left md:text-center">
36
+ <Image src={finalBadgeImageUrl} alt="KYD Badge" width={150} height={150} unoptimized />
37
+ <div className='flex flex-col'>
38
+ <h1 className="font-bold text-lg text-neutral-900 dark:text-white">
39
+ KYD Self-Check™
40
+ </h1>
41
+ <p className="text-sm text-neutral-600 dark:text-neutral-400">
42
+ {isPublic ? 'Public Report' : 'Private Report'}
43
+ </p>
44
+ </div>
45
+ </div>
46
+
47
+ {/* Middle Section */}
48
+ <div className="text-left md:text-center">
49
+ <p className="text-sm text-neutral-600 dark:text-neutral-400">Developer</p>
50
+ <p className="font-semibold text-neutral-900 dark:text-white text-2xl">{developerName || 'N/A'}</p>
51
+ </div>
52
+
53
+ {/* Right Section */}
54
+ <div className="text-left text-sm text-neutral-600 dark:text-neutral-400 space-y-1">
55
+ <p><span className="font-semibold text-neutral-800 dark:text-neutral-200">Requested By:</span> {developerName || 'N/A'}</p>
56
+ <p><span className="font-semibold text-neutral-800 dark:text-neutral-200">Organization:</span> Unaffiliated</p>
57
+ <p><span className="font-semibold text-neutral-800 dark:text-neutral-200">Date Completed:</span> {formattedDate}</p>
58
+ <p><span className="font-semibold text-neutral-800 dark:text-neutral-200">Report ID:</span> {badgeId}</p>
59
+ </div>
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default ReportHeader;
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import { FiShare2, FiLink, FiRefreshCw } from 'react-icons/fi';
5
+ import { FaTwitter, FaFacebook, FaLinkedin, FaReddit } from 'react-icons/fa';
6
+ import toast from 'react-hot-toast';
7
+
8
+ interface ShareButtonProps {
9
+ badgeId: string | undefined;
10
+ shareTitle: string;
11
+ shareText: string;
12
+ buttonText: string;
13
+ className?: string;
14
+ isPublic?: boolean;
15
+ onTogglePrivacy?: () => Promise<void> | void;
16
+ }
17
+
18
+ const ShareButton = ({ badgeId, shareTitle, shareText, buttonText, className, isPublic = true, onTogglePrivacy }: ShareButtonProps) => {
19
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
20
+ const [isToggling, setIsToggling] = useState(false);
21
+ const menuRef = useRef<HTMLDivElement>(null);
22
+ const [isNativeShareSupported, setIsNativeShareSupported] = useState(false);
23
+ const [url, setUrl] = useState('');
24
+
25
+ useEffect(() => {
26
+ setIsNativeShareSupported(!!navigator.share);
27
+ if (badgeId) {
28
+ setUrl(`${window.location.origin}/share/badge/${badgeId}`);
29
+ }
30
+ }, [badgeId]);
31
+
32
+ useEffect(() => {
33
+ const handleClickOutside = (event: MouseEvent) => {
34
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
35
+ setIsMenuOpen(false);
36
+ }
37
+ };
38
+ document.addEventListener('mousedown', handleClickOutside);
39
+ return () => {
40
+ document.removeEventListener('mousedown', handleClickOutside);
41
+ };
42
+ }, [menuRef]);
43
+
44
+ const handleButtonClick = async () => {
45
+ if (!isPublic && onTogglePrivacy) {
46
+ setIsToggling(true);
47
+ try {
48
+ await onTogglePrivacy();
49
+ setIsMenuOpen(true);
50
+ } catch (error) {
51
+ // The parent component is expected to show a toast.
52
+ console.error("Failed to make badge public for sharing.", error);
53
+ } finally {
54
+ setIsToggling(false);
55
+ }
56
+ } else {
57
+ setIsMenuOpen(!isMenuOpen);
58
+ }
59
+ };
60
+
61
+ const handleCopyLink = async () => {
62
+ if (url) {
63
+ try {
64
+ await navigator.clipboard.writeText(url);
65
+ toast.success('Link copied to clipboard!');
66
+ } catch (error) {
67
+ console.error('Failed to copy link:', error);
68
+ toast.error('Failed to copy link.');
69
+ }
70
+ }
71
+ setIsMenuOpen(false);
72
+ };
73
+
74
+ const socialPlatforms = [
75
+ {
76
+ name: 'Twitter',
77
+ icon: FaTwitter,
78
+ url: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(shareText)}`,
79
+ color: 'text-sky-500'
80
+ },
81
+ {
82
+ name: 'Facebook',
83
+ icon: FaFacebook,
84
+ url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
85
+ color: 'text-blue-600'
86
+ },
87
+ {
88
+ name: 'LinkedIn',
89
+ icon: FaLinkedin,
90
+ url: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(shareTitle)}&summary=${encodeURIComponent(shareText)}`,
91
+ color: 'text-sky-700'
92
+ },
93
+ {
94
+ name: 'Reddit',
95
+ icon: FaReddit,
96
+ url: `https://www.reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(shareTitle)}`,
97
+ color: 'text-orange-500'
98
+ }
99
+ ];
100
+
101
+ const handleNativeShare = async () => {
102
+ if (url && navigator.share) {
103
+ const shareData = {
104
+ title: shareTitle,
105
+ text: shareText,
106
+ url: url,
107
+ };
108
+ try {
109
+ await navigator.share(shareData);
110
+ toast.success('Assessment shared successfully!');
111
+ } catch (error) {
112
+ if (!(error instanceof Error && error.name === 'AbortError')) {
113
+ console.error('Native share failed:', error);
114
+ toast.error('Could not share.');
115
+ }
116
+ }
117
+ } else if (!navigator.share) {
118
+ toast.error('Native sharing is not supported on this browser.');
119
+ } else {
120
+ toast.error('Could not find a valid badge to share.');
121
+ }
122
+ setIsMenuOpen(false);
123
+ };
124
+
125
+ return (
126
+ <div className="relative" ref={menuRef}>
127
+ <button
128
+ onClick={handleButtonClick}
129
+ disabled={!badgeId || !url || isToggling}
130
+ className={className || "flex items-center px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors disabled:bg-indigo-400 disabled:cursor-not-allowed"}
131
+ >
132
+ {isToggling ? (
133
+ <FiRefreshCw className="animate-spin" />
134
+ ) : (
135
+ <FiShare2 />
136
+ )}
137
+ <span className="hidden sm:inline sm:ml-2">{isToggling ? 'Making Public...' : buttonText}</span>
138
+ </button>
139
+
140
+ {isMenuOpen && (
141
+ <div className="absolute right-0 mt-2 w-56 origin-top-right bg-white dark:bg-neutral-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
142
+ <div className="py-1">
143
+ {socialPlatforms.map((platform) => {
144
+ const Icon = platform.icon;
145
+ return (
146
+ <a
147
+ key={platform.name}
148
+ href={platform.url}
149
+ target="_blank"
150
+ rel="noopener noreferrer"
151
+ onClick={() => setIsMenuOpen(false)}
152
+ className="flex items-center w-full px-4 py-2 text-sm text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700"
153
+ >
154
+ <Icon className={`mr-3 h-5 w-5 ${platform.color}`} />
155
+ <span>Share on {platform.name}</span>
156
+ </a>
157
+ );
158
+ })}
159
+ <div className="border-t border-neutral-200 dark:border-neutral-700 my-1"></div>
160
+ <button
161
+ onClick={handleCopyLink}
162
+ className="flex items-center w-full px-4 py-2 text-sm text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700"
163
+ >
164
+ <FiLink className="mr-3 h-5 w-5" />
165
+ <span>Copy Link</span>
166
+ </button>
167
+ {isNativeShareSupported && (
168
+ <>
169
+ <div className="border-t border-neutral-200 dark:border-neutral-700 my-1"></div>
170
+ <button
171
+ onClick={handleNativeShare}
172
+ className="flex items-center w-full px-4 py-2 text-sm text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-700"
173
+ >
174
+ <FiShare2 className="mr-3 h-5 w-5" />
175
+ <span>Share via...</span>
176
+ </button>
177
+ </>
178
+ )}
179
+ </div>
180
+ </div>
181
+ )}
182
+ </div>
183
+ );
184
+ };
185
+
186
+ export default ShareButton;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export { default as SharedBadgeDisplay } from './SharedBadgeDisplay';
package/src/types.ts ADDED
@@ -0,0 +1,172 @@
1
+ export type Provider = {
2
+ id: string;
3
+ name: string;
4
+ authUrl?: string;
5
+ connectionType?: 'oauth' | 'url';
6
+ iconColor?: string;
7
+ colors: {
8
+ bg: string;
9
+ hoverBg: string;
10
+ text: string;
11
+ };
12
+ };
13
+
14
+ export type AssessmentSummary = {
15
+ badgeId: string;
16
+ status: string;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ assessmentError?: string;
20
+ kyd_self_check_score?: number;
21
+ };
22
+
23
+ export interface PublicBadgeData {
24
+ badgeId: string;
25
+ developerName?: string;
26
+ assessmentResult: AssessmentResult;
27
+ updatedAt: string;
28
+ badgeImageUrl?: string;
29
+ connectedPlatforms?: {
30
+ name: string;
31
+ url?: string;
32
+ handle?: string;
33
+ observedAt?: string;
34
+ }[];
35
+ optOutScreening?: boolean;
36
+ }
37
+
38
+ export type VerifiedBadge = {
39
+ badgeName: string;
40
+ issuedOn: string;
41
+ issuerName: string;
42
+ recipientName: string;
43
+ url: string;
44
+ };
45
+
46
+ export type User = {
47
+ userId: string;
48
+ name: string;
49
+ email: string;
50
+ fullName:string;
51
+ profilePictureUrl?: string;
52
+ verifiedBadges: VerifiedBadge[];
53
+ assessmentStatus: 'idle' | 'in-progress' | 'completed' | 'failed' | 'pending_payment';
54
+ assessmentResult?: AssessmentResult;
55
+ assessmentError?: string;
56
+ assessmentUpdatedAt?: string;
57
+ latestBadgeId?: string;
58
+ latestBadgeImageUrl?: string;
59
+ isPublic?: boolean;
60
+ assessments: AssessmentSummary[];
61
+ connectedPlatforms?: { name: string; url: string | null; handle: string | null; observedAt?: string }[];
62
+ };
63
+
64
+ export interface SummaryScore {
65
+ score?: number;
66
+ description: string;
67
+ descriptor?: string;
68
+ letter_grade?: string;
69
+ }
70
+
71
+ export interface DataPoint {
72
+ label: string;
73
+ value: string;
74
+ significance: string;
75
+ }
76
+
77
+ export interface ProviderInsight {
78
+ data_points: DataPoint[];
79
+ summary: string;
80
+ }
81
+
82
+ export interface DomainCSVRow {
83
+ Country?: string;
84
+ 'Entity Type'?: string;
85
+ 'Entity Name'?: string;
86
+ URL: string;
87
+ }
88
+
89
+ export interface IpRiskAnalysisFinding {
90
+ label: string;
91
+ details: string;
92
+ implication: string;
93
+ }
94
+
95
+ export interface IpRiskAnalysis {
96
+ checked: boolean;
97
+ findings: IpRiskAnalysisFinding[];
98
+ raw_data: {
99
+ vpn_ips?: string[];
100
+ countries?: string[];
101
+ blocklisted_ips?: { ip: string; lists: string[] }[];
102
+ abusive_ips?: string[];
103
+ impossible_travel_events?: string[];
104
+ anomalous_geo_logins?: string[];
105
+ };
106
+ }
107
+
108
+ export interface AssessmentResult {
109
+ report_summary: string;
110
+ recommendations: {
111
+ summary: string;
112
+ bullet_points: string[];
113
+ };
114
+ developer_trust_explanation: string;
115
+ industry_considerations: string;
116
+ ai_usage_summary: {
117
+ explanation: string;
118
+ key_findings: string[];
119
+ files_with_ai_findings: number;
120
+ files_analyzed: number;
121
+ };
122
+ key_skills: string[];
123
+ summary_scores: {
124
+ developer_trust: SummaryScore;
125
+ risk_score: SummaryScore;
126
+ kyd_self_check: SummaryScore;
127
+ ai_usage: SummaryScore;
128
+ };
129
+ provider_insights: {
130
+ [provider: string]: ProviderInsight;
131
+ };
132
+ badgeId?: string;
133
+ screening_sources?: {
134
+ ofac_lists: string[];
135
+ risk_profile_domains: DomainCSVRow[];
136
+ additional_watchlists?: string[];
137
+ ip_risk_analysis?: IpRiskAnalysis;
138
+ };
139
+ optOutScreening?: boolean;
140
+ clientMetadata?: ClientMetadata;
141
+ }
142
+
143
+ export interface ClientMetadata {
144
+ ipAddress: string;
145
+ userAgent: string;
146
+ language: string;
147
+ screenResolution: string;
148
+ timezone: string;
149
+ referrer: string;
150
+ }
151
+
152
+ export interface ScoresByCategory {
153
+ [category: string]: {
154
+ subtotal: number;
155
+ fields: {
156
+ [field: string]: FieldScore | AnalyzedItem | AnalyzedItem[];
157
+ };
158
+ };
159
+ }
160
+
161
+ export interface FieldScore {
162
+ score: number;
163
+ summary?: string;
164
+ justification: string;
165
+ }
166
+
167
+ export interface AnalyzedItem {
168
+ repo_name?: string;
169
+ summary: string;
170
+ quality_score?: number;
171
+ professional_score?: number;
172
+ }