Skip to content

Commit 0cab7e1

Browse files
✨ feat(scripts): introduce LinkedIn codegen
1 parent e69cefa commit 0cab7e1

File tree

2 files changed

+201
-3
lines changed

2 files changed

+201
-3
lines changed

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
"dev": "next",
1111
"build": "next build",
1212
"start": "next start",
13-
"lint": "next lint",
13+
"lint": "next lint --fix",
1414
"lint:deadcode": "! ts-prune | grep -v \"used in module\"",
1515
"format": "prettier --write src/",
16-
"codegen:schema": "graphql-codegen --config codegen.yml --require dotenv/config"
16+
"codegen:schema": "graphql-codegen --config codegen.yml --require dotenv/config",
17+
"codegen:linkedin": "esno scripts/codegen-linkedin.ts"
1718
},
1819
"dependencies": {
1920
"@apollo/client": "^3.7.7",
@@ -26,6 +27,7 @@
2627
"@graphql-codegen/typescript-operations": "^3.0.0",
2728
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
2829
"@lyrasearch/lyra": "^0.4.6",
30+
"@next/font": "^13.2.4",
2931
"@radix-ui/react-navigation-menu": "^1.1.1",
3032
"@radix-ui/react-tooltip": "^1.0.3",
3133
"@react-spring/web": "^9.6.1",
@@ -39,12 +41,13 @@
3941
"framer-motion": "^9.0.2",
4042
"fs-extra": "^11.1.0",
4143
"github-slugger": "^2.0.0",
44+
"got": "^12.6.0",
4245
"graphql": "^16.6.0",
4346
"handlebars": "^4.7.7",
4447
"mdx-bundler": "^9.2.1",
4548
"next": "^13.1.6",
4649
"next-contentlayer": "^0.3.0",
47-
"next-mdx-remote": "^4.3.0",
50+
"next-mdx-remote": "^4.4.1",
4851
"next-seo": "^5.15.0",
4952
"next-themes": "^0.2.1",
5053
"pliny": "^0.0.10",
@@ -90,6 +93,7 @@
9093
"eslint": "^8.34.0",
9194
"eslint-config-next": "13.1.6",
9295
"eslint-plugin-tailwindcss": "^3.8.3",
96+
"esno": "^0.16.3",
9397
"husky": "^8.0.3",
9498
"lint-staged": "^13.1.1",
9599
"prettier": "^2.8.4",

scripts/codegen-linkedin.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import * as dotenv from 'dotenv';
2+
import { outputFile } from 'fs-extra';
3+
import got from 'got';
4+
5+
/*~
6+
* CONSTANTS
7+
*/
8+
9+
dotenv.config();
10+
11+
const LINKEDIN_URL = 'https://www.linkedin.com/in/ythecombinator';
12+
const DEST_PATH = 'src/content/biography/experience.json';
13+
14+
const API_BASE_URL = 'https://nubela.co/proxycurl/api/v2/linkedin';
15+
const API_TOKEN = `Bearer ${process.env.NUBELA_TOKEN}`;
16+
17+
/*~
18+
* UTILS
19+
*/
20+
21+
function getMonthName(monthNumber: number) {
22+
const date = new Date();
23+
date.setMonth(monthNumber - 1);
24+
25+
return date.toLocaleString('en-US', { month: 'long' });
26+
}
27+
28+
function getStartDate(startDate: ExperienceEntry['starts_at']) {
29+
return `${getMonthName(startDate.month)}, ${startDate.year}`;
30+
}
31+
32+
function getEndDate(endDate: ExperienceEntry['ends_at']) {
33+
if (endDate) {
34+
return `${getMonthName(endDate.month)}, ${endDate.year}`;
35+
}
36+
37+
return 'Present';
38+
}
39+
40+
function getCompanyUrl(url: string) {
41+
if (url) {
42+
return url.replace('https://cz.linkedin.com', 'https://linkedin.com');
43+
}
44+
45+
return null;
46+
}
47+
48+
function mapExperience(experiences: ExperienceEntry[]) {
49+
return experiences
50+
.map((experience) => {
51+
return {
52+
company: experience.company,
53+
companyUrl: getCompanyUrl(experience.company_linkedin_profile_url),
54+
startDate: getStartDate(experience.starts_at),
55+
endDate: getEndDate(experience.ends_at),
56+
location: experience.location,
57+
title: experience.title,
58+
description: experience.description,
59+
};
60+
})
61+
.filter((experience) => Boolean(experience.companyUrl));
62+
}
63+
64+
/*~
65+
* CORE
66+
*/
67+
68+
async function fetchData() {
69+
const searchParams = new URLSearchParams([
70+
['url', LINKEDIN_URL],
71+
['fallback_to_cache', 'on-error'],
72+
]);
73+
74+
const result = await got
75+
.get(API_BASE_URL, {
76+
searchParams,
77+
headers: { Authorization: API_TOKEN },
78+
})
79+
.json<Profile>();
80+
81+
return result;
82+
}
83+
84+
async function codegenLinkedin() {
85+
const data = await fetchData();
86+
87+
const work = mapExperience(data.experiences!);
88+
const volunteering = mapExperience(data.volunteer_work!);
89+
90+
await outputFile(DEST_PATH, JSON.stringify({ work, volunteering }, null, 2));
91+
}
92+
93+
codegenLinkedin();
94+
95+
/*~
96+
* TYPES
97+
*/
98+
99+
// Derived from: https://nubela.co/proxycurl/docs#people-api-person-profile-endpoint
100+
// Using: https://jvilk.com/MakeTypes/
101+
102+
interface Profile {
103+
accomplishment_projects: AccomplishmentProject[];
104+
activities: Activity[];
105+
background_cover_image_url: string;
106+
certifications: Certification[];
107+
city: string;
108+
connections: number;
109+
country: string;
110+
country_full_name: string;
111+
education: Education[];
112+
experiences: ExperienceEntry[];
113+
first_name: string;
114+
full_name: string;
115+
headline: string;
116+
languages: string[];
117+
last_name: string;
118+
occupation: string;
119+
profile_pic_url: string;
120+
public_identifier: string;
121+
recommendations: string[];
122+
similarly_named_profiles: SimilarProfile[];
123+
state: string;
124+
summary: string;
125+
volunteer_work: VolunteerWorkEntry[];
126+
}
127+
128+
interface AccomplishmentProject {
129+
description: string;
130+
ends_at: CalendarDate;
131+
starts_at: CalendarDate;
132+
title: string;
133+
url: string;
134+
}
135+
136+
interface CalendarDate {
137+
day: number;
138+
month: number;
139+
year: number;
140+
}
141+
142+
interface Activity {
143+
activity_status: string;
144+
link: string;
145+
title: string;
146+
}
147+
148+
interface Certification {
149+
authority: string;
150+
ends_at: CalendarDate;
151+
name: string;
152+
starts_at: CalendarDate;
153+
url: string;
154+
}
155+
156+
interface Education {
157+
degree_name: string;
158+
description: string;
159+
ends_at: CalendarDate;
160+
field_of_study: string;
161+
grade: number;
162+
logo_url: string;
163+
school: string;
164+
school_linkedin_profile_url: string;
165+
starts_at: CalendarDate;
166+
}
167+
168+
interface CalendarDate {
169+
day: number;
170+
month: number;
171+
year: number;
172+
}
173+
174+
interface ExperienceEntry {
175+
company: string;
176+
company_linkedin_profile_url: string;
177+
description: string;
178+
ends_at: CalendarDate;
179+
location: string;
180+
logo_url: string;
181+
starts_at: CalendarDate;
182+
title: string;
183+
}
184+
185+
interface VolunteerWorkEntry extends ExperienceEntry {
186+
cause: string;
187+
}
188+
189+
interface SimilarProfile {
190+
link: string;
191+
location: string;
192+
name: string;
193+
summary: string;
194+
}

0 commit comments

Comments
 (0)