feat(experience): add role progression to show career advancement within a company (#2761)

* feat(experience): add role progression support for career advancement

* fix(experience): remove indent and border from role progression items

* refactor(experience): enhance role progression UI and functionality

- Updated the ExperienceItem component to improve role display and styling.
- Refactored the CreateExperienceDialog and UpdateExperienceDialog to support role reordering using drag-and-drop.
- Added role handling in JSON and ReactiveResume importers.
- Enhanced experience schema to include roles, ensuring better data structure for career progression.
- Improved overall user experience with clearer role management in the experience section.

---------

Co-authored-by: root <root@reactive-resume-dev.one.one.one.one>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
TheDuke427
2026-03-04 12:34:37 -08:00
committed by GitHub
parent df81d0342f
commit 4b46bda4e6
8 changed files with 271 additions and 49 deletions
@@ -10,11 +10,13 @@ type ExperienceItemProps = SectionItem<"experience"> & {
};
export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
const hasRoles = Array.isArray(item.roles) && item.roles.length > 0;
return (
<div className={cn("experience-item", className)}>
{/* Header */}
<div className="section-item-header experience-item-header">
{/* Row 1 */}
{/* Row 1: Company + Location */}
<div className="flex items-start justify-between gap-x-2">
<LinkedTitle
title={item.company}
@@ -25,19 +27,55 @@ export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
<span className="section-item-metadata experience-item-location shrink-0 text-end">{item.location}</span>
</div>
{/* Row 2 */}
<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>
{/* 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>
{/* Description */}
<div
className={cn("section-item-description experience-item-description", !stripHtml(item.description) && "hidden")}
>
<TiptapContent content={item.description} />
</div>
{/* Role Progression */}
{hasRoles && (
<div className="experience-item-roles mt-(--page-gap-y) flex flex-col gap-y-(--page-gap-y)">
{item.roles.map((role) => (
<div key={role.id} className="experience-item-role">
<div className="flex items-start justify-between gap-x-2">
<strong className="section-item-metadata experience-item-role-position">{role.position}</strong>
<span className="section-item-metadata experience-item-role-period shrink-0 text-end">
{role.period}
</span>
</div>
{stripHtml(role.description) && (
<div className="section-item-description experience-item-role-description mt-0.5">
<TiptapContent content={role.description} />
</div>
)}
</div>
))}
</div>
)}
{/* Single-role description */}
{!hasRoles && (
<div
className={cn(
"section-item-description experience-item-description",
!stripHtml(item.description) && "hidden",
)}
>
<TiptapContent content={item.description} />
</div>
)}
{/* Website */}
{!item.options?.showLinkInTitle && (