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">
<strong>{item.title}</strong>
</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 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 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">
<strong>{item.title}</strong>
</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 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 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">
<strong>{item.school}</strong>
</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(" • ")}
</p>
</div>
<div className="flex items-center justify-between">
<p>{item.area}</p>
<p className="section-item-metadata education-item-metadata text-right">
<p className="section-item-metadata education-item-area">{item.area}</p>
<p className="section-item-metadata education-item-location-period text-right">
{[item.location, item.period].filter(Boolean).join(" • ")}
</p>
</div>
@@ -15,11 +15,11 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
<p className="section-item-title experience-item-title">
<strong>{item.company}</strong>
</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 className="flex items-center justify-between">
<p>{item.position}</p>
<p className="section-item-metadata experience-item-metadata text-right">{item.period}</p>
<p className="section-item-metadata experience-item-position">{item.position}</p>
<p className="section-item-metadata experience-item-period text-right">{item.period}</p>
</div>
</div>
<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="section-item-header flex items-center gap-x-1.5">
<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>
</p>
</div>
@@ -10,10 +10,10 @@ export function LanguagesItem({ className, ...item }: LanguagesItemProps) {
return (
<div className={cn("languages-item", className)}>
<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>
</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>
<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="section-item-header flex items-center gap-x-1.5">
<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>
</p>
</div>
@@ -15,7 +15,7 @@ export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
<p className="section-item-title projects-item-title">
<strong>{item.name}</strong>
</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 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">
<strong>{item.title}</strong>
</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 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 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="section-item-header references-item-header">
<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>
</p>
</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 className="section-item-description references-item-description">
<TiptapContent content={item.description} />
</div>
<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" />
</div>
</div>
@@ -12,13 +12,13 @@ export function SkillsItem({ className, ...item }: SkillsItemProps) {
<div className={cn("skills-item", className)}>
<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" />
<p className="section-item-name skills-item-name">
<p className="section-item-title skills-item-name">
<strong>{item.name}</strong>
</p>
</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>
</div>
@@ -15,10 +15,10 @@ export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
<p className="section-item-title volunteer-item-title">
<strong>{item.organization}</strong>
</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 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 className="section-item-description volunteer-item-description">
@@ -236,8 +236,8 @@ type V4ResumeData = {
items: Array<{
id: string;
visible: boolean;
language?: string;
fluency?: string;
name?: string;
description?: string;
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 {
parse(json: string): ResumeData {
try {
@@ -532,12 +546,12 @@ export class ReactiveResumeV4JSONImporter {
columns: v4Data.sections.languages?.columns ?? 1,
hidden: !(v4Data.sections.languages?.visible ?? true),
items: (v4Data.sections.languages?.items ?? [])
.filter((item) => item.language && item.language.length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
language: item.language!,
fluency: item.fluency ?? "",
language: item.name!,
fluency: item.description ?? "",
level: clampLevel(item.level ?? 0),
})),
},
@@ -680,12 +694,12 @@ export class ReactiveResumeV4JSONImporter {
layout: {
sidebarWidth: clampSidebarWidth(35),
pages: (v4Data.metadata.layout ?? []).map((page) => {
const main = page[0] ?? [];
const sidebar = page[1] ?? [];
const main = transformLayoutColumn(page[0] ?? []);
const sidebar = transformLayoutColumn(page[1] ?? []);
return {
fullWidth: sidebar.length === 0,
main: main.filter((id) => id !== "summary"),
sidebar: sidebar,
main,
sidebar,
};
}),
},
+12 -6
View File
@@ -147,13 +147,19 @@ export const resumeService = {
if (!resume) throw new ORPCError("NOT_FOUND");
// 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"));
try {
if (!resume.data.picture.url) throw new Error("Picture is not available");
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;
},