mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 08:54:05 +10:00
📦 v5.0.2 · Changelog: https://docs.rxresu.me/changelog
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconContext, type IconProps } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import { ArrowRightIcon, IconContext, type IconProps, WarningIcon } from "@phosphor-icons/react";
|
||||
import { type RefObject, useMemo, useRef, useState } from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
import type z from "zod";
|
||||
import { pageDimensionsAsPixels } from "@/schema/page";
|
||||
import type { pageLayoutSchema } from "@/schema/resume/data";
|
||||
import type { Template } from "@/schema/templates";
|
||||
import { sanitizeCss } from "@/utils/sanitize";
|
||||
import { cn } from "@/utils/style";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import { useCSSVariables } from "./hooks/use-css-variables";
|
||||
import { useWebfonts } from "./hooks/use-webfonts";
|
||||
import styles from "./preview.module.css";
|
||||
@@ -27,11 +32,6 @@ export type ExtendedIconProps = IconProps & {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
type Props = React.ComponentProps<"div"> & {
|
||||
pageClassName?: string;
|
||||
showPageNumbers?: boolean;
|
||||
};
|
||||
|
||||
const CSS_RULE_SPLIT_PATTERN = /\n(?=\s*[.#a-zA-Z])/;
|
||||
const CSS_SELECTOR_PATTERN = /^([^{]+)(\{)/;
|
||||
|
||||
@@ -53,13 +53,17 @@ function getTemplateComponent(template: Template) {
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...props }: Props) => {
|
||||
type Props = React.ComponentProps<"div"> & {
|
||||
pageClassName?: string;
|
||||
showPageNumbers?: boolean;
|
||||
};
|
||||
|
||||
export const ResumePreview = ({ showPageNumbers = false, pageClassName, className, ...props }: Props) => {
|
||||
const picture = useResumeStore((state) => state.resume.data.picture);
|
||||
const metadata = useResumeStore((state) => state.resume.data.metadata);
|
||||
|
||||
useWebfonts(metadata.typography);
|
||||
const style = useCSSVariables({ picture, metadata });
|
||||
const totalNumberOfPages = metadata.layout.pages.length;
|
||||
|
||||
const iconProps = useMemo<ExtendedIconProps>(() => {
|
||||
return {
|
||||
@@ -94,36 +98,91 @@ export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...pr
|
||||
return scoped;
|
||||
}, [metadata.css.enabled, metadata.css.value]);
|
||||
|
||||
const TemplateComponent = useMemo(() => getTemplateComponent(metadata.template), [metadata.template]);
|
||||
|
||||
return (
|
||||
<IconContext.Provider value={iconProps}>
|
||||
{/** biome-ignore lint/security/noDangerouslySetInnerHtml: CSS is sanitized with sanitizeCss */}
|
||||
{scopedCSS && <style dangerouslySetInnerHTML={{ __html: scopedCSS }} />}
|
||||
|
||||
<div style={style} className={cn("resume-preview-container", className)} {...props}>
|
||||
{metadata.layout.pages.map((pageLayout, pageIndex) => {
|
||||
const pageNumber = pageIndex + 1;
|
||||
|
||||
return (
|
||||
<div key={pageIndex} data-page-index={pageIndex} className="relative">
|
||||
{showPageNumbers && totalNumberOfPages > 1 && (
|
||||
<div className="absolute -top-6 left-0">
|
||||
<span className="font-medium text-foreground text-xs">
|
||||
<Trans>
|
||||
Page {pageNumber} of {totalNumberOfPages}
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cn(`page page-${pageIndex}`, styles.page, pageClassName)}>
|
||||
<TemplateComponent pageIndex={pageIndex} pageLayout={pageLayout} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{metadata.layout.pages.map((pageLayout, pageIndex) => (
|
||||
<PageContainer
|
||||
key={pageIndex}
|
||||
pageIndex={pageIndex}
|
||||
pageLayout={pageLayout}
|
||||
pageClassName={pageClassName}
|
||||
showPageNumbers={showPageNumbers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</IconContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
type PageContainerProps = {
|
||||
pageIndex: number;
|
||||
pageLayout: z.infer<typeof pageLayoutSchema>;
|
||||
pageClassName?: string;
|
||||
showPageNumbers?: boolean;
|
||||
};
|
||||
|
||||
function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers = false }: PageContainerProps) {
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
const [pageHeight, setPageHeight] = useState<number>(0);
|
||||
|
||||
const metadata = useResumeStore((state) => state.resume.data.metadata);
|
||||
|
||||
const pageNumber = useMemo(() => pageIndex + 1, [pageIndex]);
|
||||
const maxPageHeight = useMemo(() => pageDimensionsAsPixels[metadata.page.format].height, [metadata.page.format]);
|
||||
const totalNumberOfPages = useMemo(() => metadata.layout.pages.length, [metadata.layout.pages]);
|
||||
const TemplateComponent = useMemo(() => getTemplateComponent(metadata.template), [metadata.template]);
|
||||
|
||||
useResizeObserver({
|
||||
ref: pageRef as RefObject<HTMLDivElement>,
|
||||
onResize: (size) => {
|
||||
if (!size.height) return;
|
||||
setPageHeight(size.height);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-page-index={pageIndex} className="relative">
|
||||
{showPageNumbers && totalNumberOfPages > 1 && (
|
||||
<div className="absolute -top-6 left-0">
|
||||
<span className="font-medium text-foreground text-xs">
|
||||
<Trans>
|
||||
Page {pageNumber} of {totalNumberOfPages}
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={pageRef} className={cn(`page page-${pageIndex}`, styles.page, pageClassName)}>
|
||||
<TemplateComponent pageIndex={pageIndex} pageLayout={pageLayout} />
|
||||
</div>
|
||||
|
||||
{pageHeight > maxPageHeight && (
|
||||
<div className="absolute top-full left-0 mt-4">
|
||||
<a
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="group/link"
|
||||
href="https://docs.rxresu.me/guides/fitting-content-on-a-page"
|
||||
>
|
||||
<Alert className="max-w-sm text-yellow-600">
|
||||
<WarningIcon color="currentColor" />
|
||||
<AlertTitle>
|
||||
<Trans>
|
||||
The content is too tall for this page, this may cause undesirable results when exporting to PDF.
|
||||
</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-xs underline-offset-2 group-hover/link:underline">
|
||||
<Trans>Learn more about how to fit content on a page</Trans>
|
||||
<ArrowRightIcon color="currentColor" className="ml-1 inline size-3" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,21 +36,23 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
return (
|
||||
<div className="template-ditgar page-content">
|
||||
{/* Sidebar Background */}
|
||||
{!fullWidth && (
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 left-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<aside data-layout="sidebar" className="sidebar group z-10 flex w-(--page-sidebar-width) shrink-0 flex-col">
|
||||
{isFirstPage && <Header />}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<aside data-layout="sidebar" className="sidebar group z-10 flex w-(--page-sidebar-width) shrink-0 flex-col">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="flex-1 space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
|
||||
{sidebar.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
<div className="flex-1 space-y-4 px-(--page-margin-x) pt-(--page-margin-y)">
|
||||
{sidebar.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<main data-layout="main" className={cn("main group z-10", !fullWidth ? "col-span-2" : "col-span-3")}>
|
||||
{isFirstPage && <SummaryComponent id="summary" />}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type * as React from "react";
|
||||
import { cn } from "@/utils/style";
|
||||
|
||||
const alertVariants = cva(
|
||||
"group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current",
|
||||
"group/alert relative grid w-full gap-1 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-4 *:[svg]:row-span-2 *:[svg]:translate-y-1 *:[svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Reference in New Issue
Block a user