mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: add more template
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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,86 @@ 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 "professional":
|
||||
return (
|
||||
<h3
|
||||
className={cn("pb-1 border-b-2 uppercase tracking-wide")}
|
||||
style={{
|
||||
...baseStyles,
|
||||
color: themeColor,
|
||||
borderColor: 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>
|
||||
);
|
||||
|
||||
case "compact":
|
||||
return (
|
||||
<h3
|
||||
className={cn("pb-1 border-b border-dashed mb-2")}
|
||||
style={{
|
||||
...baseStyles,
|
||||
color: themeColor,
|
||||
borderColor: themeColor,
|
||||
}}
|
||||
>
|
||||
{renderTitle}
|
||||
</h3>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<h3
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
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 CompactTemplateProps {
|
||||
data: ResumeData;
|
||||
template: ResumeTemplate;
|
||||
}
|
||||
|
||||
const CompactTemplate: React.FC<CompactTemplateProps> = ({
|
||||
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="border-b-2 border-gray-800 pb-2 mb-4">
|
||||
<BaseInfo basic={data.basic} globalSettings={data.globalSettings} template={template} />
|
||||
|
||||
{data.basic.githubContributionsVisible && (
|
||||
<GithubContribution
|
||||
className="mt-1"
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const basicSection = sortedSections.find((section) => section.id === "basic");
|
||||
const otherSections = sortedSections.filter(
|
||||
(section) => section.id !== "basic"
|
||||
);
|
||||
|
||||
// 紧凑排版:可以分为两列
|
||||
const leftSections = otherSections.filter((_, i) => i % 2 === 0);
|
||||
const rightSections = otherSections.filter((_, i) => i % 2 !== 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full min-h-screen relative"
|
||||
style={{
|
||||
backgroundColor: colorScheme.background,
|
||||
color: colorScheme.text,
|
||||
}}
|
||||
>
|
||||
{/* 顶部个人信息占满全宽 */}
|
||||
{basicSection && <div className="w-full">{renderSection(basicSection.id)}</div>}
|
||||
|
||||
{/* 瀑布流/双列紧凑排版 */}
|
||||
<div className="grid grid-cols-2 gap-4 w-full">
|
||||
<div className="flex flex-col">
|
||||
{leftSections.map((section) => (
|
||||
<div key={section.id} className="w-full">
|
||||
{renderSection(section.id)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{rightSections.map((section) => (
|
||||
<div key={section.id} className="w-full">
|
||||
{renderSection(section.id)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactTemplate;
|
||||
@@ -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,129 @@
|
||||
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 ProfessionalTemplateProps {
|
||||
data: ResumeData;
|
||||
template: ResumeTemplate;
|
||||
}
|
||||
|
||||
const ProfessionalTemplate: React.FC<ProfessionalTemplateProps> = ({ 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;
|
||||
}
|
||||
};
|
||||
|
||||
const leftColumnSections = ["basic", "skills", "education"];
|
||||
const rightColumnSections = ["experience", "projects"];
|
||||
|
||||
const leftSectionsToRender = sortedSections.filter(s => leftColumnSections.includes(s.id));
|
||||
const rightSectionsToRender = sortedSections.filter(s => rightColumnSections.includes(s.id));
|
||||
|
||||
// Custom sections go to the right by default
|
||||
const customSections = sortedSections.filter(s => !leftColumnSections.includes(s.id) && !rightColumnSections.includes(s.id));
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 w-full min-h-screen" style={{ backgroundColor: colorScheme.background, color: colorScheme.text }}>
|
||||
{/* 左侧区域(偏窄) */}
|
||||
<div
|
||||
className="col-span-4 p-6 border-r border-gray-200"
|
||||
style={{
|
||||
paddingTop: data.globalSettings.sectionSpacing,
|
||||
}}
|
||||
>
|
||||
{leftSectionsToRender.map((section) => (
|
||||
<div key={section.id}>{renderSection(section.id)}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 右侧区域(偏宽) */}
|
||||
<div
|
||||
className="col-span-8 p-6"
|
||||
style={{
|
||||
paddingTop: data.globalSettings.sectionSpacing,
|
||||
}}
|
||||
>
|
||||
{rightSectionsToRender.map((section) => (
|
||||
<div key={section.id}>{renderSection(section.id)}</div>
|
||||
))}
|
||||
{customSections.map((section) => (
|
||||
<div key={section.id}>{renderSection(section.id)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfessionalTemplate;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 ProfessionalTemplate from "./ProfessionalTemplate";
|
||||
import ElegantTemplate from "./ElegantTemplate";
|
||||
import CreativeTemplate from "./CreativeTemplate";
|
||||
import CompactTemplate from "./CompactTemplate";
|
||||
import { TemplateProvider } from "./TemplateContext";
|
||||
import { ResumeData } from "@/types/resume";
|
||||
import { ResumeTemplate } from "@/types/template";
|
||||
|
||||
@@ -23,12 +29,26 @@ 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 "professional":
|
||||
return <ProfessionalTemplate data={data} template={template} />;
|
||||
case "elegant":
|
||||
return <ElegantTemplate data={data} template={template} />;
|
||||
case "creative":
|
||||
return <CreativeTemplate data={data} template={template} />;
|
||||
case "compact":
|
||||
return <CompactTemplate 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;
|
||||
|
||||
@@ -101,6 +101,111 @@ 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: "professional",
|
||||
name: "商务模板",
|
||||
description: "经典双列布局,适合资深职场人士",
|
||||
thumbnail: "professional",
|
||||
layout: "professional",
|
||||
colorScheme: {
|
||||
primary: "#0f172a",
|
||||
secondary: "#475569",
|
||||
background: "#ffffff",
|
||||
text: "#1e293b"
|
||||
},
|
||||
spacing: {
|
||||
sectionGap: 20,
|
||||
itemGap: 16,
|
||||
contentPadding: 24
|
||||
},
|
||||
basic: {
|
||||
layout: "left"
|
||||
}
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "compact",
|
||||
name: "紧凑模板",
|
||||
description: "高效的页面利用率,适合经验丰富的信息传达",
|
||||
thumbnail: "compact",
|
||||
layout: "compact",
|
||||
colorScheme: {
|
||||
primary: "#000000",
|
||||
secondary: "#6b7280",
|
||||
background: "#ffffff",
|
||||
text: "#111827"
|
||||
},
|
||||
spacing: {
|
||||
sectionGap: 12,
|
||||
itemGap: 8,
|
||||
contentPadding: 16
|
||||
},
|
||||
basic: {
|
||||
layout: "left"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -39,4 +39,48 @@ export const templateConfigs: Record<string, TemplateConfig> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
minimalist: {
|
||||
sectionTitle: {
|
||||
className: "mb-3 tracking-widest",
|
||||
styles: {
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
professional: {
|
||||
sectionTitle: {
|
||||
className: "border-b-2 mb-3 pb-1 uppercase tracking-wider",
|
||||
styles: {
|
||||
fontSize: 18,
|
||||
borderColor: "var(--theme-color)",
|
||||
},
|
||||
},
|
||||
},
|
||||
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)",
|
||||
},
|
||||
},
|
||||
},
|
||||
compact: {
|
||||
sectionTitle: {
|
||||
className: "border-b border-dashed mb-2 pb-1",
|
||||
styles: {
|
||||
fontSize: 15,
|
||||
borderColor: "var(--theme-color)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -181,6 +181,26 @@
|
||||
"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"
|
||||
},
|
||||
"professional": {
|
||||
"name": "Professional",
|
||||
"description": "Classic two-column layout, suitable for senior professionals"
|
||||
},
|
||||
"elegant": {
|
||||
"name": "Elegant",
|
||||
"description": "Centered title single-column design, with a touch of elegance"
|
||||
},
|
||||
"creative": {
|
||||
"name": "Creative",
|
||||
"description": "Visual contrast design, vibrant and personalized"
|
||||
},
|
||||
"compact": {
|
||||
"name": "Compact",
|
||||
"description": "Efficient page utilization, suitable for extensive experiences"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -182,6 +182,26 @@
|
||||
"timeline": {
|
||||
"name": "时间轴布局",
|
||||
"description": "时间轴风格,突出经历的时间顺序"
|
||||
},
|
||||
"minimalist": {
|
||||
"name": "极简模板",
|
||||
"description": "大面积留白,干净纯粹的排版风格"
|
||||
},
|
||||
"professional": {
|
||||
"name": "商务模板",
|
||||
"description": "经典双列布局,适合资深职场人士"
|
||||
},
|
||||
"elegant": {
|
||||
"name": "优雅模板",
|
||||
"description": "居中标题单列设计,具有高级感的分隔线"
|
||||
},
|
||||
"creative": {
|
||||
"name": "创意模板",
|
||||
"description": "视觉错落设计,灵动活泼展现个性"
|
||||
},
|
||||
"compact": {
|
||||
"name": "紧凑模板",
|
||||
"description": "高效的页面利用率,适合经验丰富的信息传达"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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" | "professional" | "timeline" | "minimalist" | "elegant" | "creative" | "compact";
|
||||
colorScheme: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
|
||||
Reference in New Issue
Block a user