Compare commits

...

2 Commits

Author SHA1 Message Date
JOYCEQL 65912e4a2f feat: Implement live template previews in selection and remove Compact and Professional templates. 2026-02-25 01:40:57 +08:00
JOYCEQL 3a3d9de6f8 feat: add more template 2026-02-24 14:00:34 +08:00
16 changed files with 716 additions and 83 deletions
+40 -41
View File
@@ -3,28 +3,13 @@ import { useState } from "react";
import { useTranslations } from "@/i18n/compat/client";
import { motion } from "framer-motion";
import { useRouter } from "@/lib/navigation";
import Image from "@/lib/image";
import { cn } from "@/lib/utils";
import { DEFAULT_TEMPLATES } from "@/config";
import { useResumeStore } from "@/store/useResumeStore";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import classic from "@/assets/images/template-cover/classic.png";
import modern from "@/assets/images/template-cover/modern.png";
import leftRight from "@/assets/images/template-cover/left-right.png";
import timeline from "@/assets/images/template-cover/timeline.png";
import { cn } from "@/lib/utils";
import { DEFAULT_TEMPLATES } from "@/config";
const templateImages: Record<string, string> = {
classic,
modern,
"left-right": leftRight,
timeline,
};
const container = {
hidden: { opacity: 0 },
show: {
@@ -110,15 +95,25 @@ const TemplatesPage = () => {
</div>
</div>
<div className="h-full w-full p-3 transition-all duration-300 group-hover:scale-[1.01]">
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-sm">
<Image
src={templateImages[template.id]}
alt={t(`${templateKey}.name`)}
fill
className="object-contain"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority
<div className="h-full w-full py-6 transition-all duration-300 group-hover:scale-[1.02] flex items-center justify-center pointer-events-none">
<div
className="relative overflow-hidden bg-white shadow-sm ring-1 ring-gray-200/50 rounded-sm"
style={{
width: "210px",
height: "297px",
}}
>
<iframe
src={`/app/preview-template/${template.id}`}
className="absolute top-0 left-0 border-0"
style={{
width: "210mm",
height: "297mm",
transform: "scale(0.264583333)", // (210px / 210mm) approximation
transformOrigin: "top left"
}}
scrolling="no"
tabIndex={-1}
/>
</div>
</div>
@@ -180,21 +175,25 @@ const TemplatesPage = () => {
)}
</DialogTitle>
</div>
<div className="overflow-hidden flex items-center justify-center bg-gray-50 dark:bg-gray-950 p-4">
<div className="relative">
<Image
src={templateImages[previewTemplate.id]}
alt={t(
`${
previewTemplate.id === "left-right"
? "leftRight"
: previewTemplate.id
}.name`
)}
width={600}
height={848}
className="rounded-md shadow-sm"
priority
<div className="overflow-hidden flex items-center justify-center bg-gray-50 dark:bg-gray-950 py-8 pointer-events-none">
<div
className="relative bg-white shadow-md ring-1 ring-gray-200/50"
style={{
width: "420px",
height: "594px",
}}
>
<iframe
src={`/app/preview-template/${previewTemplate.id}`}
className="absolute top-0 left-0 border-0"
style={{
width: "210mm",
height: "297mm",
transform: "scale(0.529166667)", // (420px / 210mm) approximation
transformOrigin: "top left"
}}
scrolling="no"
tabIndex={-1}
/>
</div>
</div>
+6 -2
View File
@@ -40,6 +40,10 @@ const BaseInfo = ({
return template?.layout === "modern";
}, [template]);
const isWhiteTextTemplate = React.useMemo(() => {
return template?.layout === "modern" || template?.layout === "creative";
}, [template]);
const getOrderedFields = React.useMemo(() => {
if (!basic.fieldOrder) {
return [
@@ -221,14 +225,14 @@ const BaseInfo = ({
className={fieldsContainerClass}
style={{
fontSize: `${globalSettings?.baseFontSize || 14}px`,
color: isModernTemplate ? "#fff" : "rgb(75, 85, 99)",
color: isWhiteTextTemplate ? "#fff" : "rgb(75, 85, 99)",
maxWidth: layout === "center" ? "none" : "600px",
}}
>
{allFields.map((item) => (
<motion.div
key={item.key}
className={cn(baseFieldItemClass, isModernTemplate && "text-[#fff]")}
className={cn(baseFieldItemClass, isWhiteTextTemplate && "text-[#fff]")}
style={{
width: isModernTemplate ? "100%" : "",
}}
@@ -0,0 +1,69 @@
"use client";
import React, { useMemo } from "react";
import { useParams } from "@tanstack/react-router";
import { DEFAULT_TEMPLATES } from "../../config";
import { initialResumeState, initialResumeStateEn } from "../../config/initialResumeData";
import ResumeTemplateComponent from "../templates";
import { cn } from "../../lib/utils";
import { ResumeData } from "../../types/resume";
import { ResumeTemplate } from "../../types/template";
const IframeTemplateViewer = () => {
const { id } = useParams({ from: "/app/preview-template/$id" });
// Use cookie to determine locale
const locale =
typeof document !== "undefined"
? document.cookie
.split("; ")
.find((row) => row.startsWith("NEXT_LOCALE="))
?.split("=")[1] || "zh"
: "zh";
const template = useMemo(() => {
return DEFAULT_TEMPLATES.find((t: ResumeTemplate) => t.id === id) || DEFAULT_TEMPLATES[0];
}, [id]);
const mockData: ResumeData = useMemo(() => {
const baseData = locale === "en" ? initialResumeStateEn : initialResumeState;
return {
...baseData,
id: "preview-mock-id",
templateId: template.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
globalSettings: {
...baseData.globalSettings,
themeColor: template.colorScheme.primary,
sectionSpacing: template.spacing.sectionGap,
paragraphSpacing: template.spacing.itemGap,
pagePadding: template.spacing.contentPadding,
},
basic: {
...baseData.basic,
layout: template.basic.layout,
},
} as ResumeData;
}, [locale, template]);
return (
<div className="w-full h-full min-h-screen bg-white flex justify-center items-start overflow-hidden">
<div
className={cn(
"w-[210mm] min-w-[210mm] min-h-[297mm]",
"bg-white",
"relative mx-auto origin-top-left"
)}
style={{
fontFamily: "Alibaba PuHuiTi, sans-serif",
padding: `${template.spacing.contentPadding}px`,
}}
>
<ResumeTemplateComponent data={mockData} template={template} />
</div>
</div>
);
};
export default IframeTemplateViewer;
+61 -1
View File
@@ -4,6 +4,7 @@ import { GlobalSettings } from "@/types/resume";
import { useResumeStore } from "@/store/useResumeStore";
import { cn } from "@/lib/utils";
import { templateConfigs } from "@/config/templates";
import { useTemplateContext } from "../templates/TemplateContext";
interface SectionTitleProps {
globalSettings?: GlobalSettings;
@@ -14,7 +15,10 @@ interface SectionTitleProps {
const SectionTitle = ({ type, title, globalSettings, showTitle = true }: SectionTitleProps) => {
const { activeResume } = useResumeStore();
const { menuSections = [], templateId = "default" } = activeResume || {};
const templateContext = useTemplateContext();
const menuSections = templateContext?.menuSections ?? activeResume?.menuSections ?? [];
const templateId = templateContext?.templateId ?? activeResume?.templateId ?? "default";
const renderTitle = useMemo(() => {
if (type === "custom") {
@@ -98,6 +102,62 @@ const SectionTitle = ({ type, title, globalSettings, showTitle = true }: Section
</h3>
);
case "minimalist":
return (
<h3
className={cn("pb-1 mb-2 tracking-widest uppercase")}
style={{
...baseStyles,
color: themeColor,
}}
>
{renderTitle}
</h3>
);
case "elegant":
return (
<div className="flex items-center justify-center w-full mb-4 relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div
className="w-full border-t"
style={{ borderColor: themeColor, opacity: 0.3 }}
></div>
</div>
<h3
className={cn("relative bg-white px-4 text-center")}
style={{
...baseStyles,
color: themeColor,
marginBottom: 0,
}}
>
{renderTitle}
</h3>
</div>
);
case "creative":
return (
<h3
className={cn("inline-block px-3 py-1 rounded text-white shadow-sm mb-3")}
style={{
...baseStyles,
backgroundColor: themeColor,
color: "#ffffff",
}}
>
{renderTitle}
</h3>
);
default:
return (
<h3
+38 -37
View File
@@ -13,17 +13,7 @@ import {
import { cn } from "@/lib/utils";
import { DEFAULT_TEMPLATES } from "@/config";
import { useResumeStore } from "@/store/useResumeStore";
import classic from "@/assets/images/template-cover/classic.png";
import modern from "@/assets/images/template-cover/modern.png";
import leftRight from "@/assets/images/template-cover/left-right.png";
import timeline from "@/assets/images/template-cover/timeline.png";
const templateImages: Record<string, string> = {
classic,
modern,
"left-right": leftRight,
timeline
};
import { ScrollArea } from "@/components/ui/scroll-area";
const TemplateSheet = () => {
const t = useTranslations("templates");
@@ -45,33 +35,44 @@ const TemplateSheet = () => {
{/* 解决警告问题 */}
<SheetDescription></SheetDescription>
<div className="grid grid-cols-3 gap-4 mt-4">
{DEFAULT_TEMPLATES.map((template) => (
<button
key={template.id}
onClick={() => setTemplate(template.id)}
className={cn(
"relative group rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-[1.02]",
template.id === currentTemplate.id
? "border-primary dark:border-primary shadow-lg dark:shadow-primary/30"
: "dark:border-neutral-800 dark:hover:border-neutral-700 border-gray-100 hover:border-gray-200"
)}
>
<img
src={templateImages[template.id]}
alt={template.name}
className="w-full h-auto"
/>
{template.id === currentTemplate.id && (
<motion.div
layoutId="template-selected"
className="absolute inset-0 flex items-center justify-center bg-black/20 dark:bg-white/30"
<div className="h-[calc(100vh-8rem)] mt-4">
<ScrollArea className="h-full w-full pr-4">
<div className="grid grid-cols-4 gap-4 pb-8">
{DEFAULT_TEMPLATES.map((template) => (
<button
key={template.id}
onClick={() => setTemplate(template.id)}
className={cn(
"relative group rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-[1.02]",
template.id === currentTemplate.id
? "border-primary dark:border-primary shadow-lg dark:shadow-primary/30"
: "dark:border-neutral-800 dark:hover:border-neutral-700 border-gray-100 hover:border-gray-200"
)}
>
<Layout className="w-6 h-6 text-white dark:text-primary" />
</motion.div>
)}
</button>
))}
<div className="relative aspect-[210/297] w-full overflow-hidden bg-white pointer-events-none">
<svg viewBox="0 0 794 1123" className="absolute inset-0 w-full h-full pointer-events-none rounded-md">
<foreignObject x="0" y="0" width="794" height="1123">
<iframe
src={`/app/preview-template/${template.id}`}
className="w-[794px] h-[1123px] border-0 pointer-events-none"
scrolling="no"
tabIndex={-1}
/>
</foreignObject>
</svg>
</div>
{template.id === currentTemplate.id && (
<motion.div
layoutId="template-selected"
className="absolute inset-0 flex items-center justify-center bg-black/10 dark:bg-black/40 pointer-events-none z-20"
>
<Layout className="w-8 h-8 text-primary shadow-sm" />
</motion.div>
)}
</button>
))}
</div>
</ScrollArea>
</div>
</SheetContent>
</Sheet>
@@ -0,0 +1,119 @@
import React from "react";
import BaseInfo from "../preview/BaseInfo";
import ExperienceSection from "../preview/ExperienceSection";
import EducationSection from "../preview/EducationSection";
import SkillSection from "../preview/SkillPanel";
import ProjectSection from "../preview/ProjectSection";
import CustomSection from "../preview/CustomSection";
import { ResumeData } from "@/types/resume";
import { ResumeTemplate } from "@/types/template";
import GithubContribution from "@/components/shared/GithubContribution";
interface CreativeTemplateProps {
data: ResumeData;
template: ResumeTemplate;
}
const CreativeTemplate: React.FC<CreativeTemplateProps> = ({
data,
template,
}) => {
const { colorScheme } = template;
const enabledSections = data.menuSections.filter(
(section) => section.enabled
);
const sortedSections = [...enabledSections].sort((a, b) => a.order - b.order);
const renderSection = (sectionId: string) => {
switch (sectionId) {
case "experience":
return (
<ExperienceSection
experiences={data.experience}
globalSettings={data.globalSettings}
/>
);
case "education":
return (
<EducationSection
education={data.education}
globalSettings={data.globalSettings}
/>
);
case "skills":
return (
<SkillSection
skill={data.skillContent}
globalSettings={data.globalSettings}
/>
);
case "projects":
return (
<ProjectSection
projects={data.projects}
globalSettings={data.globalSettings}
/>
);
default:
if (sectionId in data.customData) {
const sectionTitle = data.menuSections.find(s => s.id === sectionId)?.title || sectionId;
return (
<CustomSection
title={sectionTitle}
key={sectionId}
sectionId={sectionId}
items={data.customData[sectionId]}
globalSettings={data.globalSettings}
/>
);
}
return null;
}
};
const basicSection = sortedSections.find((section) => section.id === "basic");
const otherSections = sortedSections.filter(
(section) => section.id !== "basic"
);
return (
<div
className="flex flex-col w-full min-h-screen"
style={{
backgroundColor: colorScheme.background,
color: colorScheme.text,
}}
>
{/* 顶部色块(如果存在basic区块) */}
{basicSection && (
<div
className="w-full relative py-8 px-8 mb-6 rounded-b-3xl"
style={{
backgroundColor: data.globalSettings.themeColor,
color: "#ffffff"
}}
>
<div className="relative z-10 w-full">
<BaseInfo basic={data.basic} globalSettings={data.globalSettings} template={template} />
{data.basic.githubContributionsVisible && (
<GithubContribution
className="mt-2 text-white"
githubKey={data.basic.githubKey}
username={data.basic.githubUseName}
/>
)}
</div>
</div>
)}
{/* 下方内容区 */}
<div className="px-8 w-full w-max-4xl mx-auto">
{otherSections.map((section) => (
<div key={section.id}>{renderSection(section.id)}</div>
))}
</div>
</div>
);
};
export default CreativeTemplate;
@@ -0,0 +1,108 @@
import React from "react";
import BaseInfo from "../preview/BaseInfo";
import ExperienceSection from "../preview/ExperienceSection";
import EducationSection from "../preview/EducationSection";
import SkillSection from "../preview/SkillPanel";
import ProjectSection from "../preview/ProjectSection";
import CustomSection from "../preview/CustomSection";
import { ResumeData } from "@/types/resume";
import { ResumeTemplate } from "@/types/template";
import GithubContribution from "@/components/shared/GithubContribution";
interface ElegantTemplateProps {
data: ResumeData;
template: ResumeTemplate;
}
const ElegantTemplate: React.FC<ElegantTemplateProps> = ({
data,
template,
}) => {
const { colorScheme } = template;
const enabledSections = data.menuSections.filter(
(section) => section.enabled
);
const sortedSections = [...enabledSections].sort((a, b) => a.order - b.order);
const renderSection = (sectionId: string) => {
switch (sectionId) {
case "basic":
return (
<div className="text-center w-full flex flex-col items-center">
<BaseInfo basic={data.basic} globalSettings={data.globalSettings} template={template} />
{data.basic.githubContributionsVisible && (
<GithubContribution
className="mt-2 justify-center"
githubKey={data.basic.githubKey}
username={data.basic.githubUseName}
/>
)}
</div>
);
case "experience":
return (
<ExperienceSection
experiences={data.experience}
globalSettings={data.globalSettings}
/>
);
case "education":
return (
<EducationSection
education={data.education}
globalSettings={data.globalSettings}
/>
);
case "skills":
return (
<SkillSection
skill={data.skillContent}
globalSettings={data.globalSettings}
/>
);
case "projects":
return (
<ProjectSection
projects={data.projects}
globalSettings={data.globalSettings}
/>
);
default:
if (sectionId in data.customData) {
const sectionTitle = data.menuSections.find(s => s.id === sectionId)?.title || sectionId;
return (
<CustomSection
title={sectionTitle}
key={sectionId}
sectionId={sectionId}
items={data.customData[sectionId]}
globalSettings={data.globalSettings}
/>
);
}
return null;
}
};
return (
<div
className="flex flex-col w-full min-h-screen items-center"
style={{
backgroundColor: colorScheme.background,
color: colorScheme.text,
}}
>
{/* 优雅版采用单行且较为居中的排版风格 */}
<div className="w-full max-w-4xl px-8">
{sortedSections.map((section) => (
<div key={section.id} className="w-full">
{renderSection(section.id)}
</div>
))}
</div>
</div>
);
};
export default ElegantTemplate;
@@ -0,0 +1,106 @@
import React from "react";
import GithubContribution from "@/components/shared/GithubContribution";
import BaseInfo from "../preview/BaseInfo";
import ExperienceSection from "../preview/ExperienceSection";
import EducationSection from "../preview/EducationSection";
import SkillSection from "../preview/SkillPanel";
import CustomSection from "../preview/CustomSection";
import { ResumeData } from "@/types/resume";
import { ResumeTemplate } from "@/types/template";
import ProjectSection from "../preview/ProjectSection";
interface MinimalistTemplateProps {
data: ResumeData;
template: ResumeTemplate;
}
const MinimalistTemplate: React.FC<MinimalistTemplateProps> = ({
data,
template,
}) => {
const { colorScheme } = template;
const enabledSections = data.menuSections.filter(
(section) => section.enabled
);
const sortedSections = [...enabledSections].sort((a, b) => a.order - b.order);
const renderSection = (sectionId: string) => {
switch (sectionId) {
case "basic":
return (
<>
<BaseInfo basic={data.basic} globalSettings={data.globalSettings} template={template} />
{data.basic.githubContributionsVisible && (
<GithubContribution
className="mt-2"
githubKey={data.basic.githubKey}
username={data.basic.githubUseName}
/>
)}
</>
);
case "experience":
return (
<ExperienceSection
experiences={data.experience}
globalSettings={data.globalSettings}
/>
);
case "education":
return (
<EducationSection
education={data.education}
globalSettings={data.globalSettings}
/>
);
case "skills":
return (
<SkillSection
skill={data.skillContent}
globalSettings={data.globalSettings}
/>
);
case "projects":
return (
<ProjectSection
projects={data.projects}
globalSettings={data.globalSettings}
/>
);
default:
if (sectionId in data.customData) {
const sectionTitle = data.menuSections.find(s => s.id === sectionId)?.title || sectionId;
return (
<CustomSection
title={sectionTitle}
key={sectionId}
sectionId={sectionId}
items={data.customData[sectionId]}
globalSettings={data.globalSettings}
/>
);
}
return null;
}
};
return (
<div
className="flex flex-col w-full min-h-screen"
style={{
backgroundColor: colorScheme.background,
color: colorScheme.text,
}}
>
{/* 极简风格:单列居中体验,更大留白 */}
{sortedSections.map((section) => (
<div key={section.id} className="w-full">
{renderSection(section.id)}
</div>
))}
</div>
);
};
export default MinimalistTemplate;
@@ -0,0 +1,25 @@
import React, { createContext, useContext } from "react";
import { MenuSection } from "@/types/resume";
interface TemplateContextProps {
templateId: string;
menuSections: MenuSection[];
}
const TemplateContext = createContext<TemplateContextProps | undefined>(undefined);
export const TemplateProvider: React.FC<{
templateId: string;
menuSections: MenuSection[];
children: React.ReactNode;
}> = ({ templateId, menuSections, children }) => {
return (
<TemplateContext.Provider value={{ templateId, menuSections }}>
{children}
</TemplateContext.Provider>
);
};
export const useTemplateContext = () => {
return useContext(TemplateContext);
};
+19 -1
View File
@@ -3,6 +3,12 @@ import ClassicTemplate from "./ClassicTemplate";
import ModernTemplate from "./ModernTemplate";
import LeftRightTemplate from "./LeftRightTemplate";
import TimelineTemplate from "./TimelineTemplate";
import MinimalistTemplate from "./MinimalistTemplate";
import ElegantTemplate from "./ElegantTemplate";
import CreativeTemplate from "./CreativeTemplate";
import { TemplateProvider } from "./TemplateContext";
import { ResumeData } from "@/types/resume";
import { ResumeTemplate } from "@/types/template";
@@ -23,12 +29,24 @@ const ResumeTemplateComponent: React.FC<TemplateProps> = ({
return <LeftRightTemplate data={data} template={template} />;
case "timeline":
return <TimelineTemplate data={data} template={template} />;
case "minimalist":
return <MinimalistTemplate data={data} template={template} />;
case "elegant":
return <ElegantTemplate data={data} template={template} />;
case "creative":
return <CreativeTemplate data={data} template={template} />;
default:
return <ClassicTemplate data={data} template={template} />;
}
};
return renderTemplate();
return (
<TemplateProvider templateId={template.id} menuSections={data.menuSections}>
{renderTemplate()}
</TemplateProvider>
);
};
export default ResumeTemplateComponent;
+64
View File
@@ -101,6 +101,70 @@ export const DEFAULT_TEMPLATES: ResumeTemplate[] = [
basic: {
layout: "right"
}
},
{
id: "minimalist",
name: "极简模板",
description: "大面积留白,干净纯粹的排版风格",
thumbnail: "minimalist",
layout: "minimalist",
colorScheme: {
primary: "#171717",
secondary: "#737373",
background: "#ffffff",
text: "#171717"
},
spacing: {
sectionGap: 32,
itemGap: 24,
contentPadding: 40
},
basic: {
layout: "center"
}
},
{
id: "elegant",
name: "优雅模板",
description: "居中标题单列设计,具有高级感的分隔线",
thumbnail: "elegant",
layout: "elegant",
colorScheme: {
primary: "#18181b",
secondary: "#71717a",
background: "#ffffff",
text: "#27272a"
},
spacing: {
sectionGap: 28,
itemGap: 18,
contentPadding: 32
},
basic: {
layout: "center"
}
},
{
id: "creative",
name: "创意模板",
description: "视觉错落设计,灵动活泼展现个性",
thumbnail: "creative",
layout: "creative",
colorScheme: {
primary: "#3b82f6",
secondary: "#64748b",
background: "#ffffff",
text: "#1e293b"
},
spacing: {
sectionGap: 24,
itemGap: 16,
contentPadding: 28
},
basic: {
layout: "left"
}
}
];
+28
View File
@@ -39,4 +39,32 @@ export const templateConfigs: Record<string, TemplateConfig> = {
},
},
},
minimalist: {
sectionTitle: {
className: "mb-3 tracking-widest",
styles: {
fontSize: 16,
},
},
},
elegant: {
sectionTitle: {
className: "flex items-center justify-center w-full mb-4 relative",
styles: {
fontSize: 20,
},
},
},
creative: {
sectionTitle: {
className: "inline-block px-3 py-1 mb-3 rounded-md text-white shadow-sm",
styles: {
fontSize: 16,
backgroundColor: "var(--theme-color)",
},
},
},
};
+13
View File
@@ -181,6 +181,19 @@
"timeline": {
"name": "Timeline Layout",
"description": "Timeline style, emphasizing chronological order of experiences"
},
"minimalist": {
"name": "Minimalist",
"description": "Large amount of whitespace, clean and pure layout style"
},
"elegant": {
"name": "Elegant",
"description": "Centered title single-column design, with a touch of elegance"
},
"creative": {
"name": "Creative",
"description": "Visual contrast design, vibrant and personalized"
}
}
},
+13
View File
@@ -182,6 +182,19 @@
"timeline": {
"name": "时间轴布局",
"description": "时间轴风格,突出经历的时间顺序"
},
"minimalist": {
"name": "极简模板",
"description": "大面积留白,干净纯粹的排版风格"
},
"elegant": {
"name": "优雅模板",
"description": "居中标题单列设计,具有高级感的分隔线"
},
"creative": {
"name": "创意模板",
"description": "视觉错落设计,灵动活泼展现个性"
}
}
},
+6
View File
@@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import IframeTemplateViewer from '../../../components/preview/IframeTemplateViewer'
export const Route = createFileRoute('/app/preview-template/$id')({
component: IframeTemplateViewer,
})
+1 -1
View File
@@ -5,7 +5,7 @@ export interface ResumeTemplate {
name: string;
description: string;
thumbnail: string;
layout: "classic" | "modern" | "left-right" | "professional" | "timeline";
layout: "classic" | "modern" | "left-right" | "timeline" | "minimalist" | "elegant" | "creative";
colorScheme: {
primary: string;
secondary: string;