diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx
index b168baee1..988252fab 100644
--- a/apps/console/src/components/PageError.tsx
+++ b/apps/console/src/components/PageError.tsx
@@ -13,17 +13,14 @@
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
-import { IconPageCross } from "@probo/ui";
+import {
+ Button,
+ ErrorDetailMessage,
+ ErrorDetails,
+ ErrorLayout,
+} from "@probo/ui";
import { useEffect, useRef } from "react";
-import { 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",
-};
+import { Link, useLocation, useRouteError } from "react-router";
type Props = {
resetErrorBoundary?: () => void;
@@ -31,11 +28,16 @@ type Props = {
};
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
- const error = useRouteError() ?? propsError;
+ const routeError = useRouteError();
+ const error = routeError ?? propsError;
const { __ } = useTranslate();
const location = useLocation();
const baseLocation = useRef(location);
+ const isFullPage = Boolean(routeError ?? propsError);
+ const isEmbeddedNotFound = !isFullPage
+ && /^\/organizations\/[^/]+/.test(location.pathname);
+
// Reset error boundary on page change
useEffect(() => {
if (
@@ -46,44 +48,49 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
}
}, [location, resetErrorBoundary]);
+ const actions = (
+
+ {__("Go home")}
+
+ );
+
+ const layoutProps = {
+ fullPage: isFullPage || !isEmbeddedNotFound,
+ showLogo: isFullPage,
+ actions,
+ };
+
if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
return (
-
-
-
- {__("Page not found")}
-
-
- {__("The page you are looking for does not exist")}
-
-
+
);
}
if (error instanceof Error && error.message.includes("FORBIDDEN")) {
return (
-
-
-
- {__("Page not found")}
-
-
- {__("The page you are looking for does not exist")}
-
-
+
);
}
return (
-
-
{__("Unexpected error :(")}
-
-
- {__("Something went wrong")}
-
- {error instanceof Error
- && {error.message}
}
-
-
+
+ {error instanceof Error && (
+
+ {error.message}
+
+ )}
+
);
}
diff --git a/apps/trust/src/components/PageError.tsx b/apps/trust/src/components/PageError.tsx
index 4a2f290d7..2d861b80d 100644
--- a/apps/trust/src/components/PageError.tsx
+++ b/apps/trust/src/components/PageError.tsx
@@ -13,17 +13,14 @@
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
-import { IconPageCross } from "@probo/ui";
+import {
+ Button,
+ ErrorDetailMessage,
+ ErrorDetails,
+ ErrorLayout,
+} from "@probo/ui";
import { useEffect, useRef } from "react";
-import { 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",
-};
+import { Link, useLocation, useRouteError } from "react-router";
type Props = {
resetErrorBoundary?: () => void;
@@ -31,11 +28,14 @@ type Props = {
};
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
- const error = useRouteError() ?? propsError;
+ const routeError = useRouteError();
+ const error = routeError ?? propsError;
const { __ } = useTranslate();
const location = useLocation();
const baseLocation = useRef(location);
+ const isFullPage = Boolean(routeError ?? propsError);
+
// Reset error boundary on page change
useEffect(() => {
if (
@@ -46,17 +46,25 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
}
}, [location, resetErrorBoundary]);
+ const actions = (
+
+ {__("Go home")}
+
+ );
+
+ const layoutProps = {
+ fullPage: isFullPage,
+ showLogo: isFullPage,
+ actions,
+ };
+
if (!error) {
return (
-
-
-
- {__("Page not found")}
-
-
- {__("The page you are looking for does not exist")}
-
-
+
);
}
@@ -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.",
);
return (
-
-
-
- {title}
-
-
{description}
-
+
);
}
return (
-
-
{__("Unexpected error :(")}
-
-
- {__("Something went wrong")}
-
- {error instanceof Error
- && {error.message}
}
-
-
+
+ {error instanceof Error && (
+
+ {error.message}
+
+ )}
+
);
}
diff --git a/packages/ui/src/Layouts/ErrorLayout.stories.tsx b/packages/ui/src/Layouts/ErrorLayout.stories.tsx
index 777e80b1d..6d36dad93 100644
--- a/packages/ui/src/Layouts/ErrorLayout.stories.tsx
+++ b/packages/ui/src/Layouts/ErrorLayout.stories.tsx
@@ -14,7 +14,12 @@
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 {
title: "Layouts/ErrorLayout",
@@ -26,7 +31,25 @@ type Story = StoryObj;
export const Default: Story = {
args: {
+ showLogo: true,
title: "Something went wrong",
- description: "An unexpected error occurred",
+ description: "We hit an unexpected error. Head back home to continue.",
+ actions: Go home ,
+ children: (
+
+
+ Relay: Missing @required value at path 'organization' in
+ 'ViewerMembershipLayoutQuery'.
+
+
+ ),
+ },
+};
+
+export const NotFound: Story = {
+ args: {
+ title: "Page not found",
+ description: "The page you are looking for does not exist.",
+ actions: Go home ,
},
};
diff --git a/packages/ui/src/Layouts/ErrorLayout.tsx b/packages/ui/src/Layouts/ErrorLayout.tsx
index e7dd99c9d..35e71011a 100644
--- a/packages/ui/src/Layouts/ErrorLayout.tsx
+++ b/packages/ui/src/Layouts/ErrorLayout.tsx
@@ -12,22 +12,106 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// 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<{
- title?: string;
- description?: string;
-}>;
+const errorLayout = tv({
+ slots: {
+ 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
+>;
+
+export function ErrorLayout({
+ title,
+ description,
+ fullPage,
+ showLogo = false,
+ actions,
+ children,
+}: ErrorLayoutProps) {
+ const classNames = errorLayout({ fullPage });
-export function ErrorLayout({ title, description, children }: Props) {
return (
-
-
-
{title}
-
{description}
- {children}
+
+
+ {showLogo && (
+
+ )}
+ {title}
+ {description && (
+ {description}
+ )}
+ {children && {children}
}
+ {actions && {actions}
}
+
);
}
+
+type ErrorDetailsProps = {
+ summary: string;
+ children: ReactNode;
+};
+
+export function ErrorDetails({ summary, children }: ErrorDetailsProps) {
+ const classNames = errorDetails();
+
+ return (
+
+
+ {summary}
+
+
+ {children}
+
+ );
+}
+
+export function ErrorDetailMessage({ children }: PropsWithChildren) {
+ const classNames = errorDetails();
+
+ return
{children}
;
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index a29f5b0c6..209c2c928 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -14,7 +14,11 @@
// Layouts
export { Drawer, Layout } from "./Layouts/Layout";
-export { ErrorLayout } from "./Layouts/ErrorLayout";
+export {
+ ErrorDetailMessage,
+ ErrorDetails,
+ ErrorLayout,
+} from "./Layouts/ErrorLayout";
export {
CenteredLayout,
CenteredLayoutSkeleton,