Skip to content

Commit d7a5f1a

Browse files
authored
Fix the error boundary so it is actually functional (#6)
1 parent 172704a commit d7a5f1a

15 files changed

Lines changed: 264 additions & 123 deletions

File tree

src/lib/components/Modal.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { AnimatePresence, motion } from "framer-motion";
2+
import { stopPropagation } from "@lib/utils/dom/stopPropagation";
3+
import { clsx } from "@lib/utils/clsx";
4+
5+
const MODAL = "fixed left-0 top-0 bottom-0 right-0 z-20";
6+
7+
export function Modal({
8+
isOpen,
9+
onClose,
10+
children,
11+
className,
12+
}: {
13+
isOpen: boolean;
14+
onClose?: () => void;
15+
children?: React.ReactNode;
16+
className?: string;
17+
}) {
18+
return (
19+
<AnimatePresence>
20+
{isOpen ? (
21+
<>
22+
<motion.div
23+
initial={{
24+
opacity: 0,
25+
}}
26+
animate={{ opacity: 1 }}
27+
exit={{ opacity: 0 }}
28+
transition={{ duration: 0.2, ease: "easeInOut" }}
29+
className={clsx(MODAL, "bg-black bg-opacity-50 backdrop-blur")}
30+
/>
31+
<motion.div
32+
initial={{ opacity: 0, y: 200 }}
33+
animate={{ opacity: 1, y: 0 }}
34+
exit={{ opacity: 0, y: 200 }}
35+
transition={{ duration: 0.2, ease: "easeInOut" }}
36+
className={clsx(MODAL, "flex justify-center items-center")}
37+
aria-hidden
38+
onClick={onClose}
39+
>
40+
<div
41+
// Stop propagation of the click event to prevent the modal from closing
42+
onClick={stopPropagation}
43+
className={clsx("bg-background p-4 rounded", className)}
44+
>
45+
{children}
46+
</div>
47+
</motion.div>
48+
</>
49+
) : null}
50+
</AnimatePresence>
51+
);
52+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "zod";
2+
import { useStorage } from "../useStorage";
3+
import { useCallback } from "react";
4+
5+
const ModalKey = z.literal("error-boundary");
6+
type ModalKey = z.infer<typeof ModalKey>;
7+
8+
const ModalDismissed = z.record(
9+
ModalKey,
10+
z.object({
11+
at: z.coerce.date(),
12+
duration: z.literal("permanent"),
13+
}),
14+
);
15+
type ModalDismissed = z.infer<typeof ModalDismissed>;
16+
17+
export function useModalDismissed({ key }: { key: ModalKey }) {
18+
const { data, error, setData, mutate, isLoading } = useStorage({
19+
key: "modal-dismissed",
20+
validator: ModalDismissed.parseAsync,
21+
storageType: "local",
22+
});
23+
24+
const dismiss = useCallback(() => {
25+
return setData({ [key]: { at: new Date(), duration: "permanent" } });
26+
}, [setData, key]);
27+
const restore = useCallback(() => {
28+
return setData({ [key]: undefined });
29+
}, [setData, key]);
30+
31+
return {
32+
isLoading,
33+
isDismissed: data?.[key] !== undefined,
34+
error,
35+
mutate,
36+
dismiss,
37+
restore,
38+
};
39+
}

src/lib/hooks/useStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useEffect } from "react";
22
import useSWR from "swr";
33

4-
type Key = "prompts";
4+
type Key = "modal-dismissed";
55

66
type Listener = Parameters<
77
typeof chrome.storage.local.onChanged.addListener

src/lib/integrations/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EError, EErrorOptions } from "@lib/utils/error";
22
import { IntegrationPlatform } from "./core";
3-
import { WithPartial } from "@lib/utils/WithPartial";
3+
import { WithPartial } from "@lib/utils/typeUtils/WithPartial";
44

55
type EIntegrationTargetErrorData = {
66
url: string;

src/lib/integrations/github.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { findDOMNodeByContent } from "@lib/utils/findDOMNodeByContent";
1+
import { WithPartial } from "@lib/utils/typeUtils/WithPartial";
22
import { Integration } from "./core";
33
import {
44
EIntegrationParseError,
55
EIntegrationParseErrorData,
66
EIntegrationTargetError,
77
} from "./error";
88
import { EErrorOptions } from "@lib/utils/error";
9-
import { WithPartial } from "@lib/utils/WithPartial";
109
import _ from "lodash";
10+
import { findDOMNodeByContent } from "@lib/utils/dom/findDOMNodeByContent";
1111

1212
type EGithubParseErrorData = EIntegrationParseErrorData & {
1313
integration: "github";

src/lib/utils/dom/portal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type PortalProps = { portal: HTMLElement | DocumentFragment };

src/lib/utils/dom/shadow.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { EError, EErrorOptions } from "../error";
2+
import styles from "../../../tailwind.css?inline";
3+
import { WithPartial } from "../typeUtils/WithPartial";
4+
5+
type EShadowErrorData = {
6+
selector: { content: string };
7+
};
8+
export class EShadowError extends EError<EShadowErrorData> {
9+
constructor({
10+
message = "open-devpod-browser-extension is unable to attach an element to the page.",
11+
data,
12+
}: WithPartial<EErrorOptions<EShadowErrorData>, "message">) {
13+
super({ message, data });
14+
this.name = "EShadowError";
15+
}
16+
}
17+
18+
function attachStyles(target: HTMLElement | DocumentFragment) {
19+
const style = document.createElement("style");
20+
style.innerHTML = styles;
21+
target.appendChild(style);
22+
}
23+
24+
export function attachShadow<E extends Element = Element>(target: E | null) {
25+
if (!target) {
26+
throw new EShadowError({});
27+
}
28+
const shadowTarget = document.createElement("div");
29+
target.appendChild(shadowTarget);
30+
const shadow = shadowTarget.attachShadow({ mode: "closed" });
31+
attachStyles(shadow);
32+
return { shadow, target: shadowTarget };
33+
}

src/lib/utils/error.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WithPartial } from "./WithPartial";
1+
import { WithPartial } from "./typeUtils/WithPartial";
22
import { safeStringify } from "./safeStringify";
33

44
export type EErrorOptions<T = unknown> = {
@@ -73,22 +73,7 @@ export class EError<T = unknown> extends Error {
7373
}
7474

7575
public static encode(error: unknown): string {
76-
return Buffer.from(safeStringify(this.serialize(error))).toString(
77-
"base64url",
78-
);
79-
}
80-
}
81-
82-
type EShadowErrorData = {
83-
selector: { content: string };
84-
};
85-
export class EShadowError extends EError<EShadowErrorData> {
86-
constructor({
87-
message = "open-devpod-browser-extension is unable to attach an element to the page.",
88-
data,
89-
}: WithPartial<EErrorOptions<EShadowErrorData>, "message">) {
90-
super({ message, data });
91-
this.name = "EShadowError";
76+
return btoa(safeStringify(this.serialize(error)));
9277
}
9378
}
9479

0 commit comments

Comments
 (0)