committed by
GitHub
21 changed files with 281 additions and 97 deletions
@ -37,6 +37,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", |
|||
@ -1523,6 +1524,8 @@ |
|||
|
|||
"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-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=="], |
|||
|
|||
"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=="], |
|||
|
|||
"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 { |
|||
children: React.ReactNode; |
|||
className?: string; |
|||
} |
|||
|
|||
export const P = ({ children }: PProps): JSX.Element => ( |
|||
<p className="leading-7 not-first:mt-6">{children}</p> |
|||
export const P = ({ children, className }: PProps) => ( |
|||
<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