mirror of
https://github.com/xitanggg/open-resume
synced 2024-12-04 20:55:16 +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-Bold.ttf"); font-weight: bold;}
|
||||
@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-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-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-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-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 dynamic from "next/dynamic";
|
||||
|
||||
const ResumeControlBarComponent = ({
|
||||
const ResumeControlBar = ({
|
||||
scale,
|
||||
setScale,
|
||||
documentSize,
|
||||
@ -71,9 +71,11 @@ const ResumeControlBarComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Make ResumeControlBar dynamic to make nextjs happy
|
||||
export const ResumeControlBar = dynamic(
|
||||
() => Promise.resolve(ResumeControlBarComponent),
|
||||
/**
|
||||
* Load ResumeControlBar client side since it uses usePDF, which is a web specific API
|
||||
*/
|
||||
export const ResumeControlBarCSR = dynamic(
|
||||
() => Promise.resolve(ResumeControlBar),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
|
@ -1,47 +1,58 @@
|
||||
"use client";
|
||||
import { FONT_FAMILIES } from "public/fonts/fonts";
|
||||
import { useMemo } from "react";
|
||||
import Frame from "react-frame-component";
|
||||
import {
|
||||
A4_HEIGHT_PX,
|
||||
A4_WIDTH_PX,
|
||||
A4_WIDTH_PT,
|
||||
LETTER_HEIGHT_PX,
|
||||
LETTER_WIDTH_PT,
|
||||
LETTER_WIDTH_PX,
|
||||
LETTER_WIDTH_PT,
|
||||
} from "lib/constants";
|
||||
import dynamic from "next/dynamic";
|
||||
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||
|
||||
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS = FONT_FAMILIES.map(
|
||||
(
|
||||
font
|
||||
) => `<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 getIframeInitialContent = (isA4: boolean) => {
|
||||
const width = isA4 ? A4_WIDTH_PT : LETTER_WIDTH_PT;
|
||||
const allFontFamilies = getAllFontFamiliesToLoad();
|
||||
|
||||
const IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE = FONT_FAMILIES.map(
|
||||
(
|
||||
font
|
||||
) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");}
|
||||
@font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}`
|
||||
).join("");
|
||||
const allFontFamiliesPreloadLinks = allFontFamilies
|
||||
.map(
|
||||
(
|
||||
font
|
||||
) => `<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 = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_PRELOAD_LINKS}
|
||||
<style>
|
||||
${IFRAME_INITIAL_CONTENT_FONT_FAMILIES_FONT_FACE}
|
||||
</style>
|
||||
</head>
|
||||
<body style='overflow: hidden; width: ${LETTER_WIDTH_PT}pt; margin: 0; padding: 0; -webkit-text-size-adjust:none;'>
|
||||
<div></div>
|
||||
</body>
|
||||
</html>`;
|
||||
const allFontFamiliesFontFaces = allFontFamilies
|
||||
.map(
|
||||
(
|
||||
font
|
||||
) => `@font-face {font-family: "${font}"; src: url("/fonts/${font}-Regular.ttf");}
|
||||
@font-face {font-family: "${font}"; src: url("/fonts/${font}-Bold.ttf"); font-weight: bold;}`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<!DOCTYPE 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.
|
||||
* It creates a sandbox document body that uses letter/A4 size as width.
|
||||
* Iframe is used here for style isolation, since react pdf uses pt unit.
|
||||
* It creates a sandbox document body that uses letter/A4 pt size as width.
|
||||
*/
|
||||
const ResumeIFrameComponent = ({
|
||||
const ResumeIframe = ({
|
||||
documentSize,
|
||||
scale,
|
||||
children,
|
||||
@ -52,6 +63,12 @@ const ResumeIFrameComponent = ({
|
||||
children: React.ReactNode;
|
||||
enablePDFViewer?: boolean;
|
||||
}) => {
|
||||
const isA4 = documentSize === "A4";
|
||||
const iframeInitialContent = useMemo(
|
||||
() => getIframeInitialContent(isA4),
|
||||
[isA4]
|
||||
);
|
||||
|
||||
if (enablePDFViewer) {
|
||||
return (
|
||||
<DynamicPDFViewer className="h-full w-full">
|
||||
@ -59,8 +76,8 @@ const ResumeIFrameComponent = ({
|
||||
</DynamicPDFViewer>
|
||||
);
|
||||
}
|
||||
const width = documentSize === "A4" ? A4_WIDTH_PX : LETTER_WIDTH_PX;
|
||||
const height = documentSize === "A4" ? A4_HEIGHT_PX : LETTER_HEIGHT_PX;
|
||||
const width = isA4 ? A4_WIDTH_PX : LETTER_WIDTH_PX;
|
||||
const height = isA4 ? A4_HEIGHT_PX : LETTER_HEIGHT_PX;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -82,7 +99,9 @@ const ResumeIFrameComponent = ({
|
||||
>
|
||||
<Frame
|
||||
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}
|
||||
</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(
|
||||
() => Promise.resolve(ResumeIFrameComponent),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Load iframe client side since iframe can't be SSR
|
||||
*/
|
||||
export const ResumeIframeCSR = dynamic(() => Promise.resolve(ResumeIframe), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// PDFViewer is only used for debugging. Its size is quite large, so we make it dynamic import
|
||||
const DynamicPDFViewer = dynamic(
|
||||
|
@ -1,23 +1,4 @@
|
||||
import { StyleSheet, Font } 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]);
|
||||
import { StyleSheet } from "@react-pdf/renderer";
|
||||
|
||||
// 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
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
import { useState, useMemo } from "react";
|
||||
import { ResumeIFrame } from "components/Resume/ResumeIFrame";
|
||||
import { ResumeIframeCSR } from "components/Resume/ResumeIframe";
|
||||
import { ResumePDF } from "components/Resume/ResumePDF";
|
||||
import {
|
||||
ResumeControlBar,
|
||||
ResumeControlBarCSR,
|
||||
ResumeControlBarBorder,
|
||||
} from "components/Resume/ResumeControlBar";
|
||||
import { FlexboxSpacer } from "components/FlexboxSpacer";
|
||||
@ -11,8 +11,13 @@ import { useAppSelector } from "lib/redux/hooks";
|
||||
import { selectResume } from "lib/redux/resumeSlice";
|
||||
import { selectSettings } from "lib/redux/settingsSlice";
|
||||
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 resume = useAppSelector(selectResume);
|
||||
const settings = useAppSelector(selectSettings);
|
||||
@ -21,34 +26,38 @@ const Resume = () => {
|
||||
[resume, settings]
|
||||
);
|
||||
|
||||
useRegisterReactPDFFont();
|
||||
useRegisterReactPDFHyphenationCallback(settings.fontFamily);
|
||||
|
||||
return (
|
||||
<div className="relative flex justify-center md:justify-start">
|
||||
<FlexboxSpacer maxWidth={50} className="hidden md:block" />
|
||||
<div className="relative">
|
||||
<section className="h-[calc(100vh-var(--top-nav-bar-height)-var(--resume-control-bar-height))] overflow-hidden md:p-[var(--resume-padding)]">
|
||||
<ResumeIFrame
|
||||
documentSize={settings.documentSize}
|
||||
<>
|
||||
<NonEnglishFontsCSSLoader />
|
||||
<div className="relative flex justify-center md:justify-start">
|
||||
<FlexboxSpacer maxWidth={50} className="hidden md:block" />
|
||||
<div className="relative">
|
||||
<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}
|
||||
enablePDFViewer={DEBUG_RESUME_PDF_FLAG}
|
||||
>
|
||||
<ResumePDF
|
||||
resume={resume}
|
||||
settings={settings}
|
||||
isPDF={DEBUG_RESUME_PDF_FLAG}
|
||||
/>
|
||||
</ResumeIFrame>
|
||||
</section>
|
||||
<ResumeControlBar
|
||||
scale={scale}
|
||||
setScale={setScale}
|
||||
documentSize={settings.documentSize}
|
||||
document={document}
|
||||
fileName={resume.profile.name + " - Resume"}
|
||||
/>
|
||||
setScale={setScale}
|
||||
documentSize={settings.documentSize}
|
||||
document={document}
|
||||
fileName={resume.profile.name + " - Resume"}
|
||||
/>
|
||||
</div>
|
||||
<ResumeControlBarBorder />
|
||||
</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 {
|
||||
FONT_FAMILY_TO_STANDARD_SIZE_IN_PT,
|
||||
FONT_FAMILIES,
|
||||
FONT_FAMILY_TO_DISPLAY_NAME,
|
||||
type FontFamily,
|
||||
} from "public/fonts/fonts";
|
||||
} from "components/fonts/constants";
|
||||
import { getAllFontFamiliesToLoad } from "components/fonts/lib";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const Selection = ({
|
||||
selectedColor,
|
||||
@ -45,7 +47,7 @@ const SelectionsWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="mt-2 flex flex-wrap gap-3">{children}</div>;
|
||||
};
|
||||
|
||||
export const FontFamilySelections = ({
|
||||
const FontFamilySelections = ({
|
||||
selectedFontFamily,
|
||||
themeColor,
|
||||
handleSettingsChange,
|
||||
@ -54,9 +56,10 @@ export const FontFamilySelections = ({
|
||||
themeColor: string;
|
||||
handleSettingsChange: (field: GeneralSetting, value: string) => void;
|
||||
}) => {
|
||||
const allFontFamilies = getAllFontFamiliesToLoad();
|
||||
return (
|
||||
<SelectionsWrapper>
|
||||
{FONT_FAMILIES.map((fontFamily, idx) => {
|
||||
{allFontFamilies.map((fontFamily, idx) => {
|
||||
const isSelected = selectedFontFamily === fontFamily;
|
||||
const standardSizePt = FONT_FAMILY_TO_STANDARD_SIZE_IN_PT[fontFamily];
|
||||
return (
|
||||
@ -70,7 +73,7 @@ export const FontFamilySelections = ({
|
||||
}}
|
||||
onClick={() => handleSettingsChange("fontFamily", fontFamily)}
|
||||
>
|
||||
{fontFamily.split(/(?=[A-Z])/).join(" ")}
|
||||
{FONT_FAMILY_TO_DISPLAY_NAME[fontFamily]}
|
||||
</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 = ({
|
||||
selectedFontSize,
|
||||
fontFamily,
|
||||
|
@ -4,7 +4,7 @@ import { THEME_COLORS } from "components/ResumeForm/ThemeForm/constants";
|
||||
import { InlineInput } from "components/ResumeForm/ThemeForm/InlineInput";
|
||||
import {
|
||||
DocumentSizeSelections,
|
||||
FontFamilySelections,
|
||||
FontFamilySelectionsCSR,
|
||||
FontSizeSelections,
|
||||
} from "components/ResumeForm/ThemeForm/Selection";
|
||||
import {
|
||||
@ -14,7 +14,7 @@ import {
|
||||
type GeneralSetting,
|
||||
} from "lib/redux/settingsSlice";
|
||||
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";
|
||||
|
||||
export const ThemeForm = () => {
|
||||
@ -65,7 +65,7 @@ export const ThemeForm = () => {
|
||||
</div>
|
||||
<div>
|
||||
<InputGroupWrapper label="Font Family" />
|
||||
<FontFamilySelections
|
||||
<FontFamilySelectionsCSR
|
||||
selectedFontFamily={fontFamily}
|
||||
themeColor={themeColor}
|
||||
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";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { ResumePDF } from "components/Resume/ResumePDF";
|
||||
import type { Resume } from "lib/redux/types";
|
||||
import { initialResumeState } from "lib/redux/resumeSlice";
|
||||
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 { makeObjectCharIterator } from "lib/make-object-char-iterator";
|
||||
import { useTailwindBreakpoints } from "lib/hooks/useTailwindBreakpoints";
|
||||
@ -61,7 +60,7 @@ export const AutoTypingResume = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResumeIFrame documentSize="Letter" scale={isLg ? 0.7 : 0.5}>
|
||||
<ResumeIframeCSR documentSize="Letter" scale={isLg ? 0.7 : 0.5}>
|
||||
<ResumePDF
|
||||
resume={resume}
|
||||
settings={{
|
||||
@ -78,7 +77,7 @@ export const AutoTypingResume = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ResumeIFrame>
|
||||
</ResumeIframeCSR>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "lib/redux/store";
|
||||
import { ResumeForm } from "components/ResumeForm";
|
||||
import Resume from "components/Resume";
|
||||
import { Resume } from "components/Resume";
|
||||
|
||||
export default function Create() {
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user