mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-02 09:44:26 +10:00
feat(schema): describe template layouts in one place
Columns, sidebar side, header placement and ATS safety for every template now live in templateLayouts. The template gallery metadata and the layout editor read it, and a DOCX test checks the two-column configurations against it.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { templateLayouts } from "@reactive-resume/schema/templates";
|
||||
import { templates } from "./data";
|
||||
|
||||
describe("templates metadata", () => {
|
||||
@@ -37,10 +38,11 @@ describe("templates metadata", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a recognized sidebar position for every template", () => {
|
||||
const validPositions = new Set(["left", "right", "none"]);
|
||||
for (const [id, meta] of entries) {
|
||||
expect(validPositions.has(meta.sidebarPosition), `${id}: ${meta.sidebarPosition}`).toBe(true);
|
||||
it("takes each template's layout from the shared metadata", () => {
|
||||
for (const [id] of entries) {
|
||||
const layout = templateLayouts[id as keyof typeof templateLayouts];
|
||||
expect(layout, id).toBeDefined();
|
||||
expect(layout.columns === 2, id).toBe(layout.sidebarSide !== null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export type TemplateMetadata = {
|
||||
description: MessageDescriptor;
|
||||
imageUrl: string;
|
||||
tags: string[];
|
||||
sidebarPosition: "left" | "right" | "none";
|
||||
};
|
||||
|
||||
export const templates = {
|
||||
@@ -16,104 +15,89 @@ export const templates = {
|
||||
description: msg`Two-column with a bold colored sidebar and skill bars; great for creative or tech roles where visual flair is welcome.`,
|
||||
imageUrl: "/templates/jpg/azurill.jpg",
|
||||
tags: ["Two-column", "Creative", "Tech", "Visual flair"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
bronzor: {
|
||||
name: "Bronzor",
|
||||
description: msg`Two-column, clean and professional with subtle section dividers; suits corporate, finance, or consulting positions.`,
|
||||
imageUrl: "/templates/jpg/bronzor.jpg",
|
||||
tags: ["Two-column", "Clean", "Professional", "Corporate", "Finance", "Consulting"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
chikorita: {
|
||||
name: "Chikorita",
|
||||
description: msg`Two-column with a soft header accent and circular profile photo; ideal for marketing, HR, or client-facing roles.`,
|
||||
imageUrl: "/templates/jpg/chikorita.jpg",
|
||||
tags: ["Two-column", "Soft accent", "Marketing", "HR", "Client-facing"],
|
||||
sidebarPosition: "right",
|
||||
},
|
||||
ditgar: {
|
||||
name: "Ditgar",
|
||||
description: msg`Two-column with a dark teal sidebar and skills grid; modern feel for developers, data scientists, or technical PMs.`,
|
||||
imageUrl: "/templates/jpg/ditgar.jpg",
|
||||
tags: ["Two-column", "Modern", "Developer", "Data science", "Technical PM", "Dark sidebar"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
ditto: {
|
||||
name: "Ditto",
|
||||
description: msg`Two-column, minimal and text-dense with no decorative elements; perfect for traditional industries or ATS-heavy applications.`,
|
||||
imageUrl: "/templates/jpg/ditto.jpg",
|
||||
tags: ["Two-column", "ATS friendly", "Minimal", "Text-dense", "Traditional", "No decoration"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
gengar: {
|
||||
name: "Gengar",
|
||||
description: msg`Two-column with accent colors and clean typography; balanced choice for business analysts or operations roles.`,
|
||||
imageUrl: "/templates/jpg/gengar.jpg",
|
||||
tags: ["Two-column", "Accent colors", "Clean typography", "Business analyst", "Operations"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
glalie: {
|
||||
name: "Glalie",
|
||||
description: msg`Two-column, minimal with light gray sidebar and subtle icons; professional and understated for legal, finance, or executive roles.`,
|
||||
imageUrl: "/templates/jpg/glalie.jpg",
|
||||
tags: ["Two-column", "Minimal", "Professional", "Legal", "Finance", "Executive", "Understated"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
kakuna: {
|
||||
name: "Kakuna",
|
||||
description: msg`Single-column with a magenta left border accent; compact and efficient for entry-level or internship applications.`,
|
||||
imageUrl: "/templates/jpg/kakuna.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Compact", "Efficient", "Entry level", "Internship", "Magenta accent"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
lapras: {
|
||||
name: "Lapras",
|
||||
description: msg`Single-column; polished and serious for senior or enterprise-level positions.`,
|
||||
imageUrl: "/templates/jpg/lapras.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Polished", "Senior", "Enterprise"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
leafish: {
|
||||
name: "Leafish",
|
||||
description: msg`Two-column with a muted color sidebar; earthy and calm, suits sustainability, healthcare, or nonprofit sectors.`,
|
||||
imageUrl: "/templates/jpg/leafish.jpg",
|
||||
tags: ["Two-column", "Muted sidebar", "Earthy", "Calm", "Sustainability", "Healthcare", "Nonprofit"],
|
||||
sidebarPosition: "right",
|
||||
},
|
||||
meowth: {
|
||||
name: "Meowth",
|
||||
description: msg`Single-column with an inline three-column entry header (position · organization · period); compact and ATS-friendly, well-suited for Asian resume conventions (CN/JP/KR).`,
|
||||
imageUrl: "/templates/jpg/meowth.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Inline header", "Compact", "Asian style", "CN/JP/KR"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
onyx: {
|
||||
name: "Onyx",
|
||||
description: msg`Single-column with a sidebar and clean grid layout; versatile for any professional or technical role.`,
|
||||
imageUrl: "/templates/jpg/onyx.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Sidebar", "Grid layout", "Versatile", "Professional", "Technical"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
pikachu: {
|
||||
name: "Pikachu",
|
||||
description: msg`Two-column with a left margin color; simple and approachable for creative, editorial, or junior roles.`,
|
||||
imageUrl: "/templates/jpg/pikachu.jpg",
|
||||
tags: ["Two-column", "Simple", "Creative", "Editorial", "Junior", "Accent colors"],
|
||||
sidebarPosition: "left",
|
||||
},
|
||||
rhyhorn: {
|
||||
name: "Rhyhorn",
|
||||
description: msg`Single-column with a minimal top header and lots of whitespace; clean and modern for designers or content creators.`,
|
||||
imageUrl: "/templates/jpg/rhyhorn.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Minimal", "Clean", "Modern", "Designer", "Content creator", "Whitespace"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
scizor: {
|
||||
name: "Scizor",
|
||||
description: msg`Single-column with uppercase section headings and a primary-color top rule on every page; polished for executive, consulting, or startup resumes.`,
|
||||
imageUrl: "/templates/jpg/scizor.jpg",
|
||||
tags: ["Single-column", "ATS friendly", "Uppercase headings", "Executive", "Consulting", "Startup"],
|
||||
sidebarPosition: "none",
|
||||
},
|
||||
} as const satisfies Record<Template, TemplateMetadata>;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@phosphor-icons/react";
|
||||
import { useCallback, useId, useState } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { templateLayouts } from "@reactive-resume/schema/templates";
|
||||
import { Button } from "@reactive-resume/ui/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||
import { Switch } from "@reactive-resume/ui/components/switch";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { templates } from "@/dialogs/resume/template/data";
|
||||
import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft";
|
||||
import { EASE_OUT_STRONG } from "@/libs/motion";
|
||||
import { resolveLayoutSectionTitle } from "./title";
|
||||
@@ -97,7 +97,7 @@ export function LayoutPages() {
|
||||
|
||||
const resume = useCurrentResume();
|
||||
const template = resume.data.metadata.template;
|
||||
const templateSidebarPosition = templates[template].sidebarPosition;
|
||||
const templateSidebarPosition = templateLayouts[template].sidebarSide ?? "none";
|
||||
|
||||
const layout = resume.data.metadata.layout;
|
||||
const updateResumeData = useUpdateResumeData();
|
||||
@@ -450,7 +450,7 @@ function MoveToSubmenu({ id, pageIndex, columnId }: MoveToSubmenuProps) {
|
||||
|
||||
const pages = resume.data.metadata.layout.pages;
|
||||
// When the template collapses the sidebar, no page has a usable sidebar column.
|
||||
const sidebarCollapsed = templates[resume.data.metadata.template].sidebarPosition === "none";
|
||||
const sidebarCollapsed = templateLayouts[resume.data.metadata.template].columns === 1;
|
||||
|
||||
const moveTo = (targetPageIndex: number, targetColumnId: ColumnId) => {
|
||||
updateResumeData((draft) => {
|
||||
|
||||
@@ -68,7 +68,7 @@ interface TemplateConfig {
|
||||
headerPosition: "full-width" | "main-only" | "sidebar-only";
|
||||
}
|
||||
|
||||
const TEMPLATE_CONFIGS: Record<Template, TemplateConfig> = {
|
||||
export const TEMPLATE_CONFIGS: Record<Template, TemplateConfig> = {
|
||||
azurill: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
|
||||
bronzor: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
||||
chikorita: { sidebarSide: "right", sidebarBackground: "solid", headerPosition: "main-only" },
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { templateLayouts } from "@reactive-resume/schema/templates";
|
||||
import { TEMPLATE_CONFIGS } from "./builder";
|
||||
|
||||
describe("DOCX template configs", () => {
|
||||
it("put the sidebar and header where the PDF does for two-column templates", () => {
|
||||
for (const [template, layout] of Object.entries(templateLayouts)) {
|
||||
if (layout.columns !== 2) continue;
|
||||
const config = TEMPLATE_CONFIGS[template as keyof typeof TEMPLATE_CONFIGS];
|
||||
expect({ sidebarSide: config.sidebarSide, headerPosition: config.headerPosition }, template).toEqual({
|
||||
sidebarSide: layout.sidebarSide,
|
||||
headerPosition: layout.headerPlacement,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -19,3 +19,36 @@ export const templateSchema = z.enum([
|
||||
]);
|
||||
|
||||
export type Template = z.infer<typeof templateSchema>;
|
||||
|
||||
/** How a template lays out the page. The single source for the gallery filters, the Check layout rule and DOCX. */
|
||||
export type TemplateLayout = {
|
||||
/** Two-column templates print the sidebar as a column of its own; one-column templates read top to bottom. */
|
||||
columns: 1 | 2;
|
||||
/** Where the sidebar column sits by default. Null for one-column templates. */
|
||||
sidebarSide: "left" | "right" | null;
|
||||
/** Where the header sits: across the page, over the main column only, or in the sidebar. */
|
||||
headerPlacement: "full-width" | "main-only" | "sidebar-only";
|
||||
/** One reading order and plain text headings, which applicant tracking systems read cleanly. */
|
||||
atsSafe: boolean;
|
||||
};
|
||||
|
||||
const oneColumn: TemplateLayout = { columns: 1, sidebarSide: null, headerPlacement: "full-width", atsSafe: true };
|
||||
|
||||
export const templateLayouts = {
|
||||
azurill: { columns: 2, sidebarSide: "left", headerPlacement: "full-width", atsSafe: false },
|
||||
// Bronzor prints sidebar sections as labelled rows inside one column.
|
||||
bronzor: oneColumn,
|
||||
chikorita: { columns: 2, sidebarSide: "right", headerPlacement: "main-only", atsSafe: false },
|
||||
ditgar: { columns: 2, sidebarSide: "left", headerPlacement: "sidebar-only", atsSafe: false },
|
||||
ditto: { columns: 2, sidebarSide: "left", headerPlacement: "full-width", atsSafe: false },
|
||||
gengar: { columns: 2, sidebarSide: "left", headerPlacement: "sidebar-only", atsSafe: false },
|
||||
glalie: { columns: 2, sidebarSide: "left", headerPlacement: "sidebar-only", atsSafe: false },
|
||||
kakuna: oneColumn,
|
||||
lapras: oneColumn,
|
||||
leafish: { columns: 2, sidebarSide: "right", headerPlacement: "full-width", atsSafe: false },
|
||||
meowth: oneColumn,
|
||||
onyx: oneColumn,
|
||||
pikachu: { columns: 2, sidebarSide: "left", headerPlacement: "main-only", atsSafe: false },
|
||||
rhyhorn: oneColumn,
|
||||
scizor: oneColumn,
|
||||
} as const satisfies Record<Template, TemplateLayout>;
|
||||
|
||||
Reference in New Issue
Block a user