refactor
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
|
@ -4,12 +4,25 @@
|
|||
|
||||
const TERMINAL_PROMPT = "barrett@ruth:~$ ";
|
||||
let typing = false;
|
||||
let clearing = false;
|
||||
|
||||
function promptEl() {
|
||||
return document.querySelector(".terminal-prompt");
|
||||
}
|
||||
|
||||
function promptTail() {
|
||||
const el = promptEl();
|
||||
if (!el) return "";
|
||||
const s = el.textContent || "";
|
||||
return s.startsWith(TERMINAL_PROMPT) ? s.slice(TERMINAL_PROMPT.length) : s;
|
||||
}
|
||||
function setPromptTailImmediate(tail) {
|
||||
const el = promptEl();
|
||||
if (!el) return;
|
||||
el.textContent = TERMINAL_PROMPT + tail;
|
||||
}
|
||||
function persistPrompt() {
|
||||
const el = promptEl();
|
||||
if (el) sessionStorage.setItem("terminalPromptText", el.textContent);
|
||||
}
|
||||
(function restorePrompt() {
|
||||
const saved = sessionStorage.getItem("terminalPromptText");
|
||||
const el = promptEl();
|
||||
|
|
@ -17,52 +30,80 @@
|
|||
sessionStorage.removeItem("terminalPromptText");
|
||||
})();
|
||||
|
||||
function clearPrompt(delay, callback) {
|
||||
if (clearing) return;
|
||||
clearing = true;
|
||||
const el = promptEl();
|
||||
if (!el) {
|
||||
clearing = false;
|
||||
function normalizeDisplayPath(pathname) {
|
||||
let p = pathname.replace(/\/index\.html$/, "/").replace(/\.html$/, "");
|
||||
p = p.replace(/\/{2,}/g, "/");
|
||||
if (p !== "/" && p.endsWith("/")) p = p.slice(0, -1);
|
||||
return p === "/" ? "" : p;
|
||||
}
|
||||
function displayPathFromHref(href) {
|
||||
const url = new URL(href, location.origin);
|
||||
return normalizeDisplayPath(url.pathname);
|
||||
}
|
||||
function currentDisplayPath() {
|
||||
return normalizeDisplayPath(location.pathname);
|
||||
}
|
||||
function setDocTitleForPath(displayPath) {
|
||||
if (!displayPath) {
|
||||
document.title = "Barrett Ruth";
|
||||
return;
|
||||
}
|
||||
const excess = el.textContent.length - TERMINAL_PROMPT.length;
|
||||
let i = 0;
|
||||
function tick() {
|
||||
if (i++ < excess) {
|
||||
el.textContent = el.textContent.slice(0, -1);
|
||||
setTimeout(tick, delay / Math.max(excess, 1));
|
||||
} else {
|
||||
clearing = false;
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
tick();
|
||||
document.title = displayPath.slice(1);
|
||||
}
|
||||
|
||||
function typeTerminalPath(topic, delay, callback) {
|
||||
function animateToDisplayPath(displayPath, totalMs, done) {
|
||||
if (typing) return;
|
||||
typing = true;
|
||||
const el = promptEl();
|
||||
if (!el) return;
|
||||
const txt = ` /${topic}`;
|
||||
clearPrompt(delay, () => {
|
||||
let i = 0;
|
||||
function step() {
|
||||
if (i < txt.length) {
|
||||
el.textContent += txt.charAt(i++);
|
||||
setTimeout(step, delay / txt.length);
|
||||
} else {
|
||||
typing = false;
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
step();
|
||||
});
|
||||
}
|
||||
|
||||
function persistPrompt() {
|
||||
const el = promptEl();
|
||||
if (el) sessionStorage.setItem("terminalPromptText", el.textContent);
|
||||
if (!el) {
|
||||
typing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTail = displayPath ? " " + displayPath : "";
|
||||
const curTail = promptTail();
|
||||
|
||||
let i = 0;
|
||||
const max = Math.min(curTail.length, targetTail.length);
|
||||
while (i < max && curTail.charAt(i) === targetTail.charAt(i)) i++;
|
||||
|
||||
const delSteps = curTail.length - i;
|
||||
const typeSteps = targetTail.length - i;
|
||||
const totalSteps = delSteps + typeSteps;
|
||||
|
||||
if (totalSteps === 0) {
|
||||
typing = false;
|
||||
done && done();
|
||||
return;
|
||||
}
|
||||
|
||||
const stepMs = totalMs / totalSteps;
|
||||
|
||||
let delCount = 0;
|
||||
function tickDelete() {
|
||||
if (delCount < delSteps) {
|
||||
setPromptTailImmediate(curTail.slice(0, curTail.length - delCount - 1));
|
||||
delCount++;
|
||||
setTimeout(tickDelete, stepMs);
|
||||
} else {
|
||||
let j = 0;
|
||||
function tickType() {
|
||||
if (j < typeSteps) {
|
||||
setPromptTailImmediate(
|
||||
curTail.slice(0, i) + targetTail.slice(i, i + j + 1),
|
||||
);
|
||||
j++;
|
||||
setTimeout(tickType, stepMs);
|
||||
} else {
|
||||
typing = false;
|
||||
done && done();
|
||||
}
|
||||
}
|
||||
tickType();
|
||||
}
|
||||
}
|
||||
tickDelete();
|
||||
}
|
||||
|
||||
function renderPosts(topic) {
|
||||
|
|
@ -76,11 +117,13 @@
|
|||
const div = document.createElement("div");
|
||||
div.className = "post";
|
||||
const a = document.createElement("a");
|
||||
const slug = post.id
|
||||
.split("/")
|
||||
.pop()
|
||||
.replace(/\.mdx?$/, "");
|
||||
a.href = `/posts/${topic}/${slug}.html`;
|
||||
const slug =
|
||||
post.slug ||
|
||||
post.id
|
||||
?.split("/")
|
||||
.pop()
|
||||
?.replace(/\.mdx?$/, "");
|
||||
a.href = `/${topic}/${slug}.html`;
|
||||
a.textContent = post.data.title;
|
||||
a.style.textDecoration = "underline";
|
||||
div.appendChild(a);
|
||||
|
|
@ -94,14 +137,14 @@
|
|||
e.preventDefault();
|
||||
if (typing) return;
|
||||
|
||||
const topic = link.dataset.topic.toLowerCase();
|
||||
const path = window.location.pathname;
|
||||
const isHome = path === "/" || path === "/index.html";
|
||||
const topic = link.dataset.topic?.toLowerCase() || "";
|
||||
const href = link.getAttribute("href") || "/";
|
||||
const delay = 500;
|
||||
const path = window.location.pathname;
|
||||
|
||||
const colorFn = window.getTopicColor || (() => "");
|
||||
const topics = document.querySelectorAll("[data-topic]");
|
||||
topics.forEach((t) => {
|
||||
document.querySelectorAll("[data-topic]").forEach((t) => {
|
||||
t.classList.remove("active");
|
||||
t.style.color = "";
|
||||
});
|
||||
|
|
@ -109,27 +152,29 @@
|
|||
const c = colorFn(topic);
|
||||
if (c) link.style.color = c;
|
||||
|
||||
typeTerminalPath(topic, delay, () => {
|
||||
persistPrompt();
|
||||
const displayPath = isHome ? `/${topic}` : displayPathFromHref(href);
|
||||
|
||||
if (path === "/" || path === "/index.html") {
|
||||
if (window.postsByCategory && window.postsByCategory[topic]) {
|
||||
renderPosts(topic);
|
||||
return;
|
||||
}
|
||||
animateToDisplayPath(displayPath, delay, () => {
|
||||
if (
|
||||
isHome &&
|
||||
topic &&
|
||||
window.postsByCategory &&
|
||||
window.postsByCategory[topic]
|
||||
) {
|
||||
renderPosts(topic);
|
||||
return;
|
||||
}
|
||||
persistPrompt();
|
||||
|
||||
const isMail = href.startsWith("mailto:");
|
||||
if (isMail) {
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.target === "_blank") {
|
||||
window.open(href, "_blank");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = href;
|
||||
});
|
||||
}
|
||||
|
|
@ -141,25 +186,31 @@
|
|||
const isHome =
|
||||
window.location.pathname === "/" ||
|
||||
window.location.pathname === "/index.html";
|
||||
const delay = 500;
|
||||
if (isHome) {
|
||||
clearPrompt(500, () => {
|
||||
animateToDisplayPath("", delay, () => {
|
||||
const posts = document.getElementById("posts");
|
||||
if (posts) posts.innerHTML = "";
|
||||
const topics = document.querySelectorAll("[data-topic].active");
|
||||
topics.forEach((t) => {
|
||||
document.querySelectorAll("[data-topic].active").forEach((t) => {
|
||||
t.classList.remove("active");
|
||||
t.style.color = "";
|
||||
});
|
||||
setDocTitleForPath("");
|
||||
});
|
||||
} else {
|
||||
persistPrompt();
|
||||
clearPrompt(500, () => {
|
||||
animateToDisplayPath("", delay, () => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const initial = currentDisplayPath();
|
||||
if (initial) setPromptTailImmediate(" " + initial);
|
||||
else setPromptTailImmediate("");
|
||||
if (initial) setDocTitleForPath(initial);
|
||||
|
||||
document.body.addEventListener("click", (e) => {
|
||||
if (e.target.closest(".home-link")) return handleHomeClick(e);
|
||||
if (e.target.closest("[data-topic]")) return handleDataTopicClick(e);
|
||||
|
|
@ -172,7 +223,7 @@
|
|||
if (!link) return;
|
||||
const color =
|
||||
(window.getTopicColor &&
|
||||
window.getTopicColor(link.dataset.topic.toLowerCase())) ||
|
||||
window.getTopicColor(link.dataset.topic?.toLowerCase() || "")) ||
|
||||
"";
|
||||
if (color) link.style.color = color;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ function deriveTopic() {
|
|||
if (path.startsWith("/about")) return "/about";
|
||||
if (path === "/gist" || path.startsWith("/gist/")) return "/gist";
|
||||
if (path === "/git" || path.startsWith("/git/")) return "/git";
|
||||
if (path.startsWith("/posts/")) {
|
||||
const parts = path.replace(/\/+$/, "").split("/");
|
||||
if (parts.length >= 3) return "/" + parts[2];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +23,9 @@ const promptText = topic ? `barrett@ruth:~$ ${topic}` : "barrett@ruth:~$";
|
|||
</a>
|
||||
<div class="header-links">
|
||||
<a href="/resume.pdf" data-topic="resume" target="_blank">resume</a>
|
||||
<a href="/transcript.pdf" data-topic="transcript" target="_blank">transcript</a>
|
||||
<a href="/transcript.pdf" data-topic="transcript" target="_blank"
|
||||
>transcript</a
|
||||
>
|
||||
<a href="/about" data-topic="about">about</a>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -57,4 +55,3 @@ const promptText = topic ? `barrett@ruth:~$ ${topic}` : "barrett@ruth:~$";
|
|||
</style>
|
||||
|
||||
<script type="module" src="/scripts/index.js"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ Count-Pairs($l_1$, $l_2$, $r_1$, $r_2$, $k$):
|
|||
|
||||
Each value of $n$ corresponds to a line with slope $k^n$ because $y/x=k^n\leftrightarrow y=x\cdot k^n$. The problem can be visualized as follows:
|
||||
|
||||

|
||||

|
||||
|
||||
It is sufficient to count the number of ordered $(x,y)$ pairs for all valid $n$. Because $y=x\cdot k^n\leftrightarrow n=log_k(y/x)$, $n\in [log_k(l_2/r_1), log_k(r_2/l_1)]$.
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ The Qt Framework is quite complex and beyond the scope of this post. Big picture
|
|||
|
||||
How do we optimize this? It is necessary to take a step back and observe the structure of the entire application:
|
||||
|
||||

|
||||

|
||||
|
||||
Many flaws are now clear:
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ First, let's pin down what exactly can be parallelized.
|
|||
|
||||
This lead me to the follow structure:
|
||||
|
||||

|
||||

|
||||
|
||||
- Three callback groups are triggered at differing intervals according to their urgency on the GUI node
|
||||
- A thread-safe queue[^2] processes all ingested data for each callback group
|
||||
|
|
@ -101,7 +101,7 @@ Considering each element of the state machine, I simplified it into the followin
|
|||
|
||||
Since we only have $N\ll 32$ vehicle flags and $M\ll 32$ track flags, all of this information (all of the state flags) can be encoded in a `uint32_t`:
|
||||
|
||||

|
||||

|
||||
|
||||
The state must also store a few other relevant details:
|
||||
|
||||
|
|
@ -1,36 +1,17 @@
|
|||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const postsCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
useKatex: z.boolean().optional().default(false),
|
||||
useD3: z.boolean().optional().default(false),
|
||||
scripts: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const gistsCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const gitsCollection = defineCollection({
|
||||
type: "content",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
date: z.string().optional(),
|
||||
}),
|
||||
const base = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
posts: postsCollection,
|
||||
gists: gistsCollection,
|
||||
git: gitsCollection,
|
||||
algorithms: defineCollection({ type: "content", schema: base }),
|
||||
software: defineCollection({ type: "content", schema: base }),
|
||||
meditations: defineCollection({ type: "content", schema: base }),
|
||||
"autonomous-racing": defineCollection({ type: "content", schema: base }),
|
||||
|
||||
git: defineCollection({ type: "content", schema: base }),
|
||||
gists: defineCollection({ type: "content", schema: base }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: "git-server.git"
|
||||
title: "git-server"
|
||||
date: "08/10/2025"
|
||||
---
|
||||
|
||||
the code for my git python server, hosted on [lightsail](https://aws.amazon.com/lightsail/)
|
||||
the code for my git python server hosted with [lightsail](https://aws.amazon.com/lightsail/)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "wp.git"
|
||||
title: "wp"
|
||||
date: "07/10/2025"
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ But I did not actually _need_ any of them to make this site look decent.
|
|||
|
||||
## what i've learned
|
||||
|
||||
Of course, most people build simple websites like these to learn a new technology or framework, not to use an optimal tool. That's actually why I [hosted this website on AWS](/posts/software/from-github-pages-to-aws.html).
|
||||
Of course, most people build simple websites like these to learn a new technology or framework, not to use an optimal tool. That's actually why I [hosted this website on AWS](/software/from-github-pages-to-aws.html).
|
||||
|
||||
Building this website with truly bare-bones technologies has made me appreciate _why_ these web frameworks have emerged.
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ A user request can be modelled as follows:
|
|||
4. CloudFront checks its edge caches for the requested content. If the content is stale or not cached, CloudFront fetches the content from S3. Otherwise, it uses the cached content from an edge server.
|
||||
5. CloudFront returns the content to the user's browser.
|
||||
|
||||

|
||||

|
||||
|
||||
## difficulties
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ That's it. The `makefile` relies on some scripts that compile code and run the c
|
|||
|
||||
# neovim integration
|
||||
|
||||

|
||||

|
||||
|
||||
Leveraging [LuaSnip](https://github.com/L3MON4D3/LuaSnip), a custom `CP` user command, and some scripting for window management and asynchronous jobs, I'm able to:
|
||||
|
||||
|
|
@ -1,64 +1,31 @@
|
|||
---
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import { getTopicColor } from "../utils/colors.js";
|
||||
|
||||
interface Props {
|
||||
frontmatter: {
|
||||
title: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
useKatex?: boolean;
|
||||
useD3?: boolean;
|
||||
scripts?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const { frontmatter, post } = Astro.props;
|
||||
const { title, description, useKatex = false, useD3 = false } = frontmatter;
|
||||
|
||||
const filePath = post?.id || "";
|
||||
const category = filePath.split("/")[0];
|
||||
|
||||
const topicColor = getTopicColor(category);
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} useKatex={useKatex}, useD3={useD3}>
|
||||
<slot name="head" slot="head">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="stylesheet" href="/styles/common.css" />
|
||||
<link rel="stylesheet" href="/styles/posts.css" />
|
||||
<link rel="stylesheet" href="/styles/git.css" />
|
||||
<link rel="stylesheet" href="/styles/graph.css" />
|
||||
{
|
||||
useKatex && (
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css"
|
||||
integrity="sha384-5TcZemv2l/9On385z///+d7MSYlvIEw9FuZTIdZ14vJLqWphw7e7ZPuOiCHJcFCP"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
useD3 && (
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
)
|
||||
}
|
||||
</slot>
|
||||
|
||||
<div class="post-container" style={`--topic-color: ${topicColor};`}>
|
||||
<slot name="head" />
|
||||
<script type="module" src="/scripts/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="post-header">
|
||||
<h1 class="post-title">{title}</h1>
|
||||
{frontmatter.date && <p class="post-meta">{frontmatter.date}</p>}
|
||||
<a class="home-link" href="/">barrettruth.com</a>
|
||||
<h1 class="post-title">{frontmatter.title}</h1>
|
||||
{
|
||||
frontmatter.description && (
|
||||
<p class="post-description">{frontmatter.description}</p>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<article class="post-article">
|
||||
<slot />
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<slot name="scripts" slot="scripts">
|
||||
<script src="/scripts/centerKatex.js" is:inline></script>
|
||||
{frontmatter.scripts?.map(script => (
|
||||
<script src={script} type="module"></script>
|
||||
))}
|
||||
</slot>
|
||||
</BaseLayout>
|
||||
<main class="post-container">
|
||||
<article class="post-body">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
33
src/pages/[category]/[slug].astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import PostLayout from "../../layouts/PostLayout.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const CATS = ["algorithms", "software", "meditations", "autonomous-racing"];
|
||||
|
||||
const entries = [];
|
||||
for (const c of CATS) {
|
||||
const docs = await getCollection(c);
|
||||
for (const d of docs) {
|
||||
entries.push({
|
||||
params: { category: c, slug: d.slug },
|
||||
props: { post: d },
|
||||
});
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const category = Astro.params.category;
|
||||
const pageTitle = `${category}/${post.data.title ?? post.slug}`;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<PostLayout frontmatter={post.data}>
|
||||
<Fragment slot="head">
|
||||
<title>{pageTitle}</title>
|
||||
<script type="module" src="/scripts/index.js"></script>
|
||||
</Fragment>
|
||||
<Content />
|
||||
</PostLayout>
|
||||
61
src/pages/[category]/index.astro
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("posts");
|
||||
const categories = Array.from(new Set(posts.map((p) => p.id.split("/")[0])));
|
||||
return categories.map((category) => ({ params: { category } }));
|
||||
}
|
||||
|
||||
const category = Astro.params.category;
|
||||
const title = "Barrett Ruth";
|
||||
|
||||
const allPosts = await getCollection("posts");
|
||||
const postsByCategory = allPosts.reduce((acc, post) => {
|
||||
const c = post.id.split("/")[0];
|
||||
(acc[c] ||= []).push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.keys(postsByCategory).forEach((c) => {
|
||||
postsByCategory[c].sort((a, b) => {
|
||||
const parseEuroDate = (dateStr) => {
|
||||
if (!dateStr) return new Date(0);
|
||||
const [day, month, year] = (dateStr || "").split("/");
|
||||
return new Date(year, month - 1, day);
|
||||
};
|
||||
return parseEuroDate(b.data.date) - parseEuroDate(a.data.date);
|
||||
});
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
<slot name="head" slot="head">
|
||||
<link rel="stylesheet" href="/styles/index.css" />
|
||||
</slot>
|
||||
|
||||
<div class="content">
|
||||
<ul class="topics">
|
||||
<li class="topic algorithms">
|
||||
<a href="/algorithms" data-topic="algorithms">algorithms</a>
|
||||
</li>
|
||||
<li class="topic software">
|
||||
<a href="/software" data-topic="software">software</a>
|
||||
</li>
|
||||
<li class="topic meditations">
|
||||
<a href="/meditations" data-topic="meditations">meditations</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="posts" id="posts"></div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
slot="scripts"
|
||||
define:vars={{ postsByCategory, SELECTED_CATEGORY: category }}
|
||||
>
|
||||
window.postsByCategory = postsByCategory;
|
||||
window.SELECTED_CATEGORY = SELECTED_CATEGORY;
|
||||
</script>
|
||||
<script slot="scripts" type="module" src="/scripts/index.js"></script>
|
||||
</BaseLayout>
|
||||
|
|
@ -15,17 +15,18 @@ gists.sort((a, b) => a.slug.localeCompare(b.slug));
|
|||
|
||||
<div class="content">
|
||||
<ul class="topics">
|
||||
{gists.map((gist) => (
|
||||
<li class="topic">
|
||||
<a
|
||||
href={`/gist/${gist.slug}.html`}
|
||||
data-gist={gist.slug}
|
||||
>
|
||||
{gist.data.title || gist.slug}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
gists.map((gist) => (
|
||||
<li class="topic">
|
||||
<a
|
||||
href={`/gist/${gist.slug}.html`}
|
||||
data-topic={`gist/${gist.slug}`}
|
||||
>
|
||||
{gist.data.title || gist.slug}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import PostLayout from "../../layouts/PostLayout.astro";
|
|||
|
||||
export async function getStaticPaths() {
|
||||
const gists = await getCollection("gists");
|
||||
|
||||
return gists.map((gist) => ({
|
||||
params: { slug: gist.slug },
|
||||
props: { gist },
|
||||
|
|
@ -13,9 +12,13 @@ export async function getStaticPaths() {
|
|||
|
||||
const { gist } = Astro.props;
|
||||
const { Content } = await gist.render();
|
||||
const pageTitle = `gist/${gist.data.title ?? gist.slug}`;
|
||||
---
|
||||
|
||||
<PostLayout frontmatter={gist.data}>
|
||||
<Fragment slot="head">
|
||||
<title>{pageTitle}</title>
|
||||
<script type="module" src="/scripts/index.js"></script>
|
||||
</Fragment>
|
||||
<Content />
|
||||
</PostLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const title = "Git Repositories";
|
||||
const repos = await getCollection("git");
|
||||
repos.sort((a, b) => a.slug.localeCompare(b.slug));
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
|
|
@ -11,43 +14,17 @@ const title = "Git Repositories";
|
|||
</slot>
|
||||
|
||||
<div class="content">
|
||||
<ul class="topics" id="repo-list"></ul>
|
||||
<ul class="topics" id="repo-list">
|
||||
{
|
||||
repos.map((r) => (
|
||||
<li class="topic">
|
||||
<a href={`/git/${r.slug}.html`} data-topic={`git/${r.slug}`}>
|
||||
{r.data.title || r.slug}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="posts" id="posts"></div>
|
||||
</div>
|
||||
|
||||
<script slot="scripts" type="module">
|
||||
const listEl = document.getElementById("repo-list");
|
||||
|
||||
async function loadRepos() {
|
||||
try {
|
||||
const res = await fetch("https://git.barrettruth.com/api/repositories", { mode: "cors" });
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const data = await res.json();
|
||||
const repos = Array.isArray(data?.repositories) ? data.repositories : [];
|
||||
|
||||
listEl.innerHTML = "";
|
||||
|
||||
repos.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (const name of repos) {
|
||||
const label = name.replace(/\.git$/, "");
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.className = `topic repo-${label}`;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = `/git/${label}.html`;
|
||||
a.textContent = label;
|
||||
a.dataset.topic = "git";
|
||||
a.dataset.repo = label;
|
||||
|
||||
li.appendChild(a);
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
loadRepos();
|
||||
</script>
|
||||
</BaseLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,22 +17,27 @@ const { Content } = await entry.render();
|
|||
|
||||
const repoName = `${slug}.git`;
|
||||
let cloneCommand = "";
|
||||
|
||||
try {
|
||||
const res = await fetch("https://git.barrettruth.com/api/repositories");
|
||||
const json = res.ok ? await res.json() : { repositories: [] };
|
||||
const exists = json.repositories?.includes(repoName);
|
||||
if (exists) {
|
||||
if (exists)
|
||||
cloneCommand = `git clone https://git.barrettruth.com/${repoName}`;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const pageTitle = `git/${entry.data.title ?? slug}`;
|
||||
---
|
||||
|
||||
<GitLayout frontmatter={entry.data} post={entry}>
|
||||
{cloneCommand && (
|
||||
<div class="clone-line">
|
||||
<code><span class="prompt">> </span>{cloneCommand}</code>
|
||||
</div>
|
||||
)}
|
||||
<Fragment slot="head">
|
||||
<title>{pageTitle}</title>
|
||||
</Fragment>
|
||||
{
|
||||
cloneCommand && (
|
||||
<div class="clone-line">
|
||||
<code>> {cloneCommand}</code>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Content />
|
||||
</GitLayout>
|
||||
|
|
|
|||
|
|
@ -3,34 +3,38 @@ import BaseLayout from "../layouts/BaseLayout.astro";
|
|||
import { getCollection } from "astro:content";
|
||||
|
||||
const title = "Barrett Ruth";
|
||||
const CATS = ["algorithms", "software", "meditations", "autonomous-racing"];
|
||||
|
||||
const allPosts = await getCollection("posts");
|
||||
const postsByCategory = allPosts.reduce((acc, post) => {
|
||||
const category = post.id.split("/")[0];
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(post);
|
||||
return acc;
|
||||
}, {});
|
||||
function parseEuroDate(dateStr) {
|
||||
if (!dateStr) return new Date(0);
|
||||
const [d, m, y] = (dateStr || "").split("/");
|
||||
return new Date(Number(y), Number(m) - 1, Number(d));
|
||||
}
|
||||
|
||||
Object.keys(postsByCategory).forEach((category) => {
|
||||
postsByCategory[category].sort((a, b) => {
|
||||
const parseEuroDate = (dateStr) => {
|
||||
if (!dateStr) return new Date(0);
|
||||
const [day, month, year] = dateStr.split("/");
|
||||
return new Date(year, month - 1, day);
|
||||
};
|
||||
|
||||
const dateA = parseEuroDate(a.data.date);
|
||||
const dateB = parseEuroDate(b.data.date);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
});
|
||||
const postsByCategory = {};
|
||||
for (const c of CATS) {
|
||||
const entries = await getCollection(c);
|
||||
entries.sort(
|
||||
(a, b) =>
|
||||
parseEuroDate(b.data.date).getTime() -
|
||||
parseEuroDate(a.data.date).getTime(),
|
||||
);
|
||||
postsByCategory[c] = entries.map((e) => ({
|
||||
id: `${c}/${e.slug}.mdx`,
|
||||
slug: e.slug,
|
||||
data: {
|
||||
title: e.data.title ?? e.slug,
|
||||
date: e.data.date ?? null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout title={title}>
|
||||
<slot name="head" slot="head">
|
||||
<link rel="stylesheet" href="/styles/index.css" />
|
||||
</slot>
|
||||
|
||||
<div class="content">
|
||||
<ul class="topics">
|
||||
<li class="topic algorithms">
|
||||
|
|
@ -42,6 +46,11 @@ Object.keys(postsByCategory).forEach((category) => {
|
|||
<li class="topic meditations">
|
||||
<a href="#meditations" data-topic="meditations">meditations</a>
|
||||
</li>
|
||||
<li class="topic autonomous-racing">
|
||||
<a href="#autonomous-racing" data-topic="autonomous-racing"
|
||||
>autonomous racing</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="posts" id="posts"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import PostLayout from "../../../layouts/PostLayout.astro";
|
||||
import path from "path";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getCollection("posts");
|
||||
|
||||
const routes = [];
|
||||
|
||||
for (const post of allPosts) {
|
||||
const filePath = post.id;
|
||||
|
||||
const pathParts = filePath.split("/");
|
||||
const category = pathParts[0];
|
||||
|
||||
const slug = path.basename(post.id, path.extname(post.id));
|
||||
|
||||
routes.push({
|
||||
params: { category, slug },
|
||||
props: { post },
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<PostLayout frontmatter={post.data} post={post}>
|
||||
<Content />
|
||||
</PostLayout>
|
||||