Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

81 linesCodeBlameRaw
1import { Component, type ErrorInfo, type ReactNode, useEffect, useRef, useState } from "react";
2import { TriangleAlert } from "lucide-react";
3import { useLocation, useRevalidator } from "react-router";
4
5import { Card } from "./ui/card";
6
7/**
8 * Says a section could not be loaded, without taking the page down.
9 * `compact`: one quiet line, for a sidebar's card or a count's place.
10 */
11export function Unavailable({ what, compact = false }: { what: string; compact?: boolean }) {
12 if (compact) {
13 return (
14 <p className="flex items-center gap-1.5 text-xs text-muted">
15 <TriangleAlert size={12} className="shrink-0 text-warn" />
16 {what} could not be loaded just now.
17 </p>
18 );
19 }
20 return (
21 <Card asChild tone="plain" className="flex items-center gap-2 border-dashed px-4 py-5 text-sm text-muted">
22 <p>
23 <TriangleAlert size={14} className="text-warn" />
24 {what} could not be loaded just now. It will be back on the next refresh.
25 </p>
26 </Card>
27 );
28}
29
30/**
31 * Keeps a part of a page that fails to draw to itself: in its place it says
32 * that `what` could not be loaded, and the rest of the page stays. It tries
33 * again on the next refresh or navigation (whatever reloads the page's
34 * data). Data a part could not fetch is the loader's to say
35 * (lib/partial.ts `part`); this catches what breaks while drawing.
36 */
37export function Boundary({ what, children, fallback }: { what: string; children: ReactNode; fallback?: ReactNode }) {
38 const { key } = useLocation();
39 const loads = useLoads();
40 return (
41 <Catch what={what} fallback={fallback} reset={`${key}:${loads}`}>
42 {children}
43 </Catch>
44 );
45}
46
47/** How many times the page's data has loaded again in place (a refresh, a form). */
48function useLoads(): number {
49 const { state } = useRevalidator();
50 const [loads, setLoads] = useState(0);
51 const was = useRef(state);
52 useEffect(() => {
53 if (was.current !== "idle" && state === "idle") setLoads((n) => n + 1);
54 was.current = state;
55 }, [state]);
56 return loads;
57}
58
59type CatchProps = { what: string; fallback?: ReactNode; reset: string; children: ReactNode };
60
61class Catch extends Component<CatchProps, { failed: boolean }> {
62 state = { failed: false };
63
64 static getDerivedStateFromError() {
65 return { failed: true };
66 }
67
68 componentDidCatch(error: unknown, info: ErrorInfo) {
69 console.error(`g1t: ${this.props.what} failed to draw`, error, info.componentStack);
70 }
71
72 componentDidUpdate(previous: CatchProps) {
73 // Fresh data, or another page: draw it again.
74 if (this.state.failed && previous.reset !== this.props.reset) this.setState({ failed: false });
75 }
76
77 render() {
78 if (this.state.failed) return this.props.fallback ?? <Unavailable what={this.props.what} />;
79 return this.props.children;
80 }
81}