committed by
GitHub
21 changed files with 281 additions and 97 deletions
@ -37,6 +37,7 @@ |
|||||
"maplibre-gl": "5.1.1", |
"maplibre-gl": "5.1.1", |
||||
"react": "^19.0.0", |
"react": "^19.0.0", |
||||
"react-dom": "^19.0.0", |
"react-dom": "^19.0.0", |
||||
|
"react-error-boundary": "^5.0.0", |
||||
"react-hook-form": "^7.54.2", |
"react-hook-form": "^7.54.2", |
||||
"react-map-gl": "8.0.1", |
"react-map-gl": "8.0.1", |
||||
"react-qrcode-logo": "^3.0.0", |
"react-qrcode-logo": "^3.0.0", |
||||
@ -1523,6 +1524,8 @@ |
|||||
|
|
||||
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], |
"react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], |
||||
|
|
||||
|
"react-error-boundary": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ=="], |
||||
|
|
||||
"react-hook-form": ["[email protected]", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], |
"react-hook-form": ["[email protected]", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], |
||||
|
|
||||
"react-is": ["[email protected]", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], |
"react-is": ["[email protected]", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], |
||||
@ -1543,6 +1546,8 @@ |
|||||
|
|
||||
"readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], |
"readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], |
||||
|
|
||||
|
"regenerator-runtime": ["[email protected]", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], |
||||
|
|
||||
"reflect.getprototypeof": ["[email protected]", "", { "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=="], |
"reflect.getprototypeof": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], |
"regenerate": ["[email protected]", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], |
||||
|
|||||
|
After Width: | Height: | Size: 14 KiB |
@ -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 ( |
||||
|
<article className="w-full overflow-y-auto"> |
||||
|
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center"> |
||||
|
<div> |
||||
|
<Heading as="h2" className="text-text-primary"> |
||||
|
This is a little embarrassing... |
||||
|
</Heading> |
||||
|
<P> |
||||
|
We are really sorry but an error occurred in the web client that |
||||
|
caused it to crash. <br /> |
||||
|
This is not supposed to happen, and we are working hard to fix it. |
||||
|
</P> |
||||
|
<P> |
||||
|
The best way to prevent this from happening again to you or anyone |
||||
|
else is to report the issue to us. |
||||
|
</P> |
||||
|
<P>Please include the following information in your report:</P> |
||||
|
<ul className="list-disc list-inside text-sm"> |
||||
|
<li>What you were doing when the error occurred</li> |
||||
|
<li>What you expected to happen</li> |
||||
|
<li>What actually happened</li> |
||||
|
<li>Any other relevant information</li> |
||||
|
</ul> |
||||
|
<P> |
||||
|
You can report the issue to our{" "} |
||||
|
<Link |
||||
|
href={newGithubIssueUrl({ |
||||
|
repoUrl: "https://github.com/meshtastic/web", |
||||
|
template: "bug.yml", |
||||
|
title: "[Bug]: An unhandled error occurred. <Add details here>", |
||||
|
logs: error?.stack, |
||||
|
})} |
||||
|
> |
||||
|
Github |
||||
|
</Link> |
||||
|
<ExternalLink size={24} className="inline-block ml-2" /> |
||||
|
</P> |
||||
|
<P> |
||||
|
Return to the <Link href="/">dashboard</Link> |
||||
|
</P> |
||||
|
</div> |
||||
|
|
||||
|
<div className="hidden md:block md:max-w-64 lg:max-w-80 w-full aspect-suqare"> |
||||
|
<img |
||||
|
src="/images/chirpy.svg" |
||||
|
alt="Chirpy the Meshtastic error" |
||||
|
className="max-w-full h-auto" |
||||
|
/> |
||||
|
</div> |
||||
|
</section> |
||||
|
<details className="mt-8 px-4 md:px-8 text-lg md:text-xl space-y-2 text-md whitespace-pre-wrap break-all"> |
||||
|
<summary className="cursor-pointer">Error Details</summary> |
||||
|
<span className="text-sm mt-4"> |
||||
|
{error?.message && ( |
||||
|
<> |
||||
|
<label htmlFor="message">Error message:</label> |
||||
|
<p |
||||
|
id="message" |
||||
|
className="text-slate-400 break-words overflow-wrap" |
||||
|
> |
||||
|
{error.message} |
||||
|
</p> |
||||
|
</> |
||||
|
)} |
||||
|
{error?.stack && ( |
||||
|
<> |
||||
|
<label htmlFor="stack">Stack trace:</label> |
||||
|
<p |
||||
|
id="stack" |
||||
|
className="text-slate-400 break-words overflow-wrap" |
||||
|
> |
||||
|
{error.stack} |
||||
|
</p> |
||||
|
</> |
||||
|
)} |
||||
|
{!error?.message && !error?.stack && ( |
||||
|
<p className="text-slate-400">{error.toString()}</p> |
||||
|
)} |
||||
|
</span> |
||||
|
</details> |
||||
|
</article> |
||||
|
); |
||||
|
} |
||||
@ -1,9 +0,0 @@ |
|||||
export interface H1Props { |
|
||||
children: React.ReactNode; |
|
||||
} |
|
||||
|
|
||||
export const H1 = ({ children }: H1Props): JSX.Element => ( |
|
||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> |
|
||||
{children} |
|
||||
</h1> |
|
||||
); |
|
||||
@ -1,9 +0,0 @@ |
|||||
export interface H2Props { |
|
||||
children: React.ReactNode; |
|
||||
} |
|
||||
|
|
||||
export const H2 = ({ children }: H2Props): JSX.Element => ( |
|
||||
<h2 className="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"> |
|
||||
{children} |
|
||||
</h2> |
|
||||
); |
|
||||
@ -1,9 +0,0 @@ |
|||||
export interface H3Props { |
|
||||
children: React.ReactNode; |
|
||||
} |
|
||||
|
|
||||
export const H3 = ({ children }: H3Props): JSX.Element => ( |
|
||||
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight"> |
|
||||
{children} |
|
||||
</h3> |
|
||||
); |
|
||||
@ -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 => ( |
|
||||
<h4 |
|
||||
className={cn( |
|
||||
"scroll-m-20 text-xl font-semibold tracking-tight", |
|
||||
className, |
|
||||
)} |
|
||||
> |
|
||||
{children} |
|
||||
</h4> |
|
||||
); |
|
||||
@ -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 => ( |
|
||||
<h5 |
|
||||
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)} |
|
||||
> |
|
||||
{children} |
|
||||
</h5> |
|
||||
); |
|
||||
@ -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 ( |
||||
|
<Component className={`${baseStyles} ${className}`} {...props}> |
||||
|
{children} |
||||
|
</Component> |
||||
|
); |
||||
|
}; |
||||
@ -1,7 +1,10 @@ |
|||||
|
import { cn } from "@app/core/utils/cn"; |
||||
|
|
||||
export interface PProps { |
export interface PProps { |
||||
children: React.ReactNode; |
children: React.ReactNode; |
||||
|
className?: string; |
||||
} |
} |
||||
|
|
||||
export const P = ({ children }: PProps): JSX.Element => ( |
export const P = ({ children, className }: PProps) => ( |
||||
<p className="leading-7 not-first:mt-6">{children}</p> |
<p className={cn("leading-7 not-first:mt-6", className)}>{children}</p> |
||||
); |
); |
||||
|
|||||
@ -0,0 +1,88 @@ |
|||||
|
interface RepoIdentifier { |
||||
|
user: string; |
||||
|
repo: string; |
||||
|
} |
||||
|
|
||||
|
interface GithubIssueUrlOptions extends Partial<RepoIdentifier> { |
||||
|
repoUrl?: string; |
||||
|
body?: string; |
||||
|
title?: string; |
||||
|
labels?: string[]; |
||||
|
template?: string; |
||||
|
assignee?: string; |
||||
|
projects?: string[]; |
||||
|
logs?: string; |
||||
|
version?: number; |
||||
|
} |
||||
|
|
||||
|
type ValidatedOptions = { |
||||
|
repoUrl: string; |
||||
|
} & Omit<GithubIssueUrlOptions, "repoUrl" | "user" | "repo">; |
||||
|
|
||||
|
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, |
||||
|
}; |
||||
|
} |
||||
Loading…
Reference in new issue