mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 01:44:53 +10:00
refactor(ui): improve error handling and input safety in app flows
Normalize frontend error rendering and tighten input/path handling across auth, builder, dashboard, and shared components for more resilient UX behavior. Made-with: Cursor
This commit is contained in:
@@ -37,14 +37,20 @@ export function useWebfonts(typography: z.infer<typeof typographySchema>) {
|
||||
|
||||
for (const { url, weight, style } of fontUrls) {
|
||||
const fontFace = new FontFace(family, `url("${url}")`, { style, weight, display: "swap" });
|
||||
if (!document.fonts.has(fontFace)) document.fonts.add(await fontFace.load());
|
||||
if (!document.fonts.has(fontFace)) {
|
||||
try {
|
||||
document.fonts.add(await fontFace.load());
|
||||
} catch {
|
||||
// Fail open for printer/headless environments where remote fonts may be blocked by CSP.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bodyTypography = typography.body;
|
||||
const headingTypography = typography.heading;
|
||||
|
||||
void Promise.all([
|
||||
void Promise.allSettled([
|
||||
loadFont(bodyTypography.fontFamily, bodyTypography.fontWeights),
|
||||
loadFont(headingTypography.fontFamily, headingTypography.fontWeights),
|
||||
]).then(() => {
|
||||
|
||||
@@ -36,8 +36,29 @@ export type ExtendedIconProps = IconProps & {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
const CSS_RULE_SPLIT_PATTERN = /\n(?=\s*[.#a-zA-Z])/;
|
||||
const CSS_SELECTOR_PATTERN = /^([^{]+)(\{)/;
|
||||
function scopeCustomCssSelectors(css: string): string {
|
||||
// keep @keyframes blocks unchanged, scope the remaining rule selectors
|
||||
const keyframes: string[] = [];
|
||||
const withoutKeyframes = css.replace(/@(-webkit-)?keyframes\s+[^{]+\{[\s\S]*?\}\s*\}/gi, (block) => {
|
||||
keyframes.push(block);
|
||||
return `__RR_KEYFRAMES_${keyframes.length - 1}__`;
|
||||
});
|
||||
|
||||
const scoped = withoutKeyframes.replace(/(^|})\s*([^@{}][^{]+)\{/g, (_match, prefix, rawSelectors) => {
|
||||
const selectors = rawSelectors
|
||||
.split(",")
|
||||
.map((selector: string) => selector.trim())
|
||||
.filter(Boolean)
|
||||
.map((selector: string) => `.resume-preview-container ${selector}`)
|
||||
.join(", ");
|
||||
if (!selectors) return `${prefix}${rawSelectors}{`;
|
||||
return `${prefix} ${selectors}{`;
|
||||
});
|
||||
|
||||
const restored = scoped.replace(/__RR_KEYFRAMES_(\d+)__/g, (_match, index) => keyframes[Number(index)] ?? "");
|
||||
|
||||
return restored;
|
||||
}
|
||||
|
||||
function getTemplateComponent(template: Template) {
|
||||
return match(template)
|
||||
@@ -82,24 +103,7 @@ export const ResumePreview = ({ showPageNumbers = false, pageClassName, classNam
|
||||
if (!metadata.css.enabled || !metadata.css.value.trim()) return null;
|
||||
|
||||
const sanitizedCss = sanitizeCss(metadata.css.value);
|
||||
|
||||
const scoped = sanitizedCss
|
||||
.split(CSS_RULE_SPLIT_PATTERN)
|
||||
.map((rule) => {
|
||||
const trimmed = rule.trim();
|
||||
if (!trimmed || trimmed.startsWith("@")) return trimmed;
|
||||
|
||||
return trimmed.replace(CSS_SELECTOR_PATTERN, (_match, selectors, brace) => {
|
||||
const prefixed = selectors
|
||||
.split(",")
|
||||
.map((selector: string) => `.resume-preview-container ${selector.trim()} `)
|
||||
.join(", ");
|
||||
return `${prefixed}${brace}`;
|
||||
});
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return scoped;
|
||||
return scopeCustomCssSelectors(sanitizedCss);
|
||||
}, [metadata.css.enabled, metadata.css.value]);
|
||||
|
||||
return (
|
||||
@@ -134,10 +138,10 @@ function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers =
|
||||
|
||||
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]);
|
||||
const pageNumber = pageIndex + 1;
|
||||
const maxPageHeight = pageDimensionsAsPixels[metadata.page.format].height;
|
||||
const totalNumberOfPages = metadata.layout.pages.length;
|
||||
const TemplateComponent = getTemplateComponent(metadata.template);
|
||||
|
||||
useResizeObserver({
|
||||
ref: pageRef as RefObject<HTMLDivElement>,
|
||||
@@ -152,7 +156,7 @@ function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers =
|
||||
{showPageNumbers && totalNumberOfPages > 1 && (
|
||||
<div className="absolute inset-s-0 -top-6 print:hidden">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
<Trans>
|
||||
<Trans comment="Page counter label shown above resume preview pages">
|
||||
Page {pageNumber} of {totalNumberOfPages}
|
||||
</Trans>
|
||||
</span>
|
||||
@@ -174,12 +178,14 @@ function PageContainer({ pageIndex, pageLayout, pageClassName, showPageNumbers =
|
||||
<Alert className="max-w-sm text-yellow-600">
|
||||
<WarningIcon color="currentColor" />
|
||||
<AlertTitle>
|
||||
<Trans>
|
||||
<Trans comment="Warning shown when resume content exceeds printable page height">
|
||||
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>
|
||||
<Trans comment="Help link text to documentation about fitting resume content onto a page">
|
||||
Learn more about how to fit content on a page
|
||||
</Trans>
|
||||
<ArrowRightIcon color="currentColor" className="ms-1 inline size-3" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
Reference in New Issue
Block a user