mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-06-01 23:38:48 +02:00
feat: Implement blank resume creation and enhance section management with standard modules and a new section addition UI
This commit is contained in:
@@ -114,7 +114,7 @@ const TemplateThumbnail = ({
|
||||
},
|
||||
basic: {
|
||||
...initialResumeState.basic,
|
||||
layout: template.basic?.layout || "classic",
|
||||
layout: (template.basic?.layout as any) || "left",
|
||||
},
|
||||
// Feed richer mock content in large preview.
|
||||
experience: sampleExperience,
|
||||
|
||||
@@ -253,7 +253,8 @@ const ResumeWorkbench = () => {
|
||||
}, []);
|
||||
|
||||
const handleCreateFromModal = (templateId: string | null) => {
|
||||
const newId = createResume(templateId);
|
||||
const isBlank = !templateId;
|
||||
const newId = createResume(templateId, isBlank);
|
||||
|
||||
if (templateId) {
|
||||
const template = DEFAULT_TEMPLATES.find((t) => t.id === templateId);
|
||||
|
||||
@@ -18,8 +18,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import LayoutSetting from "./layout/LayoutSetting";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { THEME_COLORS } from "@/types/resume";
|
||||
import { THEME_COLORS, MenuSection } from "@/types/resume";
|
||||
import { ColorPicker } from "@/components/ui/color-picker";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Plus } from "lucide-react";
|
||||
import { STANDARD_MODULES } from "@/config/modules";
|
||||
import { DEFAULT_TEMPLATES } from "@/config";
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: "无衬线体" },
|
||||
@@ -84,6 +92,18 @@ export function SidePanel() {
|
||||
const { themeColor = THEME_COLORS[0] } = globalSettings;
|
||||
const t = useTranslations("workbench.sidePanel");
|
||||
|
||||
const currentTemplate = DEFAULT_TEMPLATES.find(
|
||||
(t) => t.id === activeResume?.templateId
|
||||
);
|
||||
|
||||
const availableModules = useMemo(() => {
|
||||
return (
|
||||
currentTemplate?.availableSections
|
||||
?.map((id) => STANDARD_MODULES[id])
|
||||
.filter(Boolean) || []
|
||||
);
|
||||
}, [currentTemplate]);
|
||||
|
||||
const fontOptions = [
|
||||
{ value: "sans", label: t("typography.font.sans") },
|
||||
{ value: "serif", label: t("typography.font.serif") },
|
||||
@@ -145,15 +165,57 @@ export function SidePanel() {
|
||||
reorderSections={reorderSections}
|
||||
/>
|
||||
|
||||
<div className="space-y-2 py-4">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={handleCreateSection}
|
||||
className="flex justify-center w-full rounded-lg items-center gap-2 py-2 px-3 text-sm font-medium text-primary bg-primary/5"
|
||||
>
|
||||
{t("layout.addCustomSection")}
|
||||
</motion.button>
|
||||
<div className="space-y-2 py-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className="flex justify-center w-full rounded-lg items-center gap-2 py-2 px-3 text-sm font-medium text-primary bg-primary/5 border border-dashed border-primary/20 hover:bg-primary/10 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t("layout.addCustomSection")}
|
||||
</motion.button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Standard Sections Library */}
|
||||
{availableModules.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => {
|
||||
const newSection = {
|
||||
id: section.id,
|
||||
title: t(`layout.standardSections.${section.titleKey}`),
|
||||
icon: section.icon,
|
||||
enabled: true,
|
||||
order: menuSections.length,
|
||||
};
|
||||
updateMenuSections([...menuSections, newSection]);
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-left"
|
||||
>
|
||||
<span className="text-lg">{section.icon}</span>
|
||||
<span>{t(`layout.standardSections.${section.titleKey}`)}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Divider for Custom Section */}
|
||||
{availableModules.length > 0 && (
|
||||
<div className="h-px bg-border my-1" />
|
||||
)}
|
||||
|
||||
{/* Add Custom Section */}
|
||||
<button
|
||||
onClick={handleCreateSection}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-left text-muted-foreground italic"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t("layout.addCustomSectionOption")}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ export const classicConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "left",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const creativeConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "left",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const elegantConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "center",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const leftRightConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "left",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const minimalistConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "center",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const modernConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "center",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ export const timelineConfig: ResumeTemplate = {
|
||||
basic: {
|
||||
layout: "right",
|
||||
},
|
||||
availableSections: ["skills", "experience", "projects", "education"],
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export const initialResumeState = {
|
||||
id: "1",
|
||||
school: "北京大学",
|
||||
major: "计算机科学与技术",
|
||||
degree: "本科",
|
||||
degree: "",
|
||||
startDate: "2013-09",
|
||||
endDate: "2017-06",
|
||||
visible: true,
|
||||
@@ -326,3 +326,47 @@ export const initialResumeStateEn = {
|
||||
activeSection: "basic",
|
||||
globalSettings: initialGlobalSettings,
|
||||
};
|
||||
|
||||
export const blankResumeState = {
|
||||
...initialResumeState,
|
||||
title: "新建简历",
|
||||
basic: {
|
||||
...initialResumeState.basic,
|
||||
name: "",
|
||||
title: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
location: "",
|
||||
birthDate: "",
|
||||
employementStatus: "",
|
||||
photo: "",
|
||||
customFields: [],
|
||||
},
|
||||
education: [],
|
||||
skillContent: "",
|
||||
experience: [],
|
||||
projects: [],
|
||||
menuSections: [initialResumeState.menuSections[0]],
|
||||
};
|
||||
|
||||
export const blankResumeStateEn = {
|
||||
...initialResumeStateEn,
|
||||
title: "New Resume",
|
||||
basic: {
|
||||
...initialResumeStateEn.basic,
|
||||
name: "",
|
||||
title: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
location: "",
|
||||
birthDate: "",
|
||||
employementStatus: "",
|
||||
photo: "",
|
||||
customFields: [],
|
||||
},
|
||||
education: [],
|
||||
skillContent: "",
|
||||
experience: [],
|
||||
projects: [],
|
||||
menuSections: [initialResumeStateEn.menuSections[0]],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ResumeModule {
|
||||
id: string;
|
||||
titleKey: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const STANDARD_MODULES: Record<string, ResumeModule> = {
|
||||
skills: { id: "skills", titleKey: "skills", icon: "⚡" },
|
||||
experience: { id: "experience", titleKey: "experience", icon: "💼" },
|
||||
projects: { id: "projects", titleKey: "projects", icon: "🚀" },
|
||||
education: { id: "education", titleKey: "education", icon: "🎓" },
|
||||
};
|
||||
@@ -291,7 +291,14 @@
|
||||
"sidePanel": {
|
||||
"layout": {
|
||||
"title": "Layout",
|
||||
"addCustomSection": "Add Custom Section"
|
||||
"addCustomSection": "Add Module",
|
||||
"addCustomSectionOption": "Add Custom Section",
|
||||
"standardSections": {
|
||||
"skills": "Skills",
|
||||
"experience": "Experience",
|
||||
"projects": "Projects",
|
||||
"education": "Education"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Color",
|
||||
|
||||
@@ -251,7 +251,14 @@
|
||||
"sidePanel": {
|
||||
"layout": {
|
||||
"title": "布局",
|
||||
"addCustomSection": "添加自定义模块"
|
||||
"addCustomSection": "添加模块",
|
||||
"addCustomSectionOption": "添加自定义模块",
|
||||
"standardSections": {
|
||||
"skills": "专业技能",
|
||||
"experience": "工作经验",
|
||||
"projects": "项目经历",
|
||||
"education": "教育经历"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题色",
|
||||
|
||||
@@ -15,6 +15,8 @@ import { DEFAULT_TEMPLATES } from "@/config";
|
||||
import {
|
||||
initialResumeState,
|
||||
initialResumeStateEn,
|
||||
blankResumeState,
|
||||
blankResumeStateEn,
|
||||
} from "@/config/initialResumeData";
|
||||
import { generateUUID } from "@/utils/uuid";
|
||||
interface ResumeStore {
|
||||
@@ -22,7 +24,7 @@ interface ResumeStore {
|
||||
activeResumeId: string | null;
|
||||
activeResume: ResumeData | null;
|
||||
|
||||
createResume: (templateId: string | null) => string;
|
||||
createResume: (templateId: string | null, isBlank?: boolean) => string;
|
||||
deleteResume: (resume: ResumeData) => void;
|
||||
duplicateResume: (resumeId: string) => string;
|
||||
updateResume: (resumeId: string, data: Partial<ResumeData>) => void;
|
||||
@@ -113,7 +115,7 @@ export const useResumeStore = create(
|
||||
activeResumeId: null,
|
||||
activeResume: null,
|
||||
|
||||
createResume: (templateId = null) => {
|
||||
createResume: (templateId = null, isBlank = false) => {
|
||||
const locale =
|
||||
typeof document !== "undefined"
|
||||
? document.cookie
|
||||
@@ -122,8 +124,14 @@ export const useResumeStore = create(
|
||||
?.split("=")[1] || "zh"
|
||||
: "zh";
|
||||
|
||||
const initialResumeData =
|
||||
locale === "en" ? initialResumeStateEn : initialResumeState;
|
||||
let initialResumeData: any;
|
||||
if (isBlank) {
|
||||
initialResumeData =
|
||||
locale === "en" ? blankResumeStateEn : blankResumeState;
|
||||
} else {
|
||||
initialResumeData =
|
||||
locale === "en" ? initialResumeStateEn : initialResumeState;
|
||||
}
|
||||
|
||||
const id = generateUUID();
|
||||
const template = templateId
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ResumeTemplate {
|
||||
basic: {
|
||||
layout?: "left" | "center" | "right";
|
||||
};
|
||||
availableSections?: string[];
|
||||
}
|
||||
|
||||
export interface TemplateConfig {
|
||||
|
||||
Reference in New Issue
Block a user