Compare commits
14 Commits
e62e465fec
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
2fb625df9b
|
|||
|
bf224e3a65
|
|||
|
41b92fe886
|
|||
|
5c1d7f4b67
|
|||
|
d351d3d8b6
|
|||
|
12b48ac6fc
|
|||
|
20547513b6
|
|||
|
bb4891d593
|
|||
|
482f5f90c0
|
|||
|
0d30048b10
|
|||
|
605f76f133
|
|||
|
7aeff304bb
|
|||
|
7781ac660b
|
|||
|
f1ed47c164
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ gitignore
|
|||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# AI stuffs
|
||||||
|
.claude/
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"cSpell.words": [
|
|
||||||
"andi",
|
|
||||||
"mapbox",
|
|
||||||
"owickstrom",
|
|
||||||
"prismjs",
|
|
||||||
"rehype",
|
|
||||||
"solidjs",
|
|
||||||
"vinxi"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM oven/bun:1 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
COPY bun.lock package.json ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM base AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
COPY --from=build /app/.output ./.output
|
||||||
|
|
||||||
|
EXPOSE 8085
|
||||||
|
|
||||||
|
CMD ["bun", "run", "serve"]
|
||||||
36
README.md
36
README.md
@@ -1,32 +1,16 @@
|
|||||||
# SolidStart
|
# minhtran_dev website code
|
||||||
|
|
||||||
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
## Set up dev server
|
||||||
|
|
||||||
## Creating a project
|
- `bun run dev`
|
||||||
|
|
||||||
```bash
|
## Build binary
|
||||||
# create a new project in the current directory
|
|
||||||
npm init solid@latest
|
|
||||||
|
|
||||||
# create a new project in my-app
|
- `bun run build`
|
||||||
npm init solid@latest my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
## Deploy
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
- Make sure you built this first
|
||||||
|
- `rsync` to my VPS
|
||||||
```bash
|
- Run `pm2 restart 0` as `0` is the current task_id for my portfolio web app
|
||||||
npm run dev
|
- [ ] Setup CI/CD for this project
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
import { defineConfig } from "@solidjs/start/config";
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
//@ts-expect-error
|
|
||||||
import pkg from "@vinxi/plugin-mdx";
|
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";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import rehypePrismPlus from "rehype-prism-plus";
|
||||||
|
import rehypeMdxCodeProps from "rehype-mdx-code-props";
|
||||||
|
import remarkFrontmatter from "remark-frontmatter";
|
||||||
|
import { blogPostsPlugin } from "./build-helpers/blogPostsPlugin";
|
||||||
|
|
||||||
const { default: mdx } = pkg;
|
const { default: mdx } = pkg;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
preset: "static",
|
||||||
|
},
|
||||||
extensions: ["mdx", "md"],
|
extensions: ["mdx", "md"],
|
||||||
vite: {
|
vite: () => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
blogPostsPlugin(),
|
||||||
mdx.withImports({})({
|
mdx.withImports({})({
|
||||||
remarkPlugins: [remarkFrontmatter, remarkToc],
|
|
||||||
rehypePlugins: [rehypeMdxCodeProps, mdxPrism],
|
|
||||||
jsx: true,
|
jsx: true,
|
||||||
jsxImportSource: "solid-js",
|
jsxImportSource: "solid-js",
|
||||||
providerImportSource: "solid-mdx",
|
providerImportSource: "solid-mdx",
|
||||||
|
remarkPlugins: [remarkFrontmatter],
|
||||||
|
rehypePlugins: [
|
||||||
|
[rehypePrismPlus, { ignoreMissing: true }],
|
||||||
|
rehypeMdxCodeProps,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
blogPostsPlugin(),
|
|
||||||
tailwindcss(),
|
|
||||||
],
|
],
|
||||||
build: {
|
}),
|
||||||
minify: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
prerender: {
|
|
||||||
crawlLinks: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,49 +1,36 @@
|
|||||||
import type { Plugin } from "vite";
|
import type { Plugin } from "vite";
|
||||||
import { readSync } from "to-vfile";
|
|
||||||
import { matter } from "vfile-matter";
|
|
||||||
import { resolve, join } from "node:path";
|
import { resolve, join } from "node:path";
|
||||||
import { readdirSync, statSync, writeFileSync } from "node:fs";
|
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { exec } from "node:child_process";
|
import matter from "gray-matter";
|
||||||
|
|
||||||
const processFiles = () => {
|
const BLOG_DIR = resolve("src/routes/blog");
|
||||||
const outputFile = resolve("src/data/posts.json");
|
const OUTPUT_FILE = resolve("src/data/posts.json");
|
||||||
const blogDir = resolve("src/routes/blog");
|
|
||||||
const files = readdirSync(blogDir);
|
function generatePostsJson() {
|
||||||
const blogPosts = files
|
const posts = readdirSync(BLOG_DIR)
|
||||||
.filter(
|
.filter((file) => file.endsWith(".mdx"))
|
||||||
(file) => statSync(join(blogDir, file)).isFile() && file.endsWith(".mdx"),
|
|
||||||
)
|
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
const f = readSync(resolve("src/routes/blog", file));
|
const content = readFileSync(join(BLOG_DIR, file), "utf-8");
|
||||||
matter(f);
|
const { data } = matter(content);
|
||||||
return {
|
return { ...data, slug: file.replace(".mdx", "") };
|
||||||
...(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());
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.date as string).getTime() -
|
||||||
|
new Date(a.date as string).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
writeFileSync(outputFile, JSON.stringify(blogPosts, null, 2), "utf-8");
|
writeFileSync(OUTPUT_FILE, JSON.stringify(posts, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
exec("bunx @biomejs/biome format --write ./src/data/posts.json");
|
export const blogPostsPlugin = (): Plugin => ({
|
||||||
};
|
name: "blog-posts-gen",
|
||||||
|
buildStart: generatePostsJson,
|
||||||
export const blogPostsPlugin = (): Plugin => {
|
configureServer(server) {
|
||||||
return {
|
server.watcher.on("change", (path) => {
|
||||||
name: "blog-posts-gen",
|
if (path.includes("src/routes/blog") && path.endsWith(".mdx")) {
|
||||||
buildEnd() {
|
generatePostsJson();
|
||||||
processFiles();
|
}
|
||||||
},
|
});
|
||||||
configureServer(server) {
|
},
|
||||||
server.watcher.on("change", (filePath) => {
|
});
|
||||||
if (
|
|
||||||
!filePath.includes("/src/routes/blog") &&
|
|
||||||
!filePath.includes("blogPostsPlugin.ts")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
processFiles();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
11
compose.yaml
Normal file
11
compose.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: runtime
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8085:8085"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
restart: unless-stopped
|
||||||
27
package.json
27
package.json
@@ -5,20 +5,25 @@
|
|||||||
"dev": "vinxi dev",
|
"dev": "vinxi dev",
|
||||||
"build": "vinxi build",
|
"build": "vinxi build",
|
||||||
"start": "vinxi start",
|
"start": "vinxi start",
|
||||||
|
"serve": "bunx serve .output/public -l 8085",
|
||||||
"preview": "npx http-server ./.output/public"
|
"preview": "npx http-server ./.output/public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdx-js/mdx": "^2.3.0",
|
"@mdx-js/mdx": "^3.1.0",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"@solidjs/start": "^1.0.11",
|
"@solidjs/start": "^1.1.1",
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/vite": "4.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
|
||||||
"@vinxi/plugin-mdx": "^3.7.2",
|
"@vinxi/plugin-mdx": "^3.7.2",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
|
"rehype-prism-plus": "^2.0.1",
|
||||||
"remark-toc": "^9.0.0",
|
"remark-toc": "^9.0.0",
|
||||||
"solid-js": "^1.9.4",
|
"solid-js": "^1.9.5",
|
||||||
"solid-mdx": "^0.0.7",
|
"solid-mdx": "^0.0.7",
|
||||||
"vinxi": "^0.5.1"
|
"tailwind-nord": "^1.3.0",
|
||||||
|
"tailwindcss": "4.0.0",
|
||||||
|
"vinxi": "^0.5.3",
|
||||||
|
"serve": "^14.2.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
@@ -26,22 +31,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@mapbox/rehype-prism": "^0.9.0",
|
|
||||||
"@mdx-js/rollup": "^3.1.0",
|
"@mdx-js/rollup": "^3.1.0",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"dayjs": "^1.11.13",
|
"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",
|
"rehype-mdx-code-props": "^3.0.1",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"solid-jsx": "^1.1.4",
|
"solid-jsx": "^1.1.4",
|
||||||
"tailwindcss": "^4.0.0",
|
|
||||||
"to-vfile": "^8.0.0",
|
|
||||||
"typed-css-modules": "^0.9.1",
|
"typed-css-modules": "^0.9.1",
|
||||||
"unist-util-visit": "^5.0.0",
|
"vite": "^6.1.1"
|
||||||
"vfile-matter": "^5.0.0",
|
|
||||||
"vite": "^6.0.11"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6987
pnpm-lock.yaml
generated
6987
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
import tailwind from "tailwindcss";
|
|
||||||
import autoprefixer from "autoprefixer";
|
|
||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: [
|
|
||||||
autoprefixer,
|
|
||||||
tailwind
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 487 B |
@@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 388 KiB |
BIN
public/og.png
Normal file
BIN
public/og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
337
src/app.css
337
src/app.css
@@ -1,329 +1,46 @@
|
|||||||
@import url('https://fonts.cdnfonts.com/css/jetbrains-mono-2');
|
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
||||||
@import "tailwind";
|
@import "tailwindcss";
|
||||||
|
@config "../tailwind.config.ts";
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
* {
|
|
||||||
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,
|
||||||
.tree ul {
|
.tree ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul {
|
.tree ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul li {
|
.tree ul li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 1.5ch;
|
padding-left: 1.5ch;
|
||||||
margin-left: 1.5ch;
|
margin-left: 1.5ch;
|
||||||
border-left: var(--border-thickness) solid var(--text-color);
|
border-left: var(--border-thickness) solid var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul li:before {
|
.tree ul li:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
top: calc(var(--line-height) / 2);
|
top: calc(var(--line-height) / 2);
|
||||||
left: 0;
|
left: 0;
|
||||||
content: "";
|
content: "";
|
||||||
width: 1ch;
|
width: 1ch;
|
||||||
border-bottom: var(--border-thickness) solid var(--text-color);
|
border-bottom: var(--border-thickness) solid var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul li:last-child {
|
.tree ul li:last-child {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree ul li:last-child:after {
|
.tree ul li:last-child:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
content: "";
|
content: "";
|
||||||
height: calc(var(--line-height) / 2);
|
height: calc(var(--line-height) / 2);
|
||||||
border-left: var(--border-thickness) solid var(--text-color);
|
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> */
|
|
||||||
|
|||||||
5
src/app.css.d.ts
vendored
5
src/app.css.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
declare const styles: {
|
|
||||||
readonly "visually-hidden": string;
|
|
||||||
};
|
|
||||||
export = styles;
|
|
||||||
|
|
||||||
18
src/app.tsx
18
src/app.tsx
@@ -1,27 +1,13 @@
|
|||||||
import { Router } from "@solidjs/router";
|
import { Router } from "@solidjs/router";
|
||||||
import { FileRoutes } from "@solidjs/start/router";
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
import { createSignal, onCleanup, onMount, Suspense } from "solid-js";
|
import { Suspense } from "solid-js";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
import { Layout } from "./components/Layout";
|
import { Layout } from "./components/Layout";
|
||||||
import { MetaProvider, Title } from "@solidjs/meta";
|
import { MetaProvider } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function App() {
|
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 (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Title>minhtrannhat.com</Title>
|
|
||||||
<Router
|
<Router
|
||||||
root={(props) => {
|
root={(props) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { ParentComponent } from "solid-js";
|
import type { ParentComponent } from "solid-js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
export const Button: ParentComponent<{ onClick?: () => void }> = (props) => {
|
export const Button: ParentComponent<{ onClick?: () => void }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<button class="button" type="button" onClick={props.onClick}>
|
<button class="button" type="button" onClick={props.onClick}>
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
17
src/components/Experience.tsx
Normal file
17
src/components/Experience.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { For } from "solid-js";
|
||||||
|
import JobCard from "./JobCard";
|
||||||
|
import { jobs } from "~/data/jobs";
|
||||||
|
|
||||||
|
const Experience = () => {
|
||||||
|
return (
|
||||||
|
<section class="mt-16 px-4">
|
||||||
|
<h2 class="text-xl text-nord-1 font-bold mb-6">Experience</h2>
|
||||||
|
|
||||||
|
<div class="!flex !flex-col !gap-0.5v ml-2h">
|
||||||
|
<For each={jobs}>{(job) => <JobCard job={job} />}</For>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Experience;
|
||||||
23
src/components/JobCard.tsx
Normal file
23
src/components/JobCard.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Job } from "~/types";
|
||||||
|
|
||||||
|
interface JobCardProps {
|
||||||
|
job: Job;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JobCard = (props: JobCardProps) => {
|
||||||
|
return (
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-bold text-xl">
|
||||||
|
{props.job.title} @
|
||||||
|
<a href={props.job.url} class="text-blue-500 hover:text-blue-600">
|
||||||
|
{props.job.company}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p class="text-nord-1 mb-4">
|
||||||
|
{props.job.range} | {props.job.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JobCard;
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
import type { ParentComponent } from "solid-js";
|
import type { ParentComponent } from "solid-js";
|
||||||
import { clientOnly } from "@solidjs/start";
|
|
||||||
|
|
||||||
const DarkModeToggle = clientOnly(() =>
|
|
||||||
import("./DarkModeToggle").then((r) => ({
|
|
||||||
default: r.DarkModeToggle,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
function changeFavicon(newFaviconPath: string) {
|
function changeFavicon(newFaviconPath: string) {
|
||||||
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
||||||
if (link) {
|
if (link) {
|
||||||
@@ -26,22 +22,25 @@ export const Layout: ParentComponent = (props) => {
|
|||||||
<a href="#main-content" class="sr-only">
|
<a href="#main-content" class="sr-only">
|
||||||
Skip to main content
|
Skip to main content
|
||||||
</a>
|
</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">
|
<div class="bg-nord-6 flex flex-col min-h-screen pt-2v py-1v px-2h max-w-full 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">
|
<header class="flex flex-col items-center justify-center gap-2v px-4h py-2v">
|
||||||
<a href="/" class="text-2v leading-2 font-bold">
|
<a href="/" class="text-nord-3 text-2v leading-2 font-bold">
|
||||||
~/minhtrannhat
|
~/minh
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<DarkModeToggle />
|
<nav class="container mx-auto px-4 py-4">
|
||||||
|
<ul class="flex flex-wrap justify-center items-center gap-6">
|
||||||
<nav>
|
<A
|
||||||
<ul class="flex items-center gap-7h">
|
end
|
||||||
<A end class="hover:underline" activeClass="font-bold" href={"/"}>
|
class="hover:underline hover:text-nord10"
|
||||||
|
activeClass="font-bold"
|
||||||
|
href={"/"}
|
||||||
|
>
|
||||||
Home
|
Home
|
||||||
</A>
|
</A>
|
||||||
<A
|
<A
|
||||||
end
|
end
|
||||||
class="hover:underline"
|
class="hover:underline hover:text-nord10"
|
||||||
activeClass="font-bold"
|
activeClass="font-bold"
|
||||||
href={"/articles"}
|
href={"/articles"}
|
||||||
>
|
>
|
||||||
@@ -49,20 +48,21 @@ export const Layout: ParentComponent = (props) => {
|
|||||||
</A>
|
</A>
|
||||||
<A
|
<A
|
||||||
end
|
end
|
||||||
class="hover:underline"
|
class="hover:underline hover:text-nord10"
|
||||||
activeClass="font-bold"
|
activeClass="font-bold"
|
||||||
href={"/tags"}
|
href={"/tags"}
|
||||||
>
|
>
|
||||||
Tags
|
Tags
|
||||||
</A>
|
</A>
|
||||||
<a
|
<A
|
||||||
|
class="hover:text-nord10"
|
||||||
href="/resume.pdf"
|
href="/resume.pdf"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={() => changeFavicon("./favicon.ico")}
|
onClick={() => changeFavicon("./favicon.ico")}
|
||||||
>
|
>
|
||||||
Resume
|
Resume
|
||||||
</a>
|
</A>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -70,8 +70,6 @@ export const Layout: ParentComponent = (props) => {
|
|||||||
<main id="main-content" class="mt-1v flex-auto">
|
<main id="main-content" class="mt-1v flex-auto">
|
||||||
{props.children}
|
{props.children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="debug-grid" />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
onMount,
|
onMount,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
const P: ParentComponent = (props) => <p class="mt-1v">{props.children}</p>;
|
const P: ParentComponent = (props) => <p class="mt-1v">{props.children}</p>;
|
||||||
|
|
||||||
const Ol: ParentComponent = (props) => (
|
const Ol: ParentComponent = (props) => (
|
||||||
@@ -42,7 +45,7 @@ const Pre: ParentComponent<{ lang: string; lines?: string; file?: string }> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="my-1v">
|
<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">
|
<div class="bg-nord2 text-nord12 text-xl flex justify-between px-1h leading-2">
|
||||||
<Show when={props.file} fallback={<span aria-hidden />}>
|
<Show when={props.file} fallback={<span aria-hidden />}>
|
||||||
<span>{props.file}</span>
|
<span>{props.file}</span>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -186,9 +189,9 @@ export const PostImage: Component<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Aside: ParentComponent = (props) => (
|
export const Notes: ParentComponent = (props) => (
|
||||||
<aside class="border-l-2 border-black dark:border-white pl-1h mt-1v">
|
<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="uppercase text-sm leading-1 font-medium select-none">Notes</div>
|
||||||
<div class="[&_*:first-child]:mt-0">{props.children}</div>
|
<div class="[&_*:first-child]:mt-0">{props.children}</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import dayjs from "dayjs";
|
|||||||
import { type Component, For } from "solid-js";
|
import { type Component, For } from "solid-js";
|
||||||
import type { Post } from "~/types";
|
import type { Post } from "~/types";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
export const Posts: Component<{ posts: Post[] }> = (props) => {
|
export const Posts: Component<{ posts: Post[] }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<ol class="">
|
<ol class="">
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { type Component, For, Show } from "solid-js";
|
|||||||
type Node = { l: string; c: TreeNode[] };
|
type Node = { l: string; c: TreeNode[] };
|
||||||
type TreeNode = string | Node;
|
type TreeNode = string | Node;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
const Subtree: Component<{ tree: TreeNode }> = (props) => {
|
const Subtree: Component<{ tree: TreeNode }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
136
src/css/prism-nord.css
Normal file
136
src/css/prism-nord.css
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */
|
||||||
|
|
||||||
|
/*********************************************************
|
||||||
|
* General
|
||||||
|
*/
|
||||||
|
pre[class*="language-"],
|
||||||
|
code[class*="language-"] {
|
||||||
|
color: #d8dee9;
|
||||||
|
font-size: 1em;
|
||||||
|
text-shadow: none;
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
-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-"]::selection,
|
||||||
|
code[class*="language-"]::selection,
|
||||||
|
pre[class*="language-"]::mozselection,
|
||||||
|
code[class*="language-"]::mozselection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
pre[class*="language-"],
|
||||||
|
code[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
background: #2e3440;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
color: #d8dee9;
|
||||||
|
background: #2e3440;
|
||||||
|
}
|
||||||
|
/*********************************************************
|
||||||
|
* Tokens
|
||||||
|
*/
|
||||||
|
.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #606e87;
|
||||||
|
}
|
||||||
|
.token.punctuation {
|
||||||
|
color: #81a1c1;
|
||||||
|
}
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #b48ead;
|
||||||
|
}
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #a2bf8c;
|
||||||
|
}
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #80a2c1;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #81a1c1;
|
||||||
|
}
|
||||||
|
.token.function {
|
||||||
|
color: #8fbcbc;
|
||||||
|
}
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #ee9900;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
/*********************************************************
|
||||||
|
* Line highlighting
|
||||||
|
*/
|
||||||
|
pre[data-line] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
pre[class*="language-"] > code[class*="language-"] {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.line-highlight {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: inherit 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
background: #3b4251;
|
||||||
|
box-shadow: inset 5px 0 0 #d8dee9;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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: '';
|
|
||||||
}
|
|
||||||
16
src/data/jobs.json
Normal file
16
src/data/jobs.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Software Developer Engineer Intern",
|
||||||
|
"company": "Amazon Canada",
|
||||||
|
"location": "Toronto, Ontario, Canada",
|
||||||
|
"range": "May 2025 - August 2025",
|
||||||
|
"url": "https://flex.amazon.ca/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Software Engineer Intern",
|
||||||
|
"company": "Cisco Canada",
|
||||||
|
"location": "Remote",
|
||||||
|
"range": "January 2023 - May 2023",
|
||||||
|
"url": "https://developer.cisco.com/docs/modeling-labs/cat-9000v/"
|
||||||
|
}
|
||||||
|
]
|
||||||
4
src/data/jobs.ts
Normal file
4
src/data/jobs.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import Jobs from "./jobs.json";
|
||||||
|
import type { Job } from "~/types";
|
||||||
|
|
||||||
|
export const jobs: Job[] = Jobs;
|
||||||
@@ -1 +1,31 @@
|
|||||||
[]
|
[
|
||||||
|
{
|
||||||
|
"title": "A Rusty Stack Jump",
|
||||||
|
"description": "Jumping into a new stack with Rust",
|
||||||
|
"date": "2025-02-27T00:00:00.000Z",
|
||||||
|
"featuredImage": null,
|
||||||
|
"featuredImageDesc": null,
|
||||||
|
"tags": [
|
||||||
|
"rust",
|
||||||
|
"asm",
|
||||||
|
"systems",
|
||||||
|
"operating systems",
|
||||||
|
"async"
|
||||||
|
],
|
||||||
|
"slug": "rust_stack_jmp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Testing Test Test Test",
|
||||||
|
"description": "Woah this is so cool",
|
||||||
|
"date": "2025-01-29T00:00:00.000Z",
|
||||||
|
"featuredImage": null,
|
||||||
|
"featuredImageDesc": null,
|
||||||
|
"tags": [
|
||||||
|
"rust",
|
||||||
|
"python",
|
||||||
|
"mdx",
|
||||||
|
"markdown"
|
||||||
|
],
|
||||||
|
"slug": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-expect-error
|
|
||||||
import Posts from "./posts.json";
|
import Posts from "./posts.json";
|
||||||
import type { Post } from "~/types";
|
import type { Post } from "~/types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -9,12 +9,9 @@ export default createHandler(() => (
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<script>
|
|
||||||
{`document.documentElement.classList.toggle('dark', window.matchMedia('(prefers-color-scheme: dark)').matches)`}
|
|
||||||
</script>
|
|
||||||
{assets}
|
{assets}
|
||||||
</head>
|
</head>
|
||||||
<body class="font-mono bg-white dark:bg-black dark:text-white">
|
<body class="font-mono">
|
||||||
<div id="app">{children}</div>
|
<div id="app">{children}</div>
|
||||||
{scripts}
|
{scripts}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="@solidjs/start/env" />
|
|
||||||
@@ -1,34 +1,46 @@
|
|||||||
import { For } from "solid-js";
|
import { For } from "solid-js";
|
||||||
import { posts } from "~/data/posts";
|
import Experience from "../components/Experience";
|
||||||
import { Posts } from "~/components/Posts";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
"https://github.com/minhtrannhat",
|
"https://github.com/minhtrannhat",
|
||||||
"https://linkedin.com/in/minh-tran-nhat",
|
"https://linkedin.com/in/minh-tran-nhat",
|
||||||
"https://git.minhtrannhat.com/explore/repos",
|
"https://git.minhtrannhat.com/explore/repos",
|
||||||
];
|
];
|
||||||
|
|
||||||
const Homepage = () => {
|
const Homepage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section class="flex flex-col sm:flex-row gap-2v sm:gap-3h">
|
<Title>minhtran_dev</Title>
|
||||||
|
|
||||||
|
<Meta property="og:title" content="minhtran_dev" />
|
||||||
|
<Meta property="og:description" content="just trying my best :)" />
|
||||||
|
<Meta property="og:image" content="/og.png" />
|
||||||
|
<Meta property="og:image:alt" content="minhtran_dev site" />
|
||||||
|
<Meta property="og:image:width" content="1200" />
|
||||||
|
<Meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
|
<section class="mx-4 flex flex-col sm:flex-row gap-2v sm:gap-3h">
|
||||||
<div class="font-medium">
|
<div class="font-medium">
|
||||||
<div class="flex items-end mb-1v gap-1h">
|
<div class="text-nord-1 flex items-end mb-1v gap-1h">
|
||||||
<p>Hi, Minh here.</p>
|
<p>Hi, Minh here.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-1v">
|
<p class="mb-1v text-nord-1">
|
||||||
I'm Minh Tran, a Computer Engineering student at Concordia
|
I'm a Computer Engineering student at Concordia University,
|
||||||
University, Montreal, Canada.
|
Montreal, Canada.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
I'm most passionate about designing distributed systems that scales
|
Things that I'm most passionate about: distributed systems, backend
|
||||||
but I'm also interested in compilers and systems programming. When
|
development, compilers and systems programming.
|
||||||
I'm not coding, I read books, listen to podcasts or study music
|
<br />
|
||||||
|
<br />
|
||||||
|
When I'm not coding, I read books, listen to podcasts or study music
|
||||||
theory.
|
theory.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Say hi:{" "}
|
Email me at:{" "}
|
||||||
<a
|
<a
|
||||||
class="underline"
|
class="underline hover:text-nord-11"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
href="mailto:minh@minhtrannhat.com"
|
href="mailto:minh@minhtrannhat.com"
|
||||||
@@ -37,10 +49,10 @@ const Homepage = () => {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sm:mt-3v text-slate-600 dark:text-slate-200 text-base sm:text-sm leading-1">
|
<ul class="mx-4 sm:mx-6 sm:mt-3v text-slate-600 text-base sm:text-sm leading-1">
|
||||||
<For each={links}>
|
<For each={links}>
|
||||||
{(link) => (
|
{(link) => (
|
||||||
<li class="list-square hover:text-black dark:hover:text-white ml-2h leading-1">
|
<li class="list-square hover:text-nord-11 ml-2h leading-1">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@@ -55,7 +67,8 @@ const Homepage = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<br />
|
||||||
|
<Experience />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { posts } from "~/data/posts";
|
import { posts } from "~/data/posts";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { For } from "solid-js";
|
import { For } from "solid-js";
|
||||||
|
|
||||||
const Articles = () => {
|
const Articles = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Title>minhtran_dev - Articles</Title>
|
||||||
|
|
||||||
|
<Meta property="og:title" content="minhtran_dev Articles" />
|
||||||
|
<Meta property="og:description" content="My articles" />
|
||||||
|
<Meta property="og:image" content="/og.png" />
|
||||||
|
<Meta property="og:image:alt" content="minhtran_dev site" />
|
||||||
|
<Meta property="og:image:width" content="1200" />
|
||||||
|
<Meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
<ol class="flex flex-col gap-1v list-square ml-2h">
|
<ol class="flex flex-col gap-1v list-square ml-2h">
|
||||||
<For each={Object.values(posts)}>
|
<For each={Object.values(posts)}>
|
||||||
{(post) => (
|
{(post) => (
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import { For, Show } from "solid-js";
|
|||||||
import type { RouteSectionProps } from "@solidjs/router";
|
import type { RouteSectionProps } from "@solidjs/router";
|
||||||
import { Meta, Title } from "@solidjs/meta";
|
import { Meta, Title } from "@solidjs/meta";
|
||||||
import { posts } from "~/data/posts";
|
import { posts } from "~/data/posts";
|
||||||
|
//@ts-expect-error
|
||||||
import { MDXProvider } from "solid-mdx";
|
import { MDXProvider } from "solid-mdx";
|
||||||
import { markdownComponents, PostImage } from "~/components/Markdown";
|
import { markdownComponents, PostImage } from "~/components/Markdown";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "../css/prism-theme.css";
|
import "../css/prism-nord.css";
|
||||||
import type { Post } from "~/types";
|
import type { Post } from "~/types";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
const Blog = (props: RouteSectionProps<unknown>) => {
|
const Blog = (props: RouteSectionProps<unknown>) => {
|
||||||
const meta = () =>
|
const meta = () =>
|
||||||
posts.find((p) => props.location.pathname.endsWith(p.slug)) as Post;
|
posts.find((p) => props.location.pathname.endsWith(p.slug)) as Post;
|
||||||
@@ -19,10 +23,21 @@ const Blog = (props: RouteSectionProps<unknown>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article class="pb-5v">
|
<article class="pb-5v">
|
||||||
<Title>minhtrannhat.com - {meta()?.title}</Title>
|
<Title>minhtran_dev - {meta()?.title}</Title>
|
||||||
<Meta name="og:title" content={meta().title} />
|
<Meta name="og:title" content={meta().title} />
|
||||||
<Meta name="description" content={meta().description} />
|
<Meta name="description" content={meta().description} />
|
||||||
<Meta name="og:description" content={meta().description} />
|
<Meta name="og:description" content={meta().description} />
|
||||||
|
<Meta property="og:type" content="article" />
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
property="og:url"
|
||||||
|
content={`https://minhtranhat.com/blog/${meta()?.slug}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Meta property="og:image" content="/og.png" />
|
||||||
|
<Meta property="og:image:alt" content="minhtran_dev site" />
|
||||||
|
<Meta property="og:image:width" content="1200" />
|
||||||
|
<Meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
<Show when={meta().featuredImage}>
|
<Show when={meta().featuredImage}>
|
||||||
<PostImage
|
<PostImage
|
||||||
@@ -31,6 +46,7 @@ const Blog = (props: RouteSectionProps<unknown>) => {
|
|||||||
alt={meta().featuredImageDesc || ""}
|
alt={meta().featuredImageDesc || ""}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<h1 class="text-2v leading-2 font-bold mb-1v">{meta().title}</h1>
|
<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">
|
<div class="flex items-center gap-4h mb-2v text-sm leading-1">
|
||||||
|
|||||||
129
src/routes/blog/rust_stack_jmp.mdx
Normal file
129
src/routes/blog/rust_stack_jmp.mdx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
title: A Rusty Stack Jump
|
||||||
|
description: Jumping into a new stack with Rust
|
||||||
|
date: 2025-02-27
|
||||||
|
featuredImage:
|
||||||
|
featuredImageDesc:
|
||||||
|
tags:
|
||||||
|
- rust
|
||||||
|
- asm
|
||||||
|
- systems
|
||||||
|
- operating systems
|
||||||
|
- async
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Notes, PostImage } from "~/components/Markdown";
|
||||||
|
import { Tree } from "~/components/Tree";
|
||||||
|
|
||||||
|
In my quest to learn to build an async runtime in Rust, I have to learn about CPU context switching. In order to switch from one async task to another, our async runtime has to perform a context switch. This means saving the current CPU registers marked as `callee saved` by the System V ABI manual and loading the CPU registers with our new async stack.
|
||||||
|
|
||||||
|
In this article, I will show you what I have learned about jumping onto a new stack in a x86_64 CPU.
|
||||||
|
|
||||||
|
<Notes>
|
||||||
|
I'm learning about async runtimes in Rust based on the amazing book [Asynchronous Programming in Rust: Learn asynchronous programming by building working examples of futures, green threads, and runtimes](https://www.packtpub.com/en-mt/product/asynchronous-programming-in-rust-9781805128137)
|
||||||
|
|
||||||
|
It's an amazing book, don't get me wrong, but I feel like the explanation can be hand-wavy sometimes. Thus, I write this to archive my own explanation and potentially help other people who also struggle with the subject.
|
||||||
|
|
||||||
|
</Notes>
|
||||||
|
|
||||||
|
<Notes>
|
||||||
|
Most async runtimes in Rust do not use stackful coroutines (which are used by
|
||||||
|
Go's `gochannel`, Erlang's `processes`) and instead, use state machines to
|
||||||
|
manage async tasks.
|
||||||
|
</Notes>
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
## Setting the stage
|
||||||
|
|
||||||
|
Why do we need to swap the stack of async tasks in a runtime with stackful coroutines ?
|
||||||
|
|
||||||
|
Async tasks, by nature, are paused and resumed. Everytime a task is paused to move into a new task, we would have to save the current context of the task that is running and load the context of the upcoming task.
|
||||||
|
|
||||||
|
## Jumping into the new stack
|
||||||
|
|
||||||
|
Here is the code in its entirely, I'd recommend you run this on the [Rust Playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024). I have left comments through out the code so you can get the general idea.
|
||||||
|
|
||||||
|
Note that you have to manually stop the process.
|
||||||
|
|
||||||
|
```rust file="stack_swap.rs"
|
||||||
|
use core::arch::asm;
|
||||||
|
|
||||||
|
// stack size of 48 bytes so its easy to print the stack before we switch contexts
|
||||||
|
const SSIZE: isize = 48;
|
||||||
|
|
||||||
|
// a struct that represents our CPU state
|
||||||
|
//
|
||||||
|
// This struct will stores the stack pointer
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
#[repr(C)]
|
||||||
|
struct ThreadContext {
|
||||||
|
rsp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returning ! means
|
||||||
|
// it will panic OR runs forever
|
||||||
|
fn hello() -> ! {
|
||||||
|
println!("I LOVE WAKING UP ON A NEW STACK!");
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// new is a pointer to a ThreadContext
|
||||||
|
unsafe fn gt_switch(new: *const ThreadContext) {
|
||||||
|
// inline assembly
|
||||||
|
asm!(
|
||||||
|
"mov rsp, [{0} + 0x00]", // move the content of where the new pointer is pointing to, into the rsp register
|
||||||
|
"ret", // ret pops the return address from our custom stack—in our example, the address of hello.
|
||||||
|
in(reg) new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// initialize
|
||||||
|
let mut ctx = ThreadContext::default();
|
||||||
|
|
||||||
|
// stack initialize
|
||||||
|
// ie. 0x10
|
||||||
|
let mut stack = vec![0_u8; SSIZE as usize];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// we get the bottom of the stack
|
||||||
|
// remember that the stack grows downward from high memory address to low memory address
|
||||||
|
// i.e 0x40 -> because 0x30 = 0x40 - 0x10 and 0x30 = SSIZE in decimal
|
||||||
|
// NOTE: offset() is applied in units of the size of the type that the pointer points to
|
||||||
|
// in our case, stack is a pointer to u8 (a byte) so offset(SSIZE) == offset(48 bytes) == offset(0x30)
|
||||||
|
let stack_bottom = stack.as_mut_ptr().offset(SSIZE);
|
||||||
|
|
||||||
|
// we align the bottom of the stack to be 16-byte-aligned
|
||||||
|
// this is for performance reasons as some CPU instructions (SSE and SIMD)
|
||||||
|
|
||||||
|
// The technicality: 15 is b1111 so if we do (stack_bottom AND !15) we will zero out the bottom 4 bits
|
||||||
|
//
|
||||||
|
// we also want the bottom of the stack pointer to point to a byte (8bit or u8)
|
||||||
|
let sb_aligned = (stack_bottom as usize & !15) as *mut u8;
|
||||||
|
|
||||||
|
// Here, we write the address of the hello function as 64 bits(8 bytes)
|
||||||
|
// Remember that 16 bytes = 0x10 in hex
|
||||||
|
// So we go DOWN 10 memory addresses, i.e from 0x40 to 0x30
|
||||||
|
// NOTE: 16 bytes down (0x10) even though, the hello function pointer is ONLY 8 bytes
|
||||||
|
// This is because the System V ABI requires the stack pointer to be always be 16-byte aligned
|
||||||
|
std::ptr::write(sb_aligned.offset(-16) as *mut u64, hello as u64);
|
||||||
|
|
||||||
|
// we write the stack pointer into the rsp inside context
|
||||||
|
ctx.rsp = sb_aligned.offset(-16) as u64;
|
||||||
|
|
||||||
|
for i in 0..SSIZE {
|
||||||
|
println!("mem: {}, val: {}",
|
||||||
|
sb_aligned.offset(-i as isize) as usize,
|
||||||
|
*sb_aligned.offset(-i as isize))
|
||||||
|
};
|
||||||
|
|
||||||
|
// we go into the function
|
||||||
|
// we will write our stack pointer to the cpu stack pointer
|
||||||
|
// and `ret` will pop that stack pointer
|
||||||
|
gt_switch(&mut ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
118
src/routes/blog/test.mdx
Normal file
118
src/routes/blog/test.mdx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: Testing Test Test Test
|
||||||
|
description: Woah this is so cool
|
||||||
|
date: 2025-01-29
|
||||||
|
featuredImage:
|
||||||
|
featuredImageDesc:
|
||||||
|
tags:
|
||||||
|
- rust
|
||||||
|
- python
|
||||||
|
- mdx
|
||||||
|
- markdown
|
||||||
|
---
|
||||||
|
import { Notes, PostImage } from "~/components/Markdown"
|
||||||
|
import { Tree } from "~/components/Tree"
|
||||||
|
|
||||||
|
Woah this blog is sooo cool, look at all these beautifully rendered markdown stuffs :OOOO
|
||||||
|
|
||||||
|
<Notes>
|
||||||
|
|
||||||
|
Notty Notes
|
||||||
|
|
||||||
|
</Notes>
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
### longestCommonSubsequence
|
||||||
|
|
||||||
|
```python file="leetcode1143.py"
|
||||||
|
class Solution:
|
||||||
|
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
|
||||||
|
|
||||||
|
# edge cases
|
||||||
|
|
||||||
|
# both texts are just length of 1:
|
||||||
|
if len(text1) == 1 and len(text2) == 1:
|
||||||
|
return int(text1 == text2)
|
||||||
|
|
||||||
|
# only one row:
|
||||||
|
if len(text2) == 1 and len(text1) > 1:
|
||||||
|
return int(text2 in text1)
|
||||||
|
|
||||||
|
# only one col:
|
||||||
|
if len(text1) == 1 and len(text2) > 1:
|
||||||
|
return int(text1 in text2)
|
||||||
|
|
||||||
|
rows = len(text2)
|
||||||
|
cols = len(text1)
|
||||||
|
|
||||||
|
# we use bottom up 2D DP
|
||||||
|
# reaching here means it is at least 2x2
|
||||||
|
dp = [
|
||||||
|
[0 for _ in range(cols)] for _ in range(rows)
|
||||||
|
]
|
||||||
|
|
||||||
|
# seed the top left tile
|
||||||
|
dp[0][0] = 1 if text1[0] == text2[0] else 0
|
||||||
|
|
||||||
|
# we seed the first row and first col
|
||||||
|
for col in range(1, cols):
|
||||||
|
dp[0][col] = dp[0][col - 1] if text1[col] != text2[0] else 1
|
||||||
|
|
||||||
|
|
||||||
|
for row in range(1, rows):
|
||||||
|
dp[row][0] = dp[row - 1][0] if text1[0] != text2[row] else 1
|
||||||
|
|
||||||
|
|
||||||
|
# for the inner triangle, we use the following
|
||||||
|
# dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) if text1[i] != text2[j]
|
||||||
|
# else max(dp[i - 1][j], dp[i][j - 1]) + 1
|
||||||
|
|
||||||
|
for row in range(1, rows):
|
||||||
|
for col in range(1, cols):
|
||||||
|
dp[row][col] = dp[row - 1][col - 1] + 1 if text1[col] == text2[row] else max(dp[row - 1][col], dp[row][col - 1])
|
||||||
|
|
||||||
|
# return the bottom right tile
|
||||||
|
return dp[rows - 1][cols - 1]
|
||||||
|
|
||||||
|
# space and time complexity: both O(N * M)
|
||||||
|
|
||||||
|
# CAN OPTIMIZE TO GET SPACE COMPLEXITY OF O(MIN(N, M))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
### Async Programming stuffs
|
||||||
|
|
||||||
|
```rust file="ffi.rs"
|
||||||
|
// Here we have the syscalls
|
||||||
|
// Unsafe !!!
|
||||||
|
#[link(name = "c")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn epoll_create(size: i32) -> i32;
|
||||||
|
pub fn close(fd: i32) -> i32;
|
||||||
|
pub fn epoll_ctl(epfd: i32, op: i32, fd: i32, event: *mut Event) -> i32;
|
||||||
|
pub fn epoll_wait(epfd: i32, events: *mut Event, maxevents: i32, timeout: i32) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid padding by using repr(packed)
|
||||||
|
// Data struct is different in Rust compared to C
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[repr(C)]
|
||||||
|
#[cfg_attr(target_arch = "x86_64", repr(packed))]
|
||||||
|
pub struct Event {
|
||||||
|
pub(crate) events: u32,
|
||||||
|
// Using `Token` a.k.a `epoll_data` to track which socket generated the event
|
||||||
|
pub(crate) epoll_data: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub fn token(&self) -> usize {
|
||||||
|
self.epoll_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
import { For } from "solid-js";
|
import { For } from "solid-js";
|
||||||
import { tags } from "~/data/tags";
|
import { tags } from "~/data/tags";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
const Tags = () => {
|
const Tags = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl font-bold mt-2v mb-1v">All tags:</h1>
|
<Title>minhtran_dev - Article Tags</Title>
|
||||||
|
|
||||||
|
<Meta property="og:title" content="minhtran_dev Article Tags" />
|
||||||
|
<Meta property="og:description" content="Tags for my articles" />
|
||||||
|
<Meta property="og:image" content="/og.png" />
|
||||||
|
<Meta property="og:image:alt" content="minhtran_dev site" />
|
||||||
|
<Meta property="og:image:width" content="1200" />
|
||||||
|
<Meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
|
<h1 class="text-xl text-nord-1 font-bold mt-1v mb-1v">All tags:</h1>
|
||||||
<ol class="flex flex-col gap-1v list-square ml-2h">
|
<ol class="flex flex-col gap-1v list-square ml-2h">
|
||||||
<For each={Object.values(tags)}>
|
<For each={Object.values(tags)}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
|
|||||||
@@ -3,11 +3,27 @@ import { type Component, Show } from "solid-js";
|
|||||||
import { posts } from "~/data/posts";
|
import { posts } from "~/data/posts";
|
||||||
import { Posts } from "~/components/Posts";
|
import { Posts } from "~/components/Posts";
|
||||||
import { tags } from "~/data/tags";
|
import { tags } from "~/data/tags";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Code from andii.dev
|
||||||
|
*/
|
||||||
const TagId: Component<RouteSectionProps<unknown>> = (props) => {
|
const TagId: Component<RouteSectionProps<unknown>> = (props) => {
|
||||||
const tag = () => tags[props.params.id];
|
const tag = () => tags[props.params.id];
|
||||||
return (
|
return (
|
||||||
<Show when={tag()} fallback={<div>No posts with that tag</div>}>
|
<Show when={tag()} fallback={<div>No posts with that tag</div>}>
|
||||||
|
<Title>minhtran_dev - Tag: {tag().id}</Title>
|
||||||
|
|
||||||
|
<Meta
|
||||||
|
property="og:title"
|
||||||
|
content={`minhtran_dev Articles Tag: ${tag().id}`}
|
||||||
|
/>
|
||||||
|
<Meta property="og:description" content={`My ${tag().id} Articles`} />
|
||||||
|
<Meta property="og:image" content="/og.png" />
|
||||||
|
<Meta property="og:image:alt" content="minhtran_dev site" />
|
||||||
|
<Meta property="og:image:width" content="1200" />
|
||||||
|
<Meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-lg font-bold mb-6">Tag: {tag().id}</h1>
|
<h1 class="text-lg font-bold mb-6">Tag: {tag().id}</h1>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ export type Post = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Job = {
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
location: string;
|
||||||
|
range: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Tag = {
|
export type Tag = {
|
||||||
// id/name of tag
|
// id/name of tag
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -34,9 +34,6 @@ const config: Config = {
|
|||||||
medium: "600",
|
medium: "600",
|
||||||
bold: "800",
|
bold: "800",
|
||||||
},
|
},
|
||||||
maxWidth: {
|
|
||||||
thread: "calc(min(80ch, round(down, 100%, 1ch)))",
|
|
||||||
},
|
|
||||||
lineHeight: buildLineHeights(),
|
lineHeight: buildLineHeights(),
|
||||||
colors: {
|
colors: {
|
||||||
black: "#000000",
|
black: "#000000",
|
||||||
@@ -70,7 +67,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwind-nord")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"types": ["vinxi/client", "vite/client", "solid-jsx/types"],
|
"types": ["vinxi/client", "vite/client", "solid-jsx/types"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user