mirror of
https://github.com/prdlk/cv.git
synced 2026-08-02 17:31:41 +00:00
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
/** Row shapes and corpus loader for the cv-tailor D1 database. */
|
||
|
||
export interface ProfileRow {
|
||
full_name: string;
|
||
email: string | null;
|
||
phone: string | null;
|
||
website: string | null;
|
||
linkedin: string | null;
|
||
github: string | null;
|
||
twitter: string | null;
|
||
}
|
||
|
||
export interface SummaryRow {
|
||
role: string;
|
||
summary: string;
|
||
}
|
||
|
||
export interface ExperienceItem {
|
||
slug: string;
|
||
title: string;
|
||
company: string;
|
||
start_date: string;
|
||
end_date: string | null;
|
||
location: string | null;
|
||
profile_heading: string | null;
|
||
bullets: string[];
|
||
skills: string[];
|
||
}
|
||
|
||
export interface ProjectItem {
|
||
slug: string;
|
||
title: string;
|
||
start_date: string | null;
|
||
end_date: string | null;
|
||
url: string | null;
|
||
associated_with: string | null;
|
||
bullets: string[];
|
||
skills: string[];
|
||
}
|
||
|
||
export interface SkillRow {
|
||
name: string;
|
||
category: "language" | "framework" | "methodology";
|
||
grp: string | null;
|
||
tier: string | null;
|
||
position: number;
|
||
}
|
||
|
||
export interface EducationRow {
|
||
school: string;
|
||
degree: string | null;
|
||
field: string | null;
|
||
start_date: string | null;
|
||
end_date: string | null;
|
||
activities: string | null;
|
||
}
|
||
|
||
export interface HonorRow {
|
||
title: string;
|
||
date: string | null;
|
||
institution: string | null;
|
||
}
|
||
|
||
export interface OrganizationRow {
|
||
name: string;
|
||
url: string | null;
|
||
title: string | null;
|
||
}
|
||
|
||
export interface Corpus {
|
||
profile: ProfileRow;
|
||
summaries: SummaryRow[];
|
||
experience: ExperienceItem[];
|
||
projects: ProjectItem[];
|
||
skills: SkillRow[];
|
||
education: EducationRow | null;
|
||
honors: HonorRow[];
|
||
organizations: OrganizationRow[];
|
||
}
|
||
|
||
interface BulletRow {
|
||
parent_type: string;
|
||
parent_slug: string;
|
||
position: number;
|
||
text: string;
|
||
}
|
||
|
||
interface ItemSkillRow {
|
||
parent_type: string;
|
||
parent_slug: string;
|
||
skill: string;
|
||
}
|
||
|
||
type ExperienceDbRow = Omit<ExperienceItem, "bullets" | "skills">;
|
||
type ProjectDbRow = Omit<ProjectItem, "bullets" | "skills">;
|
||
|
||
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||
|
||
/**
|
||
* "2021-03-01".."" -> "Mar 2021 – Present" (month style) or "2021 – Present" (year style).
|
||
* Uses an en dash; the LaTeX escaper converts it to `--`.
|
||
*/
|
||
export function formatRange(
|
||
start: string | null,
|
||
end: string | null,
|
||
style: "month" | "year",
|
||
): string {
|
||
if (!start) return "";
|
||
let from: string;
|
||
let to = "Present";
|
||
if (style === "month") {
|
||
from = `${MONTHS[Number(start.slice(5, 7)) - 1]} ${start.slice(0, 4)}`;
|
||
if (end) to = `${MONTHS[Number(end.slice(5, 7)) - 1]} ${end.slice(0, 4)}`;
|
||
} else {
|
||
from = start.slice(0, 4);
|
||
if (end) to = end.slice(0, 4);
|
||
}
|
||
return `${from} – ${to}`;
|
||
}
|
||
|
||
export async function loadCorpus(db: D1Database): Promise<Corpus> {
|
||
const [profile, summaries, experience, projects, bullets, itemSkills, skills, education, honors, organizations] =
|
||
await Promise.all([
|
||
db.prepare("SELECT * FROM profile LIMIT 1").all<ProfileRow>(),
|
||
db.prepare("SELECT role, summary FROM summaries").all<SummaryRow>(),
|
||
db.prepare("SELECT * FROM experience ORDER BY start_date DESC").all<ExperienceDbRow>(),
|
||
db.prepare("SELECT * FROM projects ORDER BY start_date DESC").all<ProjectDbRow>(),
|
||
db
|
||
.prepare("SELECT parent_type, parent_slug, position, text FROM bullets ORDER BY position")
|
||
.all<BulletRow>(),
|
||
db.prepare("SELECT parent_type, parent_slug, skill FROM item_skills").all<ItemSkillRow>(),
|
||
db
|
||
.prepare("SELECT name, category, grp, tier, position FROM skills ORDER BY position")
|
||
.all<SkillRow>(),
|
||
db.prepare("SELECT * FROM education LIMIT 1").all<EducationRow>(),
|
||
db.prepare("SELECT title, date, institution FROM honors").all<HonorRow>(),
|
||
db.prepare("SELECT name, url, title FROM organizations").all<OrganizationRow>(),
|
||
]);
|
||
|
||
const profileRow = profile.results[0];
|
||
if (!profileRow) throw new Error("D1 is empty — run `bun run sync` first.");
|
||
|
||
const experienceItems: ExperienceItem[] = experience.results.map((r) => ({ ...r, bullets: [], skills: [] }));
|
||
const projectItems: ProjectItem[] = projects.results.map((r) => ({ ...r, bullets: [], skills: [] }));
|
||
const bySlug = new Map<string, { bullets: string[]; skills: string[] }>();
|
||
for (const item of experienceItems) bySlug.set(`experience:${item.slug}`, item);
|
||
for (const item of projectItems) bySlug.set(`project:${item.slug}`, item);
|
||
for (const b of bullets.results) bySlug.get(`${b.parent_type}:${b.parent_slug}`)?.bullets.push(b.text);
|
||
for (const s of itemSkills.results) bySlug.get(`${s.parent_type}:${s.parent_slug}`)?.skills.push(s.skill);
|
||
|
||
return {
|
||
profile: profileRow,
|
||
summaries: summaries.results,
|
||
experience: experienceItems,
|
||
projects: projectItems,
|
||
skills: skills.results,
|
||
education: education.results[0] ?? null,
|
||
honors: honors.results,
|
||
organizations: organizations.results,
|
||
};
|
||
}
|