diff --git a/package.json b/package.json
index a3adde9..4733ac1 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
+ "uuid": "^11.0.5",
"vaul": "^1.1.1",
"zustand": "^4.5.4"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5b7e4b6..0a1c8e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -164,6 +164,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17)
+ uuid:
+ specifier: ^11.0.5
+ version: 11.0.5
vaul:
specifier: ^1.1.1
version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -3459,6 +3462,10 @@ packages:
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
+ uuid@11.0.5:
+ resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==}
+ hasBin: true
+
vaul@1.1.2:
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
peerDependencies:
@@ -7160,6 +7167,8 @@ snapshots:
dependencies:
base64-arraybuffer: 1.0.2
+ uuid@11.0.5: {}
+
vaul@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-dialog': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
diff --git a/src/app/app/dashboard/resumes/page.tsx b/src/app/app/dashboard/resumes/page.tsx
index e51fd15..2bf6511 100644
--- a/src/app/app/dashboard/resumes/page.tsx
+++ b/src/app/app/dashboard/resumes/page.tsx
@@ -6,11 +6,6 @@ import { Plus, FileText, Settings, AlertCircle, Upload } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { cn } from "@/lib/utils";
-import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem";
-import { useResumeStore } from "@/store/useResumeStore";
-import { initialResumeState } from "@/config/initialResumeData";
import {
Card,
CardContent,
@@ -18,7 +13,13 @@ import {
CardFooter,
CardTitle
} from "@/components/ui/card";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { cn } from "@/lib/utils";
+import { getConfig, getFileHandle, verifyPermission } from "@/utils/fileSystem";
+import { useResumeStore } from "@/store/useResumeStore";
+import { initialResumeState } from "@/config/initialResumeData";
+import { generateUUID } from "@/utils/uuid";
const ResumesList = () => {
return ;
};
@@ -107,8 +108,7 @@ const ResumeWorkbench = () => {
const newResume = {
...initialResumeState,
...config,
- id: crypto.randomUUID(),
- title: config.title || t("dashboard.resumes.untitled"),
+ id: generateUUID(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
diff --git a/src/components/editor/basic/BasicPanel.tsx b/src/components/editor/basic/BasicPanel.tsx
index 1bc2538..44548d3 100644
--- a/src/components/editor/basic/BasicPanel.tsx
+++ b/src/components/editor/basic/BasicPanel.tsx
@@ -14,6 +14,7 @@ import { cn } from "@/lib/utils";
import { DEFAULT_FIELD_ORDER } from "@/config";
import { useResumeStore } from "@/store/useResumeStore";
import { BasicFieldType, CustomFieldType } from "@/types/resume";
+import { generateUUID } from "@/utils/uuid";
interface CustomFieldProps {
field: CustomFieldType;
onUpdate: (field: CustomFieldType) => void;
@@ -24,13 +25,13 @@ const itemAnimations = {
initial: { opacity: 0, y: 0 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 0 },
- transition: { type: "spring", stiffness: 500, damping: 50, mass: 1 },
+ transition: { type: "spring", stiffness: 500, damping: 50, mass: 1 }
};
const CustomField: React.FC = ({
field,
onUpdate,
- onDelete,
+ onDelete
}) => {
const t = useTranslations("workbench.basicPanel");
@@ -72,7 +73,7 @@ const CustomField: React.FC = ({
onChange={(value) =>
onUpdate({
...field,
- label: value,
+ label: value
})
}
placeholder={t("customFields.placeholders.label")}
@@ -88,7 +89,7 @@ const CustomField: React.FC = ({
onChange={(value) =>
onUpdate({
...field,
- value: value,
+ value: value
})
}
placeholder={t("customFields.placeholders.value")}
@@ -134,7 +135,7 @@ const BasicPanel: React.FC = () => {
const [customFields, setCustomFields] = useState(
basic?.customFields?.map((field) => ({
...field,
- visible: field.visible ?? true,
+ visible: field.visible ?? true
})) || []
);
const [basicFields, setBasicFields] = useState(() => {
@@ -143,7 +144,7 @@ const BasicPanel: React.FC = () => {
}
return basic.fieldOrder.map((field) => ({
...field,
- visible: field.visible ?? true,
+ visible: field.visible ?? true
}));
});
const t = useTranslations("workbench.basicPanel");
@@ -152,7 +153,7 @@ const BasicPanel: React.FC = () => {
setBasicFields(newOrder);
updateBasicInfo({
...basic,
- fieldOrder: newOrder,
+ fieldOrder: newOrder
});
};
@@ -163,23 +164,23 @@ const BasicPanel: React.FC = () => {
setBasicFields(newFields);
updateBasicInfo({
...basic,
- fieldOrder: newFields,
+ fieldOrder: newFields
});
};
const addCustomField = () => {
const fieldToAdd: CustomFieldType = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
label: "",
value: "",
icon: "User",
- visible: true,
+ visible: true
};
const updatedFields = [...customFields, fieldToAdd];
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
- customFields: updatedFields,
+ customFields: updatedFields
});
};
@@ -190,7 +191,7 @@ const BasicPanel: React.FC = () => {
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
- customFields: updatedFields,
+ customFields: updatedFields
});
};
@@ -199,7 +200,7 @@ const BasicPanel: React.FC = () => {
setCustomFields(updatedFields);
updateBasicInfo({
...basic,
- customFields: updatedFields,
+ customFields: updatedFields
});
};
@@ -207,7 +208,7 @@ const BasicPanel: React.FC = () => {
setCustomFields(newOrder);
updateBasicInfo({
...basic,
- customFields: newOrder,
+ customFields: newOrder
});
};
@@ -257,8 +258,8 @@ const BasicPanel: React.FC = () => {
...basic,
icons: {
...(basic?.icons || {}),
- [field.key]: value,
- },
+ [field.key]: value
+ }
});
}}
/>
@@ -273,7 +274,7 @@ const BasicPanel: React.FC = () => {
onChange={(value) =>
updateBasicInfo({
...basic,
- [field.key]: value,
+ [field.key]: value
})
}
placeholder={`请输入${field.label}`}
@@ -314,7 +315,7 @@ const BasicPanel: React.FC = () => {
onChange={(value) =>
updateBasicInfo({
...basic,
- layout: value,
+ layout: value
})
}
/>
@@ -404,7 +405,7 @@ const BasicPanel: React.FC = () => {
onCheckedChange={(checked) =>
updateBasicInfo({
...basic,
- githubContributionsVisible: checked,
+ githubContributionsVisible: checked
})
}
/>
@@ -420,7 +421,7 @@ const BasicPanel: React.FC = () => {
onChange={(e) =>
updateBasicInfo({
...basic,
- githubKey: e.target.value,
+ githubKey: e.target.value
})
}
/>
@@ -434,7 +435,7 @@ const BasicPanel: React.FC = () => {
onChange={(e) =>
updateBasicInfo({
...basic,
- githubUseName: e.target.value,
+ githubUseName: e.target.value
})
}
/>
diff --git a/src/components/editor/education/EducationPanel.tsx b/src/components/editor/education/EducationPanel.tsx
index 3ab721d..f951f54 100644
--- a/src/components/editor/education/EducationPanel.tsx
+++ b/src/components/editor/education/EducationPanel.tsx
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import EducationItem from "./EducationItem";
import { Education } from "@/types/resume";
+import { generateUUID } from "@/utils/uuid";
const EducationPanel = () => {
const t = useTranslations('workbench.educationPanel');
@@ -15,7 +16,7 @@ const EducationPanel = () => {
const { education = [] } = activeResume || {};
const handleCreateProject = () => {
const newEducation: Education = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
school: t('defaultProject.school'),
major: t('defaultProject.major'),
degree: t('defaultProject.degree'),
diff --git a/src/components/editor/experience/ExperiencePanel.tsx b/src/components/editor/experience/ExperiencePanel.tsx
index 2511bbb..cb24f62 100644
--- a/src/components/editor/experience/ExperiencePanel.tsx
+++ b/src/components/editor/experience/ExperiencePanel.tsx
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
import ExperienceItem from "./ExperienceItem";
import { Experience } from "@/types/resume";
import { useResumeStore } from "@/store/useResumeStore";
+import { generateUUID } from "@/utils/uuid";
const ExperiencePanel = () => {
const t = useTranslations("workbench.experiencePanel");
@@ -15,7 +16,7 @@ const ExperiencePanel = () => {
const { experience = [] } = activeResume || {};
const handleCreateProject = () => {
const newProject: Experience = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
company: t("defaultProject.company"),
position: t("defaultProject.position"),
date: t("defaultProject.date"),
diff --git a/src/components/editor/project/ProjectPanel.tsx b/src/components/editor/project/ProjectPanel.tsx
index 377efb4..37bf5d8 100644
--- a/src/components/editor/project/ProjectPanel.tsx
+++ b/src/components/editor/project/ProjectPanel.tsx
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import ProjectItem from "./ProjectItem";
import { Project } from "@/types/resume";
+import { generateUUID } from "@/utils/uuid";
const ProjectPanel = () => {
const t = useTranslations("workbench.projectPanel");
@@ -14,7 +15,7 @@ const ProjectPanel = () => {
const { projects = [] } = activeResume || {};
const handleCreateProject = () => {
const newProject: Project = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
name: t("defaultProject.name"),
role: t("defaultProject.role"),
date: t("defaultProject.date"),
diff --git a/src/store/useResumeStore.ts b/src/store/useResumeStore.ts
index fff6687..82916fd 100644
--- a/src/store/useResumeStore.ts
+++ b/src/store/useResumeStore.ts
@@ -1,20 +1,19 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import { DEFAULT_FIELD_ORDER } from "@/config";
-import { getFileHandle, getConfig, verifyPermission } from "@/utils/fileSystem";
+import { getFileHandle, verifyPermission } from "@/utils/fileSystem";
import {
BasicInfo,
Education,
Experience,
GlobalSettings,
- DEFAULT_CONFIG,
Project,
CustomItem,
ResumeData,
- MenuSection,
+ MenuSection
} from "../types/resume";
import { DEFAULT_TEMPLATES } from "@/config";
import { initialResumeState } from "@/config/initialResumeData";
+import { generateUUID } from "@/utils/uuid";
interface ResumeStore {
resumes: Record;
activeResumeId: string | null;
@@ -80,7 +79,6 @@ const syncResumeToFile = async (
const dirHandle = handle as FileSystemDirectoryHandle;
- // If it's the same resume (same id) but title changed, delete the old file
if (
prevResume &&
prevResume.id === resumeData.id &&
@@ -95,7 +93,7 @@ const syncResumeToFile = async (
const fileName = `${resumeData.title}.json`;
const fileHandle = await dirHandle.getFileHandle(fileName, {
- create: true,
+ create: true
});
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(resumeData, null, 2));
@@ -113,7 +111,7 @@ export const useResumeStore = create(
activeResume: null,
createResume: (templateId = null) => {
- const id = crypto.randomUUID();
+ const id = generateUUID();
const template = templateId
? DEFAULT_TEMPLATES.find((t) => t.id === templateId)
: DEFAULT_TEMPLATES[0];
@@ -124,16 +122,16 @@ export const useResumeStore = create(
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateId: template?.id,
- title: `新建简历 ${id.slice(0, 6)}`,
+ title: `新建简历 ${id.slice(0, 6)}`
};
set((state) => ({
resumes: {
...state.resumes,
- [id]: newResume,
+ [id]: newResume
},
activeResumeId: id,
- activeResume: newResume,
+ activeResume: newResume
}));
syncResumeToFile(newResume);
@@ -148,7 +146,7 @@ export const useResumeStore = create(
const updatedResume = {
...resume,
- ...data,
+ ...data
};
syncResumeToFile(updatedResume, resume);
@@ -156,12 +154,12 @@ export const useResumeStore = create(
return {
resumes: {
...state.resumes,
- [resumeId]: updatedResume,
+ [resumeId]: updatedResume
},
activeResume:
state.activeResumeId === resumeId
? updatedResume
- : state.activeResume,
+ : state.activeResume
};
});
},
@@ -171,8 +169,8 @@ export const useResumeStore = create(
set((state) => ({
resumes: {
...state.resumes,
- [resume.id]: resume,
- },
+ [resume.id]: resume
+ }
}));
},
@@ -190,7 +188,7 @@ export const useResumeStore = create(
return {
resumes: rest,
activeResumeId: null,
- activeResume: null,
+ activeResume: null
};
});
@@ -213,14 +211,14 @@ export const useResumeStore = create(
},
duplicateResume: (resumeId) => {
- const newId = crypto.randomUUID();
+ const newId = generateUUID();
const originalResume = get().resumes[resumeId];
const duplicatedResume = {
...originalResume,
id: newId,
title: `${originalResume.title} (复制)`,
createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
};
get().updateResume(newId, duplicatedResume);
get().setActiveResume(newId);
@@ -241,16 +239,16 @@ export const useResumeStore = create(
...state.activeResume,
basic: {
...state.activeResume.basic,
- ...data,
- },
+ ...data
+ }
};
const newState = {
resumes: {
...state.resumes,
- [state.activeResume.id]: updatedResume,
+ [state.activeResume.id]: updatedResume
},
- activeResume: updatedResume,
+ activeResume: updatedResume
};
syncResumeToFile(updatedResume, state.activeResume);
@@ -382,13 +380,13 @@ export const useResumeStore = create(
);
const reorderedSections = [
basicInfoSection,
- ...newOrder.filter((section) => section.id !== "basic"),
+ ...newOrder.filter((section) => section.id !== "basic")
].map((section, index) => ({
...section,
- order: index,
+ order: index
}));
get().updateResume(activeResumeId, {
- menuSections: reorderedSections as MenuSection[],
+ menuSections: reorderedSections as MenuSection[]
});
}
},
@@ -428,14 +426,14 @@ export const useResumeStore = create(
...currentResume.customData,
[sectionId]: [
{
- id: crypto.randomUUID(),
+ id: generateUUID(),
title: "未命名模块",
subtitle: "",
dateRange: "",
description: "",
- visible: true,
- },
- ],
+ visible: true
+ }
+ ]
};
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
@@ -447,7 +445,7 @@ export const useResumeStore = create(
const currentResume = get().resumes[activeResumeId];
const updatedCustomData = {
...currentResume.customData,
- [sectionId]: items,
+ [sectionId]: items
};
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
@@ -471,14 +469,14 @@ export const useResumeStore = create(
[sectionId]: [
...(currentResume.customData[sectionId] || []),
{
- id: crypto.randomUUID(),
+ id: generateUUID(),
title: "未命名模块",
subtitle: "",
dateRange: "",
description: "",
- visible: true,
- },
- ],
+ visible: true
+ }
+ ]
};
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
@@ -492,7 +490,7 @@ export const useResumeStore = create(
...currentResume.customData,
[sectionId]: currentResume.customData[sectionId].map((item) =>
item.id === itemId ? { ...item, ...updates } : item
- ),
+ )
};
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
@@ -506,7 +504,7 @@ export const useResumeStore = create(
...currentResume.customData,
[sectionId]: currentResume.customData[sectionId].filter(
(item) => item.id !== itemId
- ),
+ )
};
get().updateResume(activeResumeId, { customData: updatedCustomData });
}
@@ -518,8 +516,8 @@ export const useResumeStore = create(
updateResume(activeResumeId, {
globalSettings: {
...activeResume?.globalSettings,
- ...settings,
- },
+ ...settings
+ }
});
}
},
@@ -530,8 +528,8 @@ export const useResumeStore = create(
updateResume(activeResumeId, {
globalSettings: {
...get().activeResume?.globalSettings,
- themeColor: color,
- },
+ themeColor: color
+ }
});
}
},
@@ -551,37 +549,37 @@ export const useResumeStore = create(
themeColor: template.colorScheme.primary,
sectionSpacing: template.spacing.sectionGap,
paragraphSpacing: template.spacing.itemGap,
- pagePadding: template.spacing.contentPadding,
+ pagePadding: template.spacing.contentPadding
},
basic: {
...resumes[activeResumeId].basic,
- layout: template.basic.layout,
- },
+ layout: template.basic.layout
+ }
};
set({
resumes: {
...resumes,
- [activeResumeId]: updatedResume,
+ [activeResumeId]: updatedResume
},
- activeResume: updatedResume,
+ activeResume: updatedResume
});
},
addResume: (resume: ResumeData) => {
set((state) => ({
resumes: {
...state.resumes,
- [resume.id]: resume,
+ [resume.id]: resume
},
- activeResumeId: resume.id,
+ activeResumeId: resume.id
}));
syncResumeToFile(resume);
return resume.id;
- },
+ }
}),
{
- name: "resume-storage",
+ name: "resume-storage"
}
)
);
diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts
new file mode 100644
index 0000000..a0dc54e
--- /dev/null
+++ b/src/utils/uuid.ts
@@ -0,0 +1,5 @@
+import { v4 as uuidv4 } from "uuid";
+
+export const generateUUID = (): string => {
+ return uuidv4();
+};