normalize section items with consistent css classes

This commit is contained in:
Amruth Pillai
2026-01-23 09:28:05 +01:00
parent 06d54a9060
commit 8214f2a910
14 changed files with 59 additions and 39 deletions
@@ -15,10 +15,10 @@ export function AwardsItem({ className, ...item }: AwardsItemProps) {
<p className="section-item-title awards-item-title"> <p className="section-item-title awards-item-title">
<strong>{item.title}</strong> <strong>{item.title}</strong>
</p> </p>
<p className="section-item-metadata text-right">{item.date}</p> <p className="section-item-metadata awards-item-date text-right">{item.date}</p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="section-item-awarder awards-item-awarder">{item.awarder}</p> <p className="section-item-metadata awards-item-awarder">{item.awarder}</p>
</div> </div>
</div> </div>
<div className="section-item-description awards-item-description"> <div className="section-item-description awards-item-description">
@@ -15,10 +15,10 @@ export function CertificationsItem({ className, ...item }: CertificationsItemPro
<p className="section-item-title certifications-item-title"> <p className="section-item-title certifications-item-title">
<strong>{item.title}</strong> <strong>{item.title}</strong>
</p> </p>
<p className="section-item-metadata text-right">{item.date}</p> <p className="section-item-metadata certifications-item-date text-right">{item.date}</p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="section-item-issuer certifications-item-issuer">{item.issuer}</p> <p className="section-item-metadata certifications-item-issuer">{item.issuer}</p>
</div> </div>
</div> </div>
<div className="section-item-description certifications-item-description"> <div className="section-item-description certifications-item-description">
@@ -15,13 +15,13 @@ export function EducationItem({ className, ...item }: EducationItemProps) {
<p className="section-item-title education-item-title"> <p className="section-item-title education-item-title">
<strong>{item.school}</strong> <strong>{item.school}</strong>
</p> </p>
<p className="section-item-metadata education-item-metadata text-right"> <p className="section-item-metadata education-item-degree-grade text-right">
{[item.degree, item.grade].filter(Boolean).join(" • ")} {[item.degree, item.grade].filter(Boolean).join(" • ")}
</p> </p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p>{item.area}</p> <p className="section-item-metadata education-item-area">{item.area}</p>
<p className="section-item-metadata education-item-metadata text-right"> <p className="section-item-metadata education-item-location-period text-right">
{[item.location, item.period].filter(Boolean).join(" • ")} {[item.location, item.period].filter(Boolean).join(" • ")}
</p> </p>
</div> </div>
@@ -15,11 +15,11 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
<p className="section-item-title experience-item-title"> <p className="section-item-title experience-item-title">
<strong>{item.company}</strong> <strong>{item.company}</strong>
</p> </p>
<p className="section-item-metadata experience-item-metadata text-right">{item.location}</p> <p className="section-item-metadata experience-item-location text-right">{item.location}</p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p>{item.position}</p> <p className="section-item-metadata experience-item-position">{item.position}</p>
<p className="section-item-metadata experience-item-metadata text-right">{item.period}</p> <p className="section-item-metadata experience-item-period text-right">{item.period}</p>
</div> </div>
</div> </div>
<div className="section-item-description experience-item-description"> <div className="section-item-description experience-item-description">
@@ -11,7 +11,7 @@ export function InterestsItem({ className, ...item }: InterestsItemProps) {
<div className={cn("interests-item", className)}> <div className={cn("interests-item", className)}>
<div className="section-item-header flex items-center gap-x-1.5"> <div className="section-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon interests-item-icon shrink-0" /> <PageIcon icon={item.icon} className="section-item-icon interests-item-icon shrink-0" />
<p className="section-item-name interests-item-name"> <p className="section-item-title interests-item-name">
<strong>{item.name}</strong> <strong>{item.name}</strong>
</p> </p>
</div> </div>
@@ -10,10 +10,10 @@ export function LanguagesItem({ className, ...item }: LanguagesItemProps) {
return ( return (
<div className={cn("languages-item", className)}> <div className={cn("languages-item", className)}>
<div className="section-item-header"> <div className="section-item-header">
<p className="section-item-name languages-item-name"> <p className="section-item-title languages-item-name">
<strong>{item.language}</strong> <strong>{item.language}</strong>
</p> </p>
<p className="section-item-fluency languages-item-fluency opacity-80">{item.fluency}</p> <p className="section-item-metadata languages-item-fluency opacity-80">{item.fluency}</p>
</div> </div>
<PageLevel level={item.level} className="section-item-level languages-item-level" /> <PageLevel level={item.level} className="section-item-level languages-item-level" />
@@ -12,7 +12,7 @@ export function ProfilesItem({ className, ...item }: ProfilesItemProps) {
<div className={cn("profiles-item", className)}> <div className={cn("profiles-item", className)}>
<div className="section-item-header flex items-center gap-x-1.5"> <div className="section-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon shrink-0" /> <PageIcon icon={item.icon} className="section-item-icon profiles-item-icon shrink-0" />
<p className="section-item-network profiles-item-network"> <p className="section-item-title profiles-item-network">
<strong>{item.network}</strong> <strong>{item.network}</strong>
</p> </p>
</div> </div>
@@ -15,7 +15,7 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
<p className="section-item-title projects-item-title"> <p className="section-item-title projects-item-title">
<strong>{item.name}</strong> <strong>{item.name}</strong>
</p> </p>
<p className="section-item-metadata text-right">{item.period}</p> <p className="section-item-metadata projects-item-period text-right">{item.period}</p>
</div> </div>
</div> </div>
<div className="section-item-description projects-item-description"> <div className="section-item-description projects-item-description">
@@ -15,10 +15,10 @@ export function PublicationsItem({ className, ...item }: PublicationsItemProps)
<p className="section-item-title publications-item-title"> <p className="section-item-title publications-item-title">
<strong>{item.title}</strong> <strong>{item.title}</strong>
</p> </p>
<p className="section-item-metadata text-right">{item.date}</p> <p className="section-item-metadata publications-item-date text-right">{item.date}</p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="section-item-publisher publications-item-publisher">{item.publisher}</p> <p className="section-item-metadata publications-item-publisher">{item.publisher}</p>
</div> </div>
</div> </div>
<div className="section-item-description publications-item-description"> <div className="section-item-description publications-item-description">
@@ -12,17 +12,17 @@ export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
<div className={cn("references-item", className)}> <div className={cn("references-item", className)}>
<div className="section-item-header references-item-header"> <div className="section-item-header references-item-header">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="section-item-name references-item-name"> <p className="section-item-title references-item-name">
<strong>{item.name}</strong> <strong>{item.name}</strong>
</p> </p>
</div> </div>
<p className="section-item-subtitle references-item-position">{item.position}</p> <p className="section-item-metadata references-item-position">{item.position}</p>
</div> </div>
<div className="section-item-description references-item-description"> <div className="section-item-description references-item-description">
<TiptapContent content={item.description} /> <TiptapContent content={item.description} />
</div> </div>
<div className="section-item-footer references-item-footer flex flex-col"> <div className="section-item-footer references-item-footer flex flex-col">
<div className="section-item-metadata references-item-phone">{item.phone}</div> <p className="section-item-metadata references-item-phone">{item.phone}</p>
<PageLink {...item.website} label={item.website.label} className="section-item-link references-item-link" /> <PageLink {...item.website} label={item.website.label} className="section-item-link references-item-link" />
</div> </div>
</div> </div>
@@ -12,13 +12,13 @@ export function SkillsItem({ className, ...item }: SkillsItemProps) {
<div className={cn("skills-item", className)}> <div className={cn("skills-item", className)}>
<div className="section-item-header flex items-center gap-x-1.5"> <div className="section-item-header flex items-center gap-x-1.5">
<PageIcon icon={item.icon} className="section-item-icon skills-item-icon shrink-0" /> <PageIcon icon={item.icon} className="section-item-icon skills-item-icon shrink-0" />
<p className="section-item-name skills-item-name"> <p className="section-item-title skills-item-name">
<strong>{item.name}</strong> <strong>{item.name}</strong>
</p> </p>
</div> </div>
<div> <div>
<p className="section-item-proficiency skills-item-proficiency opacity-80">{item.proficiency}</p> <p className="section-item-metadata skills-item-proficiency opacity-80">{item.proficiency}</p>
<small className="section-item-keywords skills-item-keywords">{item.keywords.join(", ")}</small> <small className="section-item-keywords skills-item-keywords">{item.keywords.join(", ")}</small>
</div> </div>
@@ -15,10 +15,10 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
<p className="section-item-title volunteer-item-title"> <p className="section-item-title volunteer-item-title">
<strong>{item.organization}</strong> <strong>{item.organization}</strong>
</p> </p>
<p className="section-item-metadata text-right">{item.period}</p> <p className="section-item-metadata volunteer-item-period text-right">{item.period}</p>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="section-item-location volunteer-item-location">{item.location}</p> <p className="section-item-metadata volunteer-item-location">{item.location}</p>
</div> </div>
</div> </div>
<div className="section-item-description volunteer-item-description"> <div className="section-item-description volunteer-item-description">
@@ -236,8 +236,8 @@ type V4ResumeData = {
items: Array<{ items: Array<{
id: string; id: string;
visible: boolean; visible: boolean;
language?: string; name?: string;
fluency?: string; description?: string;
level?: number; level?: number;
}>; }>;
}; };
@@ -391,6 +391,20 @@ type V4ResumeData = {
}; };
}; };
// Transform layout section ID from V4 format to new format
// V4 uses "custom.{id}" for custom sections, new format just uses "{id}"
const transformLayoutSectionId = (id: string): string => {
if (id.startsWith("custom.")) return id.slice(7);
return id;
};
// Transform layout column by stripping "custom." prefix from section IDs
const transformLayoutColumn = (column: string[]): string[] => {
return column
.filter((id) => id !== "summary") // Summary is handled separately
.map(transformLayoutSectionId);
};
export class ReactiveResumeV4JSONImporter { export class ReactiveResumeV4JSONImporter {
parse(json: string): ResumeData { parse(json: string): ResumeData {
try { try {
@@ -532,12 +546,12 @@ export class ReactiveResumeV4JSONImporter {
columns: v4Data.sections.languages?.columns ?? 1, columns: v4Data.sections.languages?.columns ?? 1,
hidden: !(v4Data.sections.languages?.visible ?? true), hidden: !(v4Data.sections.languages?.visible ?? true),
items: (v4Data.sections.languages?.items ?? []) items: (v4Data.sections.languages?.items ?? [])
.filter((item) => item.language && item.language.length > 0) .filter((item) => item.name && item.name.length > 0)
.map((item) => ({ .map((item) => ({
id: item.id ?? generateId(), id: item.id ?? generateId(),
hidden: !(item.visible ?? true), hidden: !(item.visible ?? true),
language: item.language!, language: item.name!,
fluency: item.fluency ?? "", fluency: item.description ?? "",
level: clampLevel(item.level ?? 0), level: clampLevel(item.level ?? 0),
})), })),
}, },
@@ -680,12 +694,12 @@ export class ReactiveResumeV4JSONImporter {
layout: { layout: {
sidebarWidth: clampSidebarWidth(35), sidebarWidth: clampSidebarWidth(35),
pages: (v4Data.metadata.layout ?? []).map((page) => { pages: (v4Data.metadata.layout ?? []).map((page) => {
const main = page[0] ?? []; const main = transformLayoutColumn(page[0] ?? []);
const sidebar = page[1] ?? []; const sidebar = transformLayoutColumn(page[1] ?? []);
return { return {
fullWidth: sidebar.length === 0, fullWidth: sidebar.length === 0,
main: main.filter((id) => id !== "summary"), main,
sidebar: sidebar, sidebar,
}; };
}), }),
}, },
+12 -6
View File
@@ -147,13 +147,19 @@ export const resumeService = {
if (!resume) throw new ORPCError("NOT_FOUND"); if (!resume) throw new ORPCError("NOT_FOUND");
// Convert picture URL to base64 data, so there's no fetching required on the client. try {
const url = resume.data.picture.url.replace(env.APP_URL, "http://localhost:3000"); if (!resume.data.picture.url) throw new Error("Picture is not available");
const base64 = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buffer) => Buffer.from(buffer).toString("base64"));
resume.data.picture.url = `data:image/jpeg;base64,${base64}`; // Convert picture URL to base64 data, so there's no fetching required on the client.
const url = resume.data.picture.url.replace(env.APP_URL, "http://localhost:3000");
const base64 = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buffer) => Buffer.from(buffer).toString("base64"));
resume.data.picture.url = `data:image/jpeg;base64,${base64}`;
} catch {
// Ignore errors, as the picture is not always available
}
return resume; return resume;
}, },