dodil/erp-ui

src/components/states/States.tsx

/**
 * The three states every screen and panel has besides "here is the data": still loading, nothing there,
 * and failed. One look for each, so no screen invents its own — and none forgets one.
 *
 * An error shows the API's own reason (a refusal says why) and, when the caller can, a way to try again.
 */
import { Box, Button, Callout, Flex, Heading, Spinner, Text } from "@radix-ui/themes";
import type { ReactNode } from "react";

export function LoadingState({ label = "Loading…" }: { label?: string }) {
  return (
    <Flex align="center" gap="2" py="4" role="status" aria-live="polite">
      <Spinner />
      <Text color="gray">{label}</Text>
    </Flex>
  );
}

export interface EmptyStateProps {
  title: string;
  description?: ReactNode;
  /** The next step, e.g. "Import a list" — shown only when the caller can take it. */
  action?: ReactNode;
}

export function EmptyState({ title, description, action }: EmptyStateProps) {
  return (
    <Box py="5" px="4" style={{ textAlign: "center", border: "1px dashed var(--gray-a6)", borderRadius: "var(--radius-3)" }}>
      <Heading as="h3" size="3" mb="1">{title}</Heading>
      {description && <Text as="p" size="2" color="gray" mb={action ? "3" : "0"}>{description}</Text>}
      {action && <Flex justify="center">{action}</Flex>}
    </Box>
  );
}

export interface ErrorStateProps {
  /** The reason, in the API's words. */
  error: string;
  title?: string;
  onRetry?: () => void;
}

export function ErrorState({ error, title = "That did not work", onRetry }: ErrorStateProps) {
  return (
    <Callout.Root color="red" role="alert">
      <Callout.Text>
        <Text weight="bold">{title}. </Text>
        {error}
      </Callout.Text>
      {onRetry && (
        <Box>
          <Button size="1" variant="soft" color="red" onClick={onRetry}>Try again</Button>
        </Box>
      )}
    </Callout.Root>
  );
}

/** Not found — the same answer the API gives for a row the caller may not see (a 404, never a 403). */
export function NotFoundState({ what = "It", back }: { what?: string; back?: ReactNode }) {
  return (
    <Box py="6">
      <Heading as="h1" size="6" mb="2">Not found</Heading>
      <Text as="p" color="gray" mb="3">{what} does not exist, or you cannot see it.</Text>
      {back}
    </Box>
  );
}