mirror of
https://github.com/xitanggg/open-resume
synced 2024-11-03 09:19:21 +01:00
Add Noto Sans SC language and make it load conditionally
This commit is contained in:
parent
521caf98d0
commit
962f6d4f55
@ -1 +1 @@
|
|||||||
public/fonts/fonts.css
|
public/fonts/*.css
|
Binary file not shown.
Binary file not shown.
BIN
public/fonts/NotoSansSC-Bold.ttf
Normal file
BIN
public/fonts/NotoSansSC-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/NotoSansSC-Regular.ttf
Normal file
BIN
public/fonts/NotoSansSC-Regular.ttf
Normal file
Binary file not shown.
2
public/fonts/fonts-zh.css
Normal file
2
public/fonts/fonts-zh.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@font-face {font-family: "NotoSansSC"; src: url("/fonts/NotoSansSC-Regular.ttf");}
|
||||||
|
@font-face {font-family: "NotoSansSC"; src: url("/fonts/NotoSansSC-Bold.ttf"); font-weight: bold;}
|
@ -1,4 +1,5 @@
|
|||||||
/* Adding a new font family needs to keep "public\fonts\fonts.ts" in sync */
|
/* Adding a new English font family needs to keep "public\fonts\fonts.ts" in sync */
|
||||||
|
/* Sans Serif Fonts */
|
||||||
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Regular.ttf");}
|
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Regular.ttf");}
|
||||||
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Bold.ttf"); font-weight: bold;}
|
@font-face {font-family: "Roboto"; src: url("/fonts/Roboto-Bold.ttf"); font-weight: bold;}
|
||||||
@font-face {font-family: "Lato"; src: url("/fonts/Lato-Regular.ttf");}
|
@font-face {font-family: "Lato"; src: url("/fonts/Lato-Regular.ttf");}
|
||||||
@ -10,13 +11,14 @@
|
|||||||
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Regular.ttf");}
|
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Regular.ttf");}
|
||||||
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Bold.ttf"); font-weight: bold;}
|
@font-face {font-family: "Raleway"; src: url("/fonts/Raleway-Bold.ttf"); font-weight: bold;}
|
||||||
|
|
||||||
|
/* Serif Fonts */
|
||||||
|
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Regular.ttf");}
|
||||||
|
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Bold.ttf"); font-weight: bold;}
|
||||||
|
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Regular.ttf");}
|
||||||
|
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Bold.ttf"); font-weight: bold;}
|
||||||
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Regular.ttf");}
|
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Regular.ttf");}
|
||||||
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Bold.ttf"); font-weight: bold;}
|
@font-face {font-family: "RobotoSlab"; src: url("/fonts/RobotoSlab-Bold.ttf"); font-weight: bold;}
|
||||||
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Regular.ttf");}
|
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Regular.ttf");}
|
||||||
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Bold.ttf"); font-weight: bold;}
|
@font-face {font-family: "PlayfairDisplay"; src: url("/fonts/PlayfairDisplay-Bold.ttf"); font-weight: bold;}
|
||||||
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Regular.ttf");}
|
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Regular.ttf");}
|
||||||
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Bold.ttf"); font-weight: bold;}
|
@font-face {font-family: "Merriweather"; src: url("/fonts/Merriweather-Bold.ttf"); font-weight: bold;}
|
||||||
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Regular.ttf");}
|
|
||||||
@font-face {font-family: "Lora"; src: url("/fonts/Lora-Bold.ttf"); font-weight: bold;}
|
|
||||||
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Regular.ttf");}
|
|
||||||
@font-face {font-family: "Caladea"; src: url("/fonts/Caladea-Bold.ttf"); font-weight: bold;}
|
|
@ -1,50 +0,0 @@
|
|||||||
/**
|
|
||||||
* Adding a new font family needs to keep "public\fonts\fonts.css" in sync
|
|
||||||
*
|
|
||||||
* The followings are handled automatically once the font family name is added to the variable arrays
|
|
||||||
* 1. It would register font family for React PDF at "components\Resume\ResumePDF\styles.ts"
|
|
||||||
* 2. It would load font family for React PDF Iframe at "components\Resume\ResumeIFrame.tsx"
|
|
||||||
* 3. It would add font family selection to Resume Settings at "components\ResumeForm\ThemeForm.tsx"
|
|
||||||
*
|
|
||||||
* One caveat while adding a new google font is that sometimes the font doesn't work with React PDF:
|
|
||||||
* https://github.com/diegomura/react-pdf/issues/915. The solution is to re-export the font with fontforge.
|
|
||||||
* ./fontforge -lang=ff -c 'Open($1); Generate($2); Close();' old_font.ttf new_font.ttf
|
|
||||||
* (Note that some fonts might still not work after export.)
|
|
||||||
*/
|
|
||||||
export const SANS_SERI_FONT_FAMILIES = [
|
|
||||||
"FangZheng",
|
|
||||||
"Roboto",
|
|
||||||
"Lato",
|
|
||||||
"Montserrat",
|
|
||||||
"OpenSans",
|
|
||||||
"Raleway",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const SERI_FONT_FAMILIES = [
|
|
||||||
"Caladea",
|
|
||||||
"Lora",
|
|
||||||
"RobotoSlab",
|
|
||||||
"PlayfairDisplay",
|
|
||||||
"Merriweather",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const FONT_FAMILIES = [
|
|
||||||
...SANS_SERI_FONT_FAMILIES,
|
|
||||||
...SERI_FONT_FAMILIES,
|
|
||||||
];
|
|
||||||
|
|
||||||
export type FontFamily = (typeof FONT_FAMILIES)[number];
|
|
||||||
|
|
||||||
export const FONT_FAMILY_TO_STANDARD_SIZE_IN_PT: Record<FontFamily, number> = {
|
|
||||||
FangZheng: 11,
|
|
||||||
Roboto: 11,
|
|
||||||
Lato: 11,
|
|
||||||
Montserrat: 10,
|
|
||||||
OpenSans: 10,
|
|
||||||
Raleway: 10,
|
|
||||||
Caladea: 11,
|
|
||||||
Lora: 11,
|
|
||||||
RobotoSlab: 10,
|
|
||||||
PlayfairDisplay: 10,
|
|
||||||
Merriweather: 10,
|
|
||||||
};
|
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { usePDF } from "@react-pdf/renderer";
|
import { usePDF } from "@react-pdf/renderer";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const ResumeControlBarComponent = ({
|
const ResumeControlBar = ({
|
||||||
scale,
|
scale,
|
||||||
setScale,
|
setScale,
|
||||||
documentSize,
|
documentSize,
|
||||||
@ -71,9 +71,11 @@ const ResumeControlBarComponent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make ResumeControlBar dynamic to make nextjs happy
|
/**
|
||||||
export const ResumeControlBar = dynamic(
|
* Load ResumeControlBar client side since it uses usePDF, which is a web specific API
|
||||||
() => Promise.resolve(ResumeControlBarComponent),
|
*/
|
||||||
|
export const ResumeControlBarCSR = dynamic(
|
||||||
|
() => Promise.resolve(ResumeControlBar),
|
||||||
{
|
{
|
||||||
ssr: false,
|
ssr: false,
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,58 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { FONT_FAMILIES } from "public/fonts/fonts";
|
import { useMemo } from "react";
|
||||||
import Frame from "react-frame-component";
|
import Frame from "react-frame-component";
|
||||||
import {
|
import {
|
||||||
A4_HEIGHT_PX,
|
A4_HEIGHT_PX,
|
||||||
A4_WIDTH_PX,
|
A4_WIDTH_PX,
|
||||||
|
A4_WIDTH_PT,
|
||||||
LETTER_HEIGHT_PX,
|
LETTER_HEIGHT_PX,
|
||||||
LETTER_WIDTH_PT,
|
|
||||||
LETTER_WIDTH_PX,
|
LETTER_WIDTH_PX,
|
||||||
|
LETTER_WIDTH_PT,
|
||||||
} from "lib/constants";
|
} from "lib/constants";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||||
|
|
||||||
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS = FONT_FAMILIES.map(
|
const getIframeInitialContent = (isA4: boolean) => {
|
||||||
(
|
const width = isA4 ? A4_WIDTH_PT : LETTER_WIDTH_PT;
|
||||||
font
|
const allFontFamilies = getAllFontFamiliesToLoad();
|
||||||
) => `<link rel="preload" as="font" href="/fonts/${font}-Regular.ttf" type="font/ttf" crossorigin="anonymous">
|
|
||||||
<link rel="preload" as="font" href="/fonts/${font}-Bold.ttf" type="font/ttf" crossorigin="anonymous">`
|
|
||||||
).join("");
|
|
||||||
|
|
||||||
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE = FONT_FAMILIES.map(
|
const allFontFamiliesPreloadLinks = allFontFamilies
|
||||||
(
|
.map(
|
||||||
font
|
(
|
||||||
) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");}
|
font
|
||||||
@font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}`
|
) => `<link rel="preload" as="font" href="/fonts/${font}-Regular.ttf" type="font/ttf" crossorigin="anonymous">
|
||||||
).join("");
|
<link rel="preload" as="font" href="/fonts/${font}-Bold.ttf" type="font/ttf" crossorigin="anonymous">`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
const IFRAME_INITIAL_CONTENT = `<!DOCTYPE html>
|
const allFontFamiliesFontFaces = allFontFamilies
|
||||||
<html>
|
.map(
|
||||||
<head>
|
(
|
||||||
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS}
|
font
|
||||||
<style>
|
) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");}
|
||||||
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE}
|
@font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}`
|
||||||
</style>
|
)
|
||||||
</head>
|
.join("");
|
||||||
<body style='overflow: hidden; width: ${LETTER_WIDTH_PT}pt; margin: 0; padding: 0; -webkit-text-size-adjust:none;'>
|
|
||||||
<div></div>
|
return `<!DOCTYPE html>
|
||||||
</body>
|
<html>
|
||||||
</html>`;
|
<head>
|
||||||
|
${allFontFamiliesPreloadLinks}
|
||||||
|
<style>
|
||||||
|
${allFontFamiliesFontFaces}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style='overflow: hidden; width: ${width}pt; margin: 0; padding: 0; -webkit-text-size-adjust:none;'>
|
||||||
|
<div></div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IFrame is used here for style isolation, since react pdf uses pt unit.
|
* Iframe is used here for style isolation, since react pdf uses pt unit.
|
||||||
* It creates a sandbox document body that uses letter/A4 size as width.
|
* It creates a sandbox document body that uses letter/A4 pt size as width.
|
||||||
*/
|
*/
|
||||||
const ResumeIFrameComponent = ({
|
const ResumeIframe = ({
|
||||||
documentSize,
|
documentSize,
|
||||||
scale,
|
scale,
|
||||||
children,
|
children,
|
||||||
@ -52,6 +63,12 @@ const ResumeIFrameComponent = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
enablePDFViewer?: boolean;
|
enablePDFViewer?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isA4 = documentSize === "A4";
|
||||||
|
const iframeInitialContent = useMemo(
|
||||||
|
() => getIframeInitialContent(isA4),
|
||||||
|
[isA4]
|
||||||
|
);
|
||||||
|
|
||||||
if (enablePDFViewer) {
|
if (enablePDFViewer) {
|
||||||
return (
|
return (
|
||||||
<DynamicPDFViewer className="h-full w-full">
|
<DynamicPDFViewer className="h-full w-full">
|
||||||
@ -59,8 +76,8 @@ const ResumeIFrameComponent = ({
|
|||||||
</DynamicPDFViewer>
|
</DynamicPDFViewer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const width = documentSize === "A4" ? A4_WIDTH_PX : LETTER_WIDTH_PX;
|
const width = isA4 ? A4_WIDTH_PX : LETTER_WIDTH_PX;
|
||||||
const height = documentSize === "A4" ? A4_HEIGHT_PX : LETTER_HEIGHT_PX;
|
const height = isA4 ? A4_HEIGHT_PX : LETTER_HEIGHT_PX;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -82,7 +99,9 @@ const ResumeIFrameComponent = ({
|
|||||||
>
|
>
|
||||||
<Frame
|
<Frame
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
initialContent={IFRAME_INITIAL_CONTENT}
|
initialContent={iframeInitialContent}
|
||||||
|
// key is used to force component to re-mount when document size changes
|
||||||
|
key={isA4 ? "A4" : "LETTER"}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Frame>
|
</Frame>
|
||||||
@ -91,13 +110,12 @@ const ResumeIFrameComponent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iframe can't be server side rendered, so we use dynamic import to load it only on client side
|
/**
|
||||||
export const ResumeIFrame = dynamic(
|
* Load iframe client side since iframe can't be SSR
|
||||||
() => Promise.resolve(ResumeIFrameComponent),
|
*/
|
||||||
{
|
export const ResumeIframeCSR = dynamic(() => Promise.resolve(ResumeIframe), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// PDFViewer is only used for debugging. Its size is quite large, so we make it dynamic import
|
// PDFViewer is only used for debugging. Its size is quite large, so we make it dynamic import
|
||||||
const DynamicPDFViewer = dynamic(
|
const DynamicPDFViewer = dynamic(
|
||||||
|
@ -1,23 +1,4 @@
|
|||||||
import { StyleSheet, Font } from "@react-pdf/renderer";
|
import { StyleSheet } from "@react-pdf/renderer";
|
||||||
import { FONT_FAMILIES } from "public/fonts/fonts";
|
|
||||||
|
|
||||||
FONT_FAMILIES.forEach((fontFamily) => {
|
|
||||||
Font.register({
|
|
||||||
family: fontFamily,
|
|
||||||
fonts: [
|
|
||||||
{
|
|
||||||
src: `fonts/${fontFamily}-Regular.ttf`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: `fonts/${fontFamily}-Bold.ttf`,
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable hyphenation https://github.com/diegomura/react-pdf/issues/311#issuecomment-548301604
|
|
||||||
Font.registerHyphenationCallback((word) => [word]);
|
|
||||||
|
|
||||||
// Tailwindcss Spacing Design System: https://tailwindcss.com/docs/theme#spacing
|
// Tailwindcss Spacing Design System: https://tailwindcss.com/docs/theme#spacing
|
||||||
// It is converted from rem to pt (1rem = 12pt) since https://react-pdf.org/styling only accepts pt unit
|
// It is converted from rem to pt (1rem = 12pt) since https://react-pdf.org/styling only accepts pt unit
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { ResumeIFrame } from "components/Resume/ResumeIFrame";
|
import { ResumeIframeCSR } from "components/Resume/ResumeIframe";
|
||||||
import { ResumePDF } from "components/Resume/ResumePDF";
|
import { ResumePDF } from "components/Resume/ResumePDF";
|
||||||
import {
|
import {
|
||||||
ResumeControlBar,
|
ResumeControlBarCSR,
|
||||||
ResumeControlBarBorder,
|
ResumeControlBarBorder,
|
||||||
} from "components/Resume/ResumeControlBar";
|
} from "components/Resume/ResumeControlBar";
|
||||||
import { FlexboxSpacer } from "components/FlexboxSpacer";
|
import { FlexboxSpacer } from "components/FlexboxSpacer";
|
||||||
@ -11,8 +11,13 @@ import { useAppSelector } from "lib/redux/hooks";
|
|||||||
import { selectResume } from "lib/redux/resumeSlice";
|
import { selectResume } from "lib/redux/resumeSlice";
|
||||||
import { selectSettings } from "lib/redux/settingsSlice";
|
import { selectSettings } from "lib/redux/settingsSlice";
|
||||||
import { DEBUG_RESUME_PDF_FLAG } from "lib/constants";
|
import { DEBUG_RESUME_PDF_FLAG } from "lib/constants";
|
||||||
|
import {
|
||||||
|
useRegisterReactPDFFont,
|
||||||
|
useRegisterReactPDFHyphenationCallback,
|
||||||
|
} from "components/fonts/hooks";
|
||||||
|
import { NonEnglishFontsCSSLoader } from "components/fonts/NonEnglishFontsCSSLoader";
|
||||||
|
|
||||||
const Resume = () => {
|
export const Resume = () => {
|
||||||
const [scale, setScale] = useState(0.8);
|
const [scale, setScale] = useState(0.8);
|
||||||
const resume = useAppSelector(selectResume);
|
const resume = useAppSelector(selectResume);
|
||||||
const settings = useAppSelector(selectSettings);
|
const settings = useAppSelector(selectSettings);
|
||||||
@ -21,34 +26,38 @@ const Resume = () => {
|
|||||||
[resume, settings]
|
[resume, settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useRegisterReactPDFFont();
|
||||||
|
useRegisterReactPDFHyphenationCallback(settings.fontFamily);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex justify-center md:justify-start">
|
<>
|
||||||
<FlexboxSpacer maxWidth={50} className="hidden md:block" />
|
<NonEnglishFontsCSSLoader />
|
||||||
<div className="relative">
|
<div className="relative flex justify-center md:justify-start">
|
||||||
<section className="h-[calc(100vh-var(--top-nav-bar-height)-var(--resume-control-bar-height))] overflow-hidden md:p-[var(--resume-padding)]">
|
<FlexboxSpacer maxWidth={50} className="hidden md:block" />
|
||||||
<ResumeIFrame
|
<div className="relative">
|
||||||
documentSize={settings.documentSize}
|
<section className="h-[calc(100vh-var(--top-nav-bar-height)-var(--resume-control-bar-height))] overflow-hidden md:p-[var(--resume-padding)]">
|
||||||
|
<ResumeIframeCSR
|
||||||
|
documentSize={settings.documentSize}
|
||||||
|
scale={scale}
|
||||||
|
enablePDFViewer={DEBUG_RESUME_PDF_FLAG}
|
||||||
|
>
|
||||||
|
<ResumePDF
|
||||||
|
resume={resume}
|
||||||
|
settings={settings}
|
||||||
|
isPDF={DEBUG_RESUME_PDF_FLAG}
|
||||||
|
/>
|
||||||
|
</ResumeIframeCSR>
|
||||||
|
</section>
|
||||||
|
<ResumeControlBarCSR
|
||||||
scale={scale}
|
scale={scale}
|
||||||
enablePDFViewer={DEBUG_RESUME_PDF_FLAG}
|
setScale={setScale}
|
||||||
>
|
documentSize={settings.documentSize}
|
||||||
<ResumePDF
|
document={document}
|
||||||
resume={resume}
|
fileName={resume.profile.name + " - Resume"}
|
||||||
settings={settings}
|
/>
|
||||||
isPDF={DEBUG_RESUME_PDF_FLAG}
|
</div>
|
||||||
/>
|
<ResumeControlBarBorder />
|
||||||
</ResumeIFrame>
|
|
||||||
</section>
|
|
||||||
<ResumeControlBar
|
|
||||||
scale={scale}
|
|
||||||
setScale={setScale}
|
|
||||||
documentSize={settings.documentSize}
|
|
||||||
document={document}
|
|
||||||
fileName={resume.profile.name + " - Resume"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ResumeControlBarBorder />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Resume;
|
|
||||||
|
@ -2,9 +2,11 @@ import type { GeneralSetting } from "lib/redux/settingsSlice";
|
|||||||
import { PX_PER_PT } from "lib/constants";
|
import { PX_PER_PT } from "lib/constants";
|
||||||
import {
|
import {
|
||||||
FONT_FAMILY_TO_STANDARD_SIZE_IN_PT,
|
FONT_FAMILY_TO_STANDARD_SIZE_IN_PT,
|
||||||
FONT_FAMILIES,
|
FONT_FAMILY_TO_DISPLAY_NAME,
|
||||||
type FontFamily,
|
type FontFamily,
|
||||||
} from "public/fonts/fonts";
|
} from "components/fonts/constants";
|
||||||
|
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const Selection = ({
|
const Selection = ({
|
||||||
selectedColor,
|
selectedColor,
|
||||||
@ -45,7 +47,7 @@ const SelectionsWrapper = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return <div className="mt-2 flex flex-wrap gap-3">{children}</div>;
|
return <div className="mt-2 flex flex-wrap gap-3">{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FontFamilySelections = ({
|
const FontFamilySelections = ({
|
||||||
selectedFontFamily,
|
selectedFontFamily,
|
||||||
themeColor,
|
themeColor,
|
||||||
handleSettingsChange,
|
handleSettingsChange,
|
||||||
@ -54,9 +56,10 @@ export const FontFamilySelections = ({
|
|||||||
themeColor: string;
|
themeColor: string;
|
||||||
handleSettingsChange: (field: GeneralSetting, value: string) => void;
|
handleSettingsChange: (field: GeneralSetting, value: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const allFontFamilies = getAllFontFamiliesToLoad();
|
||||||
return (
|
return (
|
||||||
<SelectionsWrapper>
|
<SelectionsWrapper>
|
||||||
{FONT_FAMILIES.map((fontFamily, idx) => {
|
{allFontFamilies.map((fontFamily, idx) => {
|
||||||
const isSelected = selectedFontFamily === fontFamily;
|
const isSelected = selectedFontFamily === fontFamily;
|
||||||
const standardSizePt = FONT_FAMILY_TO_STANDARD_SIZE_IN_PT[fontFamily];
|
const standardSizePt = FONT_FAMILY_TO_STANDARD_SIZE_IN_PT[fontFamily];
|
||||||
return (
|
return (
|
||||||
@ -70,7 +73,7 @@ export const FontFamilySelections = ({
|
|||||||
}}
|
}}
|
||||||
onClick={() => handleSettingsChange("fontFamily", fontFamily)}
|
onClick={() => handleSettingsChange("fontFamily", fontFamily)}
|
||||||
>
|
>
|
||||||
{fontFamily.split(/(?=[A-Z])/).join(" ")}
|
{FONT_FAMILY_TO_DISPLAY_NAME[fontFamily]}
|
||||||
</Selection>
|
</Selection>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -78,6 +81,17 @@ export const FontFamilySelections = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load FontFamilySelections client side since it calls getAllFontFamiliesToLoad,
|
||||||
|
* which uses navigator object that is only available on client side
|
||||||
|
*/
|
||||||
|
export const FontFamilySelectionsCSR = dynamic(
|
||||||
|
() => Promise.resolve(FontFamilySelections),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const FontSizeSelections = ({
|
export const FontSizeSelections = ({
|
||||||
selectedFontSize,
|
selectedFontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
|
@ -4,7 +4,7 @@ import { THEME_COLORS } from "components/ResumeForm/ThemeForm/constants";
|
|||||||
import { InlineInput } from "components/ResumeForm/ThemeForm/InlineInput";
|
import { InlineInput } from "components/ResumeForm/ThemeForm/InlineInput";
|
||||||
import {
|
import {
|
||||||
DocumentSizeSelections,
|
DocumentSizeSelections,
|
||||||
FontFamilySelections,
|
FontFamilySelectionsCSR,
|
||||||
FontSizeSelections,
|
FontSizeSelections,
|
||||||
} from "components/ResumeForm/ThemeForm/Selection";
|
} from "components/ResumeForm/ThemeForm/Selection";
|
||||||
import {
|
import {
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
type GeneralSetting,
|
type GeneralSetting,
|
||||||
} from "lib/redux/settingsSlice";
|
} from "lib/redux/settingsSlice";
|
||||||
import { useAppDispatch, useAppSelector } from "lib/redux/hooks";
|
import { useAppDispatch, useAppSelector } from "lib/redux/hooks";
|
||||||
import type { FontFamily } from "public/fonts/fonts";
|
import type { FontFamily } from "components/fonts/constants";
|
||||||
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
|
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export const ThemeForm = () => {
|
export const ThemeForm = () => {
|
||||||
@ -65,7 +65,7 @@ export const ThemeForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InputGroupWrapper label="Font Family" />
|
<InputGroupWrapper label="Font Family" />
|
||||||
<FontFamilySelections
|
<FontFamilySelectionsCSR
|
||||||
selectedFontFamily={fontFamily}
|
selectedFontFamily={fontFamily}
|
||||||
themeColor={themeColor}
|
themeColor={themeColor}
|
||||||
handleSettingsChange={handleSettingsChange}
|
handleSettingsChange={handleSettingsChange}
|
||||||
|
7
src/app/components/fonts/FontsZh.tsx
Normal file
7
src/app/components/fonts/FontsZh.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import "public/fonts/fonts-zh.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty component. Main purpose is to load fonts-zh.css
|
||||||
|
*/
|
||||||
|
const FontsZh = () => <></>;
|
||||||
|
export default FontsZh;
|
24
src/app/components/fonts/NonEnglishFontsCSSLoader.tsx
Normal file
24
src/app/components/fonts/NonEnglishFontsCSSLoader.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||||
|
|
||||||
|
const FontsZh = dynamic(() => import("components/fonts/FontsZh"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty component to load non-english fonts CSS conditionally
|
||||||
|
*
|
||||||
|
* Reference: https://prawira.medium.com/react-conditional-import-conditional-css-import-110cc58e0da6
|
||||||
|
*/
|
||||||
|
export const NonEnglishFontsCSSLoader = () => {
|
||||||
|
const [shouldLoadFontsZh, setShouldLoadFontsZh] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getAllFontFamiliesToLoad().includes("NotoSansSC")) {
|
||||||
|
setShouldLoadFontsZh(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{shouldLoadFontsZh && <FontsZh />}</>;
|
||||||
|
};
|
94
src/app/components/fonts/constants.ts
Normal file
94
src/app/components/fonts/constants.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Adding a new font family involves 3 steps:
|
||||||
|
* Step 1. Add it to one of the below FONT_FAMILIES variable array:
|
||||||
|
* English fonts -> SANS_SERIF_ENGLISH_FONT_FAMILIES or SERIF_ENGLISH_FONT_FAMILIES
|
||||||
|
* Non-English fonts -> NON_ENGLISH_FONT_FAMILIES
|
||||||
|
* Once the font is added, it would take care of
|
||||||
|
* a. Registering font family for React PDF at "components/fonts/hooks.tsx"
|
||||||
|
* b. Loading font family for React PDF iframe at "components/Resume/ResumeIframe.tsx"
|
||||||
|
* c. Adding font family selection to Resume Settings at "components/ResumeForm/ThemeForm/Selection.tsx"
|
||||||
|
* Step 2. To load css correctly for the Resume Form:
|
||||||
|
* English fonts -> add it to the "public\fonts\fonts.css" file
|
||||||
|
* Non-English fonts -> create/update "public\fonts\fonts-<language>.css" and update "components/fonts/NonEnglishFontsCSSLoader.tsx"
|
||||||
|
* Step 3. Update FONT_FAMILY_TO_STANDARD_SIZE_IN_PT and FONT_FAMILY_TO_DISPLAY_NAME accordingly
|
||||||
|
*
|
||||||
|
* IMPORTANT NOTE:
|
||||||
|
* One major problem with adding a new font family is that most font family doesn't work with
|
||||||
|
* React PDF out of box. The texts would appear fine in the PDF, but copying and pasting them
|
||||||
|
* would result in different texts. See issues: https://github.com/diegomura/react-pdf/issues/915
|
||||||
|
* and https://github.com/diegomura/react-pdf/issues/629
|
||||||
|
*
|
||||||
|
* A solution to this problem is to import and re-export the font with a font editor, e.g. fontforge or birdfont.
|
||||||
|
*
|
||||||
|
* If using fontforge, the following command can be used to export the font:
|
||||||
|
* ./fontforge -lang=ff -c 'Open($1); Generate($2); Close();' old_font.ttf new_font.ttf
|
||||||
|
* Note that fontforge doesn't work on non-english fonts: https://github.com/fontforge/fontforge/issues/1534
|
||||||
|
* Also, some fonts might still not work after re-export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SANS_SERIF_ENGLISH_FONT_FAMILIES = [
|
||||||
|
"Roboto",
|
||||||
|
"Lato",
|
||||||
|
"Montserrat",
|
||||||
|
"OpenSans",
|
||||||
|
"Raleway",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SERIF_ENGLISH_FONT_FAMILIES = [
|
||||||
|
"Caladea",
|
||||||
|
"Lora",
|
||||||
|
"RobotoSlab",
|
||||||
|
"PlayfairDisplay",
|
||||||
|
"Merriweather",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ENGLISH_FONT_FAMILIES = [
|
||||||
|
...SANS_SERIF_ENGLISH_FONT_FAMILIES,
|
||||||
|
...SERIF_ENGLISH_FONT_FAMILIES,
|
||||||
|
];
|
||||||
|
type EnglishFontFamily = (typeof ENGLISH_FONT_FAMILIES)[number];
|
||||||
|
|
||||||
|
export const NON_ENGLISH_FONT_FAMILIES = ["NotoSansSC"] as const;
|
||||||
|
type NonEnglishFontFamily = (typeof NON_ENGLISH_FONT_FAMILIES)[number];
|
||||||
|
|
||||||
|
export const NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE: Record<
|
||||||
|
NonEnglishFontFamily,
|
||||||
|
string[]
|
||||||
|
> = {
|
||||||
|
NotoSansSC: ["zh", "zh-CN", "zh-TW"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FontFamily = EnglishFontFamily | NonEnglishFontFamily;
|
||||||
|
export const FONT_FAMILY_TO_STANDARD_SIZE_IN_PT: Record<FontFamily, number> = {
|
||||||
|
// Sans Serif Fonts
|
||||||
|
Roboto: 11,
|
||||||
|
Lato: 11,
|
||||||
|
Montserrat: 10,
|
||||||
|
OpenSans: 10,
|
||||||
|
Raleway: 10,
|
||||||
|
// Serif Fonts
|
||||||
|
Caladea: 11,
|
||||||
|
Lora: 11,
|
||||||
|
RobotoSlab: 10,
|
||||||
|
PlayfairDisplay: 10,
|
||||||
|
Merriweather: 10,
|
||||||
|
// Non-English Fonts
|
||||||
|
NotoSansSC: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FONT_FAMILY_TO_DISPLAY_NAME: Record<FontFamily, string> = {
|
||||||
|
// Sans Serif Fonts
|
||||||
|
Roboto: "Roboto",
|
||||||
|
Lato: "Lato",
|
||||||
|
Montserrat: "Montserrat",
|
||||||
|
OpenSans: "Open Sans",
|
||||||
|
Raleway: "Raleway",
|
||||||
|
// Serif Fonts
|
||||||
|
Caladea: "Caladea",
|
||||||
|
Lora: "Lora",
|
||||||
|
RobotoSlab: "Roboto Slab",
|
||||||
|
PlayfairDisplay: "Playfair Display",
|
||||||
|
Merriweather: "Merriweather",
|
||||||
|
// Non-English Fonts
|
||||||
|
NotoSansSC: "思源黑体(简体)",
|
||||||
|
};
|
47
src/app/components/fonts/hooks.tsx
Normal file
47
src/app/components/fonts/hooks.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Font } from "@react-pdf/renderer";
|
||||||
|
import { ENGLISH_FONT_FAMILIES } from "components/fonts/constants";
|
||||||
|
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all fonts to React PDF so it can render fonts correctly in PDF
|
||||||
|
*/
|
||||||
|
export const useRegisterReactPDFFont = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
const allFontFamilies = getAllFontFamiliesToLoad();
|
||||||
|
allFontFamilies.forEach((fontFamily) => {
|
||||||
|
Font.register({
|
||||||
|
family: fontFamily,
|
||||||
|
fonts: [
|
||||||
|
{
|
||||||
|
src: `fonts/${fontFamily}-Regular.ttf`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `fonts/${fontFamily}-Bold.ttf`,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegisterReactPDFHyphenationCallback = (fontFamily: string) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (ENGLISH_FONT_FAMILIES.includes(fontFamily as any)) {
|
||||||
|
// Disable hyphenation for English Font Family so the word wraps each line
|
||||||
|
// https://github.com/diegomura/react-pdf/issues/311#issuecomment-548301604
|
||||||
|
Font.registerHyphenationCallback((word) => [word]);
|
||||||
|
} else {
|
||||||
|
// React PDF doesn't understand how to wrap non-english word on line break
|
||||||
|
// A workaround is to add an empty character after each word
|
||||||
|
// Reference https://github.com/diegomura/react-pdf/issues/1568
|
||||||
|
Font.registerHyphenationCallback((word) =>
|
||||||
|
word
|
||||||
|
.split("")
|
||||||
|
.map((char) => [char, ""])
|
||||||
|
.flat()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [fontFamily]);
|
||||||
|
};
|
24
src/app/components/fonts/lib.ts
Normal file
24
src/app/components/fonts/lib.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
ENGLISH_FONT_FAMILIES,
|
||||||
|
NON_ENGLISH_FONT_FAMILIES,
|
||||||
|
NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE,
|
||||||
|
} from "components/fonts/constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getPreferredNonEnglishFontFamilies returns non-english font families that are included in
|
||||||
|
* user's preferred languages. This is to avoid loading fonts/languages that users won't use.
|
||||||
|
*/
|
||||||
|
export const getPreferredNonEnglishFontFamilies = () => {
|
||||||
|
return NON_ENGLISH_FONT_FAMILIES.filter((fontFamily) => {
|
||||||
|
const fontLanguages = NON_ENGLISH_FONT_FAMILIES_TO_LANGUAGE[fontFamily];
|
||||||
|
const userPreferredLanguages = navigator.languages ?? [navigator.language];
|
||||||
|
return userPreferredLanguages.some((preferredLanguage) =>
|
||||||
|
fontLanguages.includes(preferredLanguage)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllFontFamiliesToLoad = () => {
|
||||||
|
return [...ENGLISH_FONT_FAMILIES, ...getPreferredNonEnglishFontFamilies()];
|
||||||
|
};
|
@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { ResumePDF } from "components/Resume/ResumePDF";
|
import { ResumePDF } from "components/Resume/ResumePDF";
|
||||||
import type { Resume } from "lib/redux/types";
|
|
||||||
import { initialResumeState } from "lib/redux/resumeSlice";
|
import { initialResumeState } from "lib/redux/resumeSlice";
|
||||||
import { initialSettings } from "lib/redux/settingsSlice";
|
import { initialSettings } from "lib/redux/settingsSlice";
|
||||||
import { ResumeIFrame } from "components/Resume/ResumeIFrame";
|
import { ResumeIframeCSR } from "components/Resume/ResumeIframe";
|
||||||
import { START_HOME_RESUME, END_HOME_RESUME } from "home/constants";
|
import { START_HOME_RESUME, END_HOME_RESUME } from "home/constants";
|
||||||
import { makeObjectCharIterator } from "lib/make-object-char-iterator";
|
import { makeObjectCharIterator } from "lib/make-object-char-iterator";
|
||||||
import { useTailwindBreakpoints } from "lib/hooks/useTailwindBreakpoints";
|
import { useTailwindBreakpoints } from "lib/hooks/useTailwindBreakpoints";
|
||||||
@ -61,7 +60,7 @@ export const AutoTypingResume = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResumeIFrame documentSize="Letter" scale={isLg ? 0.7 : 0.5}>
|
<ResumeIframeCSR documentSize="Letter" scale={isLg ? 0.7 : 0.5}>
|
||||||
<ResumePDF
|
<ResumePDF
|
||||||
resume={resume}
|
resume={resume}
|
||||||
settings={{
|
settings={{
|
||||||
@ -78,7 +77,7 @@ export const AutoTypingResume = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ResumeIFrame>
|
</ResumeIframeCSR>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "lib/redux/store";
|
import { store } from "lib/redux/store";
|
||||||
import { ResumeForm } from "components/ResumeForm";
|
import { ResumeForm } from "components/ResumeForm";
|
||||||
import Resume from "components/Resume";
|
import { Resume } from "components/Resume";
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user