@@ -13,17 +13,14 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { IconPageCross } from "@probo/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
ErrorDetailMessage,
|
||||||
|
ErrorDetails,
|
||||||
|
ErrorLayout,
|
||||||
|
} from "@probo/ui";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useLocation, useRouteError } from "react-router";
|
import { Link, useLocation, useRouteError } from "react-router";
|
||||||
|
|
||||||
const classNames = {
|
|
||||||
wrapper: "py-10 text-center space-y-2 ",
|
|
||||||
title: "text-2xl flex gap-2 font-semibold items-center justify-center",
|
|
||||||
description: "text-base text-txt-tertiary",
|
|
||||||
detail:
|
|
||||||
"text-sm text-txt-tertiary font-mono text-start border border-border-low p-2 rounded bg-level-1 mt-2",
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resetErrorBoundary?: () => void;
|
resetErrorBoundary?: () => void;
|
||||||
@@ -31,11 +28,16 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||||
const error = useRouteError() ?? propsError;
|
const routeError = useRouteError();
|
||||||
|
const error = routeError ?? propsError;
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const baseLocation = useRef(location);
|
const baseLocation = useRef(location);
|
||||||
|
|
||||||
|
const isFullPage = Boolean(routeError ?? propsError);
|
||||||
|
const isEmbeddedNotFound = !isFullPage
|
||||||
|
&& /^\/organizations\/[^/]+/.test(location.pathname);
|
||||||
|
|
||||||
// Reset error boundary on page change
|
// Reset error boundary on page change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -46,44 +48,49 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
}
|
}
|
||||||
}, [location, resetErrorBoundary]);
|
}, [location, resetErrorBoundary]);
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/">{__("Go home")}</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const layoutProps = {
|
||||||
|
fullPage: isFullPage || !isEmbeddedNotFound,
|
||||||
|
showLogo: isFullPage,
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
|
||||||
if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
|
if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>
|
{...layoutProps}
|
||||||
<IconPageCross size={26} />
|
title={__("Page not found")}
|
||||||
{__("Page not found")}
|
description={__("The page you are looking for does not exist.")}
|
||||||
</h1>
|
/>
|
||||||
<p className={classNames.description}>
|
|
||||||
{__("The page you are looking for does not exist")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error && error.message.includes("FORBIDDEN")) {
|
if (error instanceof Error && error.message.includes("FORBIDDEN")) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>
|
{...layoutProps}
|
||||||
<IconPageCross size={26} />
|
title={__("Page not found")}
|
||||||
{__("Page not found")}
|
description={__("The page you are looking for does not exist.")}
|
||||||
</h1>
|
/>
|
||||||
<p className={classNames.description}>
|
|
||||||
{__("The page you are looking for does not exist")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
|
{...layoutProps}
|
||||||
<details>
|
title={__("Something went wrong")}
|
||||||
<summary className={classNames.description}>
|
description={__("We hit an unexpected error. Head back home to continue.")}
|
||||||
{__("Something went wrong")}
|
>
|
||||||
</summary>
|
{error instanceof Error && (
|
||||||
{error instanceof Error
|
<ErrorDetails summary={__("Technical details")}>
|
||||||
&& <p className={classNames.detail}>{error.message}</p>}
|
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
||||||
</details>
|
</ErrorDetails>
|
||||||
</div>
|
)}
|
||||||
|
</ErrorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,14 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { IconPageCross } from "@probo/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
ErrorDetailMessage,
|
||||||
|
ErrorDetails,
|
||||||
|
ErrorLayout,
|
||||||
|
} from "@probo/ui";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useLocation, useRouteError } from "react-router";
|
import { Link, useLocation, useRouteError } from "react-router";
|
||||||
|
|
||||||
const classNames = {
|
|
||||||
wrapper: "py-10 text-center space-y-2 ",
|
|
||||||
title: "text-2xl flex gap-2 font-semibold items-center justify-center",
|
|
||||||
description: "text-base text-txt-tertiary",
|
|
||||||
detail:
|
|
||||||
"text-sm text-txt-tertiary font-mono text-start border border-border-low p-2 rounded bg-level-1 mt-2",
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
resetErrorBoundary?: () => void;
|
resetErrorBoundary?: () => void;
|
||||||
@@ -31,11 +28,14 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||||
const error = useRouteError() ?? propsError;
|
const routeError = useRouteError();
|
||||||
|
const error = routeError ?? propsError;
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const baseLocation = useRef(location);
|
const baseLocation = useRef(location);
|
||||||
|
|
||||||
|
const isFullPage = Boolean(routeError ?? propsError);
|
||||||
|
|
||||||
// Reset error boundary on page change
|
// Reset error boundary on page change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -46,17 +46,25 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
}
|
}
|
||||||
}, [location, resetErrorBoundary]);
|
}, [location, resetErrorBoundary]);
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<Button asChild>
|
||||||
|
<Link to="/">{__("Go home")}</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const layoutProps = {
|
||||||
|
fullPage: isFullPage,
|
||||||
|
showLogo: isFullPage,
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>
|
{...layoutProps}
|
||||||
<IconPageCross size={26} />
|
title={__("Page not found")}
|
||||||
{__("Page not found")}
|
description={__("The page you are looking for does not exist.")}
|
||||||
</h1>
|
/>
|
||||||
<p className={classNames.description}>
|
|
||||||
{__("The page you are looking for does not exist")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,26 +85,25 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
|||||||
"This access link is not valid. It may have been revoked or the link might be incorrect.",
|
"This access link is not valid. It may have been revoked or the link might be incorrect.",
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>
|
{...layoutProps}
|
||||||
<IconPageCross size={26} />
|
title={title}
|
||||||
{title}
|
description={description}
|
||||||
</h1>
|
/>
|
||||||
<p className={classNames.description}>{description}</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames.wrapper}>
|
<ErrorLayout
|
||||||
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
|
{...layoutProps}
|
||||||
<details>
|
title={__("Something went wrong")}
|
||||||
<summary className={classNames.description}>
|
description={__("We hit an unexpected error. Head back home to continue.")}
|
||||||
{__("Something went wrong")}
|
>
|
||||||
</summary>
|
{error instanceof Error && (
|
||||||
{error instanceof Error
|
<ErrorDetails summary={__("Technical details")}>
|
||||||
&& <p className={classNames.detail}>{error.message}</p>}
|
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
||||||
</details>
|
</ErrorDetails>
|
||||||
</div>
|
)}
|
||||||
|
</ErrorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,12 @@
|
|||||||
|
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
import { ErrorLayout } from "./ErrorLayout";
|
import { Button } from "../Atoms/Button/Button";
|
||||||
|
import {
|
||||||
|
ErrorDetailMessage,
|
||||||
|
ErrorDetails,
|
||||||
|
ErrorLayout,
|
||||||
|
} from "./ErrorLayout";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Layouts/ErrorLayout",
|
title: "Layouts/ErrorLayout",
|
||||||
@@ -26,7 +31,25 @@ type Story = StoryObj<typeof ErrorLayout>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
|
showLogo: true,
|
||||||
title: "Something went wrong",
|
title: "Something went wrong",
|
||||||
description: "An unexpected error occurred",
|
description: "We hit an unexpected error. Head back home to continue.",
|
||||||
|
actions: <Button>Go home</Button>,
|
||||||
|
children: (
|
||||||
|
<ErrorDetails summary="Technical details">
|
||||||
|
<ErrorDetailMessage>
|
||||||
|
Relay: Missing @required value at path 'organization' in
|
||||||
|
'ViewerMembershipLayoutQuery'.
|
||||||
|
</ErrorDetailMessage>
|
||||||
|
</ErrorDetails>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotFound: Story = {
|
||||||
|
args: {
|
||||||
|
title: "Page not found",
|
||||||
|
description: "The page you are looking for does not exist.",
|
||||||
|
actions: <Button>Go home</Button>,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,22 +12,106 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren, ReactNode } from "react";
|
||||||
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
import { IconCircleInfo } from "../Atoms/Icons";
|
import { Card } from "../Atoms/Card/Card";
|
||||||
|
import { IconChevronDown } from "../Atoms/Icons";
|
||||||
|
import { Logo } from "../Atoms/Logo/Logo";
|
||||||
|
|
||||||
type Props = PropsWithChildren<{
|
const errorLayout = tv({
|
||||||
title?: string;
|
slots: {
|
||||||
description?: string;
|
root: "w-full flex items-center justify-center px-6 py-16 bg-level-0 text-txt-primary",
|
||||||
}>;
|
card: "w-full max-w-md flex flex-col items-center text-center px-8 py-10",
|
||||||
|
brand: "w-[110px] mb-8",
|
||||||
|
title: "text-xl font-semibold tracking-tight",
|
||||||
|
description: "text-sm text-txt-secondary mt-2 leading-relaxed max-w-xs",
|
||||||
|
details: "mt-6 w-full border-t border-border-mid pt-6",
|
||||||
|
actions: "mt-8 w-full flex justify-center",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
fullPage: {
|
||||||
|
true: {
|
||||||
|
root: "min-h-screen",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
root: "py-12",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
fullPage: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorDetails = tv({
|
||||||
|
slots: {
|
||||||
|
root: "w-full text-start group",
|
||||||
|
summary:
|
||||||
|
"text-sm text-txt-tertiary cursor-pointer select-none list-none flex items-center justify-center gap-1 hover:text-txt-secondary transition-colors [&::-webkit-details-marker]:hidden",
|
||||||
|
marker: "transition-transform group-open:rotate-180",
|
||||||
|
panel: "mt-3 rounded-lg bg-subtle px-3 py-2.5",
|
||||||
|
message: "text-xs font-mono text-txt-tertiary break-all leading-relaxed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ErrorLayoutProps = PropsWithChildren<
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actions?: ReactNode;
|
||||||
|
showLogo?: boolean;
|
||||||
|
} & VariantProps<typeof errorLayout>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function ErrorLayout({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
fullPage,
|
||||||
|
showLogo = false,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
}: ErrorLayoutProps) {
|
||||||
|
const classNames = errorLayout({ fullPage });
|
||||||
|
|
||||||
export function ErrorLayout({ title, description, children }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full gap-2 flex flex-col items-center justify-center">
|
<div className={classNames.root()}>
|
||||||
<IconCircleInfo className="text-txt-danger" size={40} />
|
<Card className={classNames.card()}>
|
||||||
<h1 className="text-4xl font-bold">{title}</h1>
|
{showLogo && (
|
||||||
<p className="text-txt-secondary">{description}</p>
|
<Logo withPicto className={classNames.brand()} />
|
||||||
{children}
|
)}
|
||||||
|
<h1 className={classNames.title()}>{title}</h1>
|
||||||
|
{description && (
|
||||||
|
<p className={classNames.description()}>{description}</p>
|
||||||
|
)}
|
||||||
|
{children && <div className={classNames.details()}>{children}</div>}
|
||||||
|
{actions && <div className={classNames.actions()}>{actions}</div>}
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrorDetailsProps = {
|
||||||
|
summary: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ErrorDetails({ summary, children }: ErrorDetailsProps) {
|
||||||
|
const classNames = errorDetails();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className={classNames.root()}>
|
||||||
|
<summary className={classNames.summary()}>
|
||||||
|
{summary}
|
||||||
|
<IconChevronDown size={14} className={classNames.marker()} />
|
||||||
|
</summary>
|
||||||
|
<div className={classNames.panel()}>{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorDetailMessage({ children }: PropsWithChildren) {
|
||||||
|
const classNames = errorDetails();
|
||||||
|
|
||||||
|
return <p className={classNames.message()}>{children}</p>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
|
|
||||||
// Layouts
|
// Layouts
|
||||||
export { Drawer, Layout } from "./Layouts/Layout";
|
export { Drawer, Layout } from "./Layouts/Layout";
|
||||||
export { ErrorLayout } from "./Layouts/ErrorLayout";
|
export {
|
||||||
|
ErrorDetailMessage,
|
||||||
|
ErrorDetails,
|
||||||
|
ErrorLayout,
|
||||||
|
} from "./Layouts/ErrorLayout";
|
||||||
export {
|
export {
|
||||||
CenteredLayout,
|
CenteredLayout,
|
||||||
CenteredLayoutSkeleton,
|
CenteredLayoutSkeleton,
|
||||||
|
|||||||
Reference in New Issue
Block a user