Skip to content

Commit 6f34470

Browse files
Merge pull request #337 from MMesch/mmesch/bd-design
Design overhaul 2
2 parents c0e2f60 + 9d53af9 commit 6f34470

290 files changed

Lines changed: 4019 additions & 14505 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docusaurus.config.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const config: Config = {
2020
organizationName: "/HaudinFlorence/", // Usually your GitHub org/user name.
2121
projectName: "quantstack.github.io", // Usually your repo name.
2222

23+
clientModules: [
24+
require.resolve("./src/clientModules/navbarScroll.ts"),
25+
require.resolve("./src/clientModules/backgroundScene.ts"),
26+
],
27+
2328
onBrokenLinks: "warn",
2429
onBrokenMarkdownLinks: "warn",
2530
staticDirectories: ["static"],
@@ -107,6 +112,12 @@ const config: Config = {
107112
},
108113

109114
items: [
115+
{
116+
to: "/",
117+
className: "custom_navbar_item navbar_hide_mid",
118+
label: "Home",
119+
position: "left",
120+
},
110121
{
111122
to: "/projects/",
112123
className: "custom_navbar_item",
@@ -122,7 +133,7 @@ const config: Config = {
122133
{
123134
to: "/about/",
124135
className: "custom_navbar_item",
125-
label: "About us",
136+
label: "About",
126137
position: "left",
127138
},
128139
{
@@ -138,16 +149,22 @@ const config: Config = {
138149
position: "left",
139150
},
140151
{
141-
to: "/fundable/",
142-
label: "Fundable projects",
143-
position: "right",
144-
className: "fundable_projects"
152+
to: "/sponsor/",
153+
className: "custom_navbar_item",
154+
label: "Sponsor",
155+
position: "left",
145156
},
146157
{
147158
to: "/contact/",
148-
label: "Contact us",
159+
className: "custom_navbar_item",
160+
label: "Contact",
161+
position: "left",
162+
},
163+
{
164+
to: "/notebooklink/",
165+
className: "navbar_notebooklink",
166+
label: "Notebook.link",
149167
position: "right",
150-
className: "contact",
151168
},
152169
{
153170
to: "https://github.com/QuantStack",

flake.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
3+
4+
outputs = { self, nixpkgs }:
5+
let
6+
system = "x86_64-linux";
7+
pkgs = nixpkgs.legacyPackages.${system};
8+
in {
9+
devShells.${system}.default = pkgs.mkShell {
10+
buildInputs = with pkgs; [
11+
nodejs_20
12+
python3
13+
pkg-config
14+
cairo
15+
pango
16+
libpng
17+
libjpeg
18+
giflib
19+
librsvg
20+
pixman
21+
];
22+
};
23+
};
24+
}

scripts/generate-atom-feed.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import { Feed } from 'feed';
3-
import { blogpostsDetails } from '../src/components/blog/blogpostsDetails.js';
3+
import { blogpostsDetails } from '../src/pages/blogs/_blogpostsDetails.js';
44
import path from 'path';
55
import { fileURLToPath } from 'url';
66
const __filename = fileURLToPath(import.meta.url);

scripts/generate-rss-feed.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs';
22
import RSS from 'rss';
3-
import { blogpostsDetails } from '../src/components/blog/blogpostsDetails.js';
3+
import { blogpostsDetails } from '../src/pages/blogs/_blogpostsDetails.js';
44
import path from 'path';
55
import { fileURLToPath } from 'url';
66
const __filename = fileURLToPath(import.meta.url);
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Injects a fixed full-page background scene (planets, graph, code snippets)
2+
// directly into document.body. Color switches to white when a dark section
3+
// is in the centre of the viewport, detected via a scroll listener.
4+
5+
// ─── SVG data ────────────────────────────────────────────────────────────────
6+
7+
const NODES: [number, number][] = [
8+
[110, 95], [360, 60], [680, 85], [990, 50], [1230, 105],
9+
[1385, 190], [1350, 410], [1160, 330], [1030, 190], [870, 295],
10+
[760, 470], [1210, 590], [1030, 740], [800, 830], [570, 775],
11+
[340, 845], [140, 755], [62, 510],
12+
];
13+
14+
const EDGES: [number, number][] = [
15+
[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[4,8],[8,9],[9,10],
16+
[7,6],[6,11],[11,12],[12,13],[13,14],[14,15],[15,16],[16,17],
17+
[2,9],[10,14],[7,11],[3,8],[0,17],
18+
];
19+
20+
const SNIPPETS = [
21+
{ text: "mamba install xtensor", x: 770, y: 765, angle: -4 },
22+
{ text: "import ipywidgets as w", x: 1075, y: 540, angle: -6 },
23+
{ text: "voila dashboard.ipynb", x: 1295, y: 435, angle: 5 },
24+
{ text: "xt::arange<double>(10)", x: 305, y: 855, angle: 7 },
25+
{ text: "jupyter lite build", x: 920, y: 115, angle: -3 },
26+
{ text: "import pyarrow as pa", x: 180, y: 380, angle: 4 },
27+
];
28+
29+
function buildSVG(): string {
30+
const edges = EDGES.map(([a, b]) =>
31+
`<line x1="${NODES[a][0]}" y1="${NODES[a][1]}" x2="${NODES[b][0]}" y2="${NODES[b][1]}" stroke="currentColor" stroke-width="1"/>`,
32+
).join("");
33+
34+
const nodes = NODES.map(([x, y]) =>
35+
`<circle cx="${x}" cy="${y}" r="4" fill="currentColor"/>`,
36+
).join("");
37+
38+
const snippets = SNIPPETS.map(({ text, x, y, angle }) =>
39+
`<text transform="translate(${x},${y}) rotate(${angle})">${text}</text>`,
40+
).join("");
41+
42+
return `<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice"
43+
width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
44+
<style>
45+
#_pbg_p { animation: pbg-p 58s ease-in-out infinite; }
46+
#_pbg_g { animation: pbg-g 72s ease-in-out infinite; animation-delay:-18s; }
47+
#_pbg_s { animation: pbg-s 44s ease-in-out infinite; animation-delay:-9s; }
48+
@keyframes pbg-p {
49+
0%,100%{transform:translate(0,0)} 33%{transform:translate(11px,-16px)} 66%{transform:translate(-8px,10px)}
50+
}
51+
@keyframes pbg-g {
52+
0%,100%{transform:translate(0,0)} 50%{transform:translate(-13px,8px)}
53+
}
54+
@keyframes pbg-s {
55+
0%,100%{transform:translate(0,0)} 40%{transform:translate(7px,-11px)} 75%{transform:translate(-5px,13px)}
56+
}
57+
</style>
58+
<g id="_pbg_p" opacity="0.09">
59+
<circle cx="-40" cy="875" r="178" fill="currentColor"/>
60+
<ellipse cx="-40" cy="875" rx="285" ry="46" fill="none" stroke="currentColor" stroke-width="2.5" transform="rotate(-22,-40,875)"/>
61+
<circle cx="1382" cy="72" r="64" fill="currentColor"/>
62+
<ellipse cx="1382" cy="72" rx="102" ry="21" fill="none" stroke="currentColor" stroke-width="1.8" transform="rotate(-18,1382,72)"/>
63+
<circle cx="428" cy="138" r="32" fill="currentColor"/>
64+
<ellipse cx="428" cy="138" rx="54" ry="12" fill="none" stroke="currentColor" stroke-width="1.4" transform="rotate(-28,428,138)"/>
65+
<circle cx="82" cy="642" r="26" fill="currentColor"/>
66+
<circle cx="215" cy="48" r="21" fill="currentColor"/>
67+
<circle cx="648" cy="36" r="11" fill="currentColor"/>
68+
<circle cx="1052" cy="828" r="9" fill="currentColor"/>
69+
<circle cx="582" cy="882" r="14" fill="currentColor"/>
70+
</g>
71+
<g id="_pbg_g" opacity="0.14">${edges}${nodes}</g>
72+
<g id="_pbg_s" opacity="0.11" fill="currentColor"
73+
font-family="'Roboto Mono','Courier New',monospace" font-size="13">${snippets}</g>
74+
</svg>`;
75+
}
76+
77+
// ─── DOM element ──────────────────────────────────────────────────────────────
78+
79+
let bgEl: HTMLDivElement | null = null;
80+
81+
function mount(): void {
82+
// Recover existing element after HMR resets the module variable
83+
if (!bgEl) {
84+
bgEl = document.getElementById("page-background-scene") as HTMLDivElement | null;
85+
}
86+
if (bgEl) return;
87+
88+
bgEl = document.createElement("div");
89+
bgEl.setAttribute("aria-hidden", "true");
90+
bgEl.setAttribute("id", "page-background-scene");
91+
Object.assign(bgEl.style, {
92+
position: "fixed",
93+
inset: "0",
94+
zIndex: "1",
95+
pointerEvents: "none",
96+
userSelect: "none",
97+
overflow: "hidden",
98+
color: "#1d1d1b",
99+
transition: "color 0.6s ease",
100+
});
101+
bgEl.innerHTML = buildSVG();
102+
document.body.appendChild(bgEl);
103+
}
104+
105+
function applyDark(dark: boolean): void {
106+
if (!bgEl) return;
107+
bgEl.style.color = dark ? "#ffffff" : "#1d1d1b";
108+
const p = bgEl.querySelector<SVGGElement>("#_pbg_p");
109+
const g = bgEl.querySelector<SVGGElement>("#_pbg_g");
110+
const s = bgEl.querySelector<SVGGElement>("#_pbg_s");
111+
if (p) p.setAttribute("opacity", dark ? "0.12" : "0.09");
112+
if (g) g.setAttribute("opacity", dark ? "0.18" : "0.14");
113+
if (s) s.setAttribute("opacity", dark ? "0.15" : "0.11");
114+
}
115+
116+
// ─── Scroll-based dark detection ──────────────────────────────────────────────
117+
// Re-queries the DOM on every scroll so it always reflects the current page.
118+
119+
function updateColor(): void {
120+
const vh = window.innerHeight;
121+
const darkEls = document.querySelectorAll<HTMLElement>("[data-section-bg='dark']");
122+
let anyDark = false;
123+
darkEls.forEach((el) => {
124+
const { top, bottom } = el.getBoundingClientRect();
125+
// Trigger when the dark section covers the middle 60 % of the viewport
126+
if (top < vh * 0.8 && bottom > vh * 0.2) anyDark = true;
127+
});
128+
applyDark(anyDark);
129+
}
130+
131+
let scrollListenerAdded = false;
132+
133+
// ─── Route lifecycle ──────────────────────────────────────────────────────────
134+
135+
export function onRouteDidUpdate(): void {
136+
mount();
137+
138+
// Add the scroll listener once (persists across SPA navigations)
139+
if (!scrollListenerAdded) {
140+
window.addEventListener("scroll", updateColor, { passive: true });
141+
scrollListenerAdded = true;
142+
}
143+
144+
// Always reset to light first, then re-check after the DOM settles
145+
applyDark(false);
146+
requestAnimationFrame(updateColor);
147+
}

src/clientModules/navbarScroll.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const THRESHOLD = 20;
2+
const DARK_HERO_PAGES = ["/notebooklink/"];
3+
const LOGO_DEFAULT = "/img/quantstack/logo-website-smaller.svg";
4+
const LOGO_WHITE_TEXT = "/img/quantstack/logo-website-white-text.svg";
5+
6+
function update(): void {
7+
const scrolled = window.scrollY > THRESHOLD;
8+
const { pathname } = window.location;
9+
const isHome = pathname === "/";
10+
const isDarkHero = DARK_HERO_PAGES.some((p) => pathname.startsWith(p));
11+
const useDarkLogo = isDarkHero && !scrolled;
12+
document.documentElement.toggleAttribute("data-navbar-scrolled", scrolled);
13+
document.documentElement.toggleAttribute(
14+
"data-navbar-home-top",
15+
isHome && !scrolled
16+
);
17+
document.documentElement.toggleAttribute("data-navbar-dark-top", useDarkLogo);
18+
const logoImg = document.querySelector(".navbar__logo img") as HTMLImageElement | null;
19+
if (logoImg) {
20+
logoImg.src = useDarkLogo ? LOGO_WHITE_TEXT : LOGO_DEFAULT;
21+
}
22+
}
23+
24+
export function onRouteDidUpdate(): void {
25+
update();
26+
}
27+
28+
if (typeof window !== "undefined") {
29+
window.addEventListener("scroll", update, { passive: true });
30+
update();
31+
}

src/components/Blog.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useState } from "react";
2+
import CardGrid from "./layout/CardGrid";
3+
import BlogpostCard from "./BlogpostCard";
4+
import { blogpostsDetails } from "../pages/blogs/_blogpostsDetails";
5+
import styles from "../pages/blog.module.css";
6+
7+
export default function BlogGrid() {
8+
const [searchField, setSearchField] = useState("");
9+
10+
const filtered = blogpostsDetails.filter((blogpost) =>
11+
[blogpost.title, blogpost.authors, blogpost.date, blogpost.summary]
12+
.some((field) => field.toLowerCase().includes(searchField.toLowerCase()))
13+
);
14+
15+
return (
16+
<>
17+
<input
18+
className={styles.search_input}
19+
type="search"
20+
placeholder="Search for blog posts"
21+
onChange={(e) => setSearchField(e.target.value)}
22+
/>
23+
<CardGrid cols={3}>
24+
{filtered.map((blogpost, index) => (
25+
<li key={index}>
26+
<BlogpostCard blogpost={blogpost} />
27+
</li>
28+
))}
29+
</CardGrid>
30+
</>
31+
);
32+
}

src/components/BlogpostCard.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import styles from "../pages/blog.module.css";
2+
import Link from "@docusaurus/Link";
3+
import useBaseUrl from "@docusaurus/useBaseUrl";
4+
import Card from "./layout/Card";
5+
6+
export default function BlogpostCard({ blogpost }) {
7+
return (
8+
<Card hover className={styles.blogpost_card}>
9+
<Link href={blogpost.url}>
10+
<div className={`${styles.blogpost_image} flex-full-centered`}>
11+
<img
12+
src={useBaseUrl(blogpost.image)}
13+
id={blogpost.imageID}
14+
alt="Illustration for the blog post."
15+
/>
16+
</div>
17+
<div className={styles.blogpost_header}>{blogpost.title}</div>
18+
<div className={styles.blogpost_summary}>
19+
{blogpost.summary.length < 200
20+
? blogpost.summary
21+
: blogpost.summary.substring(0, 200) + "..."}
22+
</div>
23+
<div className={styles.blogpost_footer}>
24+
<div className={styles.blogpost_date}>
25+
<span className={styles.blogpost_date_dot} />
26+
{blogpost.date}
27+
</div>
28+
<div className={styles.blogpost_authors}>{blogpost.authors}</div>
29+
</div>
30+
</Link>
31+
</Card>
32+
);
33+
}

0 commit comments

Comments
 (0)