initial commit

This commit is contained in:
2025-01-23 11:16:33 -05:00
commit 25cabeb8df
57 changed files with 11214 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import type { ParentComponent } from "solid-js";
export const Button: ParentComponent<{ onClick?: () => void }> = (props) => {
return (
<button class="button" type="button" onClick={props.onClick}>
{props.children}
</button>
);
};

View File

@@ -0,0 +1,69 @@
import { createSignal, onMount } from "solid-js";
export const DarkModeToggle = () => {
let ref!: HTMLButtonElement;
const [dark, setDark] = createSignal(false);
const size = 64;
const max = 5;
const time = 70;
let direction = dark() ? -1 : 1;
let current = dark() ? 0 : max;
onMount(() => {
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setDark(isDark);
if (isDark) {
direction = -1;
current = 0;
}
requestAnimationFrame(() => {
play();
});
});
const coord = (n: number) => -n * size;
const play = () => {
ref.style.backgroundPositionX = `${coord(current)}px`;
if (direction === -1 && current === 2) {
document.documentElement.classList.add("dark");
// ref.style.filter = "invert(0)";
}
if (direction === 1 && current === 3) {
document.documentElement.classList.remove("dark");
// ref.style.filter = "invert(1)";
}
if (direction === -1 && current === 0) return;
if (direction === 1 && current === max) return;
current += direction;
setTimeout(play, time);
};
const toggle = () => {
if (dark()) {
direction = 1;
} else {
direction = -1;
}
play();
setDark((d) => !d);
};
return (
<div class="absolute top-1v right-4h">
<button
onClick={toggle}
ref={ref}
class="pixelated"
style={{
scale: "1.5",
height: "32px",
width: "64px",
"background-image": `url("/images/toggle.png")`,
}}
aria-hidden
type="button"
/>
</div>
);
};

62
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,62 @@
import { A } from "@solidjs/router";
import { For, type ParentComponent } from "solid-js";
import { TextHoverJump } from "./TextHoverJump";
import { clientOnly } from "@solidjs/start";
const DarkModeToggle = clientOnly(() =>
import("./DarkModeToggle").then((r) => ({
default: r.DarkModeToggle,
})),
);
export const Layout: ParentComponent = (props) => {
return (
<>
<a href="#main-content" class="sr-only">
Skip to main content
</a>
<div class="flex flex-col min-h-screen pt-2v py-1v px-2h max-w-thread mx-auto relative overflow-x-hidden leading-1 box-border decoration-2 underline-offset-2">
<header class="flex flex-col items-center justify-center gap-2v px-4h py-2v">
<a href="/" class="text-2v leading-2 font-bold">
<TextHoverJump text="~/minhtrannhat.com" />
</a>
<DarkModeToggle />
<nav>
<ul class="flex items-center gap-7h">
<A end class="hover:underline" activeClass="font-bold" href={"/"}>
Home
</A>
<A
end
class="hover:underline"
activeClass="font-bold"
href={"/articles"}
>
Articles
</A>
<A
end
class="hover:underline"
activeClass="font-bold"
href={"/tags"}
>
Tags
</A>
<a href="/resume.pdf" target="_blank" rel="noreferrer">
Resume
</a>
</ul>
</nav>
</header>
<main id="main-content" class="mt-1v flex-auto">
{props.children}
</main>
<div class="debug-grid" />
</div>
</>
);
};

207
src/components/Markdown.tsx Normal file
View File

@@ -0,0 +1,207 @@
import {
type Component,
createMemo,
type ParentComponent,
type JSXElement,
createSignal,
Show,
onMount,
} from "solid-js";
const P: ParentComponent = (props) => <p class="mt-1v">{props.children}</p>;
const Ol: ParentComponent = (props) => (
<ol class="list-decimal [&>li]:ml-3h">{props.children}</ol>
);
const Ul: ParentComponent = (props) => (
<ul class="list-square [&>li]:ml-2h">{props.children}</ul>
);
const Li: ParentComponent = (props) => <li class="">{props.children}</li>;
export const Blockquote: ParentComponent = (props) => (
<blockquote class="my-2v pl-1h text-slate-700 dark:text-slate-200 font-medium italic grid grid-cols-[max-content_1fr]">
<span class="w-2h">{"> "}</span>
<div class="[&>p]:mt-0">{props.children}</div>
</blockquote>
);
const Pre: ParentComponent<{ lang: string; lines?: string; file?: string }> = (
props,
) => {
const [copied, setCopied] = createSignal(false);
let ref!: HTMLPreElement;
const onCopy = () => {
setCopied(true);
navigator.clipboard.writeText(ref.innerText);
setTimeout(() => {
setCopied(false);
}, 1500);
};
return (
<div class="my-1v">
<div class="bg-black text-white dark:bg-white dark:text-black flex justify-between px-1h text-sm leading-1">
<Show when={props.file} fallback={<span aria-hidden />}>
<span>{props.file}</span>
</Show>
<button type="button" onClick={onCopy}>
{copied() ? "Copied!" : "Copy code"}
</button>
</div>
<pre ref={ref} class={`language-${props.lang}`} data-line={props.lines}>
{props.children}
</pre>
</div>
);
};
const headingLink = (children: JSXElement) =>
children?.toString().toLowerCase().replaceAll(" ", "-").replaceAll(",", "");
const HeadlineLink: Component<{ link: string; class: string }> = (props) => {
return (
<a href={props.link} class="relative top-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class={props.class}
fill="none"
>
<title>link</title>
<path
d="M9.52051 14.4359L14.4335 9.52283"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
/>
<path
d="M12.5685 15.1086C13.3082 16.249 13.1108 17.418 12.2563 18.2725L9.26109 21.2678C8.28269 22.2462 6.69638 22.2462 5.71798 21.2678L2.73185 18.2816C1.75345 17.3032 1.75345 15.7169 2.73185 14.7385L5.72706 11.7433C6.429 11.0413 7.76312 10.636 8.90958 11.4662M15.1083 12.5688C16.2487 13.3085 17.4177 13.1111 18.2722 12.2566L21.2674 9.26138C22.2458 8.28297 22.2458 6.69666 21.2674 5.71825L18.2813 2.7321C17.3029 1.75369 15.7166 1.75369 14.7382 2.7321L11.743 5.72733C11.041 6.42927 10.6357 7.7634 11.4659 8.90986"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
);
};
const H2: ParentComponent = (props) => (
<h2
id={headingLink(props.children)}
class="text-2xl leading-2 font-bold mt-2v mb-1v flex items-center gap-1h scroll-mt-2v"
>
{props.children}
<HeadlineLink class="w-5 h-5" link={`#${headingLink(props.children)}`} />
</h2>
);
const H3: ParentComponent = (props) => (
<h3
id={headingLink(props.children)}
class="text-xl leading-2 font-bold mt-2v mb-1v flex items-center gap-1h scroll-mt-2v"
>
{props.children}
<HeadlineLink class="w-4 h-4" link={`#${headingLink(props.children)}`} />
</h3>
);
const H4: ParentComponent = (props) => (
<h4
id={headingLink(props.children)}
class="text-lg leading-1 font-bold mt-2v mb-1v flex items-center gap-1h scroll-mt-2v"
>
{props.children}
<HeadlineLink class="w-3 h-3" link={`#${headingLink(props.children)}`} />
</h4>
);
const A: ParentComponent<{ href: string }> = (props) => {
const isLocal = createMemo(() =>
["/", "./", "#"].some((s) => props.href.startsWith(s)),
);
return (
<a
href={props.href}
target={isLocal() ? "" : "_blank"}
class="underline underline-offset-2"
>
{props.children}
</a>
);
};
function gridCellDimensions() {
const element = document.createElement("div");
element.style.position = "fixed";
element.style.height = "var(--line-height)";
element.style.width = "1ch";
document.body.appendChild(element);
const rect = element.getBoundingClientRect();
document.body.removeChild(element);
return { width: rect.width, height: rect.height };
}
export const PostImage: Component<{
src: string;
alt: string;
attr?: JSXElement;
class?: string;
}> = (props) => {
let ref!: HTMLImageElement;
onMount(() => {
const cell = gridCellDimensions();
function setHeightFromRatio() {
const ratio = ref.naturalWidth / ref.naturalHeight;
const rect = ref.getBoundingClientRect();
const realHeight = rect.width / ratio;
const diff = cell.height - (realHeight % cell.height);
ref.style.setProperty("padding-bottom", `${diff}px`);
}
if (ref.complete) {
setHeightFromRatio();
} else {
ref.addEventListener("load", () => {
setHeightFromRatio();
});
}
});
return (
<div>
<img
ref={ref}
src={props.src}
alt={props.alt}
class="w-full"
classList={{ [props.class || ""]: !!props.class }}
/>
{props.attr}
</div>
);
};
export const Aside: ParentComponent = (props) => (
<aside class="border-l-2 border-black dark:border-white pl-1h mt-1v">
<div class="uppercase text-sm leading-1 font-medium select-none">Aside</div>
<div class="[&_*:first-child]:mt-0">{props.children}</div>
</aside>
);
export const markdownComponents = {
a: A,
p: P,
li: Li,
ol: Ol,
ul: Ul,
blockquote: Blockquote,
pre: Pre,
h2: H2,
h3: H3,
h4: H4,
};

22
src/components/Posts.tsx Normal file
View File

@@ -0,0 +1,22 @@
import dayjs from "dayjs";
import { type Component, For } from "solid-js";
import type { Post } from "~/types";
export const Posts: Component<{ posts: Post[] }> = (props) => {
return (
<ol class="">
<For each={props.posts}>
{(post) => (
<li class="list-square ml-2h mb-1v">
<a class="font-medium underline block" href={`/blog/${post.slug}`}>
{post.title}
</a>
<span class="text-xs leading-1 text-slate-600 dark:text-slate-400">
{dayjs(post.date).format("MMMM YYYY")}
</span>
</li>
)}
</For>
</ol>
);
};

View File

@@ -0,0 +1,20 @@
import { For, type Component } from "solid-js";
export const TextHoverJump: Component<{ text: string }> = (props) => {
return (
<span class="jump-text flex items-baseline">
<For each={[...props.text]}>
{(i, index) => (
<span
class="jump-text block"
style={{
"--animation-delay": `${index() * 20}ms`,
}}
>
{i}
</span>
)}
</For>
</span>
);
};

27
src/components/Tree.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { type Component, For, Show } from "solid-js";
type Node = { l: string; c: TreeNode[] };
type TreeNode = string | Node;
const Subtree: Component<{ tree: TreeNode }> = (props) => {
return (
<Show
when={typeof props.tree !== "string"}
fallback={<li>{props.tree as string}</li>}
>
<li>
<span>{(props.tree as Node).l}</span>
<ul class="incremental">
<For each={(props.tree as Node).c}>{(c) => <Subtree tree={c} />}</For>
</ul>
</li>
</Show>
);
};
export const Tree: Component<{ tree: TreeNode }> = (props) => {
return (
<ul class="tree [&>li>span]:font-bold">
<Subtree tree={props.tree} />
</ul>
);
};