+
+ );
+}
+
function ExperienceForm() {
const form = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ name: "roles",
+ keyName: "fieldId",
+ control: form.control,
+ });
+
+ const hasRoles = useMemo(() => fields.length > 0, [fields]);
+
+ const handleReorderRoles = (newOrder: RoleItem[]) => {
+ form.setValue("roles", newOrder);
+ };
+
return (
<>
(
-
- Position
-
+ {hasRoles ? Overall Title (optional) : Position}
-
+
@@ -206,11 +316,9 @@ function ExperienceForm() {
name="period"
render={({ field }) => (
-
- Period
-
+ {hasRoles ? Overall Period : Period}
-
+
@@ -246,28 +354,68 @@ function ExperienceForm() {
-
+ Show link in title
)}
/>
- (
-
-
- Description
-
-
-
-
-
-
- )}
- />
+ {/* Role Progression */}
+
+
+
+ Role Progression
+
+
+ Add multiple roles to show career progression at the same company.
+
+
+
+
+
+
+ {hasRoles && (
+
+
+ {fields.map((field, index) => (
+ remove(index)} />
+ ))}
+
+
+ )}
+
+ {/* Single Role Description — only show when no roles are defined */}
+ {!hasRoles && (
+ (
+
+
+ Description
+
+
+
+
+
+
+ )}
+ />
+ )}
>
);
}
diff --git a/src/integrations/import/json-resume.tsx b/src/integrations/import/json-resume.tsx
index 478899d14..165fa34bd 100644
--- a/src/integrations/import/json-resume.tsx
+++ b/src/integrations/import/json-resume.tsx
@@ -409,6 +409,7 @@ export class JSONResumeImporter {
location: work.location || "",
period: formatPeriod(work.startDate, work.endDate),
website: createUrl(work.url),
+ roles: [],
description: toHtmlDescription(work.summary, work.highlights),
})),
};
diff --git a/src/integrations/import/reactive-resume-v4-json.tsx b/src/integrations/import/reactive-resume-v4-json.tsx
index 9a278d186..3282a0d8d 100644
--- a/src/integrations/import/reactive-resume-v4-json.tsx
+++ b/src/integrations/import/reactive-resume-v4-json.tsx
@@ -482,6 +482,7 @@ export class ReactiveResumeV4JSONImporter {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
+ roles: [],
description: item.summary ?? "",
})),
},
@@ -684,6 +685,7 @@ export class ReactiveResumeV4JSONImporter {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
+ roles: [],
description: item.summary ?? "",
})),
})),
diff --git a/src/routes/builder/$resumeId/-sidebar/left/sections/experience.tsx b/src/routes/builder/$resumeId/-sidebar/left/sections/experience.tsx
index 1e1934361..f6a68d9cf 100644
--- a/src/routes/builder/$resumeId/-sidebar/left/sections/experience.tsx
+++ b/src/routes/builder/$resumeId/-sidebar/left/sections/experience.tsx
@@ -1,3 +1,4 @@
+import { plural } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { AnimatePresence, Reorder } from "motion/react";
import type z from "zod";
@@ -21,9 +22,17 @@ export function ExperienceSectionBuilder() {
- {section.items.map((item) => (
-
- ))}
+ {section.items.map((item) => {
+ return (
+
+ );
+ })}
diff --git a/src/schema/resume/data.ts b/src/schema/resume/data.ts
index c1172323b..a99f87aad 100644
--- a/src/schema/resume/data.ts
+++ b/src/schema/resume/data.ts
@@ -131,13 +131,34 @@ export const educationItemSchema = baseItemSchema.extend({
description: z.string().describe("The description of the education. This should be a HTML-formatted string."),
});
+export const roleItemSchema = z.object({
+ id: z.string().describe("The unique identifier for the role. Usually generated as a UUID."),
+ position: z.string().describe("The position or job title for this role."),
+ period: z.string().describe("The period of time this role was held."),
+ description: z.string().describe("The description of this specific role. This should be a HTML-formatted string."),
+});
+
+export type RoleItem = z.infer;
+
export const experienceItemSchema = baseItemSchema.extend({
company: z.string().min(1).describe("The name of the company or organization."),
- position: z.string().describe("The position held at the company or organization."),
+ position: z
+ .string()
+ .describe(
+ "The position held at the company or organization. Used when there is only a single role. If multiple roles are provided in the 'roles' field, this serves as a summary title or can be left blank.",
+ ),
location: z.string().describe("The location of the company or organization."),
- period: z.string().describe("The period of time the author was employed at the company or organization."),
+ period: z
+ .string()
+ .describe(
+ "The overall period of time at the company. When multiple roles are used, this should reflect the total tenure.",
+ ),
website: urlSchema.describe("The website of the company or organization, if any."),
description: z.string().describe("The description of the experience. This should be a HTML-formatted string."),
+ roles: z
+ .array(roleItemSchema)
+ .catch([])
+ .describe("Optional list of individual roles held at this company to show career progression."),
});
export const interestItemSchema = baseItemSchema.extend({
diff --git a/src/schema/resume/sample.ts b/src/schema/resume/sample.ts
index b4fcfa283..3c22abe87 100644
--- a/src/schema/resume/sample.ts
+++ b/src/schema/resume/sample.ts
@@ -91,6 +91,7 @@ export const sampleResumeData: ResumeData = {
url: "",
label: "",
},
+ roles: [],
description:
"
Lead gameplay programmer on an unannounced AAA action-adventure title built in Unreal Engine 5 for PC and next-gen consoles
Architected and implemented core combat system including hit detection, combo mechanics, and enemy AI behavior trees serving 15+ enemy types
Developed custom editor tools in C++ that reduced level designer iteration time by 40% and improved workflow efficiency across the team
Optimized rendering pipeline and gameplay systems to maintain 60 FPS performance target on all supported platforms, achieving 95% frame rate stability
Ad nostrud enim adipisicing ea proident aliqua veniam nisi amet ea irure et mollit.