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 (
|
||||
<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="flex items-center gap-x-2 text-zinc-500">
|
||||
<img src={LogoImg.src} className="w-20 h-7" />
|
||||
<h2 className="border-l border-current pl-2">Web3 Browser</h2>
|
||||
</div>
|
||||
<a href="https://lumeweb.com">
|
||||
<div className="flex items-center gap-x-2 text-zinc-500">
|
||||
<img src={LogoImg.src} className="w-20 h-7" />
|
||||
<h2 className="border-l border-current pl-2">Web3 Browser</h2>
|
||||
</div>
|
||||
</a>
|
||||
<div className="w-32 flex justify-end h-10">
|
||||
<Lume />
|
||||
</div>
|
||||
</div>
|
||||
<Navigator />
|
||||
{ethStatus?.syncState === "syncing" ||
|
||||
{true || ethStatus?.syncState === "syncing" ||
|
||||
handshakeStatus?.syncState === "syncing" ? (
|
||||
<div className="py-4 -mb-4 flex flex-row gap-x-3">
|
||||
{ethStatus?.syncState === "syncing" ? (
|
||||
<span className="rounded-full bg-neutral-800 text-white p-1 px-4">
|
||||
<span className="font-mono mr-2 font-bold">{ethStatus.sync.toFixed(0)}%</span> Syncing Ethereum Network
|
||||
<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={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>
|
||||
) : null}
|
||||
{handshakeStatus?.syncState === "syncing" ? (
|
||||
<span className="rounded-full bg-neutral-800 text-white p-1 px-4">
|
||||
<span className="font-bold font-mono mr-2">{handshakeStatus.sync.toFixed(1)}%</span> Syncing Handshake Network
|
||||
<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={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>
|
||||
) : null}
|
||||
</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 = () => {
|
||||
return (
|
||||
<BrowserStateProvider>
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
useAuth,
|
||||
useLumeStatus,
|
||||
} from "@lumeweb/sdk";
|
||||
import StartPage from "./StartPage";
|
||||
|
||||
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() {
|
||||
const { url: contextUrl, setUrl } = useBrowserState();
|
||||
const { ready } = useLumeStatus();
|
||||
const inputEl = useRef<HTMLInputElement | null>();
|
||||
|
||||
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 {
|
||||
if(inputValue === "") {
|
||||
setUrl("about:blank")
|
||||
}
|
||||
// Try to parse it as a URL
|
||||
const url = new URL(input);
|
||||
const url = parseUrl(inputValue);
|
||||
|
||||
setUrl(url.toString() || "about:blank");
|
||||
} catch (e) {
|
||||
|
@ -197,7 +205,9 @@ export function Navigator() {
|
|||
<NavInput
|
||||
ref={(el) => (inputEl.current = el)}
|
||||
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"
|
||||
/>
|
||||
<Button
|
||||
|
@ -233,7 +243,7 @@ export function Browser() {
|
|||
);
|
||||
}, []);
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
const handleIframeLoad = (event: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
|
||||
try {
|
||||
const newUrl = iframeRef?.current?.contentWindow?.location.href as string;
|
||||
const urlObj = new URL(newUrl);
|
||||
|
@ -243,7 +253,11 @@ export function Browser() {
|
|||
if (url !== realUrl) {
|
||||
setUrl(realUrl);
|
||||
}
|
||||
setIsLoadingPage(false);
|
||||
const readyState = event.currentTarget.contentDocument?.readyState;
|
||||
console.log("[debug]",{readyState});
|
||||
if(readyState === 'interactive') {
|
||||
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
|
||||
console.warn(
|
||||
|
@ -258,20 +272,23 @@ export function Browser() {
|
|||
if (iframe) {
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
for (let mutation of mutationsList) {
|
||||
console.log("[debug] Mutated ", {mutation})
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "src"
|
||||
) {
|
||||
setIsLoadingPage(true);
|
||||
}
|
||||
) {
|
||||
setIsLoadingPage(true);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(iframe, { attributes: true });
|
||||
return () => observer.disconnect(); // Clean up on unmount
|
||||
}
|
||||
}, []);
|
||||
|
||||
const shouldRenderStartPage = !url || url === "about:blank";
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoadingPage ? (
|
||||
|
@ -281,12 +298,21 @@ export function Browser() {
|
|||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
onLoad={handleIframeLoad}
|
||||
src={url ? `/browse/${url}` : "about:blank"}
|
||||
className="w-full h-full"
|
||||
></iframe>
|
||||
{shouldRenderStartPage ? (
|
||||
<StartPage
|
||||
setUrl={(url) => {
|
||||
const _url = parseUrl(url);
|
||||
setUrl(_url.toString() || "about:blank");
|
||||
}}
|
||||
/>
|
||||
) : 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 && (
|
||||
<LumeIdentity>
|
||||
<LumeIdentityTrigger asChild disabled={!inited}>
|
||||
<LumeIdentityTrigger asChild>
|
||||
{
|
||||
<button
|
||||
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} */
|
||||
module.exports = {
|
||||
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}'],
|
||||
theme: {
|
||||
container: {
|
||||
|
|
Loading…
Reference in New Issue