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:
Amruth Pillai
2026-04-25 15:31:13 +02:00
parent a42dbcd452
commit 08e9c80037
56 changed files with 1271 additions and 391 deletions
+8 -2
View File
@@ -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(() => {
+33 -27
View File
@@ -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>