feat: add start page + improve loading indicators
This commit is contained in:
parent
dd6869e6a5
commit
7e61435cc4
|
@ -25,26 +25,40 @@ const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<header className="relative border rounded-md m-4 border-neutral-800 px-3 py-4 w-[calc(100%-32px)] bg-neutral-900 flex flex-col">
|
<header className="relative border rounded-md m-4 border-neutral-800 px-3 py-4 w-[calc(100%-32px)] bg-neutral-900 flex flex-col">
|
||||||
<div className="relative px-2 my-2 mb-4 pl-2 pb-2 w-full flex justify-between">
|
<div className="relative px-2 my-2 mb-4 pl-2 pb-2 w-full flex justify-between">
|
||||||
<div className="flex items-center gap-x-2 text-zinc-500">
|
<a href="https://lumeweb.com">
|
||||||
<img src={LogoImg.src} className="w-20 h-7" />
|
<div className="flex items-center gap-x-2 text-zinc-500">
|
||||||
<h2 className="border-l border-current pl-2">Web3 Browser</h2>
|
<img src={LogoImg.src} className="w-20 h-7" />
|
||||||
</div>
|
<h2 className="border-l border-current pl-2">Web3 Browser</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
<div className="w-32 flex justify-end h-10">
|
<div className="w-32 flex justify-end h-10">
|
||||||
<Lume />
|
<Lume />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Navigator />
|
<Navigator />
|
||||||
{ethStatus?.syncState === "syncing" ||
|
{true || ethStatus?.syncState === "syncing" ||
|
||||||
handshakeStatus?.syncState === "syncing" ? (
|
handshakeStatus?.syncState === "syncing" ? (
|
||||||
<div className="py-4 -mb-4 flex flex-row gap-x-3">
|
<div className="py-4 -mb-4 flex flex-row gap-x-3">
|
||||||
{ethStatus?.syncState === "syncing" ? (
|
{ethStatus?.syncState === "syncing" ? (
|
||||||
<span className="rounded-full bg-neutral-800 text-white p-1 px-4">
|
<span className="flex items-center gap-x-2 rounded-full bg-neutral-800 text-white p-1 px-4 bg">
|
||||||
<span className="font-mono mr-2 font-bold">{ethStatus.sync.toFixed(0)}%</span> Syncing Ethereum Network
|
<CircleProgressBar radius={5} strokeWidth={3} percentage={Math.ceil(ethStatus.sync)} />
|
||||||
|
<span className="font-bold font-mono text-orange-400 mr-2">{ethStatus.sync.toFixed(1)}%</span> Syncing Ethereum Network
|
||||||
|
</span>
|
||||||
|
) : ethStatus?.syncState === "done" ? (
|
||||||
|
<span className="flex items-center gap-x-2 rounded-full bg-neutral-800 text-white p-1 px-4 bg">
|
||||||
|
<CircleProgressBar radius={5} strokeWidth={3} percentage={100} />
|
||||||
|
{" "} Ethereum Synced
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{handshakeStatus?.syncState === "syncing" ? (
|
{handshakeStatus?.syncState === "syncing" ? (
|
||||||
<span className="rounded-full bg-neutral-800 text-white p-1 px-4">
|
<span className="flex items-center gap-x-2 rounded-full bg-neutral-800 text-white p-1 px-4 bg">
|
||||||
<span className="font-bold font-mono mr-2">{handshakeStatus.sync.toFixed(1)}%</span> Syncing Handshake Network
|
<CircleProgressBar radius={5} strokeWidth={3} percentage={Math.ceil(handshakeStatus.sync)} />
|
||||||
|
<span className="font-bold font-mono text-orange-400 mr-2">{handshakeStatus.sync.toFixed(1)}%</span> Syncing Ethereum Network
|
||||||
|
</span>
|
||||||
|
) : handshakeStatus?.syncState === "done" ? (
|
||||||
|
<span className="flex items-center gap-x-2 rounded-full bg-neutral-800 text-white p-1 px-4 bg">
|
||||||
|
<CircleProgressBar radius={5} strokeWidth={3} percentage={100} />
|
||||||
|
{" "} Handshake Synced
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,6 +67,46 @@ const App: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CircleProgressBar = ({ radius, strokeWidth, textSize, percentage } : {radius: number, strokeWidth: number, textSize?: number, percentage: number}) => {
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const offset = circumference - (percentage / 100) * circumference;
|
||||||
|
const color = Math.ceil(percentage) >= 100 ? "green-500" : "orange-400"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={radius * 2 + strokeWidth} height={radius * 2 + strokeWidth}>
|
||||||
|
<circle
|
||||||
|
className="stroke-neutral-700"
|
||||||
|
fill="transparent"
|
||||||
|
r={radius}
|
||||||
|
cx={radius + strokeWidth / 2}
|
||||||
|
cy={radius + strokeWidth / 2}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className={`stroke-${color}`}
|
||||||
|
fill="transparent"
|
||||||
|
r={radius}
|
||||||
|
cx={radius + strokeWidth / 2}
|
||||||
|
cy={radius + strokeWidth / 2}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
{textSize ? <text
|
||||||
|
x="50%"
|
||||||
|
className={`fill-${color}`}
|
||||||
|
y="50%"
|
||||||
|
textAnchor="middle"
|
||||||
|
dy=".3em"
|
||||||
|
fontSize={textSize}
|
||||||
|
>
|
||||||
|
{`${percentage}%`}
|
||||||
|
</text> : null }
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Root = () => {
|
const Root = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserStateProvider>
|
<BrowserStateProvider>
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
useAuth,
|
useAuth,
|
||||||
useLumeStatus,
|
useLumeStatus,
|
||||||
} from "@lumeweb/sdk";
|
} from "@lumeweb/sdk";
|
||||||
|
import StartPage from "./StartPage";
|
||||||
|
|
||||||
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
||||||
|
|
||||||
|
@ -150,22 +151,29 @@ const NavInput = forwardRef<HTMLInputElement>(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function parseUrl(url: string) {
|
||||||
|
let input = url.trim();
|
||||||
|
|
||||||
|
// If the input doesn't contain a protocol, assume it's http
|
||||||
|
if (!input?.match(/^https?:\/\//)) {
|
||||||
|
input = `http://${input}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(input);
|
||||||
|
}
|
||||||
|
|
||||||
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 | null>();
|
const inputEl = useRef<HTMLInputElement | null>();
|
||||||
|
|
||||||
const browse = (inputValue: string) => {
|
const browse = (inputValue: string) => {
|
||||||
let input = inputValue.trim();
|
|
||||||
|
|
||||||
// If the input doesn't contain a protocol, assume it's http
|
|
||||||
if (!input?.match(/^https?:\/\//)) {
|
|
||||||
input = `http://${input}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if(inputValue === "") {
|
||||||
|
setUrl("about:blank")
|
||||||
|
}
|
||||||
// Try to parse it as a URL
|
// Try to parse it as a URL
|
||||||
const url = new URL(input);
|
const url = parseUrl(inputValue);
|
||||||
|
|
||||||
setUrl(url.toString() || "about:blank");
|
setUrl(url.toString() || "about:blank");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -197,7 +205,9 @@ export function Navigator() {
|
||||||
<NavInput
|
<NavInput
|
||||||
ref={(el) => (inputEl.current = el)}
|
ref={(el) => (inputEl.current = el)}
|
||||||
disabled={!ready}
|
disabled={!ready}
|
||||||
className={`rounded-l-full bg-neutral-800 text-white border-none focus-visible:ring-offset-0 ${!ready ? "bg-neutral-950" : ""}`}
|
className={`rounded-l-full bg-neutral-800 text-white border-none focus-visible:ring-offset-0 ${
|
||||||
|
!ready ? "bg-neutral-950" : ""
|
||||||
|
}`}
|
||||||
name="url"
|
name="url"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -233,7 +243,7 @@ export function Browser() {
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleIframeLoad = () => {
|
const handleIframeLoad = (event: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
|
||||||
try {
|
try {
|
||||||
const newUrl = iframeRef?.current?.contentWindow?.location.href as string;
|
const newUrl = iframeRef?.current?.contentWindow?.location.href as string;
|
||||||
const urlObj = new URL(newUrl);
|
const urlObj = new URL(newUrl);
|
||||||
|
@ -243,7 +253,11 @@ export function Browser() {
|
||||||
if (url !== realUrl) {
|
if (url !== realUrl) {
|
||||||
setUrl(realUrl);
|
setUrl(realUrl);
|
||||||
}
|
}
|
||||||
setIsLoadingPage(false);
|
const readyState = event.currentTarget.contentDocument?.readyState;
|
||||||
|
console.log("[debug]",{readyState});
|
||||||
|
if(readyState === 'interactive') {
|
||||||
|
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
|
||||||
console.warn(
|
console.warn(
|
||||||
|
@ -258,20 +272,23 @@ export function Browser() {
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
const observer = new MutationObserver((mutationsList, observer) => {
|
const observer = new MutationObserver((mutationsList, observer) => {
|
||||||
for (let mutation of mutationsList) {
|
for (let mutation of mutationsList) {
|
||||||
|
console.log("[debug] Mutated ", {mutation})
|
||||||
if (
|
if (
|
||||||
mutation.type === "attributes" &&
|
mutation.type === "attributes" &&
|
||||||
mutation.attributeName === "src"
|
mutation.attributeName === "src"
|
||||||
) {
|
) {
|
||||||
setIsLoadingPage(true);
|
setIsLoadingPage(true);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
observer.observe(iframe, { attributes: true });
|
observer.observe(iframe, { attributes: true });
|
||||||
return () => observer.disconnect(); // Clean up on unmount
|
return () => observer.disconnect(); // Clean up on unmount
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const shouldRenderStartPage = !url || url === "about:blank";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoadingPage ? (
|
{isLoadingPage ? (
|
||||||
|
@ -281,12 +298,21 @@ export function Browser() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<iframe
|
{shouldRenderStartPage ? (
|
||||||
ref={iframeRef}
|
<StartPage
|
||||||
onLoad={handleIframeLoad}
|
setUrl={(url) => {
|
||||||
src={url ? `/browse/${url}` : "about:blank"}
|
const _url = parseUrl(url);
|
||||||
className="w-full h-full"
|
setUrl(_url.toString() || "about:blank");
|
||||||
></iframe>
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
onLoad={handleIframeLoad}
|
||||||
|
src={url ? `/browse/${url}` : "about:blank"}
|
||||||
|
className={`${!shouldRenderStartPage ? "hidden": ""} w-full h-full`}
|
||||||
|
></iframe>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ const Lume: React.FC = () => {
|
||||||
<>
|
<>
|
||||||
{!isLoggedIn && (
|
{!isLoggedIn && (
|
||||||
<LumeIdentity>
|
<LumeIdentity>
|
||||||
<LumeIdentityTrigger asChild disabled={!inited}>
|
<LumeIdentityTrigger asChild>
|
||||||
{
|
{
|
||||||
<button
|
<button
|
||||||
className="ml-2 w-full rounded-full bg-[hsl(113,49%,55%)] text-black disabled:pointer-events-none disabled:opacity-50"
|
className="ml-2 w-full rounded-full bg-[hsl(113,49%,55%)] text-black disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { useLumeStatus } from "@lumeweb/sdk";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setUrl: (url: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AVAILABLE_PAGES = [
|
||||||
|
"blockranger.eth",
|
||||||
|
"esteroids.eth",
|
||||||
|
"ens.eth",
|
||||||
|
"sogola.eth",
|
||||||
|
"vitalik.eth",
|
||||||
|
];
|
||||||
|
|
||||||
|
const StartPage = ({ setUrl }: Props) => {
|
||||||
|
const { ready } = useLumeStatus();
|
||||||
|
return (
|
||||||
|
<div className="mx-4 relative border rounded-md mt-2 border-neutral-800 p-10 w-[calc(100%-32px)] bg-neutral-900 flex flex-col">
|
||||||
|
<h2 className="font-bold text-2xl text-white">
|
||||||
|
Welcome to the Lume Browser
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 my-4">
|
||||||
|
This browser will let you trustessly access websites with domain names
|
||||||
|
from the Ethereum Name Service (ENS) and Handshake protocol, providing a
|
||||||
|
secure and decentralized browsing experience.
|
||||||
|
</p>
|
||||||
|
{/* TODO: Add the lume loading indicators for the networks. */}
|
||||||
|
{/* <CircleProgressBar radius={20} strokeWidth={4} textSize={12} percentage={75} /> */}
|
||||||
|
{ready ? (
|
||||||
|
<div>
|
||||||
|
<hr className="my-3 border-neutral-700" />
|
||||||
|
<h3 className="text-white text-lg font-bold mt-4">
|
||||||
|
Currently Accessible Websites:
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-400 my-4">
|
||||||
|
To come back to the roots of the web, we have to change a lot of
|
||||||
|
behavior on how browsers resolve assets and make them safe by
|
||||||
|
checking their hashes in a trustlessly way. The sites listed here
|
||||||
|
are the ones we've successfully integrated with our technology.
|
||||||
|
We're working on complex tasks to ensure that file serving is
|
||||||
|
trustless and decentralized, which involves reimplementing many
|
||||||
|
functionalities that current DNSs and CDNs already provide.
|
||||||
|
</p>
|
||||||
|
<ul className="flex gap-2 flex-row flex-wrap py-3">
|
||||||
|
{AVAILABLE_PAGES.map((url, index) => (
|
||||||
|
<button
|
||||||
|
key={`AvailableSites_${index}`}
|
||||||
|
disabled={!ready}
|
||||||
|
className={`w-[calc(33%-16px)] border rounded-md py-2 text-white ${
|
||||||
|
ready
|
||||||
|
? "bg-zinc-900 border-zinc-800 hover:shadow-md hover:ring-1 hover:ring-green-400/20 hover:shadow-green-400/20 hover:transform-gpu hover:-translate-y-[3px] transition-all duration-150"
|
||||||
|
: "bg-zinc-950 border-zinc-900 cursor-not-allowed opacity-30"
|
||||||
|
}`}
|
||||||
|
onClick={() => ready && setUrl(`http://${url}`)}
|
||||||
|
>
|
||||||
|
<div className="w-full">{url}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="bg-yellow-800/40 rounded-md border border-yellow-500 text-yellow-500 p-4"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<p className="font-bold">Be patient</p>
|
||||||
|
<p>We are starting the engines.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartPage;
|
||||||
|
|
||||||
|
const CircleProgressBar = ({
|
||||||
|
radius,
|
||||||
|
strokeWidth,
|
||||||
|
textSize,
|
||||||
|
percentage,
|
||||||
|
}: {
|
||||||
|
radius: number;
|
||||||
|
strokeWidth: number;
|
||||||
|
textSize: number;
|
||||||
|
percentage: number;
|
||||||
|
}) => {
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const offset = circumference - (percentage / 100) * circumference;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={radius * 2 + strokeWidth} height={radius * 2 + strokeWidth}>
|
||||||
|
<circle
|
||||||
|
className="stroke-neutral-700"
|
||||||
|
fill="transparent"
|
||||||
|
r={radius}
|
||||||
|
cx={radius + strokeWidth / 2}
|
||||||
|
cy={radius + strokeWidth / 2}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className="stroke-primary"
|
||||||
|
fill="transparent"
|
||||||
|
r={radius}
|
||||||
|
cx={radius + strokeWidth / 2}
|
||||||
|
cy={radius + strokeWidth / 2}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
className="fill-primary"
|
||||||
|
y="50%"
|
||||||
|
textAnchor="middle"
|
||||||
|
dy=".3em"
|
||||||
|
fontSize={textSize}
|
||||||
|
>
|
||||||
|
{`${percentage}%`}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
|
safelist: ["fill-green-500", "stroke-green-500", "fill-orange-400", "stroke-orange-400"],
|
||||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
|
|
Loading…
Reference in New Issue