PageStates
Composed loader, error, and not-found states for product pages.
hardening
components/ui/page-states.tsx
Hardening
This component includes product-shaped fallback behavior. Keep route-specific navigation and API error mapping in the consuming app where possible.
Recipe Source
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/API | Type | Default | Description |
|---|---|---|---|
title | ReactNode | varies | State title. |
description | ReactNode | varies | Supporting message. |
onRetry | () => void | - | Retry action for error states. |
fullHeight | boolean | - | 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.