feat: add loading status to the browser

This commit is contained in:
Juan Di Toro 2023-11-01 23:01:55 +01:00
parent bc56ff0e07
commit 0ed8f0d80d
8 changed files with 135 additions and 63 deletions

View File

@ -1,21 +1,31 @@
import { defineConfig } from 'astro/config' import { defineConfig } from "astro/config";
import react from '@astrojs/react' import react from "@astrojs/react";
import tailwind from '@astrojs/tailwind' import tailwind from "@astrojs/tailwind";
import optimizer from 'vite-plugin-optimizer' import optimizer from "vite-plugin-optimizer";
import * as fs from "node:fs";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [react(), tailwind({ applyBaseStyles: false, })], integrations: [react(), tailwind({ applyBaseStyles: false })],
vite: { vite: {
build:{ server:
minify: false process.env.MODE === "development"
? {
https: {
cert: fs.readFileSync("./.local-ssl/localhost.pem"),
key: fs.readFileSync("./.local-ssl/localhost-key.pem"),
},
}
: {},
build: {
minify: false,
}, },
plugins: [ plugins: [
optimizer({ optimizer({
'node-fetch': "node-fetch":
'const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};', "const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};",
}), }),
] ],
} },
}) });

View File

@ -3,12 +3,13 @@
"type": "module", "type": "module",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "MODE=development astro dev",
"start": "astro dev", "start": "astro dev",
"prepare": "presetter bootstrap", "prepare": "presetter bootstrap",
"build": "run build", "build": "run build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"local-setup-https": "mkdir -p ./.local-ssl && mkcert -key-file ./.local-ssl/localhost-key.pem -cert-file ./.local-ssl/localhost.pem localhost"
}, },
"dependencies": { "dependencies": {
"@astrojs/react": "^3.0.3", "@astrojs/react": "^3.0.3",

BIN
src/assets/lume-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -10,26 +10,33 @@ import {
Navigator, Navigator,
} from "@/components/Browser.tsx"; } from "@/components/Browser.tsx";
import Lume from "@/components/Lume.tsx"; import Lume from "@/components/Lume.tsx";
import LogoImg from "@/assets/lume-logo.png";
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
<BrowserStateProvider> <BrowserStateProvider>
<LumeStatusProvider> <LumeStatusProvider>
<AuthProvider> <AuthProvider>
<header className="relative h-14 px-2 pl-2 py-2 w-full bg-neutral-900 flex"> <header className="relative border rounded-md m-2 border-neutral-800 px-3 py-4 w-[calc(100%-16px)] bg-neutral-900 flex flex-col">
<Navigator /> <div className="relative px-2 pl-2 py-2 w-full flex justify-between">
<div className="w-32 flex justify-end"> <div className="flex gap-x-2 my-2 mb-4 text-zinc-500">
<img src={LogoImg.src} className="w-20 h-7" />
<h2 className="border-l border-current pl-2">Web3 Browser</h2>
</div>
<div className="w-32 flex justify-end max-h-10">
<NetworksProvider> <NetworksProvider>
<Lume /> <Lume />
</NetworksProvider> </NetworksProvider>
</div> </div>
</header> </div>
<Navigator />
</header>
<Browser /> <Browser />
</AuthProvider> </AuthProvider>
</LumeStatusProvider> </LumeStatusProvider>
</BrowserStateProvider> </BrowserStateProvider>
); );
} };
export default App; export default App;

View File

@ -20,7 +20,7 @@ import {
import * as kernel from "@lumeweb/libkernel/kernel"; import * as kernel from "@lumeweb/libkernel/kernel";
import { kernelLoaded } from "@lumeweb/libkernel/kernel"; import { kernelLoaded } from "@lumeweb/libkernel/kernel";
import Arrow from "@/components/Arrow.tsx"; import Arrow from "@/components/Arrow.tsx";
import type React from "react"; import React from "react";
import { Input } from "@/components/ui/input.tsx"; import { Input } from "@/components/ui/input.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { import {
@ -35,6 +35,8 @@ let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
interface BrowserContextType { interface BrowserContextType {
url: string; url: string;
setUrl: React.Dispatch<React.SetStateAction<string>>; setUrl: React.Dispatch<React.SetStateAction<string>>;
isLoadingPage: boolean;
setIsLoadingPage: React.Dispatch<React.SetStateAction<boolean>>;
} }
const BrowserStateContext = createContext<BrowserContextType | undefined>( const BrowserStateContext = createContext<BrowserContextType | undefined>(
@ -47,9 +49,12 @@ export function BrowserStateProvider({
children: React.ReactElement; children: React.ReactElement;
}) { }) {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
return ( return (
<BrowserStateContext.Provider value={{ url, setUrl }}> <BrowserStateContext.Provider
value={{ url, setUrl, isLoadingPage, setIsLoadingPage }}
>
{children} {children}
</BrowserStateContext.Provider> </BrowserStateContext.Provider>
); );
@ -69,18 +74,22 @@ async function boot({
onInit, onInit,
onAuth, onAuth,
onBoot, onBoot,
}: {onInit: (inited: boolean) => Promise<void> | void, onAuth: (authed: boolean) => Promise<void> | void, onBoot: (booted: boolean) => Promise<void> | void}) { }: {
onInit: (inited: boolean) => Promise<void> | void;
onAuth: (authed: boolean) => Promise<void> | void;
onBoot: (booted: boolean) => Promise<void> | void;
}) {
const reg = await navigator.serviceWorker.register("/sw.js"); const reg = await navigator.serviceWorker.register("/sw.js");
await reg.update(); await reg.update();
await kernel.serviceWorkerReady(); await kernel.serviceWorkerReady();
await kernel.init().catch((err) => { await kernel.init().catch((err) => {
console.error("[Browser.tsx] Failed to init kernel", {error: err}); console.error("[Browser.tsx] Failed to init kernel", { error: err });
}); });
await onInit(true); await onInit(true);
await kernelLoaded().catch((err) => { await kernelLoaded().catch((err) => {
console.error("[Browser.tsx] Failed to load kernel", {error: err}); console.error("[Browser.tsx] Failed to load kernel", { error: err });
}); });
await onAuth(true); await onAuth(true);
@ -135,14 +144,16 @@ async function bootup() {
} }
} }
const NavInput = forwardRef<HTMLInputElement>((props: React.HTMLProps<HTMLInputElement>, ref) => { const NavInput = forwardRef<HTMLInputElement>(
return <Input ref={ref} {...props}></Input>; (props: React.InputHTMLAttributes<HTMLInputElement>, ref) => {
}); return <Input ref={ref} {...props} />;
},
);
export function Navigator() { export function Navigator() {
const { url: contextUrl, setUrl } = useBrowserState(); const { url: contextUrl, setUrl } = useBrowserState();
const { ready } = useLumeStatus(); const { ready } = useLumeStatus();
const inputEl = useRef<HTMLInputElement>(); const inputEl = useRef<HTMLInputElement | null>();
const browse = (inputValue: string) => { const browse = (inputValue: string) => {
let input = inputValue.trim(); let input = inputValue.trim();
@ -164,7 +175,7 @@ export function Navigator() {
}; };
useEffect(() => { useEffect(() => {
if(inputEl.current) { if (inputEl.current) {
inputEl.current.value = contextUrl; inputEl.current.value = contextUrl;
} }
}, [contextUrl]); }, [contextUrl]);
@ -172,21 +183,29 @@ export function Navigator() {
console.log("Navigator mounted"); console.log("Navigator mounted");
return ( return (
<form onSubmit={(e) => { <form
e.preventDefault(); onSubmit={(e) => {
const inputElement = e.target as HTMLFormElement; e.preventDefault();
const inputValue = inputElement?.elements.namedItem('url')?.value; const inputElement = e.target as HTMLFormElement;
if (inputValue) { const inputValue = (
browse(inputValue) inputElement?.elements.namedItem("url") as HTMLInputElement
} )?.value;
}} className="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center [&>input:focus]:ring-2 [&>input:focus]:ring-primary [&>input:focus+button]:ring-2 [&>input:focus+button]:ring-primary"> if (inputValue) {
browse(inputValue);
}
}}
className="relative h-full w-full rounded-full bg-neutral-800 border border-neutral-700 flex items-center [&>input:focus]:ring-2 [&>input:focus]:ring-primary [&>input:focus+button]:ring-2 [&>input:focus+button]:ring-primary"
>
<NavInput <NavInput
ref={inputEl} ref={(el) => (inputEl.current = el)}
disabled={!ready} disabled={!ready}
className="rounded-l-full border-none focus-visible:ring-offset-0" className="rounded-l-full border-none focus-visible:ring-offset-0"
name="url" name="url"
/> />
<Button disabled={!ready} className="rounded-r-full focus-visible:ring-offset-0"> <Button
disabled={!ready}
className="rounded-r-full focus-visible:ring-offset-0"
>
Navigate Navigate
<Arrow /> <Arrow />
</Button> </Button>
@ -195,7 +214,7 @@ export function Navigator() {
} }
export function Browser() { export function Browser() {
const { url, setUrl } = useBrowserState(); const { url, setUrl, isLoadingPage, setIsLoadingPage } = useBrowserState();
const status = useLumeStatus(); const status = useLumeStatus();
const auth = useAuth(); const auth = useAuth();
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
@ -203,15 +222,17 @@ export function Browser() {
useEffect(() => { useEffect(() => {
boot({ boot({
onAuth(authed) { onAuth(authed) {
auth.setIsLoggedIn(authed) auth.setIsLoggedIn(authed);
}, },
onBoot(booted) { onBoot(booted) {
status.setReady(booted) status.setReady(booted);
}, },
onInit(inited) { onInit(inited) {
status.setInited(inited) status.setInited(inited);
} },
}).catch((err) => console.error("[Browser.tsx] Failed to Boot Lume", {error: err})); }).catch((err) =>
console.error("[Browser.tsx] Failed to Boot Lume", { error: err }),
);
}, []); }, []);
const handleIframeLoad = () => { const handleIframeLoad = () => {
@ -223,6 +244,7 @@ export function Browser() {
.replace(/\/$/, ""); .replace(/\/$/, "");
if (url !== realUrl) { if (url !== realUrl) {
setUrl(realUrl); setUrl(realUrl);
setIsLoadingPage(false);
} }
} catch (e) { } catch (e) {
// This will catch errors related to cross-origin requests, in which case we can't access the iframe's contentWindow.location // This will catch errors related to cross-origin requests, in which case we can't access the iframe's contentWindow.location
@ -233,12 +255,40 @@ export function Browser() {
} }
}; };
useEffect(() => {
const iframe = iframeRef.current;
if (iframe) {
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "src"
) {
setIsLoadingPage(true);
}
}
});
observer.observe(iframe, { attributes: true });
return () => observer.disconnect(); // Clean up on unmount
}
}, []);
return ( return (
<iframe <>
ref={iframeRef} {isLoadingPage ? (
onLoad={handleIframeLoad} <div className="fixed bottom-2 left-3">
src={url ? `/browse/${url}` : "about:blank"} <span className="max-w-4xl w-full block my-2 py-1 px-4 rounded-lg opacity-80 bg-gray-900/70 border border-gray-600 text-gray-400">
className="w-full h-full" Loading {url}...
></iframe> </span>
</div>
) : null}
<iframe
ref={iframeRef}
onLoad={handleIframeLoad}
src={url ? `/browse/${url}` : "about:blank"}
className="w-full h-full"
></iframe>
</>
); );
} }

View File

@ -13,7 +13,7 @@ import "@/styles/globals.scss";
<title>Astro</title> <title>Astro</title>
</head> </head>
<body> <body>
<main> <main class="bg-neutral-950 w-full">
<App client:only="react" /> <App client:only="react" />
</main> </main>
</body> </body>

9
src/pages/sw.js.ts Normal file
View File

@ -0,0 +1,9 @@
import type { APIRoute } from "astro";
import * as fs from "node:fs";
import * as path from "node:path";
export const GET: APIRoute = ({params, request}) => {
const filePath = path.resolve(process.cwd(), "dist/sw.js");
const fileContents = fs.readFileSync(filePath);
return new Response(fileContents, { status: 200, headers: { 'Content-Type': 'application/javascript' } });
}

View File

@ -1,12 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: { theme: {
container: { container: {
center: true, center: true,