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(); +};