Add top nav and improve mobile experience

This commit is contained in:
Justin Starry
2020-04-06 01:34:04 +08:00
committed by Michael Vines
parent 18282c04db
commit 8eabff3911
10 changed files with 264 additions and 38 deletions

View File

@ -1645,6 +1645,11 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz",
"integrity": "sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw=="
},
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@ -1732,6 +1737,25 @@
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.5.tgz",
"integrity": "sha512-RZPdCtZympi6X7EkGyaU7ISiAujDYTWgqMF9owE3P6efITw27IWQykcti0BvA5h4Mu1LLl5rxrpO3r8kHyUZ/Q==",
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz",
"integrity": "sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==",
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -6212,6 +6236,11 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
@ -6347,6 +6376,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -6357,6 +6399,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
@ -9122,6 +9172,16 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz",
"integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY="
},
"mini-create-react-context": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
"requires": {
"@babel/runtime": "^7.4.0",
"gud": "^1.0.0",
"tiny-warning": "^1.0.2"
}
},
"mini-css-extract-plugin": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
@ -11702,6 +11762,52 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz",
"integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA=="
},
"react-router": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
"integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz",
"integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.1.2",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.0.tgz",
@ -12084,6 +12190,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -13615,6 +13726,11 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -14009,6 +14125,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -12,12 +12,14 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.3",
"bootstrap": "^4.4.1",
"bs58": "^4.0.1",
"node-sass": "^4.13.1",
"prettier": "^1.19.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.0",
"typescript": "^3.8.0"
},

View File

@ -1,4 +1,5 @@
import React from "react";
import { Link } from "react-router-dom";
import { ClusterProvider } from "./providers/cluster";
import {
@ -15,9 +16,11 @@ import TransactionsCard from "./components/TransactionsCard";
import ClusterModal from "./components/ClusterModal";
import TransactionModal from "./components/TransactionModal";
import Logo from "./img/logos-solana/light-explorer-logo.svg";
import { useCurrentTab, Tab } from "./providers/tab";
function App() {
const [showClusterModal, setShowClusterModal] = React.useState(false);
const currentTab = useCurrentTab();
return (
<ClusterProvider>
<TransactionsProvider>
@ -28,14 +31,39 @@ function App() {
/>
<TransactionModal />
<div className="main-content">
<nav className="navbar navbar-expand-xl navbar-light">
<div className="container">
<div className="row align-items-end">
<div className="col">
<img src={Logo} width="250" alt="Solana Explorer" />
</div>
</div>
</div>
</nav>
<div className="header">
<div className="container">
<div className="header-body">
<div className="row align-items-end">
<div className="col">
<img src={Logo} width="250" alt="Solana Explorer" />
<div className="row align-items-center d-md-none">
<div className="col-12">
<ClusterStatusButton
expand
onClick={() => setShowClusterModal(true)}
/>
</div>
<div className="col-auto">
</div>
<div className="row align-items-center">
<div className="col">
<ul className="nav nav-tabs nav-overflow header-tabs">
<li className="nav-item">
<NavLink href="/transactions" tab="Transactions" />
</li>
<li className="nav-item">
<NavLink href="/accounts" tab="Accounts" />
</li>
</ul>
</div>
<div className="col-auto d-none d-md-block">
<ClusterStatusButton
onClick={() => setShowClusterModal(true)}
/>
@ -48,13 +76,13 @@ function App() {
<div className="container">
<div className="row">
<div className="col-12">
<TransactionsCard />
{currentTab === "Transactions" ? <TransactionsCard /> : null}
</div>
</div>
<div className="row">
<div className="col-12">
<AccountsProvider>
<AccountsCard />
{currentTab === "Accounts" ? <AccountsCard /> : null}
</AccountsProvider>
</div>
</div>
@ -70,6 +98,19 @@ function App() {
);
}
function NavLink({ href, tab }: { href: string; tab: Tab }) {
let classes = "nav-link";
if (tab === useCurrentTab()) {
classes += " active";
}
return (
<Link to={href} className={classes}>
{tab}
</Link>
);
}
type OverlayProps = {
show: boolean;
onClick: () => void;

View File

@ -1,22 +1,41 @@
import React from "react";
import { useCluster, ClusterStatus, Cluster } from "../providers/cluster";
function ClusterStatusButton({ onClick }: { onClick: () => void }) {
function ClusterStatusButton({
onClick,
expand
}: {
onClick: () => void;
expand?: boolean;
}) {
return (
<div onClick={onClick}>
<Button />
<Button expand={expand} />
</div>
);
}
function Button() {
function Button({ expand }: { expand?: boolean }) {
const { status, cluster, name, customUrl } = useCluster();
const statusName = cluster !== Cluster.Custom ? `${name}` : `${customUrl}`;
const btnClasses = (variant: string) => {
if (expand) {
return `btn lift d-block btn-${variant}`;
} else {
return `btn lift btn-outline-${variant}`;
}
};
let spinnerClasses = "spinner-grow spinner-grow-sm mr-2";
if (!expand) {
spinnerClasses += " text-warning";
}
switch (status) {
case ClusterStatus.Connected:
return (
<span className="btn btn-outline-success lift">
<span className={btnClasses("success")}>
<span className="fe fe-check-circle mr-2"></span>
{statusName}
</span>
@ -24,9 +43,9 @@ function Button() {
case ClusterStatus.Connecting:
return (
<span className="btn btn-outline-warning lift">
<span className={btnClasses("warning")}>
<span
className="spinner-grow spinner-grow-sm text-warning mr-2"
className={spinnerClasses}
role="status"
aria-hidden="true"
></span>
@ -36,7 +55,7 @@ function Button() {
case ClusterStatus.Failure:
return (
<span className="btn btn-outline-danger lift">
<span className={btnClasses("danger")}>
<span className="fe fe-alert-circle mr-2"></span>
{statusName}
</span>

View File

@ -1,10 +1,19 @@
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import "./scss/theme.scss";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { TabProvider } from "./providers/tab";
ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(
<Router>
<TabProvider>
<App />
</TabProvider>
</Router>,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.

View File

@ -95,18 +95,19 @@ function reducer(state: State, action: Action): State {
return state;
}
export const ACCOUNT_PATHS = ["account", "accounts", "address", "addresses"];
function urlAddresses(): Array<string> {
const addresses: Array<string> = [];
return addresses
.concat(findGetParameter("account")?.split(",") || [])
.concat(findGetParameter("accounts")?.split(",") || [])
.concat(findPathSegment("account")?.split(",") || [])
.concat(findPathSegment("accounts")?.split(",") || [])
.concat(findGetParameter("address")?.split(",") || [])
.concat(findGetParameter("addresses")?.split(",") || [])
.concat(findPathSegment("address")?.split(",") || [])
.concat(findPathSegment("addresses")?.split(",") || [])
.filter(a => a.length > 0);
ACCOUNT_PATHS.forEach(path => {
const params = findGetParameter(path)?.split(",") || [];
const segments = findPathSegment(path)?.split(",") || [];
addresses.push(...params);
addresses.push(...segments);
});
return addresses.filter(a => a.length > 0);
}
function initState(): State {

View File

@ -0,0 +1,27 @@
import React from "react";
import { useLocation } from "react-router-dom";
import { ACCOUNT_PATHS } from "./accounts";
export type Tab = "Transactions" | "Accounts";
const StateContext = React.createContext<Tab | undefined>(undefined);
type TabProviderProps = { children: React.ReactNode };
export function TabProvider({ children }: TabProviderProps) {
const location = useLocation();
const paths = location.pathname.slice(1).split("/");
let tab: Tab = "Transactions";
if (ACCOUNT_PATHS.includes(paths[0].toLowerCase())) {
tab = "Accounts";
}
return <StateContext.Provider value={tab}>{children}</StateContext.Provider>;
}
export function useCurrentTab() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useCurrentTab must be used within a TabProvider`);
}
return context;
}

View File

@ -131,20 +131,26 @@ function reducer(state: State, action: Action): State {
return state;
}
export const TX_PATHS = [
"tx",
"txs",
"txn",
"txns",
"transaction",
"transactions"
];
function urlSignatures(): Array<string> {
const signatures: Array<string> = [];
return signatures
.concat(findGetParameter("tx")?.split(",") || [])
.concat(findGetParameter("txn")?.split(",") || [])
.concat(findGetParameter("txs")?.split(",") || [])
.concat(findGetParameter("txns")?.split(",") || [])
.concat(findGetParameter("transaction")?.split(",") || [])
.concat(findGetParameter("transactions")?.split(",") || [])
.concat(findPathSegment("tx")?.split(",") || [])
.concat(findPathSegment("txn")?.split(",") || [])
.concat(findPathSegment("transaction")?.split(",") || [])
.concat(findPathSegment("transactions")?.split(",") || [])
.filter(s => s.length > 0);
TX_PATHS.forEach(path => {
const params = findGetParameter(path)?.split(",") || [];
const segments = findPathSegment(path)?.split(",") || [];
signatures.push(...params);
signatures.push(...segments);
});
return signatures.filter(s => s.length > 0);
}
function initState(): State {

View File

@ -31,7 +31,7 @@ $rainbow-3: #79abd2;
$rainbow-4: #38d0bd;
$rainbow-5: #1dd79b;
$primary: #00ffbd;
$primary: #65D39F;
$primary-desat: #42ba96;
$secondary: $gray-700;
$success: #42ba96;

View File

@ -18,7 +18,7 @@ export function findGetParameter(parameterName: string): string | null {
.split("&")
.forEach(function(item) {
tmp = item.split("=");
if (tmp[0] === parameterName) {
if (tmp[0].toLowerCase() === parameterName) {
if (tmp.length === 2) {
result = decodeURIComponent(tmp[1]);
} else if (tmp.length === 1) {