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 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};",
|
||||||
}),
|
}),
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
|
@ -10,19 +10,26 @@ 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>
|
||||||
|
</div>
|
||||||
|
<Navigator />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Browser />
|
<Browser />
|
||||||
|
@ -30,6 +37,6 @@ const App: React.FC = () => {
|
||||||
</LumeStatusProvider>
|
</LumeStatusProvider>
|
||||||
</BrowserStateProvider>
|
</BrowserStateProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -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
|
||||||
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const inputElement = e.target as HTMLFormElement;
|
const inputElement = e.target as HTMLFormElement;
|
||||||
const inputValue = inputElement?.elements.namedItem('url')?.value;
|
const inputValue = (
|
||||||
|
inputElement?.elements.namedItem("url") as HTMLInputElement
|
||||||
|
)?.value;
|
||||||
if (inputValue) {
|
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
|
<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 (
|
||||||
|
<>
|
||||||
|
{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
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
onLoad={handleIframeLoad}
|
onLoad={handleIframeLoad}
|
||||||
src={url ? `/browse/${url}` : "about:blank"}
|
src={url ? `/browse/${url}` : "about:blank"}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} */
|
/** @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,
|
||||||
|
|
Loading…
Reference in New Issue