mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
214 lines
6.3 KiB
JavaScript
214 lines
6.3 KiB
JavaScript
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
|
|
/**
|
|
* Decodes markdown payload from a JSON script element.
|
|
*
|
|
* @param {HTMLScriptElement | null} payloadEl
|
|
* @returns {string}
|
|
*/
|
|
function readMarkdownPayload(payloadEl) {
|
|
if (!payloadEl) return "";
|
|
|
|
try {
|
|
const parsed = JSON.parse(payloadEl.textContent || "\"\"");
|
|
return typeof parsed === "string" ? parsed : "";
|
|
} catch (error) {
|
|
console.error("Failed to parse blog markdown payload", error);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wires copy buttons rendered by MarkdownRenderer.
|
|
*
|
|
* @param {ParentNode} container
|
|
* @returns {void}
|
|
*/
|
|
function wireCopyButtons(container) {
|
|
container.querySelectorAll(".md-copy-btn").forEach((button) => {
|
|
button.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const code = button.getAttribute("data-code");
|
|
if (!code) return;
|
|
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
const originalText = button.textContent;
|
|
button.textContent = "✓";
|
|
button.classList.add("copied");
|
|
setTimeout(() => {
|
|
button.textContent = originalText;
|
|
button.classList.remove("copied");
|
|
}, 2000);
|
|
}).catch((error) => {
|
|
console.error("Failed to copy code", error);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Renders blog markdown on static post pages using the shared renderer.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function renderStaticBlogPost() {
|
|
/** @type {HTMLElement | null} */
|
|
const target = document.querySelector("[data-blog-post-content]");
|
|
if (!target) return;
|
|
|
|
/** @type {HTMLScriptElement | null} */
|
|
const payload = document.querySelector("#blogPostMarkdown");
|
|
const markdown = readMarkdownPayload(payload);
|
|
|
|
target.innerHTML = MarkdownRenderer.render(markdown);
|
|
MarkdownRenderer.applyImageScale(target);
|
|
wireCopyButtons(target);
|
|
}
|
|
|
|
/**
|
|
* Toggles the compact top bar when the hero logo scrolls out of view.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function setupScrollTopBar() {
|
|
const body = document.body;
|
|
const bar = document.querySelector("[data-blog-scroll-topbar]");
|
|
|
|
if (!bar) return;
|
|
|
|
let lastScrollTop = 0;
|
|
|
|
const applyState = () => {
|
|
const scrollTop = body.scrollTop;
|
|
const scrollingDown = scrollTop > lastScrollTop;
|
|
lastScrollTop = scrollTop;
|
|
|
|
if (scrollTop < 80) {
|
|
body.classList.remove("blog-scrolled");
|
|
} else if (scrollingDown) {
|
|
body.classList.add("blog-scrolled");
|
|
} else {
|
|
body.classList.remove("blog-scrolled");
|
|
}
|
|
};
|
|
body.addEventListener("scroll", applyState, { passive: true });
|
|
applyState();
|
|
}
|
|
|
|
/**
|
|
* Draws SVG bezier splines from the peek-card exec pins to the main blog card.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function setupPeekSplines() {
|
|
const svg = document.querySelector(".blog-post-splines");
|
|
const layout = document.querySelector(".blog-post-layout");
|
|
const card = document.querySelector(".blog-card");
|
|
if (!svg || !layout || !card) return;
|
|
|
|
const WIRE_COLOR = "#ffffff";
|
|
const WIRE_OPACITY = "0.35";
|
|
|
|
/**
|
|
* Creates an SVG path element styled as an exec wire.
|
|
*
|
|
* @returns {SVGPathElement}
|
|
*/
|
|
function makePath() {
|
|
const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
p.setAttribute("fill", "none");
|
|
p.setAttribute("stroke", WIRE_COLOR);
|
|
p.setAttribute("stroke-width", "2.5");
|
|
p.setAttribute("stroke-linecap", "round");
|
|
p.style.opacity = WIRE_OPACITY;
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Returns the center of an element relative to the layout container.
|
|
*
|
|
* @param {Element} el
|
|
* @returns {{x: number, y: number}}
|
|
*/
|
|
function centerOf(el) {
|
|
const er = el.getBoundingClientRect();
|
|
const lr = layout.getBoundingClientRect();
|
|
return {
|
|
x: er.left - lr.left + er.width / 2,
|
|
y: er.top - lr.top + er.height / 2,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the midpoint of a card's left or right edge relative to the layout.
|
|
*
|
|
* @param {Element} el
|
|
* @param {'left'|'right'} side
|
|
* @returns {{x: number, y: number}}
|
|
*/
|
|
function edgeOf(el, side) {
|
|
const er = el.getBoundingClientRect();
|
|
const lr = layout.getBoundingClientRect();
|
|
return {
|
|
x: side === 'left' ? er.left - lr.left : er.right - lr.left,
|
|
y: er.top - lr.top + 158,
|
|
};
|
|
}
|
|
|
|
const pathPrev = makePath();
|
|
const pathNext = makePath();
|
|
svg.appendChild(pathPrev);
|
|
svg.appendChild(pathNext);
|
|
|
|
/** @returns {void} */
|
|
function draw() {
|
|
const lr = layout.getBoundingClientRect();
|
|
svg.setAttribute("width", String(lr.width));
|
|
svg.setAttribute("height", String(lr.height));
|
|
|
|
const prevPin = document.querySelector("[data-peek-pin='prev-out']");
|
|
const nextPin = document.querySelector("[data-peek-pin='next-in']");
|
|
const cardLeft = edgeOf(card, "left");
|
|
const cardRight = edgeOf(card, "right");
|
|
|
|
if (prevPin) {
|
|
const s = centerOf(prevPin);
|
|
const e = cardLeft;
|
|
const cp = Math.max(60, Math.abs(e.x - s.x) * 0.5);
|
|
pathPrev.setAttribute("d", `M ${s.x} ${s.y} C ${s.x + cp} ${s.y} ${e.x - cp} ${e.y} ${e.x} ${e.y}`);
|
|
} else {
|
|
pathPrev.setAttribute("d", "");
|
|
}
|
|
|
|
if (nextPin) {
|
|
const s = cardRight;
|
|
const e = centerOf(nextPin);
|
|
const cp = Math.max(60, Math.abs(e.x - s.x) * 0.5);
|
|
pathNext.setAttribute("d", `M ${s.x} ${s.y} C ${s.x + cp} ${s.y} ${e.x - cp} ${e.y} ${e.x} ${e.y}`);
|
|
} else {
|
|
pathNext.setAttribute("d", "");
|
|
}
|
|
}
|
|
|
|
draw();
|
|
window.addEventListener("resize", draw, { passive: true });
|
|
window.addEventListener("scroll", draw, { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Bootstraps static blog page behaviors.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function initializeBlogPage() {
|
|
renderStaticBlogPost();
|
|
setupScrollTopBar();
|
|
setupPeekSplines();
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initializeBlogPage, { once: true });
|
|
} else {
|
|
initializeBlogPage();
|
|
}
|