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
+40 -7
View File
@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";
@@ -81,33 +82,65 @@ export function CommandPalette() {
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogHeader className="sr-only print:hidden">
<DialogTitle>
<Trans>Builder Command Palette</Trans>
<Trans comment="Screen-reader dialog title for the command palette in the resume builder">
Builder Command Palette
</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Type a command or search...</Trans>
<Trans comment="Screen-reader dialog description instructing users how to use the command palette">
Type a command or search...
</Trans>
</DialogDescription>
</DialogHeader>
<DialogContent
className="overflow-hidden p-0"
aria-label={isFirstPage ? "Command Palette" : `Command Palette - ${currentPage}`}
aria-label={
isFirstPage
? t({
comment: "Accessible label for the command palette dialog",
message: "Command Palette",
})
: t({
comment: "Accessible label for command palette dialog when browsing a nested command page",
message: `Command Palette - ${currentPage}`,
})
}
>
<Command
loop
aria-label="Command Palette"
aria-label={t({
comment: "Accessible label for command list region inside command palette",
message: "Command Palette",
})}
className="[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group]]:px-2 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3"
>
<CommandInput
ref={inputRef}
value={search}
onValueChange={handleSearchChange}
placeholder={isFirstPage ? "Type a command or search..." : "Search..."}
aria-label="Search commands"
placeholder={
isFirstPage
? t({
comment: "Placeholder in command palette input on root page",
message: "Type a command or search...",
})
: t({
comment: "Placeholder in command palette input on nested pages",
message: "Search...",
})
}
aria-label={t({
comment: "Accessible label for command palette search input",
message: "Search commands",
})}
/>
<CommandList>
<CommandEmpty>
<Trans>The command you're looking for doesn't exist.</Trans>
<Trans comment="Empty-state message when no command palette results match the search query">
The command you're looking for doesn't exist.
</Trans>
</CommandEmpty>
<ResumesCommandGroup />
@@ -73,7 +73,9 @@ export function ResumesCommandGroup() {
{resume.name}
<CommandShortcut className="opacity-0 transition-opacity group-data-[selected=true]/command-item:opacity-100">
Press <Kbd>Enter</Kbd> to open
<Trans comment="Command palette hint that pressing Enter opens the selected resume">
Press <Kbd>Enter</Kbd> to open
</Trans>
</CommandShortcut>
</CommandItem>
))
+14 -4
View File
@@ -14,6 +14,7 @@ import {
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { PencilSimpleIcon, XIcon } from "@phosphor-icons/react";
import { motion } from "motion/react";
@@ -78,7 +79,11 @@ function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProp
<button
type="button"
tabIndex={-1}
aria-label={`Edit ${chip}`}
aria-label={t({
comment:
"Screen reader label for button that edits a keyword chip. Variable is the current keyword text.",
message: `Edit ${chip}`,
})}
onClick={(e) => {
e.stopPropagation();
onEdit(index);
@@ -90,7 +95,11 @@ function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProp
<button
type="button"
tabIndex={-1}
aria-label={`Remove ${chip}`}
aria-label={t({
comment:
"Screen reader label for button that removes a keyword chip. Variable is the current keyword text.",
message: `Remove ${chip}`,
})}
onClick={(e) => {
e.stopPropagation();
onRemove(index);
@@ -122,6 +131,7 @@ export function ChipInput({ value, defaultValue = [], onChange, className, hideD
const [input, setInput] = React.useState("");
const [editingIndex, setEditingIndex] = React.useState<number | null>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
const isEditingKeyword = editingIndex !== null;
const addChip = React.useCallback(
(chip: string) => {
@@ -289,8 +299,8 @@ export function ChipInput({ value, defaultValue = [], onChange, className, hideD
type="text"
value={input}
autoComplete="off"
aria-label={editingIndex !== null ? "Edit keyword" : "Add keyword"}
placeholder={editingIndex !== null ? "Editing keyword..." : "Add a keyword..."}
aria-label={isEditingKeyword ? t`Edit keyword` : t`Add keyword`}
placeholder={isEditingKeyword ? t`Editing keyword...` : t`Add a keyword...`}
onKeyDown={handleKeyDown}
onChange={handleInputChange}
/>
+8 -3
View File
@@ -24,12 +24,17 @@ type IconSearchInputProps = {
function _IconSearchInput(props: IconSearchInputProps) {
return (
<Input
autoFocus
spellCheck={false}
inputMode="search"
value={props.value}
aria-label={t`Search for an icon`}
placeholder={t`Search for an icon`}
aria-label={t({
comment: "Accessible label for icon picker search input",
message: "Search for an icon",
})}
placeholder={t({
comment: "Placeholder text in icon picker search input",
message: "Search for an icon",
})}
onChange={(e) => props.onChange(e.currentTarget.value)}
className={cn("rounded-none border-0 focus-visible:ring-0", props.className)}
/>
+10 -2
View File
@@ -172,8 +172,16 @@ export function RichInput({ value, onChange, style, className, editorClassName,
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
<DialogContent className="flex h-[95svh] max-h-none! w-[95svw] max-w-none! flex-col p-4 sm:max-w-none! 2xl:max-w-none!">
<div className="sr-only">
<DialogTitle>Fullscreen Editor</DialogTitle>
<DialogDescription>Edit content in fullscreen mode</DialogDescription>
<DialogTitle>
<Trans comment="Screen reader title for the fullscreen rich-text editor dialog">
Fullscreen Editor
</Trans>
</DialogTitle>
<DialogDescription>
<Trans comment="Screen reader description for the fullscreen rich-text editor dialog">
Edit content in fullscreen mode
</Trans>
</DialogDescription>
</div>
{editorElement}
</DialogContent>
+9 -3
View File
@@ -69,16 +69,22 @@ export function URLInput({ value, onChange, hideLabelButton, ...props }: Props)
<Popover>
<PopoverTrigger
render={
<InputGroupButton size="icon-sm" title={t`Add a label to the URL`}>
<InputGroupButton
size="icon-sm"
title={t({
comment: "Tooltip for action button that opens URL label editor",
message: "Add a label to the URL",
})}
>
<TagIcon />
</InputGroupButton>
}
/>
<PopoverContent className="pt-3">
<div className="grid gap-2" onClick={(e) => e.stopPropagation()}>
<div role="presentation" className="grid gap-2" onMouseDown={(e) => e.stopPropagation()}>
<Label htmlFor="url-label">
<Trans>Label</Trans>
<Trans comment="Short field label for custom display text associated with a URL">Label</Trans>
</Label>
<Input id="url-label" name="url-label" value={value.label} onChange={handleLabelChange} />
</div>
+5 -2
View File
@@ -16,8 +16,11 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
return (
<div
role="presentation"
aria-label={t`Level ${level} of 5`}
role="img"
aria-label={t({
comment: "Accessible label for skill/proficiency level indicator, where level is current value out of 5",
message: `Level ${level} of 5`,
})}
className={cn(
"flex items-center gap-x-1.5",
type === "progress-bar" && "gap-x-0",
+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>
+12 -3
View File
@@ -10,7 +10,12 @@ export function Copyright({ className, ...props }: Props) {
<p>
<Trans>
Licensed under{" "}
<a href="#" target="_blank" rel="noopener" className="font-medium underline underline-offset-2">
<a
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
target="_blank"
rel="noopener"
className="font-medium underline underline-offset-2"
>
MIT
</a>
.
@@ -18,7 +23,7 @@ export function Copyright({ className, ...props }: Props) {
</p>
<p>
<Trans>By the community, for the community.</Trans>
<Trans comment="Tagline shown in app footer/about area">By the community, for the community.</Trans>
</p>
<p>
@@ -36,7 +41,11 @@ export function Copyright({ className, ...props }: Props) {
</Trans>
</p>
<p className="mt-4">Reactive Resume v{__APP_VERSION__}</p>
<p className="mt-4">
<Trans comment="App version label in footer; includes semantic version variable">
Reactive Resume v{__APP_VERSION__}
</Trans>
</p>
</div>
);
}
+2 -1
View File
@@ -50,10 +50,11 @@ function InputGroupAddon({
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
onMouseDown={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.preventDefault();
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
+5 -2
View File
@@ -2,16 +2,19 @@ import type * as React from "react";
import { cn } from "@/utils/style";
function Label({ className, ...props }: React.ComponentProps<"label">) {
function Label({ className, children, htmlFor, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
htmlFor={htmlFor}
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
>
{children}
</label>
);
}
+20 -6
View File
@@ -1,5 +1,7 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { SidebarIcon } from "@phosphor-icons/react";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
@@ -176,8 +178,12 @@ function Sidebar({
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
<SheetTitle>
<Trans comment="Dialog title for the mobile navigation sidebar panel">Sidebar</Trans>
</SheetTitle>
<SheetDescription>
<Trans comment="Dialog description for the mobile sidebar panel">Displays the mobile sidebar.</Trans>
</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
@@ -210,7 +216,7 @@ function Sidebar({
data-slot="sidebar-container"
data-side={side}
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:-left-(--sidebar-width) data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:-right-(--sidebar-width) md:flex",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+2px)]"
@@ -248,7 +254,9 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
{...props}
>
<SidebarIcon />
<span className="sr-only">Toggle Sidebar</span>
<span className="sr-only">
<Trans comment="Screen-reader-only label for button that opens or closes the app sidebar">Toggle Sidebar</Trans>
</span>
</Button>
);
}
@@ -260,10 +268,16 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
aria-label={t({
comment: "Accessible label for draggable/clickable sidebar rail that toggles sidebar visibility",
message: "Toggle Sidebar",
})}
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
title={t({
comment: "Tooltip text for sidebar rail toggle action",
message: "Toggle Sidebar",
})}
className={cn(
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:inset-s-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
+12 -1
View File
@@ -1,9 +1,20 @@
import { t } from "@lingui/core/macro";
import { SpinnerIcon } from "@phosphor-icons/react";
import { cn } from "@/utils/style";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return <SpinnerIcon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />;
return (
<SpinnerIcon
role="status"
aria-label={t({
comment: "Accessible label for loading spinner icon",
message: "Loading",
})}
className={cn("size-4 animate-spin", className)}
{...props}
/>
);
}
export { Spinner };
+16 -6
View File
@@ -22,6 +22,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/integrations/auth/client";
import { getReadableErrorMessage } from "@/utils/error-message";
import { isLocale, loadLocale, localeMap, setLocaleServerFn } from "@/utils/locale";
import { isTheme } from "@/utils/theme";
@@ -56,7 +57,16 @@ export function UserDropdownMenu({ children }: Props) {
void router.invalidate();
},
onError: ({ error }) => {
toast.error(error.message, { id: toastId });
toast.error(
getReadableErrorMessage(
error,
t({
comment: "Fallback toast when signing out fails",
message: "Failed to sign out. Please try again.",
}),
),
{ id: toastId },
);
},
},
});
@@ -73,7 +83,7 @@ export function UserDropdownMenu({ children }: Props) {
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TranslateIcon />
<Trans>Language</Trans>
<Trans comment="Menu item that opens language selection submenu">Language</Trans>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="max-h-[400px] overflow-y-auto">
<DropdownMenuRadioGroup value={i18n.locale} onValueChange={handleLocaleChange}>
@@ -89,15 +99,15 @@ export function UserDropdownMenu({ children }: Props) {
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<PaletteIcon />
<Trans>Theme</Trans>
<Trans comment="Menu item that opens appearance theme selection submenu">Theme</Trans>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={theme} onValueChange={handleThemeChange}>
<DropdownMenuRadioItem value="light">
<Trans>Light</Trans>
<Trans comment="Appearance theme option for light mode">Light</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">
<Trans>Dark</Trans>
<Trans comment="Appearance theme option for dark mode">Dark</Trans>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
@@ -108,7 +118,7 @@ export function UserDropdownMenu({ children }: Props) {
<DropdownMenuItem onClick={handleLogout}>
<SignOutIcon />
<Trans>Logout</Trans>
<Trans comment="User menu action to sign out of current account">Logout</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>