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: {
|
basic: {
|
||||||
...initialResumeState.basic,
|
...initialResumeState.basic,
|
||||||
layout: template.basic?.layout || "classic",
|
layout: (template.basic?.layout as any) || "left",
|
||||||
},
|
},
|
||||||
// Feed richer mock content in large preview.
|
// Feed richer mock content in large preview.
|
||||||
experience: sampleExperience,
|
experience: sampleExperience,
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ const ResumeWorkbench = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateFromModal = (templateId: string | null) => {
|
const handleCreateFromModal = (templateId: string | null) => {
|
||||||
const newId = createResume(templateId);
|
const isBlank = !templateId;
|
||||||
|
const newId = createResume(templateId, isBlank);
|
||||||
|
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
const template = DEFAULT_TEMPLATES.find((t) => t.id === 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 LayoutSetting from "./layout/LayoutSetting";
|
||||||
import { useResumeStore } from "@/store/useResumeStore";
|
import { useResumeStore } from "@/store/useResumeStore";
|
||||||
import { cn } from "@/lib/utils";
|
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 { 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 = [
|
const fontOptions = [
|
||||||
{ value: "sans", label: "无衬线体" },
|
{ value: "sans", label: "无衬线体" },
|
||||||
@@ -84,6 +92,18 @@ export function SidePanel() {
|
|||||||
const { themeColor = THEME_COLORS[0] } = globalSettings;
|
const { themeColor = THEME_COLORS[0] } = globalSettings;
|
||||||
const t = useTranslations("workbench.sidePanel");
|
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 = [
|
const fontOptions = [
|
||||||
{ value: "sans", label: t("typography.font.sans") },
|
{ value: "sans", label: t("typography.font.sans") },
|
||||||
{ value: "serif", label: t("typography.font.serif") },
|
{ value: "serif", label: t("typography.font.serif") },
|
||||||
@@ -145,15 +165,57 @@ export function SidePanel() {
|
|||||||
reorderSections={reorderSections}
|
reorderSections={reorderSections}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2 py-4">
|
<div className="space-y-2 py-4">
|
||||||
<motion.button
|
<Popover>
|
||||||
whileHover={{ scale: 1.01 }}
|
<PopoverTrigger asChild>
|
||||||
whileTap={{ scale: 0.9 }}
|
<motion.button
|
||||||
onClick={handleCreateSection}
|
whileHover={{ scale: 1.01 }}
|
||||||
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"
|
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"
|
||||||
{t("layout.addCustomSection")}
|
>
|
||||||
</motion.button>
|
<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>
|
</div>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const classicConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "left",
|
layout: "left",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const creativeConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "left",
|
layout: "left",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const elegantConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "center",
|
layout: "center",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const leftRightConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "left",
|
layout: "left",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const minimalistConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "center",
|
layout: "center",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const modernConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "center",
|
layout: "center",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export const timelineConfig: ResumeTemplate = {
|
|||||||
basic: {
|
basic: {
|
||||||
layout: "right",
|
layout: "right",
|
||||||
},
|
},
|
||||||
|
availableSections: ["skills", "experience", "projects", "education"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const initialResumeState = {
|
|||||||
id: "1",
|
id: "1",
|
||||||
school: "北京大学",
|
school: "北京大学",
|
||||||
major: "计算机科学与技术",
|
major: "计算机科学与技术",
|
||||||
degree: "本科",
|
degree: "",
|
||||||
startDate: "2013-09",
|
startDate: "2013-09",
|
||||||
endDate: "2017-06",
|
endDate: "2017-06",
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -326,3 +326,47 @@ export const initialResumeStateEn = {
|
|||||||
activeSection: "basic",
|
activeSection: "basic",
|
||||||
globalSettings: initialGlobalSettings,
|
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": {
|
"sidePanel": {
|
||||||
"layout": {
|
"layout": {
|
||||||
"title": "Layout",
|
"title": "Layout",
|
||||||
"addCustomSection": "Add Custom Section"
|
"addCustomSection": "Add Module",
|
||||||
|
"addCustomSectionOption": "Add Custom Section",
|
||||||
|
"standardSections": {
|
||||||
|
"skills": "Skills",
|
||||||
|
"experience": "Experience",
|
||||||
|
"projects": "Projects",
|
||||||
|
"education": "Education"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "Theme Color",
|
"title": "Theme Color",
|
||||||
|
|||||||
@@ -251,7 +251,14 @@
|
|||||||
"sidePanel": {
|
"sidePanel": {
|
||||||
"layout": {
|
"layout": {
|
||||||
"title": "布局",
|
"title": "布局",
|
||||||
"addCustomSection": "添加自定义模块"
|
"addCustomSection": "添加模块",
|
||||||
|
"addCustomSectionOption": "添加自定义模块",
|
||||||
|
"standardSections": {
|
||||||
|
"skills": "专业技能",
|
||||||
|
"experience": "工作经验",
|
||||||
|
"projects": "项目经历",
|
||||||
|
"education": "教育经历"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "主题色",
|
"title": "主题色",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { DEFAULT_TEMPLATES } from "@/config";
|
|||||||
import {
|
import {
|
||||||
initialResumeState,
|
initialResumeState,
|
||||||
initialResumeStateEn,
|
initialResumeStateEn,
|
||||||
|
blankResumeState,
|
||||||
|
blankResumeStateEn,
|
||||||
} from "@/config/initialResumeData";
|
} from "@/config/initialResumeData";
|
||||||
import { generateUUID } from "@/utils/uuid";
|
import { generateUUID } from "@/utils/uuid";
|
||||||
interface ResumeStore {
|
interface ResumeStore {
|
||||||
@@ -22,7 +24,7 @@ interface ResumeStore {
|
|||||||
activeResumeId: string | null;
|
activeResumeId: string | null;
|
||||||
activeResume: ResumeData | null;
|
activeResume: ResumeData | null;
|
||||||
|
|
||||||
createResume: (templateId: string | null) => string;
|
createResume: (templateId: string | null, isBlank?: boolean) => string;
|
||||||
deleteResume: (resume: ResumeData) => void;
|
deleteResume: (resume: ResumeData) => void;
|
||||||
duplicateResume: (resumeId: string) => string;
|
duplicateResume: (resumeId: string) => string;
|
||||||
updateResume: (resumeId: string, data: Partial<ResumeData>) => void;
|
updateResume: (resumeId: string, data: Partial<ResumeData>) => void;
|
||||||
@@ -113,7 +115,7 @@ export const useResumeStore = create(
|
|||||||
activeResumeId: null,
|
activeResumeId: null,
|
||||||
activeResume: null,
|
activeResume: null,
|
||||||
|
|
||||||
createResume: (templateId = null) => {
|
createResume: (templateId = null, isBlank = false) => {
|
||||||
const locale =
|
const locale =
|
||||||
typeof document !== "undefined"
|
typeof document !== "undefined"
|
||||||
? document.cookie
|
? document.cookie
|
||||||
@@ -122,8 +124,14 @@ export const useResumeStore = create(
|
|||||||
?.split("=")[1] || "zh"
|
?.split("=")[1] || "zh"
|
||||||
: "zh";
|
: "zh";
|
||||||
|
|
||||||
const initialResumeData =
|
let initialResumeData: any;
|
||||||
locale === "en" ? initialResumeStateEn : initialResumeState;
|
if (isBlank) {
|
||||||
|
initialResumeData =
|
||||||
|
locale === "en" ? blankResumeStateEn : blankResumeState;
|
||||||
|
} else {
|
||||||
|
initialResumeData =
|
||||||
|
locale === "en" ? initialResumeStateEn : initialResumeState;
|
||||||
|
}
|
||||||
|
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
const template = templateId
|
const template = templateId
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface ResumeTemplate {
|
|||||||
basic: {
|
basic: {
|
||||||
layout?: "left" | "center" | "right";
|
layout?: "left" | "center" | "right";
|
||||||
};
|
};
|
||||||
|
availableSections?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateConfig {
|
export interface TemplateConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user