PageStates

Composed loader, error, and not-found states for product pages.

hardening

Hardening

This component includes product-shaped fallback behavior. Keep route-specific navigation and API error mapping in the consuming app where possible.

Recipe Source

components/ui/page-states.tsx
import { Button, ButtonLink } from "@giddaa-housing/ui/button";import {	Empty,	EmptyContent,	EmptyDescription,	EmptyHeader,	EmptyMedia,	EmptyTitle,} from "@giddaa-housing/ui/empty-state";import type { LinkProps } from "@tanstack/react-router";import { SearchX, TriangleAlert } from "lucide-react";import type { ReactNode } from "react";import { CustomApiError } from "./lib/api-error";import { cn } from "./lib/cn";import { pluralize } from "./lib/pluralize";type PageStateProps = {	title?: ReactNode;	description?: ReactNode;	action?: ReactNode;	className?: string;	fullHeight?: boolean;};type PageErrorStateProps = PageStateProps & {	children?: ReactNode;	error?: Error | null;	onRetry?: () => void;	/** The type of resource that failed to load, e.g., "House", "User", etc. */	resourceType?: string;	/** Optional link to the resource's fallback page, e.g., a listing page for the resource type. This can be used to guide users to a relevant page if the specific resource they tried to access is not found (404 error). */	resourceFallbackRoute?: LinkProps["to"];};function PageLoaderState({	title,	description,	className,	fullHeight,}: Omit<PageStateProps, "action">) {	return (		<div			aria-busy="true"			aria-live="polite"			className={cn(				"flex flex-1 flex-col items-center justify-center gap-4 px-4",				fullHeight ? " h-[calc(100dvh-72px)] lg:h-100dvh" : "min-h-[40dvh]",				className,			)}		>			<div className="flex items-end gap-1.5">				{[0, 150, 300].map((delay) => (					<span						key={delay}						className="size-2 rounded-full bg-fg-secondary animate-bounce"						style={{ animationDelay: `${delay}ms` }}					/>				))}			</div>			{(!!title || !!description) && (				<div className="text-center max-w-sm">					{title ? (						<p className="text-gdt-sm font-semibold text-fg-primary">{title}</p>					) : null}					{description ? (						<p className="text-gdt-xs text-fg-secondary">{description}</p>					) : null}				</div>			)}		</div>	);}function PageErrorState({	title = "Something went wrong",	description = "We could not load this page. Try again, or contact support if the problem continues.",	onRetry,	fullHeight,	className,	resourceType,	resourceFallbackRoute,	error,	children,}: PageErrorStateProps) {	const is404Error = error instanceof CustomApiError && error.status === 404;	return (		<Empty			className={cn(				"flex-1 justify-center",				fullHeight ? " h-[calc(100dvh-72px)] lg:h-100dvh" : "min-h-[40dvh]",				className,			)}		>			<EmptyMedia variant="icon">				<TriangleAlert />			</EmptyMedia>			<EmptyHeader>				<EmptyTitle>					{is404Error ? `${resourceType ?? "Resource"} not found` : title}				</EmptyTitle>				<EmptyDescription>					{is404Error						? `The ${resourceType?.toLowerCase() ?? "resource"} you are looking for does not exist or may have been moved.`						: description}				</EmptyDescription>			</EmptyHeader>			<EmptyContent>				{onRetry && !is404Error && (					<Button type="button" onClick={onRetry}>						Try again					</Button>				)}				{is404Error && resourceFallbackRoute && (					<ButtonLink href={String(resourceFallbackRoute)} variant="primary">						View all{" "}						{pluralize({ word: resourceType ?? "resources", count: 2 })}					</ButtonLink>				)}				{children}			</EmptyContent>		</Empty>	);}function NotFoundState({	title = "Page not found",	description = "The page you are looking for does not exist or may have been moved.",	action = (		<ButtonLink href="/" variant="primary">			Go to dashboard		</ButtonLink>	),	className,}: PageStateProps) {	return (		<Empty className={cn("min-h-[60vh]", className)}>			<EmptyMedia variant="icon">				<SearchX />			</EmptyMedia>			<EmptyHeader>				<EmptyTitle>{title}</EmptyTitle>				<EmptyDescription>{description}</EmptyDescription>			</EmptyHeader>			{action ? <EmptyContent>{action}</EmptyContent> : null}		</Empty>	);}export { PageLoaderState, PageErrorState, NotFoundState };

Usage

import { PageErrorState, PageLoaderState, NotFoundState } from "@/components/recipes/page-states";

<PageErrorState onRetry={refetch} />

Examples

Error State

Use this for consistent page-level loading, error, and not-found surfaces.

Unable to load applications

Try again or contact support if this continues.

Props

Prop/APITypeDefaultDescription
titleReactNodevariesState title.
descriptionReactNodevariesSupporting message.
onRetry() => void-Retry action for error states.
fullHeightboolean-Expands the state to fill the viewport area.

Accessibility

  • Use direct titles and recovery actions.
  • Keep route fallback links app-owned where possible.
  • Do not hide retry affordances behind icons only.

On this page