mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
- Deleted blog.html and associated blog post files (markdown-test, testddd, node-graphs, welcome). - Updated index.html to use dynamic navigation links from navLinks.js. - Enhanced MarkdownRenderer to include new heading pin elements. - Implemented SVG bezier splines for blog post navigation. - Adjusted CSS styles for blog layout and new elements.
214 lines
6.4 KiB
JavaScript
214 lines
6.4 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]");
|
|
const heroLogo = document.querySelector(".blog-logo-link");
|
|
|
|
if (!bar) return;
|
|
|
|
if (heroLogo && "IntersectionObserver" in window) {
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
body.classList.toggle("blog-scrolled", !entry.isIntersecting);
|
|
},
|
|
{ threshold: 0 }
|
|
);
|
|
observer.observe(heroLogo);
|
|
} else {
|
|
// Fallback: fixed pixel threshold
|
|
const applyState = () => {
|
|
body.classList.toggle("blog-scrolled", window.scrollY >= 80);
|
|
};
|
|
window.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();
|
|
}
|