From 635d0673bfbddb5fd25532fa40d8fa9c306431fd Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Sat, 22 Feb 2025 13:51:50 -0500 Subject: [PATCH 1/4] feat: add error boundary --- bun.lock | 7 ++ package.json | 1 + src/App.tsx | 48 +++++----- src/PageRouter.tsx | 42 +++++++-- src/components/Form/DynamicForm.tsx | 6 +- .../PageComponents/Map/NodeDetail.tsx | 4 +- .../PageComponents/Messages/Message.tsx | 1 - src/components/UI/ErrorPage.tsx | 81 +++++++++++++++++ src/components/UI/Sidebar/SidebarSection.tsx | 6 +- src/components/UI/Typography/H1.tsx | 9 -- src/components/UI/Typography/H2.tsx | 9 -- src/components/UI/Typography/H3.tsx | 9 -- src/components/UI/Typography/H4.tsx | 17 ---- src/components/UI/Typography/H5.tsx | 14 --- src/components/UI/Typography/Heading.tsx | 30 +++++++ src/components/UI/Typography/P.tsx | 2 +- src/core/utils/github.ts | 88 +++++++++++++++++++ src/index.css | 4 + src/pages/Dashboard/index.tsx | 7 +- 19 files changed, 285 insertions(+), 100 deletions(-) create mode 100644 src/components/UI/ErrorPage.tsx delete mode 100644 src/components/UI/Typography/H1.tsx delete mode 100644 src/components/UI/Typography/H2.tsx delete mode 100644 src/components/UI/Typography/H3.tsx delete mode 100644 src/components/UI/Typography/H4.tsx delete mode 100644 src/components/UI/Typography/H5.tsx create mode 100644 src/components/UI/Typography/Heading.tsx create mode 100644 src/core/utils/github.ts diff --git a/bun.lock b/bun.lock index ee7263e3..536f5963 100644 --- a/bun.lock +++ b/bun.lock @@ -35,6 +35,7 @@ "maplibre-gl": "4.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-map-gl": "7.1.9", "react-qrcode-logo": "^3.0.0", @@ -101,6 +102,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], + "@babel/runtime": ["@babel/runtime@7.26.9", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="], + "@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="], "@babel/traverse": ["@babel/traverse@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg=="], @@ -1119,6 +1122,8 @@ "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + "react-error-boundary": ["react-error-boundary@5.0.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="], + "react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], "react-map-gl": ["react-map-gl@7.1.9", "", { "dependencies": { "@maplibre/maplibre-gl-style-spec": "^19.2.1", "@types/mapbox-gl": ">=1.0.0" }, "peerDependencies": { "mapbox-gl": ">=1.13.0", "maplibre-gl": ">=1.13.0 <5.0.0", "react": ">=16.3.0", "react-dom": ">=16.3.0" }, "optionalPeers": ["mapbox-gl", "maplibre-gl"] }, "sha512-KsCc8Gyn05wVGlHZoopaiiCr0RCAQ6LDISo5sEy1/pV/d7RlozkF946tiX7IgyijJQMRujHol5QdwUPESjh73w=="], @@ -1135,6 +1140,8 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], diff --git a/package.json b/package.json index acec3299..e6e78d97 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "maplibre-gl": "4.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-map-gl": "7.1.9", "react-qrcode-logo": "^3.0.0", diff --git a/src/App.tsx b/src/App.tsx index eb5bb1bd..2db189e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,9 @@ import { useAppStore } from "@core/stores/appStore.ts"; import { useDeviceStore } from "@core/stores/deviceStore.ts"; import { Dashboard } from "@pages/Dashboard/index.tsx"; import type { JSX } from "react"; +import { ErrorBoundary } from "react-error-boundary"; import { MapProvider } from "react-map-gl"; +import { ErrorPage } from "./components/UI/ErrorPage"; export const App = (): JSX.Element => { const { getDevice } = useDeviceStore(); @@ -22,7 +24,7 @@ export const App = (): JSX.Element => { const device = getDevice(selectedDevice); return ( - <> + { @@ -30,30 +32,30 @@ export const App = (): JSX.Element => { }} /> - - -
-
- -
- {device ? ( -
- - - + +
+
+ +
+ {device ? ( +
+ + + + -
- ) : ( - <> - -
- - )} -
+ +
+ ) : ( + <> + +
+ + )}
- - - +
+ + ); }; diff --git a/src/PageRouter.tsx b/src/PageRouter.tsx index e0e33084..33888dfe 100644 --- a/src/PageRouter.tsx +++ b/src/PageRouter.tsx @@ -4,16 +4,44 @@ import ChannelsPage from "@pages/Channels.tsx"; import ConfigPage from "@pages/Config/index.tsx"; import MessagesPage from "@pages/Messages.tsx"; import NodesPage from "@pages/Nodes.tsx"; +import { ErrorBoundary } from "react-error-boundary"; +import { ErrorPage } from "./components/UI/ErrorPage"; + +export const ErrorBoundaryWrapper = ({ + children, +}: { children: React.ReactNode }) => ( + {children} +); export const PageRouter = () => { const { activePage } = useDevice(); return ( - <> - {activePage === "messages" && } - {activePage === "map" && } - {activePage === "config" && } - {activePage === "channels" && } - {activePage === "nodes" && } - + + {activePage === "messages" && ( + + + + )} + {activePage === "map" && ( + + + + )} + {activePage === "config" && ( + + + + )} + {activePage === "channels" && ( + + + + )} + {activePage === "nodes" && ( + + + + )} + ); }; diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 7009c89a..7ab86545 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -4,7 +4,6 @@ import { } from "@components/Form/DynamicFormField.tsx"; import { FieldWrapper } from "@components/Form/FormWrapper.tsx"; import { Button } from "@components/UI/Button.tsx"; -import { H4 } from "@components/UI/Typography/H4.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { type Control, @@ -14,6 +13,7 @@ import { type SubmitHandler, useForm, } from "react-hook-form"; +import { Heading } from "../UI/Typography/Heading"; interface DisabledBy { fieldName: Path; @@ -94,7 +94,9 @@ export function DynamicForm({ {fieldGroups.map((fieldGroup) => (
-

{fieldGroup.label}

+ + {fieldGroup.label} + {fieldGroup.description}
diff --git a/src/components/PageComponents/Map/NodeDetail.tsx b/src/components/PageComponents/Map/NodeDetail.tsx index 9065c2e3..a1143335 100644 --- a/src/components/PageComponents/Map/NodeDetail.tsx +++ b/src/components/PageComponents/Map/NodeDetail.tsx @@ -1,5 +1,5 @@ import { Separator } from "@app/components/UI/Seperator"; -import { H5 } from "@app/components/UI/Typography/H5.tsx"; +import { Heading } from "@app/components/UI/Typography/Heading"; import { Subtle } from "@app/components/UI/Typography/Subtle.tsx"; import { formatQuantity } from "@app/core/utils/string"; import { Avatar } from "@components/UI/Avatar"; @@ -62,7 +62,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
-
{name}
+ {name} {hardwareType !== "UNSET" && {hardwareType}} diff --git a/src/components/PageComponents/Messages/Message.tsx b/src/components/PageComponents/Messages/Message.tsx index 30db1c1c..35d1a26a 100644 --- a/src/components/PageComponents/Messages/Message.tsx +++ b/src/components/PageComponents/Messages/Message.tsx @@ -5,7 +5,6 @@ import { TooltipProvider, TooltipTrigger, } from "@app/components/UI/Tooltip"; -import { useAppStore } from "@app/core/stores/appStore"; import { type MessageWithState, useDeviceStore, diff --git a/src/components/UI/ErrorPage.tsx b/src/components/UI/ErrorPage.tsx new file mode 100644 index 00000000..ca0921f0 --- /dev/null +++ b/src/components/UI/ErrorPage.tsx @@ -0,0 +1,81 @@ +import newGithubIssueUrl from "@app/core/utils/github"; +import { ExternalLink } from "lucide-react"; +import { Heading } from "./Typography/Heading"; +import { Link } from "./Typography/Link"; +import { P } from "./Typography/P"; + +export function ErrorPage({ error }: { error: Error }) { + if (!error) { + return null; + } + + return ( +
+
+ + This is a little embarrassing... + +

+ We are really sorry but an error occured in the web client that caused + it to crash. This is not supposed to happen and we are working hard to + fix it. +

+

+ The best way to prevent this from happening again to you or anyone + else is to report the issue to us. +

+

Please include the following information in your report:

+
    +
  • What you were doing when the error occured
  • +
  • What you expected to happen
  • +
  • What actually happened
  • +
  • Any other information you think might be relevant
  • +
+

+ You can report the issue to our{" "} + ", + logs: error?.stack, + })} + > + Github + + +

+

+ Return to the dashboard +

+ +
+ Error Details + + {error?.message ? ( + <> + +
{`${error.message}`}
+ + ) : null} + {error?.stack ? ( + <> + +
{`${error.stack}`}
+ + ) : null} + {!error?.message && !error?.stack ? ( +
{error.toString()}
+ ) : null} +
+
+
+
+ ); +} diff --git a/src/components/UI/Sidebar/SidebarSection.tsx b/src/components/UI/Sidebar/SidebarSection.tsx index 33306288..6fd53c3f 100644 --- a/src/components/UI/Sidebar/SidebarSection.tsx +++ b/src/components/UI/Sidebar/SidebarSection.tsx @@ -1,4 +1,4 @@ -import { H4 } from "@components/UI/Typography/H4.tsx"; +import { Heading } from "../Typography/Heading"; export interface SidebarSectionProps { label: string; @@ -11,7 +11,9 @@ export const SidebarSection = ({ children, }: SidebarSectionProps) => (
-

{title}

+ + {title} +
{children}
); diff --git a/src/components/UI/Typography/H1.tsx b/src/components/UI/Typography/H1.tsx deleted file mode 100644 index dd859347..00000000 --- a/src/components/UI/Typography/H1.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface H1Props { - children: React.ReactNode; -} - -export const H1 = ({ children }: H1Props): JSX.Element => ( -

- {children} -

-); diff --git a/src/components/UI/Typography/H2.tsx b/src/components/UI/Typography/H2.tsx deleted file mode 100644 index fc2afcc3..00000000 --- a/src/components/UI/Typography/H2.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface H2Props { - children: React.ReactNode; -} - -export const H2 = ({ children }: H2Props): JSX.Element => ( -

- {children} -

-); diff --git a/src/components/UI/Typography/H3.tsx b/src/components/UI/Typography/H3.tsx deleted file mode 100644 index 56eedaaa..00000000 --- a/src/components/UI/Typography/H3.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface H3Props { - children: React.ReactNode; -} - -export const H3 = ({ children }: H3Props): JSX.Element => ( -

- {children} -

-); diff --git a/src/components/UI/Typography/H4.tsx b/src/components/UI/Typography/H4.tsx deleted file mode 100644 index 33eeb0af..00000000 --- a/src/components/UI/Typography/H4.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { cn } from "@app/core/utils/cn.ts"; - -export interface H4Props { - className?: string; - children: React.ReactNode; -} - -export const H4 = ({ className, children }: H4Props): JSX.Element => ( -

- {children} -

-); diff --git a/src/components/UI/Typography/H5.tsx b/src/components/UI/Typography/H5.tsx deleted file mode 100644 index 5bd1dfb1..00000000 --- a/src/components/UI/Typography/H5.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { cn } from "@app/core/utils/cn.ts"; - -export interface H5Props { - className?: string; - children: React.ReactNode; -} - -export const H5 = ({ className, children }: H5Props): JSX.Element => ( -
- {children} -
-); diff --git a/src/components/UI/Typography/Heading.tsx b/src/components/UI/Typography/Heading.tsx new file mode 100644 index 00000000..f8a6f0f0 --- /dev/null +++ b/src/components/UI/Typography/Heading.tsx @@ -0,0 +1,30 @@ +import type React from "react"; + +const headingStyles = { + h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl", + h2: "scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700", + h3: "scroll-m-20 text-2xl font-semibold tracking-tight", + h4: "scroll-m-20 text-xl font-semibold tracking-tight", + h5: "scroll-m-20 text-lg font-medium tracking-tight", +}; + +interface HeadingProps { + as?: "h1" | "h2" | "h3" | "h4" | "h5"; + children: React.ReactNode; + className?: string; +} + +export const Heading = ({ + as: Component = "h1", + children, + className = "", + ...props +}: HeadingProps) => { + const baseStyles = headingStyles[Component] || headingStyles.h1; + + return ( + + {children} + + ); +}; diff --git a/src/components/UI/Typography/P.tsx b/src/components/UI/Typography/P.tsx index 5a89ddfb..33467596 100644 --- a/src/components/UI/Typography/P.tsx +++ b/src/components/UI/Typography/P.tsx @@ -2,6 +2,6 @@ export interface PProps { children: React.ReactNode; } -export const P = ({ children }: PProps): JSX.Element => ( +export const P = ({ children }: PProps) => (

{children}

); diff --git a/src/core/utils/github.ts b/src/core/utils/github.ts new file mode 100644 index 00000000..bed2941b --- /dev/null +++ b/src/core/utils/github.ts @@ -0,0 +1,88 @@ +interface RepoIdentifier { + user: string; + repo: string; +} + +interface GithubIssueUrlOptions extends Partial { + repoUrl?: string; + body?: string; + title?: string; + labels?: string[]; + template?: string; + assignee?: string; + projects?: string[]; + logs?: string; + version?: number; +} + +type ValidatedOptions = { + repoUrl: string; +} & Omit; + +const VALID_PARAMS = [ + "body", + "title", + "labels", + "template", + "assignee", + "projects", + "version", + "logs", +] as const; + +/** + * Generates a URL for creating a new GitHub issue + * @param options Configuration options for the GitHub issue URL + * @returns A formatted URL string for creating a new GitHub issue + * @throws {Error} If repository information is missing or invalid + * @throws {TypeError} If labels or projects are not arrays when provided + */ +export default function newGithubIssueUrl( + options: GithubIssueUrlOptions = {}, +): string { + const validatedOptions = validateOptions(options); + const url = new URL(`${validatedOptions.repoUrl}/issues/new`); + + for (const key of VALID_PARAMS) { + const value = validatedOptions[key]; + + if (value === undefined) { + continue; + } + + if ((key === "labels" || key === "projects") && Array.isArray(value)) { + url.searchParams.set(key, value.join(",")); + continue; + } + + url.searchParams.set(key, String(value)); + } + + return url.toString(); +} + +function validateOptions(options: GithubIssueUrlOptions): ValidatedOptions { + const repoUrl = + options.repoUrl ?? + (options.user && options.repo + ? `https://github.com/${options.user}/${options.repo}` + : undefined); + + if (!repoUrl) { + throw new Error( + "You need to specify either the `repoUrl` option or both the `user` and `repo` options", + ); + } + + for (const key of ["labels", "projects"] as const) { + const value = options[key]; + if (value !== undefined && !Array.isArray(value)) { + throw new TypeError(`The \`${key}\` option should be an array`); + } + } + + return { + ...options, + repoUrl, + }; +} diff --git a/src/index.css b/src/index.css index 2cffb301..333a67e9 100644 --- a/src/index.css +++ b/src/index.css @@ -76,6 +76,10 @@ ::file-selector-button { border-color: var(--color-gray-200, currentColor); } + + body { + font-family: var(--font-sans); + } } @layer components { diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index e7f3d29f..c32e72f2 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -1,8 +1,8 @@ +import { Heading } from "@app/components/UI/Typography/Heading"; import { useAppStore } from "@app/core/stores/appStore.ts"; import { useDeviceStore } from "@app/core/stores/deviceStore.ts"; import { Button } from "@components/UI/Button.tsx"; import { Separator } from "@components/UI/Seperator.tsx"; -import { H3 } from "@components/UI/Typography/H3.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { BluetoothIcon, @@ -17,7 +17,6 @@ import { useMemo } from "react"; export const Dashboard = () => { const { setConnectDialogOpen, setSelectedDevice } = useAppStore(); const { getDevices } = useDeviceStore(); - const { darkMode } = useAppStore(); const devices = useMemo(() => getDevices(), [getDevices]); @@ -26,7 +25,7 @@ export const Dashboard = () => {
-

Connected Devices

+ Connected Devices Manage, connect and disconnect devices
@@ -90,7 +89,7 @@ export const Dashboard = () => { ) : (
-

No Devices

+ No Devices Connect at least one device to get started
- + ); }; From 9f8d88bb4e354bc95eb9964130b1a3d1300a0d0c Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Tue, 25 Feb 2025 11:47:51 -0500 Subject: [PATCH 4/4] fix overflow issue with stack trace --- src/components/UI/ErrorPage.tsx | 145 ++++++++++++++++---------------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/src/components/UI/ErrorPage.tsx b/src/components/UI/ErrorPage.tsx index a94af94d..26df17e8 100644 --- a/src/components/UI/ErrorPage.tsx +++ b/src/components/UI/ErrorPage.tsx @@ -10,84 +10,85 @@ export function ErrorPage({ error }: { error: Error }) { } return ( -
-
-
-
- - This is a little embarrassing... - -

- We are really sorry but an error occured in the web client that - caused it to crash. This is not supposed to happen and we are - working hard to fix it. -

-

- The best way to prevent this from happening again to you or anyone - else is to report the issue to us. -

-

Please include the following information in your report:

-
    -
  • What you were doing when the error occured
  • -
  • What you expected to happen
  • -
  • What actually happened
  • -
  • Any other information you think might be relevant
  • -
-

- You can report the issue to our{" "} - ", - logs: error?.stack, - })} - > - Github - - -

-

- Return to the dashboard -

+
+
+
+ + This is a little embarrassing... + +

+ We are really sorry but an error occurred in the web client that + caused it to crash.
+ This is not supposed to happen, and we are working hard to fix it. +

+

+ The best way to prevent this from happening again to you or anyone + else is to report the issue to us. +

+

Please include the following information in your report:

+
    +
  • What you were doing when the error occurred
  • +
  • What you expected to happen
  • +
  • What actually happened
  • +
  • Any other relevant information
  • +
+

+ You can report the issue to our{" "} + ", + logs: error?.stack, + })} + > + Github + + +

+

+ Return to the dashboard +

+
-
- Error Details - - {error?.message ? ( - <> - -
{`${error.message}`}
- - ) : null} - {error?.stack ? ( - <> - -
{`${error.stack}`}
- - ) : null} - {!error?.message && !error?.stack ? ( -
-                    {error.toString()}
-                  
- ) : null} -
-
-
+
Chripy the Meshtastic error
+
+ Error Details + + {error?.message && ( + <> + +

+ {error.message} +

+ + )} + {error?.stack && ( + <> + +

+ {error.stack} +

+ + )} + {!error?.message && !error?.stack && ( +

{error.toString()}

+ )} +
+
); }