Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions website/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
browserless:
image: browserless/chrome:latest
ports:
- "33000:3000"
environment:
CONNECTION_TIMEOUT: "600000"
MAX_CONCURRENT_SESSIONS: "5"
shm_size: "2gb"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: "no"
4 changes: 2 additions & 2 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build:pdf": "node scripts/generate-whitepaper-pdfs.js",
"build:full": "astro build && node scripts/generate-whitepaper-pdfs.js",
"build:pdf": "node scripts/run-whitepaper-pdf-with-browserless.mjs",
"build:full": "astro build && node scripts/run-whitepaper-pdf-with-browserless.mjs",
"preview": "astro preview",
"astro": "astro",
"format:check": "prettier --check .",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 61 additions & 27 deletions website/scripts/generate-whitepaper-pdfs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { spawn } from "node:child_process";
import * as puppeteer from "puppeteer-core";

const OUTPUT_BASE = path.join("./public", "whitepaper", "pdf");
const PORT = 4322;
const BASE_URL = `http://host.docker.internal:${PORT}`;

const BROWSERLESS_WS = process.env.BROWSERLESS_WS ?? "ws://127.0.0.1:33000";

const SUPPORTED_LOCALES = [
"en-US",
"zh-CN",
Expand All @@ -19,6 +23,55 @@ const SUPPORTED_LOCALES = [
];
const DEFAULT_LOCALE = "en-US";

/** Time with no network activity before considering the page idle (ms). */
const NETWORK_IDLE_MS = 500;
/** Max time to wait for network idle after scrolling (ms). */
const NETWORK_IDLE_TIMEOUT_MS = 90_000;
/** Pause between scroll steps so lazy observers can fire (ms). */
const SCROLL_STEP_PAUSE_MS = 75;

/**
* Scrolls the full document to trigger lazy-loaded images, waits for network
* quiet, then resolves when images have loaded (or failed).
*/
async function waitForImagesAndNetworkIdle(page) {
await page.evaluate(async (stepPause) => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const step = Math.max(240, Math.floor(window.innerHeight * 0.9));
const maxY = document.documentElement.scrollHeight;
for (let y = 0; y < maxY; y += step) {
window.scrollTo(0, y);
await sleep(stepPause);
}
window.scrollTo(0, maxY);
await sleep(stepPause);
window.scrollTo(0, 0);
await sleep(100);
}, SCROLL_STEP_PAUSE_MS);

await page.waitForNetworkIdle({
idleTime: NETWORK_IDLE_MS,
timeout: NETWORK_IDLE_TIMEOUT_MS,
});

await page.evaluate(async () => {
const imgs = Array.from(document.images);
await Promise.all(
imgs.map((img) => {
if (img.complete && img.naturalWidth > 0) {
return img.decode?.().catch(() => {}) ?? Promise.resolve();
}
return new Promise((resolve) => {
const done = () => resolve();
img.addEventListener("load", done, { once: true });
img.addEventListener("error", done, { once: true });
setTimeout(done, 15_000);
}).then(() => img.decode?.().catch(() => {}) ?? Promise.resolve());
}),
);
});
}

function discoverVersions() {
const contentDir = "./src/contents/whitepapers";
const versions = new Map();
Expand Down Expand Up @@ -98,25 +151,20 @@ async function generatePdf(puppeteer, locale, version) {

console.log(` Generating PDF: ${locale}/v${version} -> ${outputFile}`);

let browser;
try {
browser = await puppeteer.connect({
browserWSEndpoint: `ws://127.0.0.1:33000`,
});
} catch (err) {
console.warn(`\nCould not connect to browser: ${err.message}`);
console.log("Skipping PDF generation.");
return;
}
const browser = await puppeteer.connect({
browserWSEndpoint: BROWSERLESS_WS,
});

const page = await browser.newPage();

try {
await page.goto(url, {
waitUntil: "networkidle0",
timeout: 30000,
timeout: 60_000,
});

await waitForImagesAndNetworkIdle(page);

await page.pdf({
path: outputFile,
format: "A4",
Expand Down Expand Up @@ -153,11 +201,11 @@ async function generatePdf(puppeteer, locale, version) {
` Warning: Failed to generate PDF for ${locale}/v${version}: ${err.message}`,
);
} finally {
await browser.close();
await browser?.close();
}
}

async function main() {
export async function main() {
console.log("Whitepaper PDF Generation");
console.log("=".repeat(50));

Expand All @@ -172,17 +220,6 @@ async function main() {
console.log(` ${locale}: ${versions.map((v) => `v${v}`).join(", ")}`);
}

let puppeteer;
try {
puppeteer = await import("puppeteer");
} catch {
console.warn("\nPuppeteer not available. Skipping PDF generation.");
console.log(
"To enable PDF generation, run: npx puppeteer browsers install chrome",
);
return;
}

console.log("\nStarting preview server...");
const server = await startPreviewServer();

Expand All @@ -198,10 +235,7 @@ async function main() {
console.log("\nAll PDFs generated successfully.");
} catch (err) {
console.error("PDF generation failed:", err.message);
console.log("Build completed but PDF generation was skipped.");
}

server.kill();
}

main();
64 changes: 64 additions & 0 deletions website/scripts/run-whitepaper-pdf-with-browserless.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { execSync } from "node:child_process";
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";

const websiteRoot = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const runner = process.argv[2] ?? "podman";

function waitForPort(port, host = "127.0.0.1", timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve, reject) => {
const tryOnce = () => {
const socket = net.createConnection({ port, host }, () => {
socket.end();
resolve();
});
socket.on("error", () => {
socket.destroy();
if (Date.now() >= deadline) {
reject(
new Error(
`Timed out waiting for ${host}:${port} (is Browserless running?)`,
),
);
return;
}
setTimeout(tryOnce, 300);
});
};
tryOnce();
});
}

function runCompose(args) {
execSync(`${runner} compose ${args}`, {
cwd: websiteRoot,
stdio: "inherit",
env: process.env,
});
}

function stopBrowserless() {
runCompose("stop browserless");
}

async function run() {
runCompose("up -d browserless");

try {
await waitForPort(33000);
const { main } = await import("./generate-whitepaper-pdfs.js");
await main();
} finally {
stopBrowserless();
}
}

run().catch((err) => {
console.error(err);
process.exitCode = 1;
});
26 changes: 14 additions & 12 deletions website/src/components/features/launch/AllocationChart.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,22 @@
plugins: [ChartDataLabels],
data: {
labels: [
t("launch.tge.table.private_sale"),
t("launch.tge.table.public_sale"),
t("launch.tge.table.dex_liquidity"),
t("launch.mining.table.miners"),
t("launch.mining.table.community"),
t("launch.mining.table.team"),
t("launch.chart.miners"),
t("launch.chart.private"),
t("launch.chart.company"),
t("launch.chart.public"),
t("launch.chart.liquidity"),
],
datasets: [
{
label: "Launch Allocation",
data: [15, 10, 5, 35, 25, 10],
data: [50, 15, 15, 10, 10],
backgroundColor: [
"#00C2A8",
"#0000FF",
"#7C3AED",
"#ED4CCE",
"#FFE91F",
"#00C2A8",
"#FF6B35",
"#7C3AED",
],
borderColor: "#000000",
borderWidth: 2,
Expand All @@ -48,17 +46,21 @@
enabled: false,
},
datalabels: {
textShadowBlur: 10,
textShadowColor: "#000000",
formatter: (value, context) => {
const datapoints = context.chart.data.datasets[0]
.data as number[];
const label = context.chart?.data.labels?.[context.dataIndex];

const total = datapoints.reduce((acc, val) => acc + val, 0);
const percentage = ((value / total) * 100).toFixed(1) + "%";
return percentage;
return `${label}\n ${percentage}`;
},
color: "#fff",
},
legend: {
display: true,
display: false,
},
},
},
Expand Down
Loading
Loading