chore: lint using react-doctor, update translations, dynamic imports

This commit is contained in:
Amruth Pillai
2026-05-21 09:56:26 +02:00
parent 3596102c63
commit 39e88dd365
208 changed files with 5876 additions and 4778 deletions
+1 -1
View File
@@ -1,9 +1,9 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Template } from "@reactive-resume/schema/templates";
import type { SectionTitleResolver } from "./section-title";
import { pdf } from "@react-pdf/renderer";
import { createElement } from "react";
import { ResumeDocument } from "./document";
import { pdf } from "./renderer";
type CreateResumePdfBlobOptions = {
data: ResumeData;
+2 -2
View File
@@ -1,7 +1,7 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { ReactNode } from "react";
import type { SectionTitleResolver } from "./section-title";
import { createContext, useContext } from "react";
import { createContext, use } from "react";
type RenderContextValue = ResumeData & {
resolveSectionTitle?: SectionTitleResolver | undefined;
@@ -20,7 +20,7 @@ export const RenderProvider = ({ data, resolveSectionTitle, children }: RenderPr
};
export const useRender = (): RenderContextValue => {
const context = useContext(RenderContext);
const context = use(RenderContext);
if (!context) throw new Error("useRender must be called inside a <RenderProvider>.");
+7 -3
View File
@@ -3,10 +3,10 @@ import type { Template } from "@reactive-resume/schema/templates";
import type { Locale } from "@reactive-resume/utils/locale";
import type { ComponentType } from "react";
import type { SectionTitleResolver } from "./section-title";
import { Document } from "@react-pdf/renderer";
import { useMemo } from "react";
import { RenderProvider } from "./context";
import { registerFonts, resumeContentContainsCJK } from "./hooks/use-register-fonts";
import { Document } from "./renderer";
import { getTemplatePage } from "./templates";
export type TemplatePageProps = {
@@ -22,8 +22,12 @@ export type ResumeDocumentProps = {
resolveSectionTitle?: SectionTitleResolver | undefined;
};
const getLayoutPageKey = (page: LayoutPage, pageIndex: number) =>
`${page.fullWidth ? "full" : "split"}:${page.main.join(",")}:${page.sidebar.join(",")}:${pageIndex}`;
export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDocumentProps) => {
const TemplatePageComponent = getTemplatePage(template);
const creationDate = useMemo(() => new Date(), []);
const hasCjkContent = useMemo(() => resumeContentContainsCJK(data), [data]);
const typography = registerFonts(
data.metadata.typography,
@@ -40,7 +44,7 @@ export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDo
<RenderProvider data={resumeData} resolveSectionTitle={resolveSectionTitle}>
<Document
pageMode="useNone"
creationDate={new Date()}
creationDate={creationDate}
producer="Reactive Resume"
title={resumeData.basics.name}
author={resumeData.basics.name}
@@ -49,7 +53,7 @@ export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDo
language={resumeData.metadata.page.locale}
>
{resumeData.metadata.layout.pages.map((page, index) => (
<TemplatePageComponent key={index} page={page} pageIndex={index} />
<TemplatePageComponent key={getLayoutPageKey(page, index)} page={page} pageIndex={index} />
))}
</Document>
</RenderProvider>
@@ -1,8 +1,8 @@
import type { ResumeData, Typography } from "@reactive-resume/schema/resume/data";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Font } from "@react-pdf/renderer";
import { getWebFontSource } from "@reactive-resume/fonts";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { Font } from "../renderer";
const typography = {
body: {
+8 -2
View File
@@ -1,7 +1,6 @@
import type { FontWeight } from "@reactive-resume/fonts";
import type { ResumeData, Typography } from "@reactive-resume/schema/resume/data";
import type { Locale } from "@reactive-resume/utils/locale";
import { Font } from "@react-pdf/renderer";
import { letters as cjkLetters } from "cjk-regex";
import {
getFont,
@@ -12,6 +11,7 @@ import {
sortFontWeights,
} from "@reactive-resume/fonts";
import { isCJKLocale } from "@reactive-resume/utils/locale";
import { Font } from "../renderer";
type FontWeightRange = {
lowest: number;
@@ -30,7 +30,13 @@ export type PdfTypography = Omit<Typography, "body" | "heading"> & {
};
const getFontWeightRange = (fontWeights: string[]): FontWeightRange => {
const numericWeights = fontWeights.map(Number).filter((weight) => Number.isFinite(weight));
const numericWeights: number[] = [];
for (const fontWeight of fontWeights) {
const numericWeight = Number(fontWeight);
if (Number.isFinite(numericWeight)) numericWeights.push(numericWeight);
}
if (numericWeights.length === 0) return { lowest: 400, highest: 700 };
const lowest = Math.min(...numericWeights);
+12
View File
@@ -0,0 +1,12 @@
export {
Document,
Font,
Image,
Link,
Page,
pdf,
renderToBuffer,
StyleSheet,
Text,
View,
} from "#react-pdf-renderer";
+1 -1
View File
@@ -1,9 +1,9 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Template } from "@reactive-resume/schema/templates";
import type { SectionTitleResolver } from "./section-title";
import { renderToBuffer } from "@react-pdf/renderer";
import { createElement } from "react";
import { ResumeDocument } from "./document";
import { renderToBuffer } from "./renderer";
type CreateResumePdfFileOptions = {
data: ResumeData;
@@ -7,10 +7,10 @@ import type {
TemplateStyleContext,
TemplateStyleSlots,
} from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -69,16 +69,16 @@ export const AzurillPage = ({ page, pageIndex }: TemplatePageProps) => {
rowGap: metrics.sectionGap,
})}
>
{sidebarSections.map((section, index) => (
<Fragment key={index}>
{sidebarSections.map((section) => (
<Fragment key={section}>
<Section section={section} placement="sidebar" />
</Fragment>
))}
</View>
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -43,9 +43,18 @@ const getBronzorSections = ({
}) => {
if (fullWidth) return mainSections;
return Array.from({ length: Math.max(mainSections.length, sidebarSections.length) }).flatMap((_, index) =>
[sidebarSections[index], mainSections[index]].filter((section): section is string => Boolean(section)),
);
const sections: string[] = [];
const sectionCount = Math.max(mainSections.length, sidebarSections.length);
for (let index = 0; index < sectionCount; index += 1) {
const sidebarSection = sidebarSections[index];
const mainSection = mainSections[index];
if (sidebarSection) sections.push(sidebarSection);
if (mainSection) sections.push(mainSection);
}
return sections;
};
export const BronzorPage = ({ page, pageIndex }: TemplatePageProps) => {
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -60,8 +60,8 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
>
{showHeader && <Header styles={styles} />}
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
@@ -79,8 +79,8 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
rowGap: metrics.sectionGap,
})}
>
{sidebarSections.map((section, index) => (
<Fragment key={index}>
{sidebarSections.map((section) => (
<Fragment key={section}>
<Section section={section} placement="sidebar" />
</Fragment>
))}
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
@@ -75,8 +75,8 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
{!page.fullWidth && (
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
{regularSidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{regularSidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -91,8 +91,8 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
)}
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
{regularMainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{regularMainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -66,8 +66,8 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
rowGap: metrics.sectionGap,
})}
>
{sidebarSections.map((section, index) => (
<Fragment key={index}>
{sidebarSections.map((section) => (
<Fragment key={section}>
<Section section={section} placement="sidebar" />
</Fragment>
))}
@@ -80,8 +80,8 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
rowGap: metrics.sectionGap,
})}
>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { getFeaturedSummaryLayout } from "../shared/featured-summary";
@@ -70,8 +70,8 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
{!page.fullWidth && (
<View style={styles.sidebarContent}>
{regularSidebarSections.map((section, index) => (
<Fragment key={index}>
{regularSidebarSections.map((section) => (
<Fragment key={section}>
<Section section={section} placement="sidebar" />
</Fragment>
))}
@@ -88,8 +88,8 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
)}
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
{regularMainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{regularMainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -69,8 +69,8 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
{!page.fullWidth && (
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -79,8 +79,8 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
<View style={styles.mainColumn}>
<View style={composeStyles(styles.mainContent, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -50,15 +50,15 @@ export const KakunaPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && <Header styles={styles} />}
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
{!page.fullWidth && (
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -49,15 +49,15 @@ export const LaprasPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && <Header styles={styles} />}
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
{!page.fullWidth && (
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.gapY(1.5) })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -55,8 +55,8 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
<View style={styles.body}>
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
@@ -67,8 +67,8 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
rowGap: metrics.sectionGap,
})}
>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -54,15 +54,15 @@ export const MeowthPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && <Header styles={styles} />}
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
{!page.fullWidth && (
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
+5 -5
View File
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -49,15 +49,15 @@ export const OnyxPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && <Header styles={styles} />}
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
{!page.fullWidth && (
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -68,8 +68,8 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
</View>
@@ -84,8 +84,8 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
)}
<View style={{ rowGap: metrics.sectionGap }}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
</View>
@@ -1,10 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { ReactNode } from "react";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -51,15 +52,15 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
{showHeader && <Header styles={styles} />}
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{mainSections.map((section, index) => (
<Section key={index} section={section} placement="main" />
{mainSections.map((section) => (
<Section key={section} section={section} placement="main" />
))}
</View>
{!page.fullWidth && (
<View style={composeStyles(styles.sectionGroup, { rowGap: metrics.sectionGap })}>
{sidebarSections.map((section, index) => (
<Section key={index} section={section} placement="sidebar" />
{sidebarSections.map((section) => (
<Section key={section} section={section} placement="sidebar" />
))}
</View>
)}
@@ -71,32 +72,57 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
const Header = ({ styles }: { styles: RhyhornStyles }) => {
const { basics, picture } = useRender();
const hasPicture = hasTemplatePicture(picture);
const contactItems = [
basics.email && (
<Link src={`mailto:${basics.email}`} style={styles.contactItemContent}>
<Icon name="envelope" />
<Text>{basics.email}</Text>
</Link>
),
basics.phone && (
<Link src={`tel:${basics.phone}`} style={styles.contactItemContent}>
<Icon name="phone" />
<Text>{basics.phone}</Text>
</Link>
),
basics.location && (
<View style={styles.contactItemContent}>
<Icon name="map-pin" />
<Text>{basics.location}</Text>
</View>
),
basics.website.url && (
<WebsiteContactItem key="website" website={basics.website} style={styles.contactItemContent} />
),
...basics.customFields.map((field) => (
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItemContent} />
)),
].filter(Boolean);
const contactItems: { id: string; content: ReactNode }[] = [];
if (basics.email) {
contactItems.push({
id: "email",
content: (
<Link src={`mailto:${basics.email}`} style={styles.contactItemContent}>
<Icon name="envelope" />
<Text>{basics.email}</Text>
</Link>
),
});
}
if (basics.phone) {
contactItems.push({
id: "phone",
content: (
<Link src={`tel:${basics.phone}`} style={styles.contactItemContent}>
<Icon name="phone" />
<Text>{basics.phone}</Text>
</Link>
),
});
}
if (basics.location) {
contactItems.push({
id: "location",
content: (
<View style={styles.contactItemContent}>
<Icon name="map-pin" />
<Text>{basics.location}</Text>
</View>
),
});
}
if (basics.website.url) {
contactItems.push({
id: "website",
content: <WebsiteContactItem website={basics.website} style={styles.contactItemContent} />,
});
}
contactItems.push(
...basics.customFields.map((field) => ({
id: `custom-${field.id}`,
content: <CustomFieldContactItem field={field} style={styles.contactItemContent} />,
})),
);
return (
<View style={styles.header}>
@@ -109,13 +135,13 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
<View style={styles.contactList}>
{contactItems.map((item, index) => (
<View
key={index}
key={item.id}
style={composeStyles(
styles.contactItem,
index === contactItems.length - 1 ? styles.contactItemLast : undefined,
)}
>
{item}
{item.content}
</View>
))}
</View>
@@ -1,10 +1,10 @@
import type { Style } from "@react-pdf/types";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
@@ -1,7 +1,7 @@
import type { Style } from "@react-pdf/types";
import type { CustomField } from "@reactive-resume/schema/resume/data";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import { View } from "@react-pdf/renderer";
import { View } from "../../renderer";
import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
import { Icon, Link, Text } from "./primitives";
@@ -10,7 +10,7 @@ import type {
TemplateStyleSlot,
TemplateStyleSlots,
} from "./types";
import { createContext, useContext } from "react";
import { createContext, use } from "react";
type TemplateContextValue = {
styles: TemplateStyleSlots;
@@ -50,11 +50,14 @@ type TemplateStyleContextValue = {
colors: TemplateColorRoles;
};
const EMPTY_FEATURE_STYLES: TemplateFeatureStyleSlots = {};
const EMPTY_FEATURES: TemplateFeatures = {};
export const TemplateProvider = ({
styles,
featureStyles = {},
featureStyles = EMPTY_FEATURE_STYLES,
colors,
features = {},
features = EMPTY_FEATURES,
children,
}: TemplateProviderProps) => {
return (
@@ -73,7 +76,7 @@ export const TemplatePlacementProvider = ({
};
const useTemplateContext = () => {
const context = useContext(TemplateContext);
const context = use(TemplateContext);
if (!context) throw new Error("useTemplateContext must be called inside a <TemplateProvider>.");
@@ -86,7 +89,7 @@ export const useTemplateFeature = (feature: keyof TemplateFeatures): boolean =>
return features[feature] ?? false;
};
export const useTemplatePlacement = () => useContext(TemplatePlacementContext);
export const useTemplatePlacement = () => use(TemplatePlacementContext);
const useTemplateStyleContext = (): TemplateStyleContextValue => {
const { colors } = useTemplateContext();
@@ -81,9 +81,14 @@ const filterExperienceRoles = <T extends HiddenItem>(item: T, sectionType?: stri
};
export const filterItems = <T extends HiddenItem>(items: T[], sectionType?: string): T[] => {
return items
.filter((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType))
.map((item) => filterExperienceRoles(item, sectionType));
const visibleItems: T[] = [];
for (const item of items) {
if (item.hidden || !hasValidPrimaryTitle(item, sectionType)) continue;
visibleItems.push(filterExperienceRoles(item, sectionType));
}
return visibleItems;
};
export const hasVisibleItems = (section: ItemSectionLike, sectionType?: string): boolean => {
@@ -1,12 +1,14 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import { View } from "@react-pdf/renderer";
import { useRender } from "../../context";
import { View } from "../../renderer";
import { useTemplateIconSlot, useTemplateStyle } from "./context";
import { getTemplateMetrics } from "./metrics";
import { Icon } from "./primitives";
import { composeStyles } from "./styles";
const LEVEL_ITEM_KEYS = ["level-1", "level-2", "level-3", "level-4", "level-5"] as const;
export const LevelDisplay = ({ level }: { level: number }) => {
const data = useRender();
const levelDesign = data.metadata.design.level;
@@ -40,13 +42,13 @@ export const LevelDisplay = ({ level }: { level: number }) => {
levelContainerStyle,
)}
>
{Array.from({ length: 5 }).map((_, index) => {
{LEVEL_ITEM_KEYS.map((itemKey, index) => {
const isActive = index < level;
if (levelDesign.type === "icon") {
return (
<Icon
key={index}
key={itemKey}
size={iconSize + 4}
name={levelDesign.icon as IconName}
style={{ opacity: isActive ? 1 : 0.35 }}
@@ -57,7 +59,7 @@ export const LevelDisplay = ({ level }: { level: number }) => {
if (levelDesign.type === "progress-bar") {
return (
<View
key={index}
key={itemKey}
style={composeStyles(
{
flex: 1,
@@ -92,7 +94,7 @@ export const LevelDisplay = ({ level }: { level: number }) => {
return (
<View
key={index}
key={itemKey}
style={composeStyles(
{
width,
@@ -1,8 +1,8 @@
import type { Style } from "@react-pdf/types";
import type { ComponentProps } from "react";
import type { StyleInput } from "./styles";
import { Link as PdfLink, Text as PdfText, View } from "@react-pdf/renderer";
import { Icon as PhosphorIcon } from "phosphor-icons-react-pdf/dynamic";
import { Link as PdfLink, Text as PdfText, View } from "../../renderer";
import { useTemplateIconSlot, useTemplateStyle } from "./context";
import { composeLinkStyles, composeStyles } from "./styles";
@@ -1,7 +1,7 @@
import type { ReactElement } from "react";
import { describe, expect, it } from "vitest";
import { Text as PdfText } from "@react-pdf/renderer";
import { renderHtml } from "react-pdf-html";
import { Text as PdfText } from "../../renderer";
import { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
type PdfElement = ReactElement<{ children?: unknown; element?: { tag: string } }>;
@@ -29,12 +29,19 @@ const getTagName = (node: Node) => node.rawTagName.toLowerCase();
const hasBlockDescendant = (node: Node): boolean =>
node.childNodes.some((child) => child.nodeType === NodeType.ELEMENT_NODE && !isInlineNode(child));
const mergeClassNames = (...classNames: (string | undefined)[]): string =>
classNames
.flatMap((className) => className?.split(/\s+/) ?? [])
.filter(Boolean)
.filter((className, index, classNames) => classNames.indexOf(className) === index)
.join(" ");
const mergeClassNames = (...classNames: (string | undefined)[]): string => {
const uniqueClassNames = new Set<string>();
for (const className of classNames) {
if (!className) continue;
for (const part of className.split(/\s+/)) {
if (part) uniqueClassNames.add(part);
}
}
return [...uniqueClassNames].join(" ");
};
const normalizeMarkElements = (root: ReturnType<typeof parse>) => {
for (const mark of root.querySelectorAll("mark")) {
@@ -83,13 +83,29 @@ const getRootElement = (element: RichTextSpacingElement): RichTextSpacingElement
return root;
};
const getTopLevelFlowElements = (root: RichTextSpacingElement): RichTextSpacingElement[] =>
(root.childNodes ?? []).filter(isElementNode).flatMap((child) => {
if (isRichTextTag(child, "p")) return [child];
if (!isRichTextTag(child, "ul", "ol")) return [];
const getTopLevelFlowElements = (root: RichTextSpacingElement): RichTextSpacingElement[] => {
const flowElements: RichTextSpacingElement[] = [];
return (child.childNodes ?? []).filter(isElementNode).filter((listChild) => isRichTextTag(listChild, "li"));
});
for (const childNode of root.childNodes ?? []) {
if (!isElementNode(childNode)) continue;
const child = childNode;
if (isRichTextTag(child, "p")) {
flowElements.push(child);
continue;
}
if (!isRichTextTag(child, "ul", "ol")) continue;
for (const listChildNode of child.childNodes ?? []) {
if (isElementNode(listChildNode) && isRichTextTag(listChildNode, "li")) {
flowElements.push(listChildNode);
}
}
}
return flowElements;
};
export const createRichTextProseSpacing = (bodyLineHeight: number | undefined): RichTextProseSpacing => {
if (bodyLineHeight === undefined) return { paragraph: {}, listItem: {} };
@@ -1,6 +1,6 @@
import type { Style } from "@react-pdf/types";
import { Text as PdfText, View } from "@react-pdf/renderer";
import { Html } from "react-pdf-html";
import { Text as PdfText, View } from "../../renderer";
import { useTemplateStyle } from "./context";
import { safeTextStyle } from "./primitives";
import { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
+23 -6
View File
@@ -19,10 +19,10 @@ import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { ReactNode } from "react";
import type { StyleInput, TemplatePlacement } from "./styles";
import type { CustomItemSection, ItemSection } from "./types";
import { View } from "@react-pdf/renderer";
import { Children, createContext, useContext } from "react";
import { Children, createContext, isValidElement, use } from "react";
import { match } from "ts-pattern";
import { useRender } from "../../context";
import { View } from "../../renderer";
import { getResumeSectionTitle } from "../../section-title";
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
import { getWebsiteDisplayText } from "./contact";
@@ -48,8 +48,25 @@ type SectionItemsContextValue = {
};
const SectionItemsContext = createContext<SectionItemsContextValue>({ itemStyle: undefined, useTimeline: false });
const SECTION_ITEM_PLACEHOLDER_KEYS = [
"placeholder-1",
"placeholder-2",
"placeholder-3",
"placeholder-4",
"placeholder-5",
"placeholder-6",
] as const;
const useSectionItemsContext = () => useContext(SectionItemsContext);
const useSectionItemsContext = () => use(SectionItemsContext);
const getChildKey = (child: ReactNode, fallbackIndex: number) => {
return isValidElement(child) && child.key !== null ? String(child.key) : `child-${fallbackIndex}`;
};
const getRowKey = (row: ReactNode[], rowIndex: number) => {
const childKeys = row.map(getChildKey).join("|");
return childKeys || `row-${rowIndex}`;
};
const getVisibleItems = <T extends { hidden: boolean }>(section: ItemSection<T>, sectionType?: string): T[] => {
if (!hasVisibleItems(section, sectionType)) return [];
@@ -109,10 +126,10 @@ const SectionItems = ({ children, columns = 1 }: { children: ReactNode; columns?
<SectionItemsContext.Provider value={context}>
<View style={composeStyles(layout.containerStyle, sectionItemsStyle)}>
{rows.map((row, rowIndex) => (
<View key={rowIndex} style={composeStyles(layout.rowStyle)}>
<View key={getRowKey(row, rowIndex)} style={composeStyles(layout.rowStyle)}>
{row}
{Array.from({ length: layout.columns - row.length }, (_, index) => (
<View key={`placeholder-${index}`} style={composeStyles(layout.itemStyle)} />
{SECTION_ITEM_PLACEHOLDER_KEYS.slice(0, layout.columns - row.length).map((placeholderKey) => (
<View key={placeholderKey} style={composeStyles(layout.itemStyle)} />
))}
</View>
))}