mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
website: redesign part 3
This commit is contained in:
@@ -25,3 +25,4 @@ ALWAYS
|
||||
- reads like english
|
||||
- very easy to understand
|
||||
- very easy to maintain
|
||||
- avoid defensive checks when the code itself guarantees correctness
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function createBuildPage() {
|
||||
const main = document.createElement("main");
|
||||
main.className = "build";
|
||||
const title = document.createElement("h1");
|
||||
title.append("Build");
|
||||
main.append(title);
|
||||
return main;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
main.build {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
font-size: 4rem;
|
||||
}
|
||||
@@ -93,10 +93,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.liquid {
|
||||
&.front.top {
|
||||
--x: calc(1 - var(--fill));
|
||||
}
|
||||
.liquid.front.top {
|
||||
--x: calc(1 - var(--fill));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function createExplorePage() {
|
||||
const main = document.createElement("main");
|
||||
main.className = "explore";
|
||||
const title = document.createElement("h1");
|
||||
title.append("Explore");
|
||||
main.append(title);
|
||||
return main;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
main.explore {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
font-size: 4rem;
|
||||
}
|
||||
@@ -31,8 +31,6 @@ body {
|
||||
}
|
||||
|
||||
> nav {
|
||||
grid-column: 2;
|
||||
justify-self: center;
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
ul {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function createHomePage() {
|
||||
const main = document.createElement("main");
|
||||
main.className = "home";
|
||||
const title = document.createElement("h1");
|
||||
title.append("Home");
|
||||
main.append(title);
|
||||
return main;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
main.home {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
font-size: 4rem;
|
||||
}
|
||||
@@ -39,10 +39,17 @@
|
||||
|
||||
<!-- IMPORTMAP -->
|
||||
<link rel="stylesheet" href="/styles/reset.css" />
|
||||
<link rel="stylesheet" href="/styles/main.css" />
|
||||
<link rel="stylesheet" href="/styles/fonts.css" />
|
||||
<link rel="stylesheet" href="/styles/variables.css" />
|
||||
|
||||
<link rel="stylesheet" href="/cube/style.css" />
|
||||
<link rel="stylesheet" href="/header/style.css" />
|
||||
|
||||
<link rel="stylesheet" href="/home/style.css" />
|
||||
<link rel="stylesheet" href="/explore/style.css" />
|
||||
<link rel="stylesheet" href="/learn/style.css" />
|
||||
<link rel="stylesheet" href="/build/style.css" />
|
||||
<!-- /IMPORTMAP -->
|
||||
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
export const sections = [
|
||||
{
|
||||
title: "Supply",
|
||||
description:
|
||||
"How bitcoin moves from issuance into long-term ownership, profit, loss, and distribution.",
|
||||
chart: "Circulating supply",
|
||||
children: [
|
||||
{
|
||||
title: "Profitability",
|
||||
description:
|
||||
"Which coins sit in profit or loss, and how that balance changes through cycles.",
|
||||
chart: "Supply in profit",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Age",
|
||||
description:
|
||||
"How long coins have remained still, from fresh movement to deep dormancy.",
|
||||
chart: "Supply by age",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Distribution",
|
||||
description:
|
||||
"How supply is spread across addresses, scripts, cohorts, and balance ranges.",
|
||||
chart: "Supply distribution",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Capitalization",
|
||||
description:
|
||||
"Different ways to value the network by market price, realized cost, and accumulated flows.",
|
||||
chart: "Capitalization overview",
|
||||
children: [
|
||||
{
|
||||
title: "Market Cap",
|
||||
description:
|
||||
"The current market value of circulating bitcoin at spot price.",
|
||||
chart: "Market capitalization",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Realized Cap",
|
||||
description:
|
||||
"The aggregate value of coins priced where they last moved on-chain.",
|
||||
chart: "Realized capitalization",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Value Bands",
|
||||
description:
|
||||
"How market value compares with cost basis and historical valuation ranges.",
|
||||
chart: "Valuation bands",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Activity",
|
||||
description:
|
||||
"How often the chain is used, how value moves, and how demand appears in fees and transactions.",
|
||||
chart: "Network activity",
|
||||
children: [
|
||||
{
|
||||
title: "Transactions",
|
||||
description:
|
||||
"Confirmed transaction count, throughput, and block-level settlement patterns.",
|
||||
chart: "Transaction count",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Fees",
|
||||
description:
|
||||
"The cost users pay for block space and what that reveals about demand.",
|
||||
chart: "Fee rate",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Addresses",
|
||||
description:
|
||||
"Address creation, reuse, activity, and balance changes across the network.",
|
||||
chart: "Active addresses",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Mining",
|
||||
description:
|
||||
"The security budget, difficulty adjustments, pool behavior, and miner revenue.",
|
||||
chart: "Mining overview",
|
||||
children: [
|
||||
{
|
||||
title: "Hashrate",
|
||||
description:
|
||||
"Estimated computational power securing the network over time.",
|
||||
chart: "Hashrate",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Difficulty",
|
||||
description:
|
||||
"How Bitcoin adjusts mining difficulty to keep block production steady.",
|
||||
chart: "Difficulty",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Rewards",
|
||||
description:
|
||||
"Subsidy, fees, and the changing economics of block production.",
|
||||
chart: "Miner rewards",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Market",
|
||||
description:
|
||||
"Price behavior, returns, volatility, and the market context around on-chain patterns.",
|
||||
chart: "Market overview",
|
||||
children: [
|
||||
{
|
||||
title: "Price",
|
||||
description:
|
||||
"Bitcoin price across time, cycles, drawdowns, and all-time highs.",
|
||||
chart: "Price",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Returns",
|
||||
description:
|
||||
"How returns vary by holding period, entry point, and cycle phase.",
|
||||
chart: "Returns",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
title: "Volatility",
|
||||
description:
|
||||
"The scale and rhythm of price movement across different windows.",
|
||||
chart: "Volatility",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,147 @@
|
||||
import { sections } from "./data.js";
|
||||
|
||||
/** @param {string} label */
|
||||
function createChart(label) {
|
||||
const figure = document.createElement("figure");
|
||||
const chart = document.createElement("div");
|
||||
const caption = document.createElement("figcaption");
|
||||
|
||||
chart.append(label);
|
||||
caption.append(label);
|
||||
figure.append(chart, caption);
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {number[]} indexes
|
||||
*/
|
||||
function createSectionId(title, indexes) {
|
||||
return `${indexes.join("-")}-${title.toLowerCase().replaceAll(" ", "-")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Section} section
|
||||
* @param {number[]} indexes
|
||||
*/
|
||||
function createSection(section, indexes) {
|
||||
const element = document.createElement("section");
|
||||
const title = document.createElement(indexes.length === 1 ? "h1" : "h2");
|
||||
const anchor = document.createElement("a");
|
||||
const description = document.createElement("p");
|
||||
|
||||
element.id = createSectionId(section.title, indexes);
|
||||
anchor.href = `#${element.id}`;
|
||||
anchor.append(section.title);
|
||||
title.append(anchor);
|
||||
description.append(section.description);
|
||||
element.append(title, description, createChart(section.chart));
|
||||
|
||||
for (const [index, child] of section.children.entries()) {
|
||||
element.append(createSection(child, indexes.concat(index + 1)));
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ title: string, children: Section[] }} section
|
||||
* @param {number[]} indexes
|
||||
*/
|
||||
function createContentsItem(section, indexes) {
|
||||
const item = document.createElement("li");
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = `#${createSectionId(section.title, indexes)}`;
|
||||
anchor.append(section.title);
|
||||
item.append(anchor);
|
||||
|
||||
if (section.children.length) {
|
||||
const list = document.createElement("ol");
|
||||
for (const [index, child] of section.children.entries()) {
|
||||
list.append(createContentsItem(child, indexes.concat(index + 1)));
|
||||
}
|
||||
item.append(list);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function createContents() {
|
||||
const nav = document.createElement("nav");
|
||||
const list = document.createElement("ol");
|
||||
|
||||
nav.setAttribute("aria-label", "Learn contents");
|
||||
|
||||
for (const [index, section] of sections.entries()) {
|
||||
list.append(createContentsItem(section, [index + 1]));
|
||||
}
|
||||
|
||||
nav.append(list);
|
||||
return nav;
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} main */
|
||||
function initScrollSpy(main) {
|
||||
const sections = [...main.querySelectorAll("section[id]")];
|
||||
const visible = new Set();
|
||||
const links = new Map(
|
||||
[...main.querySelectorAll('nav a[href^="#"]')].map((link) => [
|
||||
link.getAttribute("href"),
|
||||
link,
|
||||
]),
|
||||
);
|
||||
|
||||
/** @type {string | null} */
|
||||
let current = null;
|
||||
|
||||
function update() {
|
||||
const section = sections.find((section) => visible.has(section.id));
|
||||
if (!section) return;
|
||||
|
||||
const hash = `#${section.id}`;
|
||||
if (hash === current) return;
|
||||
|
||||
links.get(current)?.removeAttribute("aria-current");
|
||||
links.get(hash)?.setAttribute("aria-current", "location");
|
||||
history.replaceState(null, "", `/learn${hash}`);
|
||||
current = hash;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
visible.add(entry.target.id);
|
||||
} else {
|
||||
visible.delete(entry.target.id);
|
||||
}
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
for (const section of sections) {
|
||||
observer.observe(section);
|
||||
}
|
||||
}
|
||||
|
||||
export function createLearnPage() {
|
||||
const main = document.createElement("main");
|
||||
main.className = "learn";
|
||||
const article = document.createElement("article");
|
||||
|
||||
for (const [index, section] of sections.entries()) {
|
||||
article.append(createSection(section, [index + 1]));
|
||||
}
|
||||
|
||||
main.append(article, createContents());
|
||||
initScrollSpy(main);
|
||||
return main;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Section
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} chart
|
||||
* @property {Section[]} children
|
||||
*/
|
||||
@@ -0,0 +1,129 @@
|
||||
main.learn {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 14rem;
|
||||
gap: 4rem;
|
||||
padding: 8rem 2rem 6rem;
|
||||
color: white;
|
||||
|
||||
article {
|
||||
width: min(100%, 52rem);
|
||||
justify-self: center;
|
||||
|
||||
section {
|
||||
scroll-margin-top: 6rem;
|
||||
}
|
||||
|
||||
> section + section {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
|
||||
section section {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
content: "#";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
translate: 0 -50%;
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.125em;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
color: var(--dark-white);
|
||||
}
|
||||
|
||||
figure {
|
||||
margin-top: 2rem;
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-xs);
|
||||
text-transform: uppercase;
|
||||
|
||||
> div {
|
||||
height: 18rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--dark-gray);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
> nav {
|
||||
position: sticky;
|
||||
top: 6rem;
|
||||
align-self: start;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol ol {
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -1 +1,105 @@
|
||||
import "./header/index.js";
|
||||
import { createBuildPage } from "./build/index.js";
|
||||
import { createExplorePage } from "./explore/index.js";
|
||||
import { createHomePage } from "./home/index.js";
|
||||
import { createLearnPage } from "./learn/index.js";
|
||||
|
||||
/** @type {Record<string, () => HTMLElement>} */
|
||||
const routes = {
|
||||
"/": createHomePage,
|
||||
"/build": createBuildPage,
|
||||
"/explore": createExplorePage,
|
||||
"/learn": createLearnPage,
|
||||
};
|
||||
|
||||
/** @type {HTMLElement | undefined} */
|
||||
let currentPage;
|
||||
|
||||
/** @type {Map<string, HTMLElement>} */
|
||||
const pageByPath = new Map();
|
||||
|
||||
/** @param {string} pathname */
|
||||
function normalizePath(pathname) {
|
||||
return pathname in routes ? pathname : "/";
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
function updateCurrentLink(pathname) {
|
||||
const currentPath = normalizePath(pathname);
|
||||
|
||||
for (const link of document.querySelectorAll("nav a")) {
|
||||
const linkPath = new URL(/** @type {HTMLAnchorElement} */ (link).href)
|
||||
.pathname;
|
||||
|
||||
if (linkPath === currentPath) {
|
||||
link.setAttribute("aria-current", "page");
|
||||
} else {
|
||||
link.removeAttribute("aria-current");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
function getPage(pathname) {
|
||||
let page = pageByPath.get(pathname);
|
||||
|
||||
if (!page) {
|
||||
page = routes[pathname]();
|
||||
page.inert = true;
|
||||
pageByPath.set(pathname, page);
|
||||
document.body.append(page);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/** @param {HTMLElement} page */
|
||||
function activatePage(page) {
|
||||
if (currentPage) {
|
||||
currentPage.inert = true;
|
||||
delete currentPage.dataset.active;
|
||||
}
|
||||
|
||||
page.inert = false;
|
||||
page.dataset.active = "";
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
const pathname = normalizePath(window.location.pathname);
|
||||
activatePage(getPage(pathname));
|
||||
updateCurrentLink(pathname);
|
||||
}
|
||||
|
||||
/** @param {string} pathname */
|
||||
function navigate(pathname) {
|
||||
if (pathname !== window.location.pathname) {
|
||||
history.pushState(null, "", pathname);
|
||||
}
|
||||
|
||||
renderPage();
|
||||
}
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = /** @type {HTMLAnchorElement | null} */ (
|
||||
/** @type {HTMLElement} */ (event.target).closest("a[href]")
|
||||
);
|
||||
if (!anchor) return;
|
||||
|
||||
const url = new URL(anchor.href);
|
||||
if (url.origin !== window.location.origin) return;
|
||||
if (url.pathname === window.location.pathname && url.hash) return;
|
||||
|
||||
if (!(url.pathname in routes)) return;
|
||||
|
||||
event.preventDefault();
|
||||
navigate(url.pathname);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", renderPage);
|
||||
|
||||
renderPage();
|
||||
|
||||
@@ -42,7 +42,9 @@ html {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
h1 {
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
body {
|
||||
> main {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
|
||||
> main[data-active] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body > main {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,3 @@
|
||||
--font-weight-base: 400;
|
||||
--max-main-width: 70dvw;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user