mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
perf: preview section
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlobalSettings, CustomItem } from "@/types/resume";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import { GlobalSettings, CustomItem } from "@/types/resume";
|
||||
|
||||
interface CustomSectionProps {
|
||||
sectionId: string;
|
||||
@@ -31,58 +31,61 @@ const CustomSection = ({
|
||||
type="custom"
|
||||
globalSettings={globalSettings}
|
||||
/>
|
||||
{visibleItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<h4
|
||||
className="font-semibold text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`,
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</h4>
|
||||
<motion.div
|
||||
layout
|
||||
className={"font-medium text-baseFont"}
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleItems.map((item) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
layout="position"
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
<h4
|
||||
className="font-semibold text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`,
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</h4>
|
||||
<motion.div
|
||||
layout
|
||||
className={"font-medium text-baseFont"}
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
{item.dateRange && (
|
||||
<span
|
||||
className="text-baseFont shrink-0 ml-4"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</motion.div>
|
||||
</div>
|
||||
{item.dateRange}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.dateRange && (
|
||||
<span
|
||||
className="text-baseFont shrink-0 ml-4"
|
||||
{item.description && (
|
||||
<div
|
||||
className="text-baseFont mt-1"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
>
|
||||
{item.dateRange}
|
||||
</span>
|
||||
dangerouslySetInnerHTML={{ __html: item.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div
|
||||
className="text-baseFont mt-1"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: item.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Education, GlobalSettings } from "@/types/resume";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
interface EducationSectionProps {
|
||||
@@ -14,7 +14,6 @@ const EducationSection = ({
|
||||
const visibleEducation = education?.filter((edu) => edu.visible);
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
style={{
|
||||
marginTop: `${globalSettings?.sectionSpacing || 24}px`,
|
||||
}}
|
||||
@@ -23,68 +22,70 @@ const EducationSection = ({
|
||||
type="education"
|
||||
globalSettings={globalSettings}
|
||||
></SectionTitle>
|
||||
|
||||
{visibleEducation?.map((edu) => (
|
||||
<div
|
||||
key={edu.id}
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4
|
||||
className="font-semibold text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`,
|
||||
}}
|
||||
>
|
||||
{edu.school}
|
||||
</h4>
|
||||
{edu.location && (
|
||||
<span
|
||||
className="text-baseFont"
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleEducation?.map((edu) => (
|
||||
<motion.div
|
||||
layout="position"
|
||||
key={edu.id}
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4
|
||||
className="font-semibold text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`,
|
||||
}}
|
||||
>
|
||||
· {edu.location}
|
||||
</span>
|
||||
)}
|
||||
{edu.school}
|
||||
</h4>
|
||||
{edu.location && (
|
||||
<span
|
||||
className="text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
· {edu.location}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className="font-medium text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
{[edu.major, edu.degree].filter(Boolean).join(" · ")}
|
||||
{edu.gpa && ` · GPA ${edu.gpa}`}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="font-medium text-baseFont"
|
||||
<span
|
||||
className="text-baseFont shrink-0 ml-4"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{[edu.major, edu.degree].filter(Boolean).join(" · ")}
|
||||
{edu.gpa && ` · GPA ${edu.gpa}`}
|
||||
</p>
|
||||
{`${new Date(edu.startDate).toLocaleDateString()} - ${new Date(edu.endDate).toLocaleDateString()}`}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-baseFont shrink-0 ml-4"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{`${new Date(edu.startDate).toLocaleDateString()} - ${new Date(edu.endDate).toLocaleDateString()}`}
|
||||
</span>
|
||||
</div>
|
||||
{edu.description && (
|
||||
<div
|
||||
className="text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: edu.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{edu.description && (
|
||||
<div
|
||||
className="text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: edu.description }}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
import { motion } from "framer-motion";
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Experience, GlobalSettings } from "@/types/resume";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
|
||||
interface ExperienceSectionProps {
|
||||
experience: Experience[];
|
||||
globalSettings: GlobalSettings | undefined;
|
||||
experiences?: Experience[];
|
||||
globalSettings?: GlobalSettings;
|
||||
}
|
||||
|
||||
const ExperienceSection = ({
|
||||
experience,
|
||||
interface ExperienceItemProps {
|
||||
experience: Experience;
|
||||
globalSettings?: GlobalSettings;
|
||||
}
|
||||
|
||||
const ExperienceItem: React.FC<ExperienceItemProps> = React.forwardRef(
|
||||
({ experience, globalSettings }, ref) => {
|
||||
return (
|
||||
<motion.div
|
||||
style={{ marginTop: `${globalSettings?.paragraphSpacing}px` }}
|
||||
layout
|
||||
>
|
||||
<motion.div
|
||||
layout="position"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="font-medium text-baseFont">{experience.company}</div>
|
||||
{experience.date && <div>{experience.date}</div>}
|
||||
</motion.div>
|
||||
{experience.position && (
|
||||
<motion.div layout="position" className="font-medium text-baseFont">
|
||||
{experience.position}
|
||||
</motion.div>
|
||||
)}
|
||||
{experience.details && (
|
||||
<motion.div
|
||||
layout="position"
|
||||
dangerouslySetInnerHTML={{ __html: experience.details }}
|
||||
></motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const ExperienceSection: React.FC<ExperienceSectionProps> = ({
|
||||
experiences,
|
||||
globalSettings,
|
||||
}: ExperienceSectionProps) => {
|
||||
}) => {
|
||||
if (!experiences?.length) return null;
|
||||
|
||||
const visibleExperiences = experiences.filter(
|
||||
(experience) => experience.visible
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
@@ -19,54 +62,17 @@ const ExperienceSection = ({
|
||||
}}
|
||||
>
|
||||
<SectionTitle type="experience" globalSettings={globalSettings} />
|
||||
{experience?.map(
|
||||
(exp) =>
|
||||
exp.visible && (
|
||||
<div
|
||||
key={exp.id}
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4
|
||||
className="font-semibold text-gray-800"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.subheaderSize || 16}px`,
|
||||
}}
|
||||
>
|
||||
{exp.company}
|
||||
</h4>
|
||||
<p
|
||||
className="font-medium text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
{exp.position}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className="text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
{exp.date}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="text-baseFont"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: exp.details }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleExperiences.map((experience) => (
|
||||
<ExperienceItem
|
||||
key={experience.id}
|
||||
experience={experience}
|
||||
globalSettings={globalSettings}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import { Project, GlobalSettings } from "@/types/resume";
|
||||
|
||||
@@ -14,6 +16,8 @@ const ProjectSection: React.FC<ProjectSectionProps> = ({
|
||||
}) => {
|
||||
if (!projects?.length) return null;
|
||||
|
||||
const visibleProjects = projects.filter((project) => project.visible);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
@@ -25,14 +29,16 @@ const ProjectSection: React.FC<ProjectSectionProps> = ({
|
||||
type="projects"
|
||||
globalSettings={globalSettings}
|
||||
></SectionTitle>
|
||||
<div className="space-y-4">
|
||||
{projects.map((project) => (
|
||||
<ProjectItem
|
||||
key={project.id}
|
||||
project={project}
|
||||
globalSettings={globalSettings}
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleProjects.map((project) => (
|
||||
<ProjectItem
|
||||
key={project.id}
|
||||
project={project}
|
||||
globalSettings={globalSettings}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -43,33 +49,40 @@ interface ProjectItemProps {
|
||||
globalSettings?: GlobalSettings;
|
||||
}
|
||||
|
||||
const ProjectItem: React.FC<ProjectItemProps> = ({
|
||||
project,
|
||||
globalSettings,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">{project.name}</h3>
|
||||
<div>{project.date}</div>
|
||||
</div>
|
||||
{project.role && (
|
||||
<div className="font-medium text-baseFont">{project.role}</div>
|
||||
)}
|
||||
{project.description && (
|
||||
<div
|
||||
className=" mt-2 whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: project.description }}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ProjectItem: React.FC<ProjectItemProps> = React.forwardRef(
|
||||
({ project, globalSettings }, ref) => {
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
style={{
|
||||
marginTop: `${globalSettings?.paragraphSpacing}px`,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
layout="position"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<h3 className="font-medium">{project.name}</h3>
|
||||
{project.date && <div>{project.date}</div>}
|
||||
</motion.div>
|
||||
{project.role && (
|
||||
<motion.div layout="position" className="font-medium text-baseFont">
|
||||
{project.role}
|
||||
</motion.div>
|
||||
)}
|
||||
{project.description && (
|
||||
<motion.div
|
||||
layout="position"
|
||||
style={{
|
||||
fontSize: `${globalSettings?.baseFontSize || 14}px`,
|
||||
lineHeight: globalSettings?.lineHeight || 1.6,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: project.description }}
|
||||
></motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ProjectSection;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import { GlobalSettings } from "@/types/resume";
|
||||
import { useResumeStore } from "@/store/useResumeStore";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlobalSettings } from "@/types/resume";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import { GlobalSettings } from "@/types/resume";
|
||||
|
||||
interface SkillSectionProps {
|
||||
skill: string | undefined;
|
||||
|
||||
@@ -36,7 +36,7 @@ const ClassicTemplate: React.FC<ClassicTemplateProps> = ({
|
||||
case "experience":
|
||||
return (
|
||||
<ExperienceSection
|
||||
experience={data.experience}
|
||||
experiences={data.experience}
|
||||
globalSettings={data.globalSettings}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ const LeftRightTemplate: React.FC<LeftRightTemplateProps> = ({
|
||||
case "experience":
|
||||
return (
|
||||
<ExperienceSection
|
||||
experience={data.experience}
|
||||
experiences={data.experience}
|
||||
globalSettings={data.globalSettings}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ const ModernTemplate: React.FC<ModernTemplateProps> = ({ data, template }) => {
|
||||
case "experience":
|
||||
return (
|
||||
<ExperienceSection
|
||||
experience={data.experience}
|
||||
experiences={data.experience}
|
||||
globalSettings={data.globalSettings}
|
||||
/>
|
||||
);
|
||||
@@ -92,7 +92,7 @@ const ModernTemplate: React.FC<ModernTemplateProps> = ({ data, template }) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="col-span-2 p-4 space-y-6"
|
||||
className="col-span-2 p-4"
|
||||
style={{
|
||||
backgroundColor: colorScheme.background,
|
||||
color: colorScheme.text,
|
||||
|
||||
Reference in New Issue
Block a user