feat: add loading status to the browser
This commit is contained in:
parent
bc56ff0e07
commit
0ed8f0d80d
|
@ -1,21 +1,31 @@
|
|||
import { defineConfig } from 'astro/config'
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
import react from '@astrojs/react'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
import optimizer from 'vite-plugin-optimizer'
|
||||
import react from "@astrojs/react";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import optimizer from "vite-plugin-optimizer";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [react(), tailwind({ applyBaseStyles: false, })],
|
||||
integrations: [react(), tailwind({ applyBaseStyles: false })],
|
||||
vite: {
|
||||
server:
|
||||
process.env.MODE === "development"
|
||||
? {
|
||||
https: {
|
||||
cert: fs.readFileSync("./.local-ssl/localhost.pem"),
|
||||
key: fs.readFileSync("./.local-ssl/localhost-key.pem"),
|
||||
},
|
||||
}
|
||||
: {},
|
||||
build: {
|
||||
minify: false
|
||||
minify: false,
|
||||
},
|
||||
plugins: [
|
||||
optimizer({
|
||||
'node-fetch':
|
||||
'const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};',
|
||||
"node-fetch":
|
||||
"const e = undefined; export default e;export {e as Response, e as FormData, e as Blob};",
|
||||
}),
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev": "MODE=development astro dev",
|
||||
"start": "astro dev",
|
||||
"prepare": "presetter bootstrap",
|
||||
"build": "run build",
|
||||
"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": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
|
@ -10,19 +10,26 @@ import {
|
|||
Navigator,
|
||||
} from "@/components/Browser.tsx";
|
||||
import Lume from "@/components/Lume.tsx";
|
||||
import LogoImg from "@/assets/lume-logo.png";
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<BrowserStateProvider>
|
||||
<LumeStatusProvider>
|
||||
<AuthProvider>
|
||||
<header className="relative h-14 px-2 pl-2 py-2 w-full bg-neutral-900 flex">
|
||||
<Navigator />
|
||||
<div className="w-32 flex justify-end">
|
||||
<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">
|
||||
<div className="relative px-2 pl-2 py-2 w-full flex justify-between">
|
||||
<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>
|
||||
<Lume />
|
||||
</NetworksProvider>
|
||||
</div>
|
||||
</div>
|
||||
<Navigator />
|
||||
</header>
|
||||
|
||||
<Browser />
|
||||
|
@ -30,6 +37,6 @@ const App: React.FC = () => {
|
|||
</LumeStatusProvider>
|
||||
</BrowserStateProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import * as kernel from "@lumeweb/libkernel/kernel";
|
||||
import { kernelLoaded } from "@lumeweb/libkernel/kernel";
|
||||
import Arrow from "@/components/Arrow.tsx";
|
||||
import type React from "react";
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import {
|
||||
|
@ -35,6 +35,8 @@ let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
|||
interface BrowserContextType {
|
||||
url: string;
|
||||
setUrl: React.Dispatch<React.SetStateAction<string>>;
|
||||
isLoadingPage: boolean;
|
||||
setIsLoadingPage: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const BrowserStateContext = createContext<BrowserContextType | undefined>(
|
||||
|
@ -47,9 +49,12 @@ export function BrowserStateProvider({
|
|||
children: React.ReactElement;
|
||||
}) {
|
||||
const [url, setUrl] = useState("");
|
||||
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<BrowserStateContext.Provider value={{ url, setUrl }}>
|
||||
<BrowserStateContext.Provider
|
||||
value={{ url, setUrl, isLoadingPage, setIsLoadingPage }}
|
||||
>
|
||||
{children}
|
||||
</BrowserStateContext.Provider>
|
||||
);
|
||||
|
@ -69,7 +74,11 @@ async function boot({
|
|||
onInit,
|
||||
onAuth,
|
||||
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");
|
||||
await reg.update();
|
||||
|
||||
|
@ -135,14 +144,16 @@ async function bootup() {
|
|||
}
|
||||
}
|
||||
|
||||
const NavInput = forwardRef<HTMLInputElement>((props: React.HTMLProps<HTMLInputElement>, ref) => {
|
||||
return <Input ref={ref} {...props}></Input>;
|
||||
});
|
||||
const NavInput = forwardRef<HTMLInputElement>(
|
||||
(props: React.InputHTMLAttributes<HTMLInputElement>, ref) => {
|
||||
return <Input ref={ref} {...props} />;
|
||||
},
|
||||
);
|
||||
|
||||
export function Navigator() {
|
||||
const { url: contextUrl, setUrl } = useBrowserState();
|
||||
const { ready } = useLumeStatus();
|
||||
const inputEl = useRef<HTMLInputElement>();
|
||||
const inputEl = useRef<HTMLInputElement | null>();
|
||||
|
||||
const browse = (inputValue: string) => {
|
||||
let input = inputValue.trim();
|
||||
|
@ -172,21 +183,29 @@ export function Navigator() {
|
|||
console.log("Navigator mounted");
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const inputElement = e.target as HTMLFormElement;
|
||||
const inputValue = inputElement?.elements.namedItem('url')?.value;
|
||||
const inputValue = (
|
||||
inputElement?.elements.namedItem("url") as HTMLInputElement
|
||||
)?.value;
|
||||
if (inputValue) {
|
||||
browse(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">
|
||||
}}
|
||||
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
|
||||
ref={inputEl}
|
||||
ref={(el) => (inputEl.current = el)}
|
||||
disabled={!ready}
|
||||
className="rounded-l-full border-none focus-visible:ring-offset-0"
|
||||
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
|
||||
<Arrow />
|
||||
</Button>
|
||||
|
@ -195,7 +214,7 @@ export function Navigator() {
|
|||
}
|
||||
|
||||
export function Browser() {
|
||||
const { url, setUrl } = useBrowserState();
|
||||
const { url, setUrl, isLoadingPage, setIsLoadingPage } = useBrowserState();
|
||||
const status = useLumeStatus();
|
||||
const auth = useAuth();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
@ -203,15 +222,17 @@ export function Browser() {
|
|||
useEffect(() => {
|
||||
boot({
|
||||
onAuth(authed) {
|
||||
auth.setIsLoggedIn(authed)
|
||||
auth.setIsLoggedIn(authed);
|
||||
},
|
||||
onBoot(booted) {
|
||||
status.setReady(booted)
|
||||
status.setReady(booted);
|
||||
},
|
||||
onInit(inited) {
|
||||
status.setInited(inited)
|
||||
}
|
||||
}).catch((err) => console.error("[Browser.tsx] Failed to Boot Lume", {error: err}));
|
||||
status.setInited(inited);
|
||||
},
|
||||
}).catch((err) =>
|
||||
console.error("[Browser.tsx] Failed to Boot Lume", { error: err }),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
|
@ -223,6 +244,7 @@ export function Browser() {
|
|||
.replace(/\/$/, "");
|
||||
if (url !== realUrl) {
|
||||
setUrl(realUrl);
|
||||
setIsLoadingPage(false);
|
||||
}
|
||||
} catch (e) {
|
||||
// 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 (
|
||||
<>
|
||||
{isLoadingPage ? (
|
||||
<div className="fixed bottom-2 left-3">
|
||||
<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">
|
||||
Loading {url}...
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
onLoad={handleIframeLoad}
|
||||
src={url ? `/browse/${url}` : "about:blank"}
|
||||
className="w-full h-full"
|
||||
></iframe>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import "@/styles/globals.scss";
|
|||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<main class="bg-neutral-950 w-full">
|
||||
<App client:only="react" />
|
||||
</main>
|
||||
</body>
|
||||
|
|
|
@ -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' } });
|
||||
}
|
|
@ -1,12 +1,7 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
|
|
Loading…
Reference in New Issue