Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/app/components/Resume/ResumeControlBar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"use client";
import { useEffect } from "react";
import { useSetDefaultScale } from "components/Resume/hooks";
import { useJSON, useSetDefaultScale } from "components/Resume/hooks";
import {
MagnifyingGlassIcon,
ArrowDownTrayIcon,
} from "@heroicons/react/24/outline";
import { usePDF } from "@react-pdf/renderer";
import dynamic from "next/dynamic";
import { loadStateFromLocalStorage } from "lib/redux/local-storage";

const ResumeControlBar = ({
scale,
Expand All @@ -28,11 +29,18 @@ const ResumeControlBar = ({

const [instance, update] = usePDF({ document });

const { url, update: updateUrl } = useJSON();

// Hook to update pdf when document changes
useEffect(() => {
update();
}, [update, document]);

// Hook to update json download when document changes
useEffect(() => {
updateUrl();
}, [updateUrl, document]);

return (
<div className="sticky bottom-0 left-0 right-0 flex h-[var(--resume-control-bar-height)] items-center justify-center px-[var(--resume-padding)] text-gray-600 lg:justify-between">
<div className="flex items-center gap-2">
Expand All @@ -59,14 +67,24 @@ const ResumeControlBar = ({
<span className="select-none">Autoscale</span>
</label>
</div>
<a
className="ml-1 flex items-center gap-1 rounded-md border border-gray-300 px-3 py-0.5 hover:bg-gray-100 lg:ml-8"
href={instance.url!}
download={fileName}
>
<ArrowDownTrayIcon className="h-4 w-4" />
<span className="whitespace-nowrap">Download Resume</span>
</a>
<div className="flex items-center gap-2">
<a
className="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-0.5 hover:bg-gray-100"
href={url!}
download="my-open-resume.json"
>
<ArrowDownTrayIcon className="h-4 w-4" />
<span className="whitespace-nowrap">Save</span>
</a>
<a
className="flex items-center gap-1 rounded-md border border-gray-300 px-3 py-0.5 hover:bg-gray-100"
href={instance.url!}
download={fileName}
>
<ArrowDownTrayIcon className="h-4 w-4" />
<span className="whitespace-nowrap">Download Resume</span>
</a>
</div>
</div>
);
};
Expand Down
111 changes: 12 additions & 99 deletions src/app/components/Resume/ResumePDF/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { Page, View, Document } from "@react-pdf/renderer";
import { styles, spacing } from "components/Resume/ResumePDF/styles";
import { ResumePDFProfile } from "components/Resume/ResumePDF/ResumePDFProfile";
import { ResumePDFWorkExperience } from "components/Resume/ResumePDF/ResumePDFWorkExperience";
import { ResumePDFEducation } from "components/Resume/ResumePDF/ResumePDFEducation";
import { ResumePDFProject } from "components/Resume/ResumePDF/ResumePDFProject";
import { ResumePDFSkills } from "components/Resume/ResumePDF/ResumePDFSkills";
import { ResumePDFCustom } from "components/Resume/ResumePDF/ResumePDFCustom";
import { DEFAULT_FONT_COLOR } from "lib/redux/settingsSlice";
import type { Settings, ShowForm } from "lib/redux/settingsSlice";
import type { Settings } from "lib/redux/settingsSlice";
import type { Resume } from "lib/redux/types";
import { SuppressResumePDFErrorMessage } from "components/Resume/ResumePDF/common/SuppressResumePDFErrorMessage";
import { SuppressResumePDFErrorMessage } from "components/themes/core/SuppressResumePDFErrorMessage";
import { getTheme } from "components/themes/lib";

/**
* Note: ResumePDF is supposed to be rendered inside PDFViewer. However,
Expand All @@ -35,102 +28,22 @@ export const ResumePDF = ({
settings: Settings;
isPDF?: boolean;
}) => {
const { profile, workExperiences, educations, projects, skills, custom } =
resume;
const { name } = profile;
const {
fontFamily,
fontSize,
documentSize,
formToHeading,
formToShow,
formsOrder,
showBulletPoints,
} = settings;
const { theme, formsOrder, formToShow } = settings;
const themeColor = settings.themeColor || DEFAULT_FONT_COLOR;

const showFormsOrder = formsOrder.filter((form) => formToShow[form]);

const formTypeToComponent: { [type in ShowForm]: () => JSX.Element } = {
workExperiences: () => (
<ResumePDFWorkExperience
heading={formToHeading["workExperiences"]}
workExperiences={workExperiences}
themeColor={themeColor}
/>
),
educations: () => (
<ResumePDFEducation
heading={formToHeading["educations"]}
educations={educations}
themeColor={themeColor}
showBulletPoints={showBulletPoints["educations"]}
/>
),
projects: () => (
<ResumePDFProject
heading={formToHeading["projects"]}
projects={projects}
themeColor={themeColor}
/>
),
skills: () => (
<ResumePDFSkills
heading={formToHeading["skills"]}
skills={skills}
themeColor={themeColor}
showBulletPoints={showBulletPoints["skills"]}
/>
),
custom: () => (
<ResumePDFCustom
heading={formToHeading["custom"]}
custom={custom}
themeColor={themeColor}
showBulletPoints={showBulletPoints["custom"]}
/>
),
};
const ThemeComponent = getTheme(theme);

return (
<>
<Document title={`${name} Resume`} author={name} producer={"OpenResume"}>
<Page
size={documentSize === "A4" ? "A4" : "LETTER"}
style={{
...styles.flexCol,
color: DEFAULT_FONT_COLOR,
fontFamily,
fontSize: fontSize + "pt",
}}
>
{Boolean(settings.themeColor) && (
<View
style={{
width: spacing["full"],
height: spacing[3.5],
backgroundColor: themeColor,
}}
/>
)}
<View
style={{
...styles.flexCol,
padding: `${spacing[0]} ${spacing[20]}`,
}}
>
<ResumePDFProfile
profile={profile}
themeColor={themeColor}
isPDF={isPDF}
/>
{showFormsOrder.map((form) => {
const Component = formTypeToComponent[form];
return <Component key={form} />;
})}
</View>
</Page>
</Document>
<ThemeComponent
resume={resume}
settings={settings}
themeColor={themeColor}
showFormsOrder={showFormsOrder}
isPDF={isPDF}
/>
<SuppressResumePDFErrorMessage />
</>
);
Expand Down
32 changes: 31 additions & 1 deletion src/app/components/Resume/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { A4_HEIGHT_PX, LETTER_HEIGHT_PX } from "lib/constants";
import { getPxPerRem } from "lib/get-px-per-rem";
import { CSS_VARIABLES } from "globals-css";
import { loadStateFromLocalStorage } from "lib/redux/local-storage";

/**
* useSetDefaultScale sets the default scale of the resume on load.
Expand Down Expand Up @@ -60,3 +61,32 @@ export const useSetDefaultScale = ({

return { scaleOnResize, setScaleOnResize };
};

/**
* useJSON is a hook to retrieve the document data from local storage
* as a URL.
*
* It retrieves the document data from local storage and creates a URL to
* download the document.
*/
export const useJSON = () => {
const [url, setUrl] = useState<string | null>(null);

const update = useCallback(() => {
const state = loadStateFromLocalStorage();
if (state) {
const newBlob = new Blob([JSON.stringify(state)], {
type: "application/pdf",
});
setUrl((prevUrl) => {
// Revoke the previous URL to avoid memory leaks
if (prevUrl) {
URL.revokeObjectURL(prevUrl);
}
return URL.createObjectURL(newBlob);
});
}
}, []);

return { url, update };
};
35 changes: 23 additions & 12 deletions src/app/components/ResumeDropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const defaultFileState = {
name: "",
size: 0,
fileUrl: "",
data: null as File | null,
};

export const ResumeDropzone = ({
Expand All @@ -30,7 +31,7 @@ export const ResumeDropzone = ({
}) => {
const [file, setFile] = useState(defaultFileState);
const [isHoveredOnDropzone, setIsHoveredOnDropzone] = useState(false);
const [hasNonPdfFile, setHasNonPdfFile] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const router = useRouter();

const hasFile = Boolean(file.name);
Expand All @@ -42,18 +43,18 @@ export const ResumeDropzone = ({

const { name, size } = newFile;
const fileUrl = URL.createObjectURL(newFile);
setFile({ name, size, fileUrl });
setFile({ name, size, fileUrl, data: newFile });
onFileUrlChange(fileUrl);
};

const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const newFile = event.dataTransfer.files[0];
if (newFile.name.endsWith(".pdf")) {
setHasNonPdfFile(false);
if (newFile.name.endsWith(".pdf") || newFile.name.endsWith(".json")) {
setErrorMsg("");
setNewFile(newFile);
} else {
setHasNonPdfFile(true);
setErrorMsg("Only pdf/json file is supported");
}
setIsHoveredOnDropzone(false);
};
Expand All @@ -72,8 +73,20 @@ export const ResumeDropzone = ({
};

const onImportClick = async () => {
const resume = await parseResumeFromPdf(file.fileUrl);
const settings = deepClone(initialSettings);
let resume, settings;
if (file.name.endsWith(".json")) {
const data = JSON.parse(await file.data!.text());
if (!data.resume || !data.settings) {
setErrorMsg("Invalid json file");
return;
}
resume = data.resume;
settings = data.settings;
} else {
resume = await parseResumeFromPdf(file.fileUrl);
settings = deepClone(initialSettings);
}


// Set formToShow settings based on uploaded resume if users have used the app before
if (getHasUsedAppBefore()) {
Expand Down Expand Up @@ -132,7 +145,7 @@ export const ResumeDropzone = ({
!playgroundView && "text-lg font-semibold"
)}
>
Browse a pdf file or drop it here
Browse a pdf/json file or drop it here
</p>
<p className="flex text-sm text-gray-500">
<LockClosedIcon className="mr-1 mt-1 h-3 w-3 text-gray-400" />
Expand Down Expand Up @@ -167,13 +180,11 @@ export const ResumeDropzone = ({
<input
type="file"
className="sr-only"
accept=".pdf"
accept=".pdf,.json"
onChange={onInputChange}
/>
</label>
{hasNonPdfFile && (
<p className="mt-6 text-red-400">Only pdf file is supported</p>
)}
{errorMsg && <p className="mt-6 text-red-400">{errorMsg}</p>}
</>
) : (
<>
Expand Down
32 changes: 32 additions & 0 deletions src/app/components/ResumeForm/ThemeForm/Selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "components/fonts/constants";
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
import dynamic from "next/dynamic";
import { getAllThemesAvailable } from "components/themes/lib";

const Selection = ({
selectedColor,
Expand Down Expand Up @@ -161,3 +162,34 @@ export const DocumentSizeSelections = ({
</SelectionsWrapper>
);
};

export const ThemeSelections = ({
selectedTheme,
themeColor,
handleSettingsChange,
}: {
themeColor: string;
selectedTheme: string;
handleSettingsChange: (field: GeneralSetting, value: string) => void;
}) => {
const allThemes = getAllThemesAvailable();
return (
<SelectionsWrapper>
{allThemes.map((type, idx) => {
return (
<Selection
key={idx}
selectedColor={themeColor}
isSelected={type === selectedTheme}
onClick={() => handleSettingsChange("theme", type)}
style={{
textTransform: "capitalize"
}}
>
{type}
</Selection>
);
})}
</SelectionsWrapper>
);
};
Loading