feat: migrate site to TanStack Start

This commit is contained in:
Jet 2026-04-25 14:14:51 -07:00
parent 056daa6460
commit 1bf7b32040
No known key found for this signature in database
33 changed files with 8684 additions and 1106 deletions

View file

@ -5,7 +5,7 @@ export default tseslint.config(
ignores: ["dist"],
},
{
files: ["**/*.ts"],
files: ["**/*.{ts,tsx}"],
extends: [
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,

View file

@ -22,6 +22,7 @@
./package-lock.json
./package.json
./public
./server.mjs
./src
./tsconfig.json
./vite-plugin-ansi.ts
@ -33,14 +34,18 @@
pname = "jet-website";
version = "0.1.0";
src = websiteSrc;
npmDepsHash = "sha256-UDz4tXNvEa8uiDDGg16K9JbNeQZR3BsVNKtuOgcyurQ=";
npmDepsHash = "sha256-tcWPiPTOfCEKBBt/ZilAnFcfWKD3FkWUM49vLqw41f0=";
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r dist/* $out/
mkdir -p $out/share/jet-website $out/bin
cp -r dist node_modules package.json server.mjs $out/share/jet-website/
makeWrapper ${pkgs.nodejs}/bin/node $out/bin/jet-website \
--add-flags $out/share/jet-website/server.mjs
runHook postInstall
'';
nativeBuildInputs = [ pkgs.makeWrapper ];
};
qa-api = pkgs.rustPlatform.buildRustPackage {
pname = "jetpham-qa-api";

View file

@ -46,92 +46,9 @@
name="twitter:image"
content="https://jetpham.com/web-app-manifest-512x512.png"
/>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Jet Pham",
"givenName": "Jet",
"familyName": "Pham",
"description": "Software extremist.",
"url": "https://jetpham.com",
"jobTitle": "Software Extremist",
"hasOccupation": {
"@type": "Occupation",
"name": "Hacker"
},
"email": "jet@extremist.software",
"image": "https://jetpham.com/jet.svg",
"alumniOf": {
"@type": "CollegeOrUniversity",
"name": "University of San Francisco",
"url": "https://www.usfca.edu"
},
"hasCredential": {
"@type": "EducationalOccupationalCredential",
"credentialCategory": "degree",
"name": "Bachelor of Science in Computer Science"
},
"homeLocation": {
"@type": "City",
"name": "San Francisco, CA"
},
"workLocation": {
"@type": "City",
"name": "San Francisco, CA"
},
"memberOf": {
"@type": "Organization",
"name": "Noisebridge",
"url": "https://www.noisebridge.net"
},
"affiliation": {
"@type": "Organization",
"name": "Noisebridge",
"url": "https://www.noisebridge.net"
},
"sameAs": [
"https://github.com/jetpham",
"https://x.com/exmistsoftware",
"https://bsky.app/profile/extremist.software",
"https://git.extremist.software"
]
}
</script>
</head>
<body style="background: #000">
<a class="skip-link" href="#outlet">Skip to content</a>
<canvas
id="canvas"
class="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
aria-hidden="true"
></canvas>
<div class="page-frame relative z-10">
<nav aria-label="Main navigation" class="site-region">
<div class="site-shell site-panel-frame px-[2ch] py-[1ch]">
<div class="site-panel-frost" aria-hidden="true"></div>
<div class="site-panel-border" aria-hidden="true"></div>
<div
class="site-panel-content site-nav-links flex justify-center gap-[2ch]"
>
<a href="/" data-nav-link class="site-nav-link"
><span class="site-nav-marker" aria-hidden="true">&gt;</span
><span>Home</span
><span class="site-nav-marker" aria-hidden="true">&lt;</span></a
>
<a href="/qa" data-nav-link class="site-nav-link"
><span class="site-nav-marker" aria-hidden="true">&gt;</span
><span>Q&amp;A</span
><span class="site-nav-marker" aria-hidden="true">&lt;</span></a
>
</div>
</div>
</nav>
<main id="outlet" class="site-region" tabindex="-1"></main>
<footer class="site-region site-footer">
<div id="site-footer" class="site-shell"></div>
</footer>
</div>
<script type="module" src="/src/main.ts"></script>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>

View file

@ -10,6 +10,7 @@ let
cfg = config.services.jetpham-website;
package = cfg.package;
qaApi = cfg.apiPackage;
websiteListen = "${cfg.websiteListenAddress}:${toString cfg.websiteListenPort}";
apiListen = "${cfg.apiListenAddress}:${toString cfg.apiListenPort}";
usingDefaultWebhookSecret = cfg.webhookSecretFile == null;
webhookSecretPath =
@ -45,9 +46,7 @@ let
}
handle {
root * ${package}
try_files {path} /index.html
file_server
reverse_proxy ${websiteListen}
}
${cfg.caddy.extraConfig}
@ -64,6 +63,18 @@ in
description = "Static site package served by Caddy.";
};
websiteListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address for the local TanStack Start frontend listener.";
};
websiteListenPort = lib.mkOption {
type = lib.types.port;
default = 3002;
description = "Port for the local TanStack Start frontend listener.";
};
apiPackage = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.system}.qa-api;
@ -203,6 +214,29 @@ in
services.caddy.enable = cfg.caddy.enable;
systemd.services.jetpham-website = {
description = "Jet Pham website frontend";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
WEBSITE_LISTEN_ADDRESS = cfg.websiteListenAddress;
WEBSITE_LISTEN_PORT = toString cfg.websiteListenPort;
QA_API_BASE_URL = "http://${apiListen}";
NODE_ENV = "production";
};
serviceConfig = {
DynamicUser = true;
ExecStart = "${package}/bin/jet-website";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = 5;
};
};
services.tor = lib.mkIf cfg.tor.enable {
enable = true;
relay.onionServices.jetpham-website = {

7893
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,8 @@
"build": "vite build",
"check": "npm run lint && tsc --noEmit",
"dev": "vite",
"format:check": "prettier --check \"**/*.{ts,js,jsx,mdx}\" --cache",
"format:write": "prettier --write \"**/*.{ts,js,jsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"preview": "vite preview",
@ -17,6 +17,8 @@
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@types/node": "^25.3.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"anser": "^2.3.5",
"escape-carriage": "^1.3.1",
"eslint": "^10",
@ -25,8 +27,15 @@
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
"vite": "^7.3.1"
},
"knip": {}
"knip": {},
"dependencies": {
"@tanstack/react-router": "^1.168.24",
"@tanstack/react-start": "^1.167.49",
"@vitejs/plugin-react": "^5.2.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"vinxi": "^0.5.11"
}
}

View file

@ -0,0 +1,2 @@
Contact: jet@extremist.software
If you really did find something insecure about this website, I'll take you out to dinner if you're in SF. Obviously besides the fact that the qa page literally sends an email directly to me

1
public/humans.txt Normal file
View file

@ -0,0 +1 @@
Hi! I hope that you have a great day. Thank you for visiting my site!

1
public/llms.txt Normal file
View file

@ -0,0 +1 @@
go fuck yourself

View file

@ -6,9 +6,9 @@
<priority>1.0</priority>
</url>
<url>
<loc>https://jetpham.com/projects</loc>
<loc>https://jetpham.com/colophon</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<priority>0.6</priority>
</url>
<url>
<loc>https://jetpham.com/qa</loc>

97
server.mjs Normal file
View file

@ -0,0 +1,97 @@
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { createServer } from "node:http";
import { extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
import startServer from "./dist/server/server.js";
const root = fileURLToPath(new URL(".", import.meta.url));
const clientDir = join(root, "dist", "client");
const host = process.env.HOST ?? process.env.WEBSITE_LISTEN_ADDRESS ?? "127.0.0.1";
const port = Number(process.env.PORT ?? process.env.WEBSITE_LISTEN_PORT ?? "3002");
const contentTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
[".ico", "image/x-icon"],
[".js", "text/javascript; charset=utf-8"],
[".json", "application/json; charset=utf-8"],
[".png", "image/png"],
[".svg", "image/svg+xml"],
[".txt", "text/plain; charset=utf-8"],
[".woff", "font/woff"],
]);
function writeResponse(res, response) {
res.writeHead(response.status, Object.fromEntries(response.headers));
if (!response.body) {
res.end();
return;
}
response.body.pipeTo(
new WritableStream({
write(chunk) {
res.write(chunk);
},
close() {
res.end();
},
abort(error) {
res.destroy(error);
},
}),
);
}
async function tryServeStatic(req, res, pathname) {
if (pathname === "/" || !extname(pathname)) return false;
const decoded = decodeURIComponent(pathname);
const normalized = normalize(decoded).replace(/^\.\.(\/|$)/, "");
const filePath = join(clientDir, normalized);
if (!filePath.startsWith(clientDir)) return false;
try {
const fileStat = await stat(filePath);
if (!fileStat.isFile()) return false;
} catch {
return false;
}
res.writeHead(200, {
"Content-Type": contentTypes.get(extname(filePath)) ?? "application/octet-stream",
"Cache-Control": filePath.includes("/assets/")
? "public, max-age=31536000, immutable"
: "no-store",
});
createReadStream(filePath).pipe(res);
return true;
}
createServer(async (req, res) => {
try {
const origin = `http://${req.headers.host ?? `${host}:${port}`}`;
const url = new URL(req.url ?? "/", origin);
if (req.method === "GET" || req.method === "HEAD") {
const served = await tryServeStatic(req, res, url.pathname);
if (served) return;
}
const request = new Request(url, {
method: req.method,
headers: req.headers,
body: req.method === "GET" || req.method === "HEAD" ? undefined : req,
duplex: "half",
});
const response = await startServer.fetch(request);
writeResponse(res, response);
} catch (error) {
console.error(error);
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Internal Server Error");
}
}).listen(port, host, () => {
console.log(`Listening on http://${host}:${port}`);
});

4
src/client.tsx Normal file
View file

@ -0,0 +1,4 @@
import { StartClient } from "@tanstack/react-start/client";
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document, <StartClient />);

View file

@ -1,10 +0,0 @@
export function frostedBox(content: string, extraClasses?: string): string {
return `
<div class="site-shell site-panel-frame relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${extraClasses ?? ""}">
<div class="site-panel-frost pointer-events-none" aria-hidden="true"></div>
<div class="site-panel-border" aria-hidden="true"></div>
<div class="site-panel-content h-full min-h-0">
${content}
</div>
</div>`;
}

View file

@ -0,0 +1,21 @@
import type { ReactNode } from "react";
interface FrostedBoxProps {
children: ReactNode;
className?: string;
}
export function FrostedBox({ children, className = "" }: FrostedBoxProps) {
return (
<div
className={`site-shell site-panel-frame relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${className}`}
>
<div
className="site-panel-frost pointer-events-none"
aria-hidden="true"
/>
<div className="site-panel-border" aria-hidden="true" />
<div className="site-panel-content h-full min-h-0">{children}</div>
</div>
);
}

View file

@ -0,0 +1,108 @@
import { HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router";
import { useEffect } from "react";
import { initWebGLBackground } from "~/lib/webgl-background";
const CLEARNET_HOST = "jetpham.com";
const ONION_HOST =
"jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion";
const REPO_URL = "https://git.extremist.software/jet/website";
function getMirrorLink() {
if (typeof location !== "undefined" && location.hostname.endsWith(".onion")) {
return { href: `https://${CLEARNET_HOST}`, label: "clearnet" };
}
return { href: `http://${ONION_HOST}`, label: ".onion" };
}
function NavLink({ to, children }: { to: string; children: string }) {
return (
<Link
to={to}
className="site-nav-link"
activeProps={{ "aria-current": "page" }}
>
<span className="site-nav-marker" aria-hidden="true">
&gt;
</span>
<span>{children}</span>
<span className="site-nav-marker" aria-hidden="true">
&lt;
</span>
</Link>
);
}
function Footer() {
const mirror = getMirrorLink();
return (
<div className="site-shell">
<div className="site-panel-frame px-[2ch] py-[1ch]">
<div className="site-panel-frost" aria-hidden="true" />
<div className="site-panel-border" aria-hidden="true" />
<div className="site-panel-content site-footer-inner">
<a href={REPO_URL}>Src</a>
<span aria-hidden="true">|</span>
<a href="/qa/rss.xml">RSS</a>
<span aria-hidden="true">|</span>
<a href="/pgp.txt">PGP</a>
<span aria-hidden="true">|</span>
<a href="/ssh.txt">SSH</a>
<span aria-hidden="true">|</span>
<a href={mirror.href}>{mirror.label}</a>
</div>
</div>
</div>
);
}
function WebGLBackground() {
useEffect(() => {
initWebGLBackground();
}, []);
return (
<canvas
id="canvas"
className="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
aria-hidden="true"
/>
);
}
export function SiteDocument() {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body style={{ background: "#000" }}>
<a className="skip-link" href="#outlet">
Skip to content
</a>
<WebGLBackground />
<div className="page-frame relative z-10">
<nav aria-label="Main navigation" className="site-region">
<div className="site-shell site-panel-frame px-[2ch] py-[1ch]">
<div className="site-panel-frost" aria-hidden="true" />
<div className="site-panel-border" aria-hidden="true" />
<div className="site-panel-content site-nav-links flex justify-center gap-[2ch]">
<NavLink to="/">Home</NavLink>
<NavLink to="/qa">Q&amp;A</NavLink>
<NavLink to="/colophon">Colophon</NavLink>
</div>
</div>
</nav>
<main id="outlet" className="site-region" tabIndex={-1}>
<Outlet />
</main>
<footer className="site-region site-footer">
<Footer />
</footer>
</div>
<Scripts />
</body>
</html>
);
}

View file

@ -1,132 +0,0 @@
export interface Question {
id: number;
question: string;
answer: string;
created_at: string;
answered_at: string;
}
export interface QuestionStats {
asked: number;
answered: number;
}
const DEV_QUESTIONS: Question[] = [
{
id: 1,
question: "What is a fact about octopuses?",
answer: "An octopus has three hearts and blue blood.",
created_at: "2026-03-23T18:10:00.000Z",
answered_at: "2026-03-23T19:00:00.000Z",
},
{
id: 2,
question: "What is a fact about axolotls?",
answer:
"An axolotl can regrow limbs, parts of its heart, and even parts of its brain.",
created_at: "2026-03-24T02:15:00.000Z",
answered_at: "2026-03-24T05:45:00.000Z",
},
{
id: 3,
question: "What is a fact about crows?",
answer: "Crows can recognize human faces and remember them for years.",
created_at: "2026-03-25T08:30:00.000Z",
answered_at: "2026-03-25T09:05:00.000Z",
},
{
id: 4,
question: "What is a fact about wombats?",
answer: "Wombats produce cube-shaped poop.",
created_at: "2026-03-25T11:10:00.000Z",
answered_at: "2026-03-25T11:40:00.000Z",
},
{
id: 5,
question: "What is a fact about mantis shrimp?",
answer:
"A mantis shrimp can punch so fast it creates tiny cavitation bubbles in water.",
created_at: "2026-03-25T13:00:00.000Z",
answered_at: "2026-03-25T13:18:00.000Z",
},
{
id: 6,
question: "What is a fact about sloths?",
answer:
"Some sloths can hold their breath longer than dolphins by slowing their heart rate.",
created_at: "2026-03-25T14:25:00.000Z",
answered_at: "2026-03-25T15:00:00.000Z",
},
{
id: 7,
question: "What is a fact about owls?",
answer:
"An owl cannot rotate its eyes, so it turns its whole head instead.",
created_at: "2026-03-25T16:05:00.000Z",
answered_at: "2026-03-25T16:21:00.000Z",
},
{
id: 8,
question: "What is a fact about capybaras?",
answer:
"Capybaras are the largest rodents in the world and are excellent swimmers.",
created_at: "2026-03-25T18:45:00.000Z",
answered_at: "2026-03-25T19:07:00.000Z",
},
{
id: 9,
question: "What is a fact about penguins?",
answer: "Penguins have solid bones, which help them dive instead of float.",
created_at: "2026-03-25T21:20:00.000Z",
answered_at: "2026-03-25T21:55:00.000Z",
},
{
id: 10,
question: "What is a fact about bats?",
answer: "Bats are the only mammals capable of sustained powered flight.",
created_at: "2026-03-26T00:10:00.000Z",
answered_at: "2026-03-26T00:32:00.000Z",
},
];
const DEV_QUESTION_STATS: QuestionStats = {
asked: 16,
answered: DEV_QUESTIONS.length,
};
export async function getQuestions(): Promise<Question[]> {
if (import.meta.env.DEV) return DEV_QUESTIONS;
const res = await fetch("/api/questions");
if (!res.ok) throw new Error("Failed to fetch questions");
return res.json() as Promise<Question[]>;
}
export async function getQuestionStats(): Promise<QuestionStats> {
if (import.meta.env.DEV) return DEV_QUESTION_STATS;
try {
const res = await fetch("/api/questions/stats");
if (!res.ok) throw new Error("Failed to fetch question stats");
return res.json() as Promise<QuestionStats>;
} catch {
const questions = await getQuestions();
return {
asked: questions.length,
answered: questions.length,
};
}
}
export async function submitQuestion(question: string): Promise<void> {
const res = await fetch("/api/questions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
});
if (!res.ok) {
if (res.status === 429)
throw new Error("Too many questions. Please try again later.");
throw new Error("Failed to submit question");
}
}

101
src/lib/qa-server.ts Normal file
View file

@ -0,0 +1,101 @@
import { createServerFn } from "@tanstack/react-start";
export interface Question {
id: number;
question: string;
answer: string;
created_at: string;
answered_at: string;
}
export interface QuestionStats {
asked: number;
answered: number;
}
const DEV_QUESTIONS: Question[] = [
{
id: 1,
question: "What is a fact about octopuses?",
answer: "An octopus has three hearts and blue blood.",
created_at: "2026-03-23T18:10:00.000Z",
answered_at: "2026-03-23T19:00:00.000Z",
},
{
id: 2,
question: "What is a fact about axolotls?",
answer:
"An axolotl can regrow limbs, parts of its heart, and even parts of its brain.",
created_at: "2026-03-24T02:15:00.000Z",
answered_at: "2026-03-24T05:45:00.000Z",
},
{
id: 3,
question: "What is a fact about crows?",
answer: "Crows can recognize human faces and remember them for years.",
created_at: "2026-03-25T08:30:00.000Z",
answered_at: "2026-03-25T09:05:00.000Z",
},
];
const DEV_QUESTION_STATS: QuestionStats = {
asked: 16,
answered: DEV_QUESTIONS.length,
};
function apiUrl(path: string) {
const base = process.env.QA_API_BASE_URL ?? "http://127.0.0.1:3003";
return new URL(path, base).toString();
}
export const getQuestions = createServerFn({ method: "GET" }).handler(
async (): Promise<Question[]> => {
if (process.env.NODE_ENV === "development") return DEV_QUESTIONS;
try {
const res = await fetch(apiUrl("/api/questions"));
if (!res.ok) throw new Error("Failed to fetch questions");
return (await res.json()) as Question[];
} catch {
return [];
}
},
);
export const getQuestionStats = createServerFn({ method: "GET" }).handler(
async (): Promise<QuestionStats> => {
if (process.env.NODE_ENV === "development") return DEV_QUESTION_STATS;
try {
const res = await fetch(apiUrl("/api/questions/stats"));
if (!res.ok) throw new Error("Failed to fetch question stats");
return (await res.json()) as QuestionStats;
} catch {
const questions = await getQuestions.__executeServer({
method: "GET",
data: undefined,
});
return {
asked: (questions as Question[]).length,
answered: (questions as Question[]).length,
};
}
},
);
export const submitQuestion = createServerFn({ method: "POST" })
.inputValidator((question: string) => question)
.handler(async ({ data: question }): Promise<void> => {
const res = await fetch(apiUrl("/api/questions"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
});
if (!res.ok) {
if (res.status === 429) {
throw new Error("Too many questions. Please try again later.");
}
throw new Error("Failed to submit question");
}
});

View file

@ -1,46 +0,0 @@
const CLEARNET_HOST = "jetpham.com";
const ONION_HOST =
"jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion";
const REPO_URL = "https://git.extremist.software/jet/website";
function isOnionHost(hostname: string): boolean {
return hostname.endsWith(".onion");
}
function getMirrorLink() {
if (isOnionHost(location.hostname)) {
return {
href: `https://${CLEARNET_HOST}`,
label: "clearnet",
};
}
return {
href: `http://${ONION_HOST}`,
label: ".onion",
};
}
export function renderFooter() {
const footer = document.getElementById("site-footer");
if (!footer) return;
const mirror = getMirrorLink();
footer.innerHTML = `
<div class="site-panel-frame px-[2ch] py-[1ch]">
<div class="site-panel-frost" aria-hidden="true"></div>
<div class="site-panel-border" aria-hidden="true"></div>
<div class="site-panel-content site-footer-inner">
<a href="${REPO_URL}">Src</a>
<span aria-hidden="true">|</span>
<a href="/qa/rss.xml" data-native-link>RSS</a>
<span aria-hidden="true">|</span>
<a href="/pgp.txt" data-native-link>PGP</a>
<span aria-hidden="true">|</span>
<a href="/ssh.txt" data-native-link>SSH</a>
<span aria-hidden="true">|</span>
<a href="${mirror.href}">${mirror.label}</a>
</div>
</div>`;
}

View file

@ -1,15 +0,0 @@
import "~/styles/globals.css";
import { route, initRouter } from "~/router";
import { initWebGLBackground } from "~/lib/webgl-background";
import { renderFooter } from "~/lib/site";
import { homePage } from "~/pages/home";
import { qaPage } from "~/pages/qa";
import { notFoundPage } from "~/pages/not-found";
route("/", "Jet Pham - Home", homePage);
route("/qa", "Jet Pham - Q+A", qaPage);
route("*", "404 - Jet Pham", notFoundPage);
renderFooter();
initWebGLBackground();
initRouter();

View file

@ -1,68 +0,0 @@
import Jet from "~/assets/Jet.txt?ansi";
import { frostedBox } from "~/components/frosted-box";
export function homePage(outlet: HTMLElement) {
outlet.classList.remove("qa-outlet");
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start">
${frostedBox(`
<div class="flex flex-col items-center justify-center gap-[1.25ch] md:gap-[2ch] md:flex-row">
<div class="order-1 flex flex-col items-center md:order-2">
<h1 class="sr-only">Jet Pham</h1>
<div aria-hidden="true" data-emitter-ansi>${Jet}</div>
<p class="mt-[2ch]">Software Extremist</p>
</div>
<div class="order-2 shrink-0 md:order-1">
<img
data-emitter-image
src="/jet.svg"
alt="A picture of Jet wearing a beanie in purple and blue lighting"
width="250"
height="250"
class="aspect-square w-full max-w-[220px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
style="background-color: #a80055; color: transparent"
/>
</div>
</div>
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Contact</legend>
<button type="button" id="copy-email" class="qa-inline-action">jet@extremist.software</button>
<span id="copy-email-status" class="qa-meta ml-[1ch]" aria-live="polite"></span>
</fieldset>
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Links</legend>
<ol>
<li><a href="https://git.extremist.software" class="inline-flex items-center">Forgejo</a></li>
<li><a href="https://github.com/jetpham" class="inline-flex items-center">GitHub</a></li>
<li><a href="https://x.com/exmistsoftware" class="inline-flex items-center">X</a></li>
<li><a href="https://bsky.app/profile/extremist.software" class="inline-flex items-center">Bluesky</a></li>
</ol>
</fieldset>
`)}
</div>`;
const copyButton = document.getElementById("copy-email") as HTMLButtonElement;
const copyStatus = document.getElementById(
"copy-email-status",
) as HTMLSpanElement;
let resetTimer: number | null = null;
copyButton.addEventListener("click", () => {
void (async () => {
try {
await navigator.clipboard.writeText("jet@extremist.software");
copyStatus.textContent = "copied";
copyStatus.style.color = "var(--light-green)";
} catch {
copyStatus.textContent = "copy failed";
copyStatus.style.color = "var(--light-red)";
}
if (resetTimer !== null) window.clearTimeout(resetTimer);
resetTimer = window.setTimeout(() => {
copyStatus.textContent = "";
resetTimer = null;
}, 1400);
})();
});
}

View file

@ -1,13 +0,0 @@
import { frostedBox } from "~/components/frosted-box";
export function notFoundPage(outlet: HTMLElement) {
outlet.classList.remove("qa-outlet");
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start">
${frostedBox(`
<h1 style="color: var(--light-red);">404</h1>
<p class="mt-[1ch]">Page not found.</p>
<p class="mt-[1ch]"><a href="/">[BACK TO HOME]</a></p>
`)}
</div>`;
}

View file

@ -1,283 +0,0 @@
import {
getQuestions,
getQuestionStats,
submitQuestion,
type Question,
type QuestionStats,
} from "~/lib/api";
import {
clearQuestionDraft,
formatDateOnly,
pickPlaceholder,
readQuestionDraft,
writeQuestionDraft,
} from "~/lib/qa";
import { frostedBox } from "~/components/frosted-box";
const PLACEHOLDER_QUESTIONS = [
"Why call yourself a software extremist?",
"What are you building at Noisebridge?",
"Why Forgejo over GitHub?",
"What is the weirdest thing in your nix-config?",
"Why did you write HolyC?",
"What do you like about San Francisco hacker culture?",
"What is your favorite project you've seen at TIAT?",
"What is your favorite project you've seen at Noisebridge?",
"What is your favorite hacker conference?",
"What is your cat's name?",
"What are your favorite programming languages and tools?",
"Who are your biggest inspirations?",
] as const;
function escapeHtml(str: string): string {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function formatQuestionTooltip(question: Question): string {
const askedExact = formatDateOnly(question.created_at);
const answeredExact = formatDateOnly(question.answered_at);
return `
<p>Asked ${escapeHtml(askedExact)}</p>
<p>Answered ${escapeHtml(answeredExact)}</p>`;
}
function formatRatio(stats: QuestionStats): string {
if (stats.asked === 0) return "0%";
return `${Math.round((stats.answered / stats.asked) * 100)}%`;
}
function renderQuestions(list: HTMLElement, questions: Question[]) {
if (questions.length === 0) {
list.innerHTML = `
<section class="qa-item qa-list-item px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
<p class="qa-list-label">No answers yet</p>
<p>...</p>
</section>`;
return;
}
list.innerHTML = questions
.map(
(q) => `
<section class="qa-item qa-list-item mb-[2ch] px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" tabindex="0">
<div class="qa-item-meta" role="note">
${formatQuestionTooltip(q)}
</div>
<p style="color: var(--light-gray);">${escapeHtml(q.question)}</p>
<p class="mt-[1ch]" style="color: var(--light-blue);">${escapeHtml(q.answer)}</p>
</section>`,
)
.join("");
}
export async function qaPage(outlet: HTMLElement) {
outlet.classList.add("qa-outlet");
const draft = readQuestionDraft();
const placeholderQuestion = pickPlaceholder(PLACEHOLDER_QUESTIONS);
outlet.innerHTML = `
<div class="qa-page flex h-full flex-col items-center justify-start">
${frostedBox(
`
<div class="flex h-full flex-col">
<form id="qa-form" novalidate>
<section class="section-block">
<label class="sr-only" for="qa-input">Question</label>
<div class="qa-input-wrap">
<textarea id="qa-input" maxlength="200" rows="3"
class="qa-textarea"
aria-describedby="qa-status char-count"
placeholder="${escapeHtml(placeholderQuestion)}">${escapeHtml(draft)}</textarea>
<div class="qa-input-bar">
<span id="char-count" class="qa-bar-text">${draft.length}/200</span>
<button id="qa-submit" type="submit" class="qa-button">[SUBMIT]</button>
</div>
</div>
<p id="qa-status" class="qa-meta" aria-live="polite"></p>
<div id="qa-stats" class="qa-stats mt-[1ch]" aria-live="polite">Asked ... | Answered ... | Ratio ...</div>
</section>
</form>
<div id="qa-list" class="qa-list-scroll mt-[2ch] min-h-0 flex-1 overflow-y-auto pr-[1ch]" aria-live="polite">Loading answered questions...</div>
</div>
`,
"my-0 flex h-full min-h-0 flex-col",
)}
</div>`;
const form = document.getElementById("qa-form") as HTMLFormElement;
const input = document.getElementById("qa-input") as HTMLTextAreaElement;
const submitButton = document.getElementById(
"qa-submit",
) as HTMLButtonElement;
const charCount = document.getElementById("char-count") as HTMLSpanElement;
const status = document.getElementById("qa-status") as HTMLParagraphElement;
const stats = document.getElementById("qa-stats") as HTMLDivElement;
const list = document.getElementById("qa-list") as HTMLDivElement;
let isSubmitting = false;
let hasInteracted = draft.trim().length > 0;
let buttonResetTimer: number | null = null;
const defaultButtonText = "[SUBMIT]";
function setStatus(message: string, color: string) {
status.textContent = message;
status.style.color = color;
}
function setStatsDisplay(nextStats: QuestionStats) {
stats.textContent = `Asked ${nextStats.asked} | Answered ${nextStats.answered} | Ratio ${formatRatio(nextStats)}`;
}
function showButtonMessage(message: string, color: string, duration = 1600) {
if (buttonResetTimer !== null) window.clearTimeout(buttonResetTimer);
submitButton.textContent = message;
submitButton.style.color = color;
submitButton.classList.add("qa-button-message");
buttonResetTimer = window.setTimeout(() => {
buttonResetTimer = null;
submitButton.textContent = defaultButtonText;
submitButton.style.color = "";
submitButton.classList.remove("qa-button-message");
if (!input.matches(":focus") && input.value.trim().length === 0) {
input.removeAttribute("aria-invalid");
}
}, duration);
}
function updateValidation() {
const trimmed = input.value.trim();
const remaining = 200 - input.value.length;
charCount.textContent = `${input.value.length}/200`;
if (trimmed.length === 0 && hasInteracted) {
input.setAttribute("aria-invalid", "true");
return false;
}
input.removeAttribute("aria-invalid");
if (remaining <= 20) {
charCount.style.color =
remaining === 0
? "var(--light-red)"
: remaining <= 5
? "var(--yellow)"
: "var(--dark-gray)";
return true;
}
charCount.style.color = "var(--dark-gray)";
return true;
}
async function loadQuestions() {
list.textContent = "Loading answered questions...";
list.style.color = "var(--light-gray)";
list.style.textAlign = "left";
try {
const questions = await getQuestions();
renderQuestions(list, questions);
} catch {
showButtonMessage("[LOAD FAILED]", "var(--light-red)");
list.innerHTML = `
<section class="qa-item qa-list-item px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
<p class="qa-list-label" style="color: var(--light-red);">Load failed</p>
<p>Failed to load answered questions.</p>
<p class="qa-meta">
<button type="button" id="qa-retry" class="qa-inline-action">Retry loading questions</button>
</p>
</section>`;
list.style.color = "var(--light-red)";
const retryButton = document.getElementById(
"qa-retry",
) as HTMLButtonElement | null;
retryButton?.addEventListener("click", () => {
void loadQuestions();
});
}
}
async function loadStats() {
try {
const nextStats = await getQuestionStats();
setStatsDisplay(nextStats);
} catch {
stats.textContent = "Asked ? | Answered ? | Ratio ?";
}
}
input.addEventListener("input", () => {
hasInteracted = true;
writeQuestionDraft(input.value);
updateValidation();
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
form.requestSubmit();
}
});
form.addEventListener("submit", (e) => {
e.preventDefault();
if (isSubmitting) return;
const question = input.value.trim();
hasInteracted = true;
if (!updateValidation() || !question) {
setStatus("", "var(--dark-gray)");
showButtonMessage("[EMPTY]", "var(--light-red)");
input.focus();
return;
}
isSubmitting = true;
submitButton.disabled = true;
submitButton.textContent = "[SENDING]";
setStatus("Submitting...", "var(--light-gray)");
submitQuestion(question)
.then(() => {
input.value = "";
clearQuestionDraft();
hasInteracted = false;
submitButton.textContent = defaultButtonText;
submitButton.style.color = "";
updateValidation();
setStatus(
"Question submitted! It will appear here once answered.",
"var(--light-green)",
);
input.focus();
})
.catch((err: unknown) => {
const message =
err instanceof Error ? err.message : "Failed to submit question.";
if (message.includes("Too many questions")) {
showButtonMessage("[RATE LIMIT]", "var(--light-red)");
setStatus(message, "var(--light-red)");
} else {
showButtonMessage("[FAILED]", "var(--light-red)");
setStatus("", "var(--dark-gray)");
}
})
.finally(() => {
isSubmitting = false;
submitButton.disabled = false;
if (buttonResetTimer === null) {
submitButton.textContent = defaultButtonText;
submitButton.style.color = "";
}
});
});
updateValidation();
await loadStats();
await loadQuestions();
}

123
src/routeTree.gen.ts Normal file
View file

@ -0,0 +1,123 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as QaRouteImport } from './routes/qa'
import { Route as ColophonRouteImport } from './routes/colophon'
import { Route as SplatRouteImport } from './routes/$'
import { Route as IndexRouteImport } from './routes/index'
const QaRoute = QaRouteImport.update({
id: '/qa',
path: '/qa',
getParentRoute: () => rootRouteImport,
} as any)
const ColophonRoute = ColophonRouteImport.update({
id: '/colophon',
path: '/colophon',
getParentRoute: () => rootRouteImport,
} as any)
const SplatRoute = SplatRouteImport.update({
id: '/$',
path: '/$',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/$': typeof SplatRoute
'/colophon': typeof ColophonRoute
'/qa': typeof QaRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/$': typeof SplatRoute
'/colophon': typeof ColophonRoute
'/qa': typeof QaRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/$': typeof SplatRoute
'/colophon': typeof ColophonRoute
'/qa': typeof QaRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/$' | '/colophon' | '/qa'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/$' | '/colophon' | '/qa'
id: '__root__' | '/' | '/$' | '/colophon' | '/qa'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
SplatRoute: typeof SplatRoute
ColophonRoute: typeof ColophonRoute
QaRoute: typeof QaRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/qa': {
id: '/qa'
path: '/qa'
fullPath: '/qa'
preLoaderRoute: typeof QaRouteImport
parentRoute: typeof rootRouteImport
}
'/colophon': {
id: '/colophon'
path: '/colophon'
fullPath: '/colophon'
preLoaderRoute: typeof ColophonRouteImport
parentRoute: typeof rootRouteImport
}
'/$': {
id: '/$'
path: '/$'
fullPath: '/$'
preLoaderRoute: typeof SplatRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SplatRoute: SplatRoute,
ColophonRoute: ColophonRoute,
QaRoute: QaRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { startInstance } from './start.ts'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
config: Awaited<ReturnType<typeof startInstance.getOptions>>
}
}

View file

@ -1,95 +0,0 @@
type PageHandler = (
outlet: HTMLElement,
params: Record<string, string>,
) => void | Promise<void>;
interface Route {
pattern: RegExp;
keys: string[];
handler: PageHandler;
title: string;
}
const routes: Route[] = [];
let notFoundHandler: PageHandler | null = null;
let notFoundTitle = "404 - Jet Pham";
export function route(path: string, title: string, handler: PageHandler) {
if (path === "*") {
notFoundHandler = handler;
notFoundTitle = title;
return;
}
const keys: string[] = [];
const pattern = path.replace(/:(\w+)/g, (_, key: string) => {
keys.push(key);
return "([^/]+)";
});
routes.push({ pattern: new RegExp(`^${pattern}$`), keys, handler, title });
}
export function navigate(path: string) {
history.pushState(null, "", path);
window.scrollTo({ top: 0, behavior: "auto" });
void render();
}
function updateNavState(path: string) {
const navLinks =
document.querySelectorAll<HTMLAnchorElement>("[data-nav-link]");
navLinks.forEach((link) => {
if (link.pathname === path) {
link.setAttribute("aria-current", "page");
} else {
link.removeAttribute("aria-current");
}
});
}
async function render() {
const path = location.pathname;
const outlet = document.getElementById("outlet")!;
updateNavState(path);
for (const r of routes) {
const match = path.match(r.pattern);
if (match) {
const params: Record<string, string> = {};
r.keys.forEach((key, i) => {
params[key] = match[i + 1]!;
});
outlet.innerHTML = "";
await r.handler(outlet, params);
document.title = r.title;
document.dispatchEvent(new Event("site:render"));
return;
}
}
outlet.innerHTML = "";
if (notFoundHandler) {
await notFoundHandler(outlet, {});
}
document.title = notFoundTitle;
document.dispatchEvent(new Event("site:render"));
}
export function initRouter() {
window.addEventListener("popstate", () => void render());
document.addEventListener("click", (e) => {
const anchor = (e.target as HTMLElement).closest("a");
if (
anchor?.origin === location.origin &&
!anchor.hash &&
!anchor.hasAttribute("data-native-link") &&
!anchor.hasAttribute("download") &&
!anchor.hasAttribute("target")
) {
e.preventDefault();
navigate(anchor.pathname);
}
});
void render();
}

15
src/router.tsx Normal file
View file

@ -0,0 +1,15 @@
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
export async function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
});
}
declare module "@tanstack/react-router" {
interface Register {
router: Awaited<ReturnType<typeof getRouter>>;
}
}

23
src/routes/$.tsx Normal file
View file

@ -0,0 +1,23 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { FrostedBox } from "~/components/frosted-box";
export const Route = createFileRoute("/$")({
head: () => ({
meta: [{ title: "404 - Jet Pham" }],
}),
component: NotFoundPage,
});
function NotFoundPage() {
return (
<div className="flex flex-col items-center justify-start">
<FrostedBox>
<h1 style={{ color: "var(--light-red)" }}>404</h1>
<p className="mt-[1ch]">Page not found.</p>
<p className="mt-[1ch]">
<Link to="/">[BACK TO HOME]</Link>
</p>
</FrostedBox>
</div>
);
}

114
src/routes/__root.tsx Normal file
View file

@ -0,0 +1,114 @@
import "~/styles/globals.css";
import { createRootRoute } from "@tanstack/react-router";
import { SiteDocument } from "~/components/site-shell";
const personJsonLd = {
"@context": "https://schema.org",
"@type": "Person",
name: "Jet Pham",
givenName: "Jet",
familyName: "Pham",
description: "Software extremist.",
url: "https://jetpham.com",
jobTitle: "Software Extremist",
hasOccupation: {
"@type": "Occupation",
name: "Hacker",
},
email: "jet@extremist.software",
image: "https://jetpham.com/jet.svg",
alumniOf: {
"@type": "CollegeOrUniversity",
name: "University of San Francisco",
url: "https://www.usfca.edu",
},
hasCredential: {
"@type": "EducationalOccupationalCredential",
credentialCategory: "degree",
name: "Bachelor of Science in Computer Science",
},
homeLocation: {
"@type": "City",
name: "San Francisco, CA",
},
workLocation: {
"@type": "City",
name: "San Francisco, CA",
},
memberOf: {
"@type": "Organization",
name: "Noisebridge",
url: "https://www.noisebridge.net",
},
affiliation: {
"@type": "Organization",
name: "Noisebridge",
url: "https://www.noisebridge.net",
},
sameAs: [
"https://github.com/jetpham",
"https://x.com/exmistsoftware",
"https://bsky.app/profile/extremist.software",
"https://git.extremist.software",
],
};
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "UTF-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
{ name: "theme-color", content: "#000000" },
{ name: "apple-mobile-web-app-title", content: "Jet Pham" },
{
name: "description",
content: "Jet Pham's personal website. Software extremist.",
},
{ property: "og:title", content: "Jet Pham - Software Extremist" },
{
property: "og:description",
content: "Jet Pham's personal website. Software extremist.",
},
{ property: "og:type", content: "website" },
{ property: "og:url", content: "https://jetpham.com/" },
{ property: "og:site_name", content: "Jet Pham" },
{
property: "og:image",
content: "https://jetpham.com/web-app-manifest-512x512.png",
},
{ property: "og:image:width", content: "512" },
{ property: "og:image:height", content: "512" },
{ property: "og:image:alt", content: "Jet Pham" },
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: "Jet Pham - Software Extremist" },
{
name: "twitter:description",
content: "Jet Pham's personal website. Software extremist.",
},
{
name: "twitter:image",
content: "https://jetpham.com/web-app-manifest-512x512.png",
},
],
links: [
{ rel: "canonical", href: "https://jetpham.com/" },
{ rel: "manifest", href: "/manifest.json" },
{ rel: "icon", href: "/favicon.ico" },
{ rel: "apple-touch-icon", href: "/apple-icon.png" },
{
rel: "preload",
href: "/Web437_IBM_VGA_8x16.woff",
as: "font",
type: "font/woff",
crossOrigin: "anonymous",
},
],
scripts: [
{
type: "application/ld+json",
children: JSON.stringify(personJsonLd),
},
],
}),
component: SiteDocument,
});

99
src/routes/colophon.tsx Normal file
View file

@ -0,0 +1,99 @@
import { createFileRoute } from "@tanstack/react-router";
import { FrostedBox } from "~/components/frosted-box";
export const Route = createFileRoute("/colophon")({
head: () => ({
meta: [
{ title: "Jet Pham - Colophon" },
{
name: "description",
content:
"A short note on how Jet Pham's personal website is built and served.",
},
],
links: [{ rel: "canonical", href: "https://jetpham.com/colophon" }],
}),
component: ColophonPage,
});
function ColophonPage() {
return (
<div className="flex flex-col items-center justify-start">
<FrostedBox>
<h1 style={{ color: "var(--yellow)" }}>Colophon</h1>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Frontend
</legend>
<ol>
<li>TanStack Start renders the routes and server functions.</li>
<li>
React owns the page shell, homepage, Q&amp;A, and this page.
</li>
<li>Tailwind CSS v4 handles utility styling.</li>
<li>IBM VGA is the font.</li>
</ol>
</fieldset>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Background
</legend>
<p>
The background is a WebGL2 Conway&apos;s Game of Life, but new cells get the average color of it's parents. Page
text, links, ANSI art, and images act as emitters that feed color
back into the simulation.
</p>
</fieldset>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Backend
</legend>
<ol>
<li>Q&amp;A storage lives in SQLite behind a Rust API.</li>
<li>New questions send local SMTP notifications.</li>
<li>Email replies can become public answers through a webhook.</li>
<li>Caddy serves the public edge and proxies the app/API.</li>
</ol>
</fieldset>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Deployment
</legend>
<p>
The project ships as a Nix flake with a reusable NixOS module for
the frontend service, Rust Q&amp;A API, Caddy, and onion
service.
</p>
</fieldset>
</FrostedBox>
</div>
);
}

100
src/routes/index.tsx Normal file
View file

@ -0,0 +1,100 @@
import { createFileRoute } from "@tanstack/react-router";
import Jet from "~/assets/Jet.txt?ansi";
import { FrostedBox } from "~/components/frosted-box";
export const Route = createFileRoute("/")({
head: () => ({
meta: [{ title: "Jet Pham - Home" }],
}),
component: HomePage,
});
function HomePage() {
return (
<div className="flex flex-col items-center justify-start">
<FrostedBox>
<div className="flex flex-col items-center justify-center gap-[1.25ch] md:flex-row md:gap-[2ch]">
<div className="order-1 flex flex-col items-center md:order-2">
<h1 className="sr-only">Jet Pham</h1>
<div
aria-hidden="true"
data-emitter-ansi
dangerouslySetInnerHTML={{ __html: Jet }}
/>
<p className="mt-[2ch]">Software Extremist</p>
</div>
<div className="order-2 shrink-0 md:order-1">
<img
data-emitter-image
src="/jet.svg"
alt="A picture of Jet wearing a beanie in purple and blue lighting"
width="250"
height="250"
className="aspect-square w-full max-w-[220px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
style={{ backgroundColor: "#a80055", color: "transparent" }}
/>
</div>
</div>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Contact
</legend>
<a href="mailto:jet@extremist.software" className="qa-inline-action">
jet@extremist.software
</a>
</fieldset>
<fieldset
className="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
style={{ borderColor: "var(--white)" }}
>
<legend
className="-mx-[0.5ch] px-[0.5ch]"
style={{ color: "var(--white)" }}
>
Links
</legend>
<ol>
<li>
<a
href="https://git.extremist.software"
className="inline-flex items-center"
>
Forgejo
</a>
</li>
<li>
<a
href="https://github.com/jetpham"
className="inline-flex items-center"
>
GitHub
</a>
</li>
<li>
<a
href="https://x.com/exmistsoftware"
className="inline-flex items-center"
>
X
</a>
</li>
<li>
<a
href="https://bsky.app/profile/extremist.software"
className="inline-flex items-center"
>
Bluesky
</a>
</li>
</ol>
</fieldset>
</FrostedBox>
</div>
);
}

248
src/routes/qa.tsx Normal file
View file

@ -0,0 +1,248 @@
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState, type FormEvent, type KeyboardEvent } from "react";
import { FrostedBox } from "~/components/frosted-box";
import {
getQuestions,
getQuestionStats,
submitQuestion,
type Question,
type QuestionStats,
} from "~/lib/qa-server";
import {
clearQuestionDraft,
pickPlaceholder,
readQuestionDraft,
writeQuestionDraft,
} from "~/lib/qa";
const PLACEHOLDER_QUESTIONS = [
"Why call yourself a software extremist?",
"What are you building at Noisebridge?",
"Why Forgejo over GitHub?",
"What is the weirdest thing in your nix-config?",
"Why did you write HolyC?",
"What do you like about San Francisco hacker culture?",
"What is your favorite project you've seen at TIAT?",
"What is your favorite project you've seen at Noisebridge?",
"What is your favorite hacker conference?",
"What is your cat's name?",
"What are your favorite programming languages and tools?",
"Who are your biggest inspirations?",
] as const;
export const Route = createFileRoute("/qa")({
head: () => ({
meta: [{ title: "Jet Pham - Q+A" }],
}),
loader: async () => {
const [questions, stats] = await Promise.all([
getQuestions(),
getQuestionStats(),
]);
return { questions, stats };
},
component: QaPage,
});
function formatRatio(stats: QuestionStats): string {
if (stats.asked === 0) return "0%";
return `${Math.round((stats.answered / stats.asked) * 100)}%`;
}
function QuestionList({ questions }: { questions: Question[] }) {
if (questions.length === 0) {
return (
<section className="qa-item qa-list-item px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]">
<p className="qa-list-label">No answers yet</p>
<p>...</p>
</section>
);
}
return questions.map((question) => (
<section
key={question.id}
className="qa-item qa-list-item mb-[2ch] px-[calc(1.5ch-0.5px)] pt-0 pb-[1ch]"
>
<p style={{ color: "var(--light-gray)" }}>{question.question}</p>
<p className="mt-[1ch]" style={{ color: "var(--light-blue)" }}>
{question.answer}
</p>
</section>
));
}
function QaPage() {
const loaderData = Route.useLoaderData();
const [questions, setQuestions] = useState(loaderData.questions);
const [stats, setStats] = useState(loaderData.stats);
const [draft, setDraft] = useState("");
const [placeholder, setPlaceholder] = useState<string>(
PLACEHOLDER_QUESTIONS[0],
);
const [hasInteracted, setHasInteracted] = useState(false);
const [status, setStatus] = useState("");
const [statusColor, setStatusColor] = useState("var(--dark-gray)");
const [buttonMessage, setButtonMessage] = useState("[SUBMIT]");
const [buttonColor, setButtonColor] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const trimmed = draft.trim();
const isEmptyInvalid = hasInteracted && trimmed.length === 0;
const remaining = 200 - draft.length;
const charCountColor =
remaining === 0
? "var(--light-red)"
: remaining <= 5
? "var(--yellow)"
: remaining <= 20
? "var(--dark-gray)"
: "var(--dark-gray)";
useEffect(() => {
const savedDraft = readQuestionDraft();
setDraft(savedDraft);
setHasInteracted(savedDraft.trim().length > 0);
setPlaceholder(pickPlaceholder(PLACEHOLDER_QUESTIONS));
}, []);
function showButtonMessage(message: string, color: string, duration = 1600) {
setButtonMessage(message);
setButtonColor(color);
window.setTimeout(() => {
setButtonMessage("[SUBMIT]");
setButtonColor("");
}, duration);
}
function updateDraft(value: string) {
setHasInteracted(true);
setDraft(value);
writeQuestionDraft(value);
}
async function refreshQa() {
const [nextQuestions, nextStats] = await Promise.all([
getQuestions(),
getQuestionStats(),
]);
setQuestions(nextQuestions);
setStats(nextStats);
}
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isSubmitting) return;
setHasInteracted(true);
if (!trimmed) {
setStatus("");
setStatusColor("var(--dark-gray)");
showButtonMessage("[EMPTY]", "var(--light-red)");
return;
}
setIsSubmitting(true);
setButtonMessage("[SENDING]");
setStatus("Submitting...");
setStatusColor("var(--light-gray)");
try {
await submitQuestion({ data: trimmed });
setDraft("");
clearQuestionDraft();
setHasInteracted(false);
setButtonMessage("[SUBMIT]");
setButtonColor("");
setStatus("Question submitted! It will appear here once answered.");
setStatusColor("var(--light-green)");
await refreshQa();
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to submit question.";
if (message.includes("Too many questions")) {
showButtonMessage("[RATE LIMIT]", "var(--light-red)");
setStatus(message);
setStatusColor("var(--light-red)");
} else {
showButtonMessage("[FAILED]", "var(--light-red)");
setStatus("");
setStatusColor("var(--dark-gray)");
}
} finally {
setIsSubmitting(false);
}
}
function onKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
event.currentTarget.form?.requestSubmit();
}
}
return (
<div className="qa-page flex h-full flex-col items-center justify-start">
<FrostedBox className="my-0 flex h-full min-h-0 flex-col">
<div className="flex h-full flex-col">
<form noValidate onSubmit={onSubmit}>
<section className="section-block">
<label className="sr-only" htmlFor="qa-input">
Question
</label>
<div className="qa-input-wrap">
<textarea
id="qa-input"
maxLength={200}
rows={3}
className="qa-textarea"
aria-describedby="qa-status char-count"
aria-invalid={isEmptyInvalid || undefined}
placeholder={placeholder}
value={draft}
onChange={(event) => updateDraft(event.currentTarget.value)}
onKeyDown={onKeyDown}
/>
<div className="qa-input-bar">
<span
id="char-count"
className="qa-bar-text"
style={{ color: charCountColor }}
>
{draft.length}/200
</span>
<button
type="submit"
className={`qa-button ${buttonMessage !== "[SUBMIT]" ? "qa-button-message" : ""}`}
disabled={isSubmitting}
style={{ color: buttonColor || undefined }}
>
{buttonMessage}
</button>
</div>
</div>
<p
id="qa-status"
className="qa-meta"
aria-live="polite"
style={{ color: statusColor }}
>
{status}
</p>
<div className="qa-stats mt-[1ch]" aria-live="polite">
Asked {stats.asked} | Answered {stats.answered} | Ratio{" "}
{formatRatio(stats)}
</div>
</section>
</form>
<div
className="qa-list-scroll mt-[2ch] min-h-0 flex-1 overflow-y-auto pr-[1ch]"
aria-live="polite"
>
<QuestionList questions={questions} />
</div>
</div>
</FrostedBox>
</div>
);
}

5
src/start.ts Normal file
View file

@ -0,0 +1,5 @@
import { createStart } from "@tanstack/react-start";
export const startInstance = createStart(() => ({
defaultSsr: true,
}));

View file

@ -3,6 +3,7 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": true,

View file

@ -1,14 +1,11 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import { viteSingleFile } from "vite-plugin-singlefile";
import react from "@vitejs/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import ansi from "./vite-plugin-ansi";
export default defineConfig({
plugins: [
ansi(),
tailwindcss(),
viteSingleFile({ useRecommendedBuildConfig: false }),
],
plugins: [tanstackStart(), ansi(), tailwindcss(), react()],
resolve: {
alias: {
"~": "/src",