19 changed files with 285 additions and 100 deletions
@ -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/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], |
|||
|
|||
"@babel/runtime": ["@babel/[email protected]", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg=="], |
|||
|
|||
"@babel/template": ["@babel/[email protected]", "", { "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/[email protected]", "", { "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": ["[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-map-gl": ["[email protected]", "", { "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": ["[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=="], |
|||
|
|||
"resolve": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], |
|||
|
|||
@ -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 ( |
|||
<article> |
|||
<section className="prose mx-auto mb-20 mt-28 max-w-prose px-8 text-2xl transition-all duration-150 ease-linear space-y-2"> |
|||
<Heading as="h2" className="text-text-primary"> |
|||
This is a little embarrassing... |
|||
</Heading> |
|||
<P> |
|||
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. |
|||
</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 occured</li> |
|||
<li>What you expected to happen</li> |
|||
<li>What actually happened</li> |
|||
<li>Any other information you think might be relevant</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> |
|||
|
|||
<details className="mt-6 text-md"> |
|||
<summary className="cursor-pointer">Error Details</summary> |
|||
<span className="block text-sm mt-4 overflow-auto"> |
|||
{error?.message ? ( |
|||
<> |
|||
<label htmlFor="message">Error message:</label> |
|||
<pre |
|||
id="message" |
|||
className="w-full text-slate-400" |
|||
>{`${error.message}`}</pre> |
|||
</> |
|||
) : null} |
|||
{error?.stack ? ( |
|||
<> |
|||
<label htmlFor="stack">Stack trace:</label> |
|||
<pre |
|||
id="stack" |
|||
className="w-full text-slate-400" |
|||
>{`${error.stack}`}</pre> |
|||
</> |
|||
) : null} |
|||
{!error?.message && !error?.stack ? ( |
|||
<pre className=" w-full text-slate-400">{error.toString()}</pre> |
|||
) : null} |
|||
</span> |
|||
</details> |
|||
</section> |
|||
</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> |
|||
); |
|||
}; |
|||
@ -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