website: redesign part 3

This commit is contained in:
nym21
2026-06-03 16:26:55 +02:00
parent a2fd1e03ad
commit 07734b8bab
17 changed files with 623 additions and 12 deletions
+1
View File
@@ -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
+8
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
main.build {
min-height: 100dvh;
display: grid;
place-items: center;
color: white;
font-size: 4rem;
}
+2 -4
View File
@@ -93,10 +93,8 @@
}
}
.liquid {
&.front.top {
--x: calc(1 - var(--fill));
}
.liquid.front.top {
--x: calc(1 - var(--fill));
}
}
+8
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
main.explore {
min-height: 100dvh;
display: grid;
place-items: center;
color: white;
font-size: 4rem;
}
-2
View File
@@ -31,8 +31,6 @@ body {
}
> nav {
grid-column: 2;
justify-self: center;
font-size: var(--font-size-xs);
ul {
+8
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
main.home {
min-height: 100dvh;
display: grid;
place-items: center;
color: white;
font-size: 4rem;
}
+7
View File
@@ -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>
+147
View File
@@ -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: [],
},
],
},
];
+147
View File
@@ -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
*/
+129
View File
@@ -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
View File
@@ -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();
+3 -1
View File
@@ -42,7 +42,9 @@ html {
font-family: var(--font-mono);
}
h1 {
h1,
h2,
h3 {
font-family: var(--font-serif);
}
+38
View File
@@ -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;
}
}
-5
View File
@@ -49,8 +49,3 @@
--font-weight-base: 400;
--max-main-width: 70dvw;
}
html,
body {
background: var(--black);
}