fixes #2733: Bug where date range is displayed on separate line (#2862)

This commit is contained in:
Amruth Pillai
2026-04-04 12:03:09 +02:00
committed by GitHub
parent 9332e1e3ff
commit 923f5f6173
68 changed files with 2042 additions and 1704 deletions
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,24 +13,44 @@ type AwardsItemProps = SectionItem<"awards"> & {
};
export function AwardsItem({ className, ...item }: AwardsItemProps) {
const headerValues = {
title: item.title,
date: item.date,
awarder: item.awarder,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "title",
content: (
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title awards-item-title"
/>
),
},
{
key: "date",
content: <span className="section-item-metadata awards-item-date">{item.date}</span>,
},
{
key: "awarder",
content: <span className="section-item-metadata awards-item-awarder">{item.awarder}</span>,
},
);
return (
<div className={cn("awards-item", className)}>
{/* Header */}
<div className="section-item-header awards-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title awards-item-title"
/>
<span className="section-item-metadata awards-item-date shrink-0 text-end">{item.date}</span>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata awards-item-awarder">{item.awarder}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,24 +13,44 @@ type CertificationsItemProps = SectionItem<"certifications"> & {
};
export function CertificationsItem({ className, ...item }: CertificationsItemProps) {
const headerValues = {
title: item.title,
date: item.date,
issuer: item.issuer,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "title",
content: (
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title certifications-item-title"
/>
),
},
{
key: "date",
content: <span className="section-item-metadata certifications-item-date">{item.date}</span>,
},
{
key: "issuer",
content: <span className="section-item-metadata certifications-item-issuer">{item.issuer}</span>,
},
);
return (
<div className={cn("certifications-item", className)}>
{/* Header */}
<div className="section-item-header certifications-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title certifications-item-title"
/>
<span className="section-item-metadata certifications-item-date shrink-0 text-end">{item.date}</span>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata certifications-item-issuer">{item.issuer}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,29 +13,51 @@ type EducationItemProps = SectionItem<"education"> & {
};
export function EducationItem({ className, ...item }: EducationItemProps) {
const degreeAndGrade = [item.degree, item.grade].filter(Boolean).join(" • ");
const locationAndPeriod = [item.location, item.period].filter(Boolean).join(" • ");
const headerValues = {
school: item.school,
degreeAndGrade,
area: item.area,
locationAndPeriod,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "school",
content: (
<LinkedTitle
title={item.school}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title education-item-title"
/>
),
},
{
key: "degreeAndGrade",
content: <span className="section-item-metadata education-item-degree-grade">{degreeAndGrade}</span>,
},
{
key: "area",
content: <span className="section-item-metadata education-item-area">{item.area}</span>,
},
{
key: "locationAndPeriod",
content: <span className="section-item-metadata education-item-location-period">{locationAndPeriod}</span>,
},
);
return (
<div className={cn("education-item", className)}>
{/* Header */}
<div className="section-item-header education-item-header mb-2">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.school}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title education-item-title"
/>
<span className="section-item-metadata education-item-degree-grade shrink-0 text-end">
{[item.degree, item.grade].filter(Boolean).join(" • ")}
</span>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata education-item-area">{item.area}</span>
<span className="section-item-metadata education-item-location-period shrink-0 text-end">
{[item.location, item.period].filter(Boolean).join(" • ")}
</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -13,36 +14,50 @@ type ExperienceItemProps = SectionItem<"experience"> & {
export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
const hasRoles = Array.isArray(item.roles) && item.roles.length > 0;
const headerValues = {
company: item.company,
location: item.location,
position: item.position,
period: item.period,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "company",
content: (
<LinkedTitle
title={item.company}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title experience-item-title"
/>
),
},
{
key: "location",
content: <span className="section-item-metadata experience-item-location">{item.location}</span>,
},
{
key: "position",
content: <span className="section-item-metadata experience-item-position">{item.position}</span>,
},
{
key: "period",
content: <span className="section-item-metadata experience-item-period">{item.period}</span>,
},
);
return (
<div className={cn("experience-item", className)}>
{/* Header */}
<div className="section-item-header experience-item-header">
{/* Row 1: Company + Location */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.company}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title experience-item-title"
/>
<span className="section-item-metadata experience-item-location shrink-0 text-end">{item.location}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
{/* Row 2: Position + Period */}
{(!hasRoles || item.position) && (
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata experience-item-position">{item.position}</span>
<span className="section-item-metadata experience-item-period shrink-0 text-end">{item.period}</span>
</div>
)}
{/* Overall period when hasRoles and no summary position */}
{hasRoles && !item.position && item.period && (
<div className="flex items-start justify-end gap-x-2">
<span className="section-item-metadata experience-item-period shrink-0 text-end">{item.period}</span>
</div>
)}
</div>
{/* Role Progression */}
@@ -52,7 +67,7 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
<div key={role.id} className="experience-item-role">
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata experience-item-role-position">{role.position}</span>
<span className="section-item-metadata experience-item-role-period shrink-0 text-end">
<span className="section-item-metadata experience-item-role-period shrink-0 justify-self-end text-end">
{role.period}
</span>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,24 +13,44 @@ type PublicationsItemProps = SectionItem<"publications"> & {
};
export function PublicationsItem({ className, ...item }: PublicationsItemProps) {
const headerValues = {
title: item.title,
date: item.date,
publisher: item.publisher,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "title",
content: (
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title publications-item-title"
/>
),
},
{
key: "date",
content: <span className="section-item-metadata publications-item-date">{item.date}</span>,
},
{
key: "publisher",
content: <span className="section-item-metadata publications-item-publisher">{item.publisher}</span>,
},
);
return (
<div className={cn("publications-item", className)}>
{/* Header */}
<div className="section-item-header publications-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.title}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title publications-item-title"
/>
<span className="section-item-metadata publications-item-date shrink-0 text-end">{item.date}</span>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata publications-item-publisher">{item.publisher}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,23 +13,39 @@ type ReferencesItemProps = SectionItem<"references"> & {
};
export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
const headerValues = {
name: item.name,
position: item.position,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "name",
content: (
<LinkedTitle
title={item.name}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title references-item-name"
/>
),
},
{
key: "position",
content: <span className="section-item-metadata references-item-position">{item.position}</span>,
},
);
return (
<div className={cn("references-item", className)}>
{/* Header */}
<div className="section-item-header references-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.name}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title references-item-name"
/>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata references-item-position">{item.position}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
@@ -1,6 +1,7 @@
import type { SectionItem } from "@/schema/resume/data";
import { TiptapContent } from "@/components/input/rich-input";
import { filterFieldValues } from "@/utils/field";
import { stripHtml } from "@/utils/string";
import { cn } from "@/utils/style";
@@ -12,24 +13,44 @@ type VolunteerItemProps = SectionItem<"volunteer"> & {
};
export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
const headerValues = {
organization: item.organization,
period: item.period,
location: item.location,
};
const headerFields = filterFieldValues(
headerValues,
{
key: "organization",
content: (
<LinkedTitle
title={item.organization}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title volunteer-item-title"
/>
),
},
{
key: "period",
content: <span className="section-item-metadata volunteer-item-period">{item.period}</span>,
},
{
key: "location",
content: <span className="section-item-metadata volunteer-item-location">{item.location}</span>,
},
);
return (
<div className={cn("volunteer-item", className)}>
{/* Header */}
<div className="section-item-header volunteer-item-header">
{/* Row 1 */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.organization}
website={item.website}
showLinkInTitle={item.options?.showLinkInTitle}
className="section-item-title volunteer-item-title"
/>
<span className="section-item-metadata volunteer-item-period shrink-0 text-end">{item.period}</span>
</div>
{/* Row 2 */}
<div className="flex items-start justify-between gap-x-2">
<span className="section-item-metadata volunteer-item-location">{item.location}</span>
<div className="grid grid-cols-2 items-start gap-x-2">
{headerFields.map((field, index) => (
<div key={field.key} className={cn(index % 2 === 1 && "shrink-0 justify-self-end text-end")}>
{field.content}
</div>
))}
</div>
</div>
+4 -4
View File
@@ -164,11 +164,11 @@ function EducationForm() {
<FormField
control={form.control}
name="degree"
name="area"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Degree</Trans>
<Trans>Area of Study</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
@@ -178,11 +178,11 @@ function EducationForm() {
<FormField
control={form.control}
name="area"
name="degree"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Area of Study</Trans>
<Trans>Degree</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
+18 -16
View File
@@ -262,20 +262,6 @@ function ExperienceForm() {
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Title (optional)</Trans> : <Trans>Position</Trans>}</FormLabel>
<FormControl
render={<Input {...field} placeholder={hasRoles ? "e.g. Software Engineer → Senior Engineer" : ""} />}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
@@ -290,13 +276,29 @@ function ExperienceForm() {
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="period"
render={({ field }) => (
<FormItem>
<FormLabel>{hasRoles ? <Trans>Overall Period</Trans> : <Trans>Period</Trans>}</FormLabel>
<FormControl render={<Input {...field} placeholder={hasRoles ? "e.g. 2018 Present" : ""} />} />
<FormLabel>
<Trans>Period</Trans>
</FormLabel>
<FormControl render={<Input {...field} />} />
<FormMessage />
</FormItem>
)}
+23
View File
@@ -0,0 +1,23 @@
type KeyedField<TKey extends string> = {
key: TKey;
};
type KeyValueMap<TKey extends string> = Partial<Record<TKey, string | null | undefined>>;
/**
* Filters keyed field descriptors to only those whose corresponding value in the given
* values object is a non-empty string.
*
* This is useful for rendering or processing only the fields with actual, non-blank content,
* e.g., when displaying optional sections in a UI or assembling objects with present values.
*
* @param values - An object mapping keys to string values (which may be empty, null, or undefined)
* @param fields - Field descriptors with { key: TKey }
* @returns An array of fields whose corresponding values[key] is a non-empty string
*/
export function filterFieldValues<TKey extends string, TField extends KeyedField<TKey>>(
values: KeyValueMap<TKey>,
...fields: TField[]
) {
return fields.filter((field) => Boolean(values[field.key]?.trim()));
}