initial commit
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
dist
|
||||||
|
.solid
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
netlify
|
||||||
|
.vinxi
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
gitignore
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"andi",
|
||||||
|
"mapbox",
|
||||||
|
"owickstrom",
|
||||||
|
"prismjs",
|
||||||
|
"rehype",
|
||||||
|
"solidjs",
|
||||||
|
"vinxi"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# SolidStart
|
||||||
|
|
||||||
|
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm init solid@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm init solid@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||||
|
|
||||||
|
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||||
|
|
||||||
|
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
//@ts-expect-error
|
||||||
|
import pkg from "@vinxi/plugin-mdx";
|
||||||
|
import { blogPostsPlugin } from "./build-helpers/blogPostsPlugin";
|
||||||
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
|
import rehypeMdxCodeProps from "rehype-mdx-code-props";
|
||||||
|
import { mdxPrism } from "./build-helpers/mdxPrism";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
|
||||||
|
const { default: mdx } = pkg;
|
||||||
|
export default defineConfig({
|
||||||
|
extensions: ["mdx", "md"],
|
||||||
|
vite: {
|
||||||
|
plugins: [
|
||||||
|
mdx.withImports({})({
|
||||||
|
remarkPlugins: [remarkFrontmatter, remarkToc],
|
||||||
|
rehypePlugins: [rehypeMdxCodeProps, mdxPrism],
|
||||||
|
jsx: true,
|
||||||
|
jsxImportSource: "solid-js",
|
||||||
|
providerImportSource: "solid-mdx",
|
||||||
|
}),
|
||||||
|
blogPostsPlugin(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
minify: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
prerender: {
|
||||||
|
crawlLinks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Plugin } from "vite";
|
||||||
|
import { readSync } from "to-vfile";
|
||||||
|
import { matter } from "vfile-matter";
|
||||||
|
import { resolve, join } from "node:path";
|
||||||
|
import { readdirSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { exec } from "node:child_process";
|
||||||
|
|
||||||
|
const processFiles = () => {
|
||||||
|
const outputFile = resolve("src/data/posts.json");
|
||||||
|
const blogDir = resolve("src/routes/blog");
|
||||||
|
const files = readdirSync(blogDir);
|
||||||
|
const blogPosts = files
|
||||||
|
.filter(
|
||||||
|
(file) => statSync(join(blogDir, file)).isFile() && file.endsWith(".mdx"),
|
||||||
|
)
|
||||||
|
.map((file) => {
|
||||||
|
const f = readSync(resolve("src/routes/blog", file));
|
||||||
|
matter(f);
|
||||||
|
return {
|
||||||
|
...(f.data.matter as object),
|
||||||
|
slug: file.replace(".mdx", ""),
|
||||||
|
} as { date: string; slug: string };
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
writeFileSync(outputFile, JSON.stringify(blogPosts, null, 2), "utf-8");
|
||||||
|
|
||||||
|
exec("bunx @biomejs/biome format --write ./src/data/posts.json");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const blogPostsPlugin = (): Plugin => {
|
||||||
|
return {
|
||||||
|
name: "blog-posts-gen",
|
||||||
|
buildEnd() {
|
||||||
|
processFiles();
|
||||||
|
},
|
||||||
|
configureServer(server) {
|
||||||
|
server.watcher.on("change", (filePath) => {
|
||||||
|
if (
|
||||||
|
!filePath.includes("/src/routes/blog") &&
|
||||||
|
!filePath.includes("blogPostsPlugin.ts")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
processFiles();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { visit } from "unist-util-visit";
|
||||||
|
import { toString as nodeToString } from "hast-util-to-string";
|
||||||
|
import { refractor } from "refractor";
|
||||||
|
import tsx from "refractor/lang/tsx.js";
|
||||||
|
|
||||||
|
refractor.register(tsx);
|
||||||
|
|
||||||
|
export const mdxPrism = () => {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
return (tree: any) => {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
visit(tree, "element" as any, visitor);
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
function visitor(node: any, index: number | undefined, parent: any) {
|
||||||
|
if (parent.type !== "mdxJsxFlowElement") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
const attrs = parent.attributes.reduce((a: any, c: any) => {
|
||||||
|
if (c.type === "mdxJsxAttribute") {
|
||||||
|
a[c.name] = c.value;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const lang = attrs.lang;
|
||||||
|
if (!lang) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = refractor.highlight(nodeToString(node), lang);
|
||||||
|
|
||||||
|
node.children = result.children;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "example-with-mdx",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vinxi dev",
|
||||||
|
"build": "vinxi build",
|
||||||
|
"start": "vinxi start",
|
||||||
|
"preview": "npx http-server ./.output/public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdx-js/mdx": "^2.3.0",
|
||||||
|
"@solidjs/router": "^0.15.3",
|
||||||
|
"@solidjs/start": "^1.0.11",
|
||||||
|
"@vinxi/plugin-mdx": "^3.7.2",
|
||||||
|
"prismjs": "^1.29.0",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
|
"solid-js": "^1.9.4",
|
||||||
|
"solid-mdx": "^0.0.7",
|
||||||
|
"vinxi": "^0.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"yarn": "3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
|
"@mapbox/rehype-prism": "^0.9.0",
|
||||||
|
"@mdx-js/rollup": "^3.1.0",
|
||||||
|
"@solidjs/meta": "^0.29.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"hast-util-to-string": "^3.0.1",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
|
"refractor": "^4.8.1",
|
||||||
|
"rehype-mdx-code-props": "^3.0.1",
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"solid-jsx": "^1.1.4",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"to-vfile": "^8.0.0",
|
||||||
|
"typed-css-modules": "^0.9.1",
|
||||||
|
"unist-util-visit": "^5.0.0",
|
||||||
|
"vfile-matter": "^5.0.0",
|
||||||
|
"vite": "^6.0.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import tailwind from "tailwindcss";
|
||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: [
|
||||||
|
autoprefixer,
|
||||||
|
tailwind
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
After Width: | Height: | Size: 487 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="16" height="16"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 13.28L12.2 2H13.76L9.56 13.28H8Z" fill="black"></path>
|
||||||
|
<path d="M2 8.89995V7.95195C2 7.45595 2.16 7.05995 2.48 6.76395C2.8 6.46795 3.22 6.31995 3.74 6.31995C4.068 6.31995 4.34 6.37595 4.556 6.48795C4.772 6.59995 4.952 6.73595 5.096 6.89595C5.248 7.05595 5.38 7.21995 5.492 7.38795C5.612 7.54795 5.732 7.68395 5.852 7.79595C5.972 7.89995 6.112 7.95195 6.272 7.95195C6.424 7.95195 6.536 7.90795 6.608 7.81995C6.688 7.72395 6.728 7.59595 6.728 7.43595V6.55995H8V7.50795C8 7.99595 7.84 8.39195 7.52 8.69595C7.208 8.99195 6.788 9.13995 6.26 9.13995C5.932 9.13995 5.66 9.08395 5.444 8.97195C5.228 8.85995 5.044 8.72395 4.892 8.56395C4.748 8.40395 4.616 8.24395 4.496 8.08395C4.384 7.91595 4.268 7.77995 4.148 7.67595C4.028 7.56395 3.888 7.50795 3.728 7.50795C3.584 7.50795 3.472 7.55195 3.392 7.63995C3.312 7.72795 3.272 7.85595 3.272 8.02395V8.89995H2Z" fill="black"></path>
|
||||||
|
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||||
|
@media (prefers-color-scheme: dark) { :root { filter: invert(100%); } }
|
||||||
|
</style></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 258 B |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 388 KiB |
@@ -0,0 +1,331 @@
|
|||||||
|
@import url('https://fonts.cdnfonts.com/css/jetbrains-mono-2');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--line-height: 1.2rem;
|
||||||
|
--border-thickness: 2px;
|
||||||
|
--text-color: black;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-variant-numeric: tabular-nums lining-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
:root {
|
||||||
|
--text-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-decoration-thickness: var(--border-thickness);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::selection {
|
||||||
|
@apply bg-black text-white dark:bg-white dark:text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
border: var(--border-thickness) solid;
|
||||||
|
padding:
|
||||||
|
calc(var(--line-height) / 2 - var(--border-thickness)) calc(1ch - var(--border-thickness));
|
||||||
|
margin: 0;
|
||||||
|
height: calc(var(--line-height) * 2);
|
||||||
|
width: auto;
|
||||||
|
overflow: visible;
|
||||||
|
line-height: normal;
|
||||||
|
-webkit-font-smoothing: inherit;
|
||||||
|
-moz-osx-font-smoothing: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
@apply select-none bg-white dark:bg-black px-1h shadow-box active:shadow-none active:translate-x-[3px] active:translate-y-[3px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus:not(:active) {
|
||||||
|
--border-thickness: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
@apply h-2v block relative text-black dark:text-white border-none my-1v;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr:after {
|
||||||
|
@apply block absolute left-0 h-0 w-full border-black dark:border-white;
|
||||||
|
content: "";
|
||||||
|
top: calc(var(--line-height) - var(--border-thickness));
|
||||||
|
border-top: calc(var(--border-thickness) * 3) double;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-text:hover>.jump-text {
|
||||||
|
animation: jump 0.25s ease-in-out;
|
||||||
|
animation-delay: var(--animation-delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes jump {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-7px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
border: var(--border-thickness) solid var(--text-color);
|
||||||
|
padding: calc(var(--line-height) - var(--border-thickness)) 1ch;
|
||||||
|
margin-bottom: var(--line-height);
|
||||||
|
margin-top: var(--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary {
|
||||||
|
margin-bottom: var(--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
details ::marker {
|
||||||
|
display: inline-block;
|
||||||
|
content: '▶';
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] ::marker {
|
||||||
|
content: '▼';
|
||||||
|
}
|
||||||
|
|
||||||
|
details :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* DITHER ANIMATION */
|
||||||
|
.dither {
|
||||||
|
background-repeat: repeat;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
image-rendering: optimizeSpeed;
|
||||||
|
/* STOP SMOOTHING, GIVE ME SPEED */
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
/* Firefox */
|
||||||
|
image-rendering: -o-crisp-edges;
|
||||||
|
/* Opera */
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
/* Chrome (and eventually Safari) */
|
||||||
|
image-rendering: pixelated;
|
||||||
|
/* Universal support since 2021 */
|
||||||
|
image-rendering: optimize-contrast;
|
||||||
|
/* CSS3 Proposed */
|
||||||
|
-ms-interpolation-mode: nearest-neighbor;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.dither {
|
||||||
|
filter: invert(1)
|
||||||
|
}
|
||||||
|
.wave-image {
|
||||||
|
filter: invert(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dither-1 {
|
||||||
|
background-image: url(/images/dither_light_3.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dither-2 {
|
||||||
|
background-image: url(/images/dither_light_3.png);
|
||||||
|
background-position: 50px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dither-3 {
|
||||||
|
background-image: url(/images/dither_light_3.png);
|
||||||
|
background-position: 100px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tree,
|
||||||
|
.tree ul {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5ch;
|
||||||
|
margin-left: 1.5ch;
|
||||||
|
border-left: var(--border-thickness) solid var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li:before {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: calc(var(--line-height) / 2);
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
width: 1ch;
|
||||||
|
border-bottom: var(--border-thickness) solid var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li:last-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li:last-child:after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
height: calc(var(--line-height) / 2);
|
||||||
|
border-left: var(--border-thickness) solid var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* DEBUG UTILITIES */
|
||||||
|
|
||||||
|
.debug .debug-grid {
|
||||||
|
--line-height: 1.2rem;
|
||||||
|
--text-color: #000000;
|
||||||
|
--background-color: #FFFFFF;
|
||||||
|
--color: color-mix(in srgb, var(--text-color) 10%, var(--background-color) 90%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(var(--color) 0 1px, transparent 1px 100%),
|
||||||
|
repeating-linear-gradient(90deg, var(--color) 0 1px, transparent 1px 100%);
|
||||||
|
background-size: 1ch var(--line-height);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(pre code) {
|
||||||
|
@apply bg-black text-white dark:bg-white dark:text-black px-1h;
|
||||||
|
@apply selection:dark:bg-black selection:dark:text-white selection:bg-white selection:text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug .off-grid {
|
||||||
|
background: rgba(255, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-toggle-label {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug img {
|
||||||
|
/* border: 1px solid black; */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.formkit-form {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-form [data-style="clean"] {
|
||||||
|
@apply !pt-2v !px-0 !pb-1v;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-fields {
|
||||||
|
@apply !m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-field {
|
||||||
|
@apply !m-0 !mr-2h;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input {
|
||||||
|
border: var(--border-thickness) solid !important;
|
||||||
|
padding:
|
||||||
|
calc(var(--line-height) / 2 - var(--border-thickness)) calc(1ch - var(--border-thickness)) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: calc(var(--line-height) * 2) !important;
|
||||||
|
width: 100% !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
line-height: normal !important;
|
||||||
|
-webkit-font-smoothing: inherit !important;
|
||||||
|
-moz-osx-font-smoothing: inherit !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
@apply !font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-input:focus:not(:active) {
|
||||||
|
--border-thickness: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-submit {
|
||||||
|
border: var(--border-thickness) solid !important;
|
||||||
|
padding:
|
||||||
|
calc(var(--line-height) / 2 - var(--border-thickness)) calc(1ch - var(--border-thickness)) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: calc(var(--line-height) * 2) !important;
|
||||||
|
width: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
line-height: normal !important;
|
||||||
|
-webkit-font-smoothing: inherit !important;
|
||||||
|
-moz-osx-font-smoothing: inherit !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
@apply !select-none !bg-white !text-black !px-1h !shadow-box !py-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-submit:active {
|
||||||
|
@apply !shadow-none !translate-x-[3px] !translate-y-[3px]
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-alert-success {
|
||||||
|
border-width: 0 !important;
|
||||||
|
@apply !bg-transparent !text-black !m-0 !p-0 !font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.formkit-input {
|
||||||
|
@apply !border-white !bg-black !text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-submit {
|
||||||
|
@apply !bg-black !text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-alert-success {
|
||||||
|
@apply !text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.formkit-submit span {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-submit:hover span {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formkit-submit:focus:not(:active) {
|
||||||
|
--border-thickness: 3px !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixelated {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <div id="a6d9b30e24" class="formkit-alert formkit-alert-success" data-element="success" data-group="alert">Success! Check your email to confirm the subscription.</div> */
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
declare const styles: {
|
||||||
|
readonly "visually-hidden": string;
|
||||||
|
};
|
||||||
|
export = styles;
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Router } from "@solidjs/router";
|
||||||
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
|
import { createSignal, onCleanup, onMount, Suspense } from "solid-js";
|
||||||
|
import "./app.css";
|
||||||
|
import { Layout } from "./components/Layout";
|
||||||
|
import { MetaProvider, Title } from "@solidjs/meta";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
onMount(() => {
|
||||||
|
const listener = (e: KeyboardEvent) => {
|
||||||
|
if (e.metaKey && e.key.toLowerCase() === "k") {
|
||||||
|
e.preventDefault(); // Prevent the default action (optional)
|
||||||
|
document.body.classList.toggle("debug");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", listener);
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("keydown", listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetaProvider>
|
||||||
|
<Title>minhtrannhat.com</Title>
|
||||||
|
<Router
|
||||||
|
root={(props) => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Suspense>{props.children}</Suspense>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileRoutes />
|
||||||
|
</Router>
|
||||||
|
</MetaProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* Shades of Purple Theme for Prism.js
|
||||||
|
*
|
||||||
|
* @author Ahmad Awais <https://twitter.com/MrAhmadAwais/>
|
||||||
|
* @support Follow/tweet at https://twitter.com/MrAhmadAwais/
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*='language-'],
|
||||||
|
pre[class*='language-'] {
|
||||||
|
@apply text-black dark:text-white font-medium;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*='language-']::-moz-selection,
|
||||||
|
pre[class*='language-'] ::-moz-selection,
|
||||||
|
code[class*='language-']::-moz-selection,
|
||||||
|
code[class*='language-'] ::-moz-selection,
|
||||||
|
pre[class*='language-']::selection,
|
||||||
|
pre[class*='language-'] ::selection,
|
||||||
|
code[class*='language-']::selection,
|
||||||
|
code[class*='language-'] ::selection {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks. */
|
||||||
|
pre[class*='language-'] {
|
||||||
|
@apply box-border border-black dark:border-white border-2;
|
||||||
|
padding: calc(var(--line-height) - var(--border-thickness)) calc(2ch - var(--border-thickness));
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
:not(pre)>code[class*='language-'],
|
||||||
|
pre[class*='language-'] {
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
|
:not(pre)>code[class*='language-'],
|
||||||
|
pre[class*='language-'] {
|
||||||
|
@apply border-white;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
/* :not(pre)>code[class*='language-'] {
|
||||||
|
|
||||||
|
} */
|
||||||
|
|
||||||
|
.token {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.cdata {
|
||||||
|
@apply text-slate-600 dark:text-slate-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.delimiter,
|
||||||
|
.token.keyword,
|
||||||
|
.token.selector,
|
||||||
|
.token.important,
|
||||||
|
.token.atrule {
|
||||||
|
@apply text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.attr-name {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
@apply text-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag,
|
||||||
|
.token.tag .punctuation,
|
||||||
|
.token.doctype,
|
||||||
|
.token.builtin {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity,
|
||||||
|
.token.symbol {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.number {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.constant,
|
||||||
|
.token.variable {
|
||||||
|
@apply font-medium text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.string,
|
||||||
|
.token.char {
|
||||||
|
@apply text-slate-800 dark:text-slate-200;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.attr-value .punctuation {
|
||||||
|
color: #a5c261;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value .punctuation:first-child {
|
||||||
|
color: #a9b7c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.url {
|
||||||
|
color: #287bde;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function {
|
||||||
|
@apply font-bold text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .token.regex {
|
||||||
|
|
||||||
|
} */
|
||||||
|
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.inserted {
|
||||||
|
background: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.deleted {
|
||||||
|
background: #ff000d;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.language-css .token.property,
|
||||||
|
code.language-css .token.property+.token.punctuation {
|
||||||
|
color: #a9b7c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.language-css .token.id {
|
||||||
|
color: #ffc66d;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.language-css .token.selector>.token.class,
|
||||||
|
code.language-css .token.selector>.token.attribute,
|
||||||
|
code.language-css .token.selector>.token.pseudo-class,
|
||||||
|
code.language-css .token.selector>.token.pseudo-element {
|
||||||
|
color: #ffc66d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.class-name {
|
||||||
|
@apply font-medium text-slate-700 dark:text-slate-300;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-highlight.line-highlight {
|
||||||
|
margin-top: 36px;
|
||||||
|
background: linear-gradient(to right, rgba(179, 98, 255, 0.17), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-highlight.line-highlight:before,
|
||||||
|
.line-highlight.line-highlight[data-end]:after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// @ts-expect-error
|
||||||
|
import Posts from "./posts.json";
|
||||||
|
import type { Post } from "~/types";
|
||||||
|
|
||||||
|
export const posts: Post[] = Posts.map((p: Post) => ({
|
||||||
|
...p,
|
||||||
|
date: new Date(p.date),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { posts } from "~/data/posts";
|
||||||
|
import type { Tag } from "~/types";
|
||||||
|
|
||||||
|
export const tags: Record<string, Tag> = posts.reduce(
|
||||||
|
(a, p, i) => {
|
||||||
|
if (Array.isArray(p.tags)) {
|
||||||
|
for (const t of p.tags) {
|
||||||
|
if (!a[t]) {
|
||||||
|
a[t] = {
|
||||||
|
id: t,
|
||||||
|
posts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
a[t].posts.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
},
|
||||||
|
{} as Record<string, Tag>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
date: 20204-05-11
|
||||||
|
tags:
|
||||||
|
- solidjs
|
||||||
|
- webdev
|
||||||
|
title: How to enumerate routes in solid-start
|
||||||
|
---
|
||||||
|
|
||||||
|
This website is built with [solid-start](https://start.solidjs.com/getting-started/what-is-solidstart).
|
||||||
|
|
||||||
|
I needed a way to enumerate all of the posts under `/blog` so that I could dynamically generate lists, both for the [home page](/), and for the tags pages: [/tags/solidjs](/tags/solidjs).
|
||||||
|
|
||||||
|
However, the solid-start `<FileRouter>` doesn't expose any of that information, either at build-time or run-time. We have to figure it out ourselves.
|
||||||
|
|
||||||
|
My first thought was to have a script that watches the directory and writes to a file.
|
||||||
|
|
||||||
|
That would've worked. But while browsing through the docs trying to figure out how to get a file watcher to be part of the vite dev server, I found out about [virtual modules](https://vitejs.dev/guide/api-plugin#virtual-modules-convention).
|
||||||
|
|
||||||
|
Essentially, you can define a `js` module that has dynamic content by exporting a `js` string.
|
||||||
|
|
||||||
|
```js lang="js" lines="2,14"
|
||||||
|
export default function myPlugin() {
|
||||||
|
const virtualModuleId = 'virtual:my-module'
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'my-plugin', // required, will show up in warnings and errors
|
||||||
|
resolveId(id) {
|
||||||
|
if (id === virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
return `export const msg = "from virtual module"`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After adding `myPlugin` to your vite plugins list, you can import the virtual module anywhere in the code and use it as if it were a real file.
|
||||||
|
|
||||||
|
```js lang="js"
|
||||||
|
import {msg} from "virtual:my-module"
|
||||||
|
```
|
||||||
|
|
||||||
|
This seemed more "elegant" than a file watcher.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
date: 20204-05-11
|
||||||
|
tags:
|
||||||
|
- solidjs
|
||||||
|
- webdev
|
||||||
|
title: This blog uses solid-start.
|
||||||
|
description: How to set up a blog using solid-start and mdx.
|
||||||
|
---
|
||||||
|
|
||||||
|
This website uses [solid-start](https://start.solidjs.com/getting-started/what-is-solidstart).
|
||||||
|
|
||||||
|
solid-start is a Next.js-like framework for creating SSR / SSG websites. But instead of react, it builds on top of solid-js.
|
||||||
|
|
||||||
|
I've been using solid and solid-start professionally for almost 2 years. And while solid-js feels rock-solid, solid-start still feels rough around the edges.
|
||||||
|
|
||||||
|
|
||||||
|
For each of the features I wanted to implement, I ran into issues that delayed this from being a weekend project into taking about 4 weekends instead.
|
||||||
|
|
||||||
|
Here's what I wanted to accomplish:
|
||||||
|
1. Use [mdx](https://mdxjs.com/) for writing the posts
|
||||||
|
2. Define metadata for each post in the same mdx file
|
||||||
|
3. Full SSG
|
||||||
|
4. Code highlighting at compile time
|
||||||
|
5. Tags
|
||||||
|
|
||||||
|
And here's how I did it:
|
||||||
|
|
||||||
|
## Using mdx
|
||||||
|
|
||||||
|
MDX is markdown that you can intersprinkle with jsx.
|
||||||
|
|
||||||
|
The initial scaffolding is easy, follow [these steps](https://docs.solidjs.com/solid-start/getting-started), and choose the `with-mdx` option.
|
||||||
|
|
||||||
|
I opted to have my posts live under the `/blog` path.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @refresh reload
|
||||||
|
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||||
|
|
||||||
|
export default createHandler(() => (
|
||||||
|
<StartServer
|
||||||
|
document={({ assets, children, scripts }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<script>
|
||||||
|
{`document.documentElement.classList.toggle('dark', window.matchMedia('(prefers-color-scheme: dark)').matches)`}
|
||||||
|
</script>
|
||||||
|
{assets}
|
||||||
|
</head>
|
||||||
|
<body class="font-mono bg-white dark:bg-black dark:text-white">
|
||||||
|
<div id="app">{children}</div>
|
||||||
|
{scripts}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@solidjs/start/env" />
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { For } from "solid-js";
|
||||||
|
import { posts } from "~/data/posts";
|
||||||
|
import { Posts } from "~/components/Posts";
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
"https://github.com/minhtrannhat",
|
||||||
|
"https://linkedin.com/in/minh-tran-nhat",
|
||||||
|
];
|
||||||
|
const Homepage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section class="flex flex-col sm:flex-row gap-2v sm:gap-3h">
|
||||||
|
<div class="font-medium">
|
||||||
|
<div class="flex items-end mb-1v gap-1h">
|
||||||
|
<img
|
||||||
|
class="inline-block h-2v select-none wave-image"
|
||||||
|
alt="wave emoji"
|
||||||
|
src="/images/wave-pixel.png"
|
||||||
|
/>
|
||||||
|
<p>Hi, Minh here.</p>
|
||||||
|
</div>
|
||||||
|
<p class="mb-1v">
|
||||||
|
I'm Minh Tran, a Computer Engineering student at Concordia
|
||||||
|
University, Montreal, Canada.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
I'm most passionate about designing distributed systems that scales
|
||||||
|
but I'm also interested in compilers and systems programming. When
|
||||||
|
I'm not coding, I read books, listen to podcasts or study music
|
||||||
|
theory.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Say hi:{" "}
|
||||||
|
<a
|
||||||
|
class="underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="mailto:minh@minhtrannhat.com"
|
||||||
|
>
|
||||||
|
minh@minhtrannhat.com
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="sm:mt-3v text-slate-600 dark:text-slate-200 text-base sm:text-sm leading-1">
|
||||||
|
<For each={links}>
|
||||||
|
{(link) => (
|
||||||
|
<li class="list-square hover:text-black dark:hover:text-white ml-2h leading-1">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href={link}
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
{link.replace("https://", "")}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Homepage;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { HttpStatusCode } from "@solidjs/start";
|
||||||
|
|
||||||
|
<HttpStatusCode code={404} />
|
||||||
|
|
||||||
|
# Page Not Found
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { posts } from "~/data/posts";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { For } from "solid-js";
|
||||||
|
|
||||||
|
const Articles = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ol class="flex flex-col gap-1v list-square ml-2h">
|
||||||
|
<For each={Object.values(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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Articles;
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { For, Show } from "solid-js";
|
||||||
|
import type { RouteSectionProps } from "@solidjs/router";
|
||||||
|
import { Meta, Title } from "@solidjs/meta";
|
||||||
|
import { posts } from "~/data/posts";
|
||||||
|
import { MDXProvider } from "solid-mdx";
|
||||||
|
import { markdownComponents, PostImage } from "~/components/Markdown";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import "../css/prism-theme.css";
|
||||||
|
import type { Post } from "~/types";
|
||||||
|
|
||||||
|
const Blog = (props: RouteSectionProps<unknown>) => {
|
||||||
|
const meta = () =>
|
||||||
|
posts.find((p) => props.location.pathname.endsWith(p.slug)) as Post;
|
||||||
|
const index = () => posts.indexOf(meta());
|
||||||
|
|
||||||
|
const prevMeta = () =>
|
||||||
|
index() === posts.length - 1 ? undefined : posts[index() + 1];
|
||||||
|
const nextMeta = () => (index() === 0 ? undefined : posts[index() - 1]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article class="pb-5v">
|
||||||
|
<Title>minhtrannhat.com - {meta()?.title}</Title>
|
||||||
|
<Meta name="og:title" content={meta().title} />
|
||||||
|
<Meta name="description" content={meta().description} />
|
||||||
|
<Meta name="og:description" content={meta().description} />
|
||||||
|
|
||||||
|
<Show when={meta().featuredImage}>
|
||||||
|
<PostImage
|
||||||
|
class="mb-3v saturate-0"
|
||||||
|
src={meta().featuredImage || ""}
|
||||||
|
alt={meta().featuredImageDesc || ""}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<h1 class="text-2v leading-2 font-bold mb-1v">{meta().title}</h1>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4h mb-2v text-sm leading-1">
|
||||||
|
<p>{dayjs(meta().date).format("D MMMM YYYY")}</p>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<For each={meta().tags}>
|
||||||
|
{(tag, index) => (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={`/tags/${tag}`}
|
||||||
|
class="font-medium underline underline-offset-2 italic"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
{index() === meta().tags.length - 1 ? "" : ", "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MDXProvider components={markdownComponents}>
|
||||||
|
{props.children}
|
||||||
|
</MDXProvider>
|
||||||
|
|
||||||
|
<div class="mt-3v flex flex-col gap-1v">
|
||||||
|
<Show when={prevMeta()} fallback={<div />}>
|
||||||
|
<div class="flex gap-1h">
|
||||||
|
<span>Previous:</span>
|
||||||
|
<a
|
||||||
|
class="underline underline-offset-2"
|
||||||
|
href={`/blog/${prevMeta()?.slug}`}
|
||||||
|
>
|
||||||
|
{prevMeta()?.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={nextMeta()} fallback={<div />}>
|
||||||
|
<div class="flex gap-1h">
|
||||||
|
<span>Next:</span>
|
||||||
|
<a
|
||||||
|
class="underline underline-offset-2"
|
||||||
|
href={`/blog/${nextMeta()?.slug}`}
|
||||||
|
>
|
||||||
|
{nextMeta()?.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Blog;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { For } from "solid-js";
|
||||||
|
import { tags } from "~/data/tags";
|
||||||
|
|
||||||
|
const Tags = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-bold mt-2v mb-1v">All tags:</h1>
|
||||||
|
<ol class="flex flex-col gap-1v list-square ml-2h">
|
||||||
|
<For each={Object.values(tags)}>
|
||||||
|
{(tag) => (
|
||||||
|
<li class="">
|
||||||
|
<a class="underline underline-offset-2" href={`/tags/${tag.id}`}>
|
||||||
|
{tag.id}
|
||||||
|
</a>
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
- {tag.posts.length} Post{tag.posts.length === 1 ? "" : "s"}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tags;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { RouteSectionProps } from "@solidjs/router";
|
||||||
|
import { type Component, Show } from "solid-js";
|
||||||
|
import { posts } from "~/data/posts";
|
||||||
|
import { Posts } from "~/components/Posts";
|
||||||
|
import { tags } from "~/data/tags";
|
||||||
|
|
||||||
|
const TagId: Component<RouteSectionProps<unknown>> = (props) => {
|
||||||
|
const tag = () => tags[props.params.id];
|
||||||
|
return (
|
||||||
|
<Show when={tag()} fallback={<div>No posts with that tag</div>}>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-lg font-bold mb-6">Tag: {tag().id}</h1>
|
||||||
|
|
||||||
|
<Posts posts={tag().posts.map((i) => posts[i])} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagId;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export type Post = {
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
slug: string;
|
||||||
|
tags: string[];
|
||||||
|
featuredImage?: string;
|
||||||
|
featuredImageDesc?: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tag = {
|
||||||
|
// id/name of tag
|
||||||
|
id: string;
|
||||||
|
// indexes of posts with tag (they point to the posts list coming from virtual:blog-posts)
|
||||||
|
posts: number[];
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Accessor } from "solid-js";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
import { useRouteTransitionTiming } from "./useRouteTransitionTiming";
|
||||||
|
|
||||||
|
export const useDitherAnimation = (ref: Accessor<HTMLElement | undefined>) => {
|
||||||
|
if (!isServer) {
|
||||||
|
const d1 = document.createElement("div");
|
||||||
|
d1.classList.add("dither", "dither-1");
|
||||||
|
const d2 = document.createElement("div");
|
||||||
|
d2.classList.add("dither", "dither-2");
|
||||||
|
const d3 = document.createElement("div");
|
||||||
|
d3.classList.add("dither", "dither-3");
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
useRouteTransitionTiming(
|
||||||
|
300,
|
||||||
|
() => {
|
||||||
|
ref()?.appendChild(d1);
|
||||||
|
setTimeout(() => {
|
||||||
|
ref()?.appendChild(d2);
|
||||||
|
}, 100);
|
||||||
|
setTimeout(() => {
|
||||||
|
ref()?.appendChild(d3);
|
||||||
|
}, 200);
|
||||||
|
started = true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const rnd = () =>
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
d1.style.backgroundPosition = `${Math.round(
|
||||||
|
Math.random() * 100,
|
||||||
|
)}px ${Math.round(Math.random() * 100)}px`;
|
||||||
|
d2.style.backgroundPosition = `${Math.round(
|
||||||
|
Math.random() * 100,
|
||||||
|
)}px ${Math.round(Math.random() * 100)}px`;
|
||||||
|
d3.style.backgroundPosition = `${Math.round(
|
||||||
|
Math.random() * 100,
|
||||||
|
)}px ${Math.round(Math.random() * 100)}px`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
if (started) {
|
||||||
|
rnd();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
rnd();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
started = false;
|
||||||
|
try {
|
||||||
|
ref()?.removeChild(d3);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
ref()?.removeChild(d2);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
ref()?.removeChild(d1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useIsRouting, useBeforeLeave } from "@solidjs/router";
|
||||||
|
import { createEffect } from "solid-js";
|
||||||
|
|
||||||
|
export const useRouteTransitionTiming = (
|
||||||
|
transitionTime: number,
|
||||||
|
onEnter: () => void,
|
||||||
|
onLoading: () => void,
|
||||||
|
onExit: () => void,
|
||||||
|
) => {
|
||||||
|
const isRouting = useIsRouting();
|
||||||
|
createEffect((oldR: boolean | undefined) => {
|
||||||
|
const r = isRouting();
|
||||||
|
if (oldR && !r) onExit();
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
useBeforeLeave((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onEnter();
|
||||||
|
setTimeout(() => {
|
||||||
|
e.retry(true);
|
||||||
|
onLoading();
|
||||||
|
}, transitionTime);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const lineHeight = {
|
||||||
|
val: 1.2,
|
||||||
|
unit: "rem",
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLineHeights = () => {
|
||||||
|
const h: Record<string, string> = {};
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
h[i.toString()] = `${i * lineHeight.val}${lineHeight.unit}`;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSpacing = () => {
|
||||||
|
const h: Record<string, string> = {};
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
h[`${i}v`] = `${i * lineHeight.val}${lineHeight.unit}`;
|
||||||
|
h[`${i}h`] = `${i}ch`;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: ["./src/**/*.{html,tsx,mdx}"],
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: '"JetBrains Mono", monospace;',
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
normal: "500",
|
||||||
|
medium: "600",
|
||||||
|
bold: "800",
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
thread: "calc(min(80ch, round(down, 100%, 1ch)))",
|
||||||
|
},
|
||||||
|
lineHeight: buildLineHeights(),
|
||||||
|
colors: {
|
||||||
|
black: "#000000",
|
||||||
|
"slate-50": "#f8fafc",
|
||||||
|
"slate-100": "#f1f5f9",
|
||||||
|
"slate-200": "#e2e8f0",
|
||||||
|
"slate-300": "#cbd5e1",
|
||||||
|
"slate-400": "#94a3b8",
|
||||||
|
"slate-500": "#64748b",
|
||||||
|
"slate-600": "#475569",
|
||||||
|
"slate-700": "#334155",
|
||||||
|
"slate-800": "#1e293b",
|
||||||
|
"slate-900": "#0f172a",
|
||||||
|
"slate-950": "#020617",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
transparent: "transparent",
|
||||||
|
},
|
||||||
|
borderWidth: {
|
||||||
|
"2": "2px",
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
spacing: buildSpacing(),
|
||||||
|
listStyleType: {
|
||||||
|
square: "square",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
box: "3px 3px 0px",
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
"2v": "2rem",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["vinxi/client", "vite/client", "solid-jsx/types"],
|
||||||
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||