initial commit
This commit is contained in:
9
src/components/Button.tsx
Normal file
9
src/components/Button.tsx
Normal 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>
|
||||
);
|
||||
};
|
69
src/components/DarkModeToggle.tsx
Normal file
69
src/components/DarkModeToggle.tsx
Normal 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
62
src/components/Layout.tsx
Normal 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
207
src/components/Markdown.tsx
Normal 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
22
src/components/Posts.tsx
Normal 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>
|
||||
);
|
||||
};
|
20
src/components/TextHoverJump.tsx
Normal file
20
src/components/TextHoverJump.tsx
Normal 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
27
src/components/Tree.tsx
Normal 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>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user