diff --git a/bun.lock b/bun.lock index 036c024d..570e06d9 100644 --- a/bun.lock +++ b/bun.lock @@ -38,6 +38,7 @@ "maplibre-gl": "5.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-map-gl": "8.0.1", "react-qrcode-logo": "^3.0.0", @@ -1524,6 +1525,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-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -1544,6 +1547,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=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], diff --git a/package.json b/package.json index 37385c8d..13646c57 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "maplibre-gl": "5.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-map-gl": "8.0.1", "react-qrcode-logo": "^3.0.0", diff --git a/public/images/chirpy.svg b/public/images/chirpy.svg new file mode 100644 index 00000000..d215662d --- /dev/null +++ b/public/images/chirpy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 2d02ab5b..c65e73e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,8 +12,11 @@ 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 { ErrorPage } from "./components/UI/ErrorPage"; import { MapProvider } from "react-map-gl/maplibre"; + export const App = (): JSX.Element => { const { getDevice } = useDeviceStore(); const { selectedDevice, setConnectDialogOpen, connectDialogOpen } = @@ -22,7 +25,7 @@ export const App = (): JSX.Element => { const device = getDevice(selectedDevice); return ( - <> + { @@ -30,30 +33,30 @@ export const App = (): JSX.Element => { }} /> - - -
-
- -
- {device ? ( -
- - - + +
+
+ +
+ {device ? ( +
+ + + + -
- ) : ( - <> - -
- - )} -
+ +
+ ) : ( + <> + +
+ + )}
- - - +
+ + ); }; diff --git a/src/PageRouter.tsx b/src/PageRouter.tsx index e0e33084..ceff7bdb 100644 --- a/src/PageRouter.tsx +++ b/src/PageRouter.tsx @@ -4,16 +4,24 @@ 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" && } - + ); }; diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index e883b004..a498307a 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 e89bcdae..2eac8d14 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 17b659b2..ce2d4705 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/PageLayout.tsx b/src/components/PageLayout.tsx index d15a8dda..4eba1221 100644 --- a/src/components/PageLayout.tsx +++ b/src/components/PageLayout.tsx @@ -1,5 +1,7 @@ import { cn } from "@app/core/utils/cn.ts"; import { AlignLeftIcon, type LucideIcon } from "lucide-react"; +import { ErrorBoundary } from "react-error-boundary"; +import { ErrorPage } from "./UI/ErrorPage"; import Footer from "./UI/Footer"; import { Spinner } from "./UI/Spinner"; @@ -23,7 +25,7 @@ export const PageLayout = ({ children, }: PageLayoutProps) => { return ( - <> +
- +
); }; diff --git a/src/components/UI/ErrorPage.tsx b/src/components/UI/ErrorPage.tsx new file mode 100644 index 00000000..26df17e8 --- /dev/null +++ b/src/components/UI/ErrorPage.tsx @@ -0,0 +1,94 @@ +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 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 +

+
+ +
+ Chirpy the Meshtastic error +
+
+
+ Error Details + + {error?.message && ( + <> + +

+ {error.message} +

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

+ {error.stack} +

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

{error.toString()}

+ )} +
+
+
+ ); +} 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..4bfe56c3 100644 --- a/src/components/UI/Typography/P.tsx +++ b/src/components/UI/Typography/P.tsx @@ -1,7 +1,10 @@ +import { cn } from "@app/core/utils/cn"; + export interface PProps { children: React.ReactNode; + className?: string; } -export const P = ({ children }: PProps): JSX.Element => ( -

{children}

+export const P = ({ children, className }: 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 ad1c4a26..ca82121b 100644 --- a/src/index.css +++ b/src/index.css @@ -76,6 +76,10 @@ ::file-selector-button { border-color: var(--color-slate-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 c8e8bc2e..66ac49c9 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, @@ -25,7 +25,7 @@ export const Dashboard = () => {
-

Connected Devices

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

No Devices

+ No Devices Connect at least one device to get started