feat: add start page + improve loading indicators

This commit is contained in:
Juan Di Toro 2023-11-03 19:06:57 +01:00
parent dd6869e6a5
commit 7e61435cc4
5 changed files with 237 additions and 32 deletions

View File

@ -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>

View File

@ -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>
</>
);
}

View File

@ -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"

View File

@ -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>
);
};

View File

@ -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: {