diff --git a/.gitignore b/.gitignore index bd422ab..1e32026 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # misc .DS_Store *.pem +*.har # debug npm-debug.log* diff --git a/README.md b/README.md index 8e4f453..52e5c9c 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,24 @@ Personal site for Jet Pham. -The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a Rust/WebAssembly Conway's Game of Life background. +The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a WebGL2 Conway's Game of Life background. + +It also ships as a Nix flake with a reusable NixOS module for serving the static frontend and the Q+A API on a host. ## Features - ASCII/ANSI-inspired visual style with the IBM VGA font -- Conway's Game of Life running in the background via Rust + WebAssembly +- Conway's Game of Life running in the background via WebGL2 - Q+A page backed by the site API - Single-file oriented frontend build with Vite -- Reduced-motion aware background controls with a static fallback mode +- Fullscreen GPU blur/composite for the frosted panel effect ## Stack - Vite - TypeScript - Tailwind CSS v4 -- Rust + WebAssembly +- WebGL2 - npm ## Development @@ -25,9 +27,6 @@ The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a ### Prerequisites - Node.js + npm -- Rust -- `wasm-pack` -- `wasm-opt` (used by `build:wasm`) ### Install @@ -35,12 +34,6 @@ The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a npm install ``` -### Build the WASM package - -```bash -npm run build:wasm -``` - ### Start the dev server ```bash @@ -62,11 +55,40 @@ npm run build ## Structure ```text +api/ Q+A backend +module.nix NixOS module src/ frontend app -cgol/ Rust + WASM Conway's Game of Life module ``` +## NixOS module + +Import the module from the flake and point it at the host-managed secret files you want to use. + +```nix +{ + inputs.website.url = "github:jetpham/website"; + + outputs = { self, nixpkgs, website, ... }: { + nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + website.nixosModules.default + ({ config, ... }: { + services.jetpham-website = { + enable = true; + domain = "jetpham.com"; + webhookSecretFile = config.age.secrets.webhook-secret.path; + }; + }) + ]; + }; + }; +} +``` + +Optional Tor support is configured by setting `services.jetpham-website.tor.enable = true;` and providing the three onion key file paths. + ## Notes - The homepage and Q+A page are the intended public pages. -- If WebAssembly fails or motion is disabled, the site falls back to a static background treatment. +- The background renderer targets WebGL2 and falls back to the site shell background if WebGL2 is unavailable. diff --git a/api/src/serve.rs b/api/src/serve.rs index 0bb67a9..a797234 100644 --- a/api/src/serve.rs +++ b/api/src/serve.rs @@ -25,6 +25,10 @@ pub async fn run() -> Result<(), Box> { let qa_reply_domain = std::env::var("QA_REPLY_DOMAIN").unwrap_or_else(|_| mail_domain.clone()); let webhook_secret = std::env::var("WEBHOOK_SECRET").expect("WEBHOOK_SECRET must be set"); + let listen_address = + std::env::var("QA_LISTEN_ADDRESS").unwrap_or_else(|_| "127.0.0.1".to_string()); + let listen_port = std::env::var("QA_LISTEN_PORT").unwrap_or_else(|_| "3003".to_string()); + let listen_target = format!("{listen_address}:{listen_port}"); let conn = Connection::open(&db_path)?; conn.execute_batch("PRAGMA journal_mode=WAL;")?; @@ -58,8 +62,8 @@ pub async fn run() -> Result<(), Box> { .layer(CorsLayer::permissive()) .with_state(state); - let listener = tokio::net::TcpListener::bind("127.0.0.1:3003").await?; - println!("Listening on 127.0.0.1:3003"); + let listener = tokio::net::TcpListener::bind(&listen_target).await?; + println!("Listening on {listen_target}"); axum::serve(listener, app).await?; Ok(()) } diff --git a/cgol/.cargo/config.toml b/cgol/.cargo/config.toml deleted file mode 100644 index 39c6f19..0000000 --- a/cgol/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" - -[target.wasm32-unknown-unknown] -rustflags = ["-C", "link-arg=--strip-all"] diff --git a/cgol/.gitignore b/cgol/.gitignore deleted file mode 100644 index 9f33f25..0000000 --- a/cgol/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/target -**/*.rs.bk -bin/ -pkg/ -wasm-pack.log \ No newline at end of file diff --git a/cgol/.vscode/settings.json b/cgol/.vscode/settings.json deleted file mode 100644 index 8974c7a..0000000 --- a/cgol/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", -} diff --git a/cgol/Cargo.lock b/cgol/Cargo.lock deleted file mode 100644 index 4a58688..0000000 --- a/cgol/Cargo.lock +++ /dev/null @@ -1,427 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cgol" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-test", - "web-sys", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" -dependencies = [ - "async-trait", - "cast", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" - -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/cgol/Cargo.toml b/cgol/Cargo.toml deleted file mode 100644 index 71116a6..0000000 --- a/cgol/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "cgol" -version = "0.1.0" -authors = ["jet"] -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wasm-bindgen = "0.2" -js-sys = "0.3" -web-sys = { version = "0.3", features = [ - "CanvasRenderingContext2d", - "Document", - "HtmlCanvasElement", - "Window", - "MouseEvent", - "Element", - "EventTarget", - "Performance", - "TouchEvent", - "Touch", - "TouchList", - "ImageData", -] } -console_error_panic_hook = { version = "0.1", optional = true } - -[features] -default = ["console_error_panic_hook"] - -[dev-dependencies] -wasm-bindgen-test = "0.3" - -[package.metadata.wasm-pack.profile.release] -wasm-opt = false - -[profile.dev] -debug = "line-tables-only" - -[profile.release] -debug = 0 -opt-level = 3 -lto = true -codegen-units = 1 -panic = "abort" -strip = true diff --git a/cgol/README.md b/cgol/README.md deleted file mode 100644 index 6b68408..0000000 --- a/cgol/README.md +++ /dev/null @@ -1,84 +0,0 @@ -
- -

wasm-pack-template

- - A template for kick starting a Rust and WebAssembly project using wasm-pack. - -

- Build Status -

- -

- Tutorial - | - Chat -

- - Built with 🦀🕸 by The Rust and WebAssembly Working Group -
- -## About - -[**📚 Read this template tutorial! 📚**][template-docs] - -This template is designed for compiling Rust libraries into WebAssembly and -publishing the resulting package to NPM. - -Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other -templates and usages of `wasm-pack`. - -[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html -[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html - -## 🚴 Usage - -### 🐑 Use `cargo generate` to Clone this Template - -[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) - -``` -cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project -cd my-project -``` - -### 🛠️ Build with `wasm-pack build` - -``` -wasm-pack build -``` - -### 🔬 Test in Headless Browsers with `wasm-pack test` - -``` -wasm-pack test --headless --firefox -``` - -### 🎁 Publish to NPM with `wasm-pack publish` - -``` -wasm-pack publish -``` - -## 🔋 Batteries Included - -* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating - between WebAssembly and JavaScript. -* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) - for logging panic messages to the developer console. -* `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you - -## License - -Licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. diff --git a/cgol/src/lib.rs b/cgol/src/lib.rs deleted file mode 100644 index 31d55b0..0000000 --- a/cgol/src/lib.rs +++ /dev/null @@ -1,483 +0,0 @@ -mod utils; - -use std::cell::Cell; -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::prelude::*; -use wasm_bindgen::Clamped; -use wasm_bindgen::JsCast; - -use js_sys::Math; -use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window}; - -const CELL_SIZE: u32 = 10; -const TICK_MS: f64 = 1000.0 / 60.0; -const HUE_PERIOD_MS: f64 = 3000.0; -const STILL_STEPS: u32 = 5; - -/// Cells: 0 = dead, 1-255 = alive with that hue value. -const DEAD: u8 = 0; - -#[inline(always)] -fn safe_hue(h: u8) -> u8 { - if h == DEAD { - 1 - } else { - h - } -} - -// ── Lookup tables (computed once) ────────────────────────────────── - -struct Luts { - /// RGBA for each hue. Index 0 = black (dead cell). - rgb: [[u8; 4]; 256], - sin: [f32; 256], - cos: [f32; 256], -} - -impl Luts { - fn new() -> Self { - let mut t = Self { - rgb: [[0, 0, 0, 255]; 256], - sin: [0.0; 256], - cos: [0.0; 256], - }; - for i in 1..256u16 { - let h = i as f32 / 255.0 * 6.0; - let x = 1.0 - (h % 2.0 - 1.0).abs(); - let (r, g, b) = match h as u32 { - 0 => (1.0f32, x, 0.0), - 1 => (x, 1.0, 0.0), - 2 => (0.0, 1.0, x), - 3 => (0.0, x, 1.0), - 4 => (x, 0.0, 1.0), - _ => (1.0, 0.0, x), - }; - t.rgb[i as usize] = [(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255]; - } - for i in 0..256 { - let rad = i as f32 * std::f32::consts::TAU / 256.0; - t.sin[i] = rad.sin(); - t.cos[i] = rad.cos(); - } - t - } - - #[inline] - fn mix(&self, hues: &[u8]) -> u8 { - let mut s = 0.0f32; - let mut c = 0.0f32; - for &h in hues { - s += self.sin[h as usize]; - c += self.cos[h as usize]; - } - safe_hue( - (s.atan2(c).rem_euclid(std::f32::consts::TAU) * 256.0 / std::f32::consts::TAU) as u8, - ) - } -} - -// ── Grid ─────────────────────────────────────────────────────────── - -struct Grid { - w: u32, - h: u32, - cells: Vec, - buf: Vec, - hues: Vec, -} - -impl Grid { - fn new(w: u32, h: u32) -> Self { - let n = (w * h) as usize; - let mut g = Self { - w, - h, - cells: vec![DEAD; n], - buf: vec![DEAD; n], - hues: Vec::with_capacity(8), - }; - g.randomize(); - g - } - - fn randomize(&mut self) { - for c in &mut self.cells { - *c = if Math::random() < 0.5 { - safe_hue((Math::random() * 255.0) as u8) - } else { - DEAD - }; - } - } - - fn tick(&mut self, luts: &Luts) { - let (w, h) = (self.w, self.h); - - for row in 0..h { - let n_off = (if row == 0 { h - 1 } else { row - 1 }) * w; - let r_off = row * w; - let s_off = (if row == h - 1 { 0 } else { row + 1 }) * w; - - for col in 0..w { - let we = if col == 0 { w - 1 } else { col - 1 }; - let ea = if col == w - 1 { 0 } else { col + 1 }; - - // Read all 8 neighbors - let ns = [ - self.cells[(n_off + we) as usize], - self.cells[(n_off + col) as usize], - self.cells[(n_off + ea) as usize], - self.cells[(r_off + we) as usize], - self.cells[(r_off + ea) as usize], - self.cells[(s_off + we) as usize], - self.cells[(s_off + col) as usize], - self.cells[(s_off + ea) as usize], - ]; - - // Count alive neighbors (branchless) - let count = ns.iter().fold(0u8, |a, &v| a + (v != DEAD) as u8); - - let i = (r_off + col) as usize; - let cell = self.cells[i]; - let alive = cell != DEAD; - - self.buf[i] = if alive { - if count == 2 || count == 3 { - cell - } else { - DEAD - } - } else if count == 3 { - // Only collect hues for births - self.hues.clear(); - for &v in &ns { - if v != DEAD { - self.hues.push(v); - } - } - luts.mix(&self.hues) - } else { - DEAD - }; - } - } - std::mem::swap(&mut self.cells, &mut self.buf); - } - - fn stamp(&mut self, cr: i32, cc: i32, half: i32, hue: u8) { - let (h, w) = (self.h as i32, self.w as i32); - let hue = safe_hue(hue); - for dr in -half..=half { - for dc in -half..=half { - let r = (cr + dr).rem_euclid(h) as u32; - let c = (cc + dc).rem_euclid(w) as u32; - self.cells[(r * self.w + c) as usize] = hue; - } - } - } -} - -// ── App state ────────────────────────────────────────────────────── - -struct App { - win: Window, - canvas: HtmlCanvasElement, - ctx: CanvasRenderingContext2d, - luts: Luts, - grid: Grid, - pixels: Vec, - cw: u32, - ch: u32, - crow: i32, - ccol: i32, - cursor_on: bool, - last_tick: f64, - dirty: bool, -} - -thread_local! { - static APP_STATE: RefCell>>> = const { RefCell::new(None) }; - static RUNNING: Cell = const { Cell::new(false) }; - static LISTENERS_READY: Cell = const { Cell::new(false) }; - static RAF_CLOSURE: RefCell>> = const { RefCell::new(None) }; -} - -impl App { - fn new() -> Result { - utils::set_panic_hook(); - let win = web_sys::window().ok_or("no window")?; - let doc = win.document().ok_or("no document")?; - let canvas: HtmlCanvasElement = doc - .get_element_by_id("canvas") - .ok_or("no canvas")? - .dyn_into()?; - let ctx: CanvasRenderingContext2d = - canvas.get_context("2d")?.ok_or("no 2d ctx")?.dyn_into()?; - ctx.set_image_smoothing_enabled(false); - - let mut app = Self { - win, - canvas, - ctx, - luts: Luts::new(), - grid: Grid::new(1, 1), - pixels: Vec::new(), - cw: 0, - ch: 0, - crow: 0, - ccol: 0, - cursor_on: false, - last_tick: 0.0, - dirty: true, - }; - app.resize(); - Ok(app) - } - - fn now(&self) -> f64 { - self.win.performance().unwrap().now() - } - - fn resize(&mut self) { - let vw = self.win.inner_width().unwrap().as_f64().unwrap(); - let vh = self.win.inner_height().unwrap().as_f64().unwrap(); - let cols = (vw as u32 / CELL_SIZE).max(1); - let rows = (vh as u32 / CELL_SIZE).max(1); - - self.cw = cols; - self.ch = rows; - self.canvas.set_width(cols); - self.canvas.set_height(rows); - self.canvas - .dyn_ref::() - .unwrap() - .set_attribute( - "style", - &format!( - "position:fixed;inset:0;width:{vw}px;height:{vh}px;image-rendering:pixelated" - ), - ) - .ok(); - self.ctx.set_image_smoothing_enabled(false); - - self.grid = Grid::new(cols, rows); - self.pixels = vec![0u8; (cols * rows * 4) as usize]; - self.dirty = true; - } - - fn draw(&mut self) { - if !self.dirty { - return; - } - self.dirty = false; - - let cells = &self.grid.cells; - let rgb = &self.luts.rgb; - let px = &mut self.pixels; - - for (i, &cell) in cells.iter().enumerate() { - px[i * 4..i * 4 + 4].copy_from_slice(&rgb[cell as usize]); - } - - if let Ok(img) = ImageData::new_with_u8_clamped_array_and_sh(Clamped(px), self.cw, self.ch) - { - self.ctx.put_image_data(&img, 0.0, 0.0).ok(); - } - } - - fn render_steps(&mut self, steps: u32) { - for _ in 0..steps { - let luts = &self.luts as *const Luts; - // SAFETY: luts is not mutated during tick() - self.grid.tick(unsafe { &*luts }); - } - self.dirty = true; - self.draw(); - } -} - -fn app() -> Result>, JsValue> { - APP_STATE.with(|state| { - if let Some(app) = state.borrow().as_ref() { - return Ok(app.clone()); - } - - let app = Rc::new(RefCell::new(App::new()?)); - state.borrow_mut().replace(app.clone()); - Ok(app) - }) -} - -fn init_listeners(app: Rc>) -> Result<(), JsValue> { - let already_ready = LISTENERS_READY.with(|ready| { - if ready.get() { - true - } else { - ready.set(true); - false - } - }); - - if already_ready { - return Ok(()); - } - - let win = app.borrow().win.clone(); - let doc = win.document().unwrap(); - - let s = app.clone(); - let cb = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| { - let mut a = s.borrow_mut(); - a.ccol = e.client_x() / CELL_SIZE as i32; - a.crow = e.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - }) as Box); - doc.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app.clone(); - let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| { - e.prevent_default(); - if let Some(t) = e.touches().get(0) { - let mut a = s.borrow_mut(); - a.ccol = t.client_x() / CELL_SIZE as i32; - a.crow = t.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - } - }) as Box); - doc.add_event_listener_with_callback("touchmove", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app.clone(); - let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| { - e.prevent_default(); - if let Some(t) = e.touches().get(0) { - let mut a = s.borrow_mut(); - a.ccol = t.client_x() / CELL_SIZE as i32; - a.crow = t.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - } - }) as Box); - doc.add_event_listener_with_callback("touchstart", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app.clone(); - let cb = Closure::wrap(Box::new(move |_: web_sys::TouchEvent| { - s.borrow_mut().cursor_on = false; - }) as Box); - doc.add_event_listener_with_callback("touchend", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app; - let cb = Closure::wrap(Box::new(move || { - let mut a = s.borrow_mut(); - a.resize(); - if RUNNING.with(|running| running.get()) { - a.draw(); - } else { - a.render_steps(STILL_STEPS); - } - }) as Box); - win.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?; - cb.forget(); - - Ok(()) -} - -fn ensure_animation_loop(app: Rc>) { - RAF_CLOSURE.with(|slot| { - if slot.borrow().is_some() { - return; - } - - let win = app.borrow().win.clone(); - let f: Rc>>> = Rc::new(RefCell::new(None)); - let g = f.clone(); - let s = app.clone(); - let w = win.clone(); - - *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { - if !RUNNING.with(|running| running.get()) { - return; - } - - let now = s.borrow().now(); - - { - let mut a = s.borrow_mut(); - - if now - a.last_tick >= TICK_MS { - a.last_tick = now; - let luts = &a.luts as *const Luts; - // SAFETY: luts is not mutated during tick() - a.grid.tick(unsafe { &*luts }); - a.dirty = true; - } - - if a.cursor_on { - let (cr, cc) = (a.crow, a.ccol); - let hue = ((now % HUE_PERIOD_MS) / HUE_PERIOD_MS * 256.0) as u8; - a.grid.stamp(cr, cc, 2, hue); - a.dirty = true; - } - - a.draw(); - } - - if RUNNING.with(|running| running.get()) { - w.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref()) - .ok(); - } - }) as Box)); - - slot.borrow_mut().replace(g.borrow_mut().take().unwrap()); - }); -} - -// ── Entry point ──────────────────────────────────────────────────── - -#[wasm_bindgen] -pub fn start() -> Result<(), JsValue> { - let app = app()?; - init_listeners(app.clone())?; - - if RUNNING.with(|running| running.get()) { - return Ok(()); - } - - RUNNING.with(|running| running.set(true)); - ensure_animation_loop(app.clone()); - - RAF_CLOSURE.with(|slot| { - if let Some(cb) = slot.borrow().as_ref() { - app.borrow() - .win - .request_animation_frame(cb.as_ref().unchecked_ref()) - } else { - Err(JsValue::from_str("no animation closure")) - } - })?; - - Ok(()) -} - -#[wasm_bindgen] -pub fn stop() -> Result<(), JsValue> { - let _ = app()?; - RUNNING.with(|running| running.set(false)); - Ok(()) -} - -#[wasm_bindgen] -pub fn render_still(steps: u32) -> Result<(), JsValue> { - let app = app()?; - init_listeners(app.clone())?; - RUNNING.with(|running| running.set(false)); - let steps = steps.max(1); - let mut app = app.borrow_mut(); - app.resize(); - app.render_steps(steps); - Ok(()) -} diff --git a/cgol/src/utils.rs b/cgol/src/utils.rs deleted file mode 100644 index b1d7929..0000000 --- a/cgol/src/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} diff --git a/check_cleanup.txt b/check_cleanup.txt deleted file mode 100644 index 8ee83ad..0000000 --- a/check_cleanup.txt +++ /dev/null @@ -1,2 +0,0 @@ -$ bun run lint && tsc --noEmit -$ eslint . diff --git a/eslint.config.js b/eslint.config.js index 46809a3..a06087d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,39 +1,42 @@ -import tseslint from 'typescript-eslint'; +import tseslint from "typescript-eslint"; export default tseslint.config( { - ignores: ['dist', 'cgol/pkg/**/*'] + ignores: ["dist"], }, { - files: ['**/*.ts'], - extends: [ - ...tseslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - ...tseslint.configs.stylisticTypeChecked - ], - rules: { - "@typescript-eslint/array-type": "off", - "@typescript-eslint/consistent-type-definitions": "off", - "@typescript-eslint/consistent-type-imports": [ - "warn", - { prefer: "type-imports", fixStyle: "inline-type-imports" }, + files: ["**/*.ts"], + extends: [ + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, ], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { checksVoidReturn: { attributes: false } }, - ], - }, + rules: { + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-misused-promises": [ + "error", + { checksVoidReturn: { attributes: false } }, + ], + }, }, { - linterOptions: { - reportUnusedDisableDirectives: true - }, - languageOptions: { - parserOptions: { - projectService: true - } - } - } -) + linterOptions: { + reportUnusedDisableDirectives: true, + }, + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + }, +); diff --git a/flake.lock b/flake.lock index 8b83aa9..f194334 100644 --- a/flake.lock +++ b/flake.lock @@ -1,53 +1,8 @@ { "nodes": { - "agenix": { - "inputs": { - "darwin": "darwin", - "home-manager": "home-manager", - "nixpkgs": [ - "nixpkgs" - ], - "systems": "systems" - }, - "locked": { - "lastModified": 1770165109, - "narHash": "sha256-9VnK6Oqai65puVJ4WYtCTvlJeXxMzAp/69HhQuTdl/I=", - "owner": "ryantm", - "repo": "agenix", - "rev": "b027ee29d959fda4b60b57566d64c98a202e0feb", - "type": "github" - }, - "original": { - "owner": "ryantm", - "repo": "agenix", - "type": "github" - } - }, - "darwin": { - "inputs": { - "nixpkgs": [ - "agenix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1744478979, - "narHash": "sha256-dyN+teG9G82G+m+PX/aSAagkC+vUv0SgUw3XkPhQodQ=", - "owner": "lnl7", - "repo": "nix-darwin", - "rev": "43975d782b418ebf4969e9ccba82466728c2851b", - "type": "github" - }, - "original": { - "owner": "lnl7", - "ref": "master", - "repo": "nix-darwin", - "type": "github" - } - }, "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1731533236, @@ -63,27 +18,6 @@ "type": "github" } }, - "home-manager": { - "inputs": { - "nixpkgs": [ - "agenix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1745494811, - "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "home-manager", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1774386573, @@ -100,46 +34,10 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { - "agenix": "agenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": "nixpkgs_2" - }, - "locked": { - "lastModified": 1774581174, - "narHash": "sha256-258qgkMkYPkJ9qpIg63Wk8GoIbVjszkGGPU1wbVHYTk=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "a313afc75b85fc77ac154bf0e62c36f68361fd0b", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" + "nixpkgs": "nixpkgs" } }, "systems": { @@ -156,21 +54,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 12dec4d..9ea1199 100644 --- a/flake.nix +++ b/flake.nix @@ -2,93 +2,38 @@ description = "Jet Pham's personal website"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; - agenix.url = "github:ryantm/agenix"; - agenix.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, - rust-overlay, flake-utils, - agenix, }: (flake-utils.lib.eachDefaultSystem ( system: let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - agenixPkg = agenix.packages.${system}.default; - rustToolchain = pkgs.rust-bin.selectLatestNightlyWith ( - toolchain: - toolchain.default.override { - extensions = [ "rust-src" ]; - targets = [ "wasm32-unknown-unknown" ]; - } - ); - rustPlatform = pkgs.makeRustPlatform { - cargo = rustToolchain; - rustc = rustToolchain; - }; - - cgol-wasm = rustPlatform.buildRustPackage { - pname = "cgol-wasm"; - version = "0.1.0"; - src = ./cgol; - cargoLock.lockFile = ./cgol/Cargo.lock; - doCheck = false; - - nativeBuildInputs = [ - pkgs.wasm-bindgen-cli - pkgs.binaryen + pkgs = import nixpkgs { inherit system; }; + lib = pkgs.lib; + websiteSrc = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./index.html + ./package-lock.json + ./package.json + ./public + ./src + ./tsconfig.json + ./vite-plugin-ansi.ts + ./vite.config.ts ]; - - buildPhase = '' - runHook preBuild - cargo build --release --frozen --target wasm32-unknown-unknown - wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/cgol.wasm - wasm-opt pkg/cgol_bg.wasm -o pkg/cgol_bg.wasm -O4 \ - --enable-bulk-memory --enable-nontrapping-float-to-int \ - --enable-sign-ext --low-memory-unused --converge - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - mkdir -p $out - cp pkg/cgol_bg.wasm $out/ - cp pkg/cgol.js $out/ - cp pkg/cgol.d.ts $out/ - cp pkg/cgol_bg.wasm.d.ts $out/ 2>/dev/null || true - cat > $out/package.json <<'EOF' - { - "name": "cgol", - "type": "module", - "version": "0.1.0", - "files": ["cgol_bg.wasm", "cgol.js", "cgol.d.ts"], - "main": "cgol.js", - "types": "cgol.d.ts", - "sideEffects": ["./snippets/*"] - } - EOF - runHook postInstall - ''; }; - # Stage 2: Build the website with npm website = pkgs.buildNpmPackage { pname = "jet-website"; version = "0.1.0"; - src = pkgs.lib.cleanSource ./.; - npmDepsHash = "sha256-O4ZUSYyVWOxP15saIadsaZuRO47Y0AvsL4pwvo5b76U="; - - # Inject the Nix-built WASM before npm install resolves file: dep - postPatch = '' - mkdir -p cgol/pkg - cp -r ${cgol-wasm}/* cgol/pkg/ - ''; + src = websiteSrc; + npmDepsHash = "sha256-UDz4tXNvEa8uiDDGg16K9JbNeQZR3BsVNKtuOgcyurQ="; installPhase = '' runHook preInstall @@ -110,7 +55,6 @@ { packages = { default = website; - cgol-wasm = cgol-wasm; inherit qa-api; }; devShells.default = pkgs.mkShell { @@ -119,12 +63,11 @@ git curl openssl - agenixPkg typescript-language-server + rust-analyzer + rustc + cargo pkg-config - wasm-pack - binaryen - rustToolchain ]; }; } diff --git a/index.html b/index.html index 51ddb40..740d2c0 100644 --- a/index.html +++ b/index.html @@ -103,15 +103,27 @@ -
+
diff --git a/module.nix b/module.nix index 92b9669..1b5dba7 100644 --- a/module.nix +++ b/module.nix @@ -1,27 +1,139 @@ self: -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.jetpham-website; - package = self.packages.x86_64-linux.default; - qaApi = self.packages.x86_64-linux.qa-api; + package = cfg.package; + qaApi = cfg.apiPackage; + apiListen = "${cfg.apiListenAddress}:${toString cfg.apiListenPort}"; + usingDefaultWebhookSecret = cfg.webhookSecretFile == null; webhookSecretPath = - if cfg.webhookSecretFile != null then - cfg.webhookSecretFile + if usingDefaultWebhookSecret then config.age.secrets.webhook-secret.path else cfg.webhookSecretFile; + usingDefaultTorSecretKey = cfg.tor.onionSecretKeyFile == null; + usingDefaultTorPublicKey = cfg.tor.onionPublicKeyFile == null; + usingDefaultTorHostname = cfg.tor.onionHostnameFile == null; + torOnionSecretKeyPath = + if usingDefaultTorSecretKey then + config.age.secrets.tor-onion-secret-key.path else - config.age.secrets.webhook-secret.path; + cfg.tor.onionSecretKeyFile; + torOnionPublicKeyPath = + if usingDefaultTorPublicKey then + config.age.secrets.tor-onion-public-key.path + else + cfg.tor.onionPublicKeyFile; + torOnionHostnamePath = + if usingDefaultTorHostname then + config.age.secrets.tor-onion-hostname.path + else + cfg.tor.onionHostnameFile; + caddyCommonConfig = '' + header Cross-Origin-Opener-Policy "same-origin" + header Cross-Origin-Embedder-Policy "require-corp" + + handle /api/* { + reverse_proxy ${apiListen} + } + + handle /qa/rss.xml { + reverse_proxy ${apiListen} + } + + handle { + root * ${package} + try_files {path} /index.html + file_server + } + + ${cfg.caddy.extraConfig} + ''; in { options.services.jetpham-website = { enable = lib.mkEnableOption "Jet Pham's personal website"; + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.default; + defaultText = lib.literalExpression "self.packages.${pkgs.system}.default"; + description = "Static site package served by Caddy."; + }; + + apiPackage = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.qa-api; + defaultText = lib.literalExpression "self.packages.${pkgs.system}.qa-api"; + description = "Q&A API package run by systemd."; + }; + domain = lib.mkOption { type = lib.types.str; default = "jetpham.com"; description = "Domain to serve the website on."; }; - tor.enable = lib.mkEnableOption "Tor hidden service for the website"; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Open HTTP and HTTPS ports when Caddy is enabled."; + }; + + apiListenAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Address for the local Q&A API listener."; + }; + + apiListenPort = lib.mkOption { + type = lib.types.port; + default = 3003; + description = "Port for the local Q&A API listener."; + }; + + caddy.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Serve the static site and reverse proxy the API through Caddy."; + }; + + caddy.extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Extra Caddy directives appended inside the virtual host block."; + }; + + tor = { + enable = lib.mkEnableOption "Tor hidden service for the website"; + + port = lib.mkOption { + type = lib.types.port; + default = 8888; + description = "Local Caddy port exposed through the onion service."; + }; + + onionSecretKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service secret key file."; + }; + + onionPublicKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service public key file."; + }; + + onionHostnameFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the Tor hidden service hostname file."; + }; + }; qaNotifyEmail = lib.mkOption { type = lib.types.str; @@ -44,35 +156,53 @@ in webhookSecretFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; - description = "File containing the WEBHOOK_SECRET for MTA Hook authentication."; + description = "File containing the WEBHOOK_SECRET for MTA Hook authentication. Defaults to the module-managed agenix secret when left unset."; }; }; config = lib.mkIf cfg.enable { - age.secrets.webhook-secret = lib.mkIf (cfg.webhookSecretFile == null) { + age.secrets.webhook-secret = lib.mkIf usingDefaultWebhookSecret { file = "${self}/secrets/webhook-secret.age"; mode = "0400"; }; - age.secrets.tor-onion-secret-key = lib.mkIf cfg.tor.enable { + age.secrets.tor-onion-secret-key = lib.mkIf (cfg.tor.enable && usingDefaultTorSecretKey) { file = "${self}/secrets/tor-onion-secret-key.age"; owner = "tor"; group = "tor"; mode = "0400"; }; - age.secrets.tor-onion-public-key = lib.mkIf cfg.tor.enable { + + age.secrets.tor-onion-public-key = lib.mkIf (cfg.tor.enable && usingDefaultTorPublicKey) { file = "${self}/secrets/tor-onion-public-key.age"; owner = "tor"; group = "tor"; mode = "0444"; }; - age.secrets.tor-onion-hostname = lib.mkIf cfg.tor.enable { + + age.secrets.tor-onion-hostname = lib.mkIf (cfg.tor.enable && usingDefaultTorHostname) { file = "${self}/secrets/tor-onion-hostname.age"; owner = "tor"; group = "tor"; mode = "0444"; }; + assertions = [ + { + assertion = + !cfg.tor.enable + || (torOnionSecretKeyPath != null && torOnionPublicKeyPath != null && torOnionHostnamePath != null); + message = "services.jetpham-website.tor requires onionSecretKeyFile, onionPublicKeyFile, and onionHostnameFile."; + } + ]; + + networking.firewall.allowedTCPPorts = lib.mkIf (cfg.caddy.enable && cfg.openFirewall) [ + 80 + 443 + ]; + + services.caddy.enable = cfg.caddy.enable; + services.tor = lib.mkIf cfg.tor.enable { enable = true; relay.onionServices.jetpham-website = { @@ -81,7 +211,7 @@ in port = 80; target = { addr = "127.0.0.1"; - port = 8888; + port = cfg.tor.port; }; } ]; @@ -90,36 +220,47 @@ in systemd.services.tor-onion-keys = lib.mkIf cfg.tor.enable { description = "Copy Tor onion keys into place"; - after = [ "agenix.service" ]; + after = lib.optional ( + usingDefaultTorSecretKey || usingDefaultTorPublicKey || usingDefaultTorHostname + ) "agenix.service"; before = [ "tor.service" ]; wantedBy = [ "tor.service" ]; serviceConfig.Type = "oneshot"; script = '' dir="/var/lib/tor/onion/jetpham-website" mkdir -p "$dir" - cp ${config.age.secrets.tor-onion-secret-key.path} "$dir/hs_ed25519_secret_key" - cp ${config.age.secrets.tor-onion-public-key.path} "$dir/hs_ed25519_public_key" - cp ${config.age.secrets.tor-onion-hostname.path} "$dir/hostname" + cp ${torOnionSecretKeyPath} "$dir/hs_ed25519_secret_key" + cp ${torOnionPublicKeyPath} "$dir/hs_ed25519_public_key" + cp ${torOnionHostnamePath} "$dir/hostname" chown -R tor:tor "$dir" chmod 700 "$dir" chmod 400 "$dir/hs_ed25519_secret_key" chmod 444 "$dir/hs_ed25519_public_key" "$dir/hostname" ''; }; - # Q&A API systemd service + systemd.services.jetpham-qa-api = { description = "Jet Pham Q&A API"; - after = [ "network.target" ]; + after = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service"; + wants = [ "network-online.target" ] ++ lib.optional usingDefaultWebhookSecret "agenix.service"; wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = true; StateDirectory = "jetpham-qa"; + WorkingDirectory = "/var/lib/jetpham-qa"; Environment = [ "QA_DB_PATH=/var/lib/jetpham-qa/qa.db" "QA_NOTIFY_EMAIL=${cfg.qaNotifyEmail}" "QA_MAIL_DOMAIN=${cfg.qaMailDomain}" "QA_REPLY_DOMAIN=${cfg.qaReplyDomain}" + "QA_LISTEN_ADDRESS=${cfg.apiListenAddress}" + "QA_LISTEN_PORT=${toString cfg.apiListenPort}" ]; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ReadWritePaths = [ "/var/lib/jetpham-qa" ]; Restart = "on-failure"; RestartSec = 5; LoadCredential = "webhook-secret:${webhookSecretPath}"; @@ -135,47 +276,17 @@ in ''; }; - services.caddy.virtualHosts.${cfg.domain} = { - extraConfig = '' - header Cross-Origin-Opener-Policy "same-origin" - header Cross-Origin-Embedder-Policy "require-corp" - - handle /api/* { - reverse_proxy 127.0.0.1:3003 - } - - handle /qa/rss.xml { - reverse_proxy 127.0.0.1:3003 - } - - handle { - root * ${package} - try_files {path} /index.html - file_server - } - ''; + services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable { + extraConfig = caddyCommonConfig; }; - services.caddy.virtualHosts."http://:8888" = lib.mkIf cfg.tor.enable { - extraConfig = '' - bind 127.0.0.1 - header Cross-Origin-Opener-Policy "same-origin" - header Cross-Origin-Embedder-Policy "require-corp" - - handle /api/* { - reverse_proxy 127.0.0.1:3003 - } - - handle /qa/rss.xml { - reverse_proxy 127.0.0.1:3003 - } - - handle { - root * ${package} - try_files {path} /index.html - file_server - } - ''; - }; + services.caddy.virtualHosts."http://:${toString cfg.tor.port}" = + lib.mkIf (cfg.caddy.enable && cfg.tor.enable) + { + extraConfig = '' + bind 127.0.0.1 + ${caddyCommonConfig} + ''; + }; }; } diff --git a/package-lock.json b/package-lock.json index ebad12d..a37fe33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,6 @@ "": { "name": "website", "version": "0.1.0", - "dependencies": { - "cgol": "file:./cgol/pkg" - }, "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@types/node": "^25.3.3", @@ -22,15 +19,9 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.3.1", - "vite-plugin-singlefile": "^2.3.0", - "vite-plugin-top-level-await": "^1.6.0", - "vite-plugin-wasm": "^3.5.0" + "vite-plugin-singlefile": "^2.3.0" } }, - "cgol/pkg": { - "name": "cgol", - "version": "0.1.0" - }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -693,24 +684,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", - "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -1061,239 +1034,6 @@ "win32" ] }, - "node_modules/@swc/core": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", - "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.18", - "@swc/core-darwin-x64": "1.15.18", - "@swc/core-linux-arm-gnueabihf": "1.15.18", - "@swc/core-linux-arm64-gnu": "1.15.18", - "@swc/core-linux-arm64-musl": "1.15.18", - "@swc/core-linux-x64-gnu": "1.15.18", - "@swc/core-linux-x64-musl": "1.15.18", - "@swc/core-win32-arm64-msvc": "1.15.18", - "@swc/core-win32-ia32-msvc": "1.15.18", - "@swc/core-win32-x64-msvc": "1.15.18" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", - "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", - "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", - "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", - "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", - "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", - "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", - "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", - "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", - "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", - "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@swc/wasm": { - "version": "1.15.18", - "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.18.tgz", - "integrity": "sha512-zeSORFArxqUwfVMTRHu8AN9k9LlfSn0CKDSzLhJDITpgLoS0xpnocxsgMjQjUcVYDgO47r9zLP49HEjH/iGsFg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -1910,10 +1650,6 @@ "node": ">=8" } }, - "node_modules/cgol": { - "resolved": "cgol/pkg", - "link": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3273,20 +3009,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3379,32 +3101,6 @@ "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" } }, - "node_modules/vite-plugin-top-level-await": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz", - "integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-virtual": "^3.0.2", - "@swc/core": "^1.12.14", - "@swc/wasm": "^1.12.14", - "uuid": "10.0.0" - }, - "peerDependencies": { - "vite": ">=2.8" - } - }, - "node_modules/vite-plugin-wasm": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.5.0.tgz", - "integrity": "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b3819be..10992d7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "type": "module", "scripts": { "build": "vite build", - "build:wasm": "cd cgol && wasm-pack build --release --target web && wasm-opt pkg/cgol_bg.wasm -o pkg/cgol_bg.wasm -O4 --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --low-memory-unused --converge", "check": "npm run lint && tsc --noEmit", "dev": "vite", "format:check": "prettier --check \"**/*.{ts,js,jsx,mdx}\" --cache", @@ -15,9 +14,6 @@ "preview": "vite preview", "typecheck": "tsc --noEmit" }, - "dependencies": { - "cgol": "file:./cgol/pkg" - }, "devDependencies": { "@tailwindcss/vite": "^4.2.1", "@types/node": "^25.3.3", @@ -30,13 +26,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.3.1", - "vite-plugin-singlefile": "^2.3.0", - "vite-plugin-top-level-await": "^1.6.0", - "vite-plugin-wasm": "^3.5.0" + "vite-plugin-singlefile": "^2.3.0" }, - "knip": { - "ignore": [ - "cgol/pkg/**" - ] - } + "knip": {} } diff --git a/src/components/frosted-box.ts b/src/components/frosted-box.ts index 3254ebc..36d05c2 100644 --- a/src/components/frosted-box.ts +++ b/src/components/frosted-box.ts @@ -1,13 +1,9 @@ export function frostedBox(content: string, extraClasses?: string): string { return ` -
- - -
+
+ + +
${content}
`; diff --git a/src/lib/background.ts b/src/lib/background.ts deleted file mode 100644 index 5fbba65..0000000 --- a/src/lib/background.ts +++ /dev/null @@ -1,78 +0,0 @@ -const MOTION_QUERY = "(prefers-reduced-motion: reduce)"; -const STILL_STEPS = 5; - -type BackgroundMode = "animated" | "still" | "failed"; - -interface BackgroundActions { - start: () => void; - stop: () => void; - renderStill: (steps: number) => void; -} - -function getMode(reducedMotion: boolean): BackgroundMode { - return reducedMotion ? "still" : "animated"; -} - -function applyCanvasState(mode: BackgroundMode) { - const canvas = document.getElementById("canvas"); - document.body.dataset.backgroundMode = mode; - if (canvas) { - canvas.toggleAttribute("hidden", mode === "failed"); - } -} - -export function initBackgroundControls(actions: BackgroundActions) { - const media = window.matchMedia(MOTION_QUERY); - let mode: BackgroundMode = getMode(media.matches); - let failed = false; - - const applyMode = (restartAnimation = false) => { - if (failed) return; - - mode = getMode(media.matches); - applyCanvasState(mode); - - if (mode === "animated") { - if (restartAnimation) { - actions.stop(); - } - - actions.start(); - return; - } - - actions.stop(); - actions.renderStill(STILL_STEPS); - }; - - const restartAnimation = () => { - if (document.visibilityState === "hidden" || media.matches) { - return; - } - - applyMode(true); - }; - - media.addEventListener("change", () => { - applyMode(); - }); - - document.addEventListener("visibilitychange", () => { - restartAnimation(); - }); - - window.addEventListener("pageshow", () => { - restartAnimation(); - }); - - return { - applyInitialMode() { - applyMode(); - }, - setFailed() { - failed = true; - mode = "failed"; - applyCanvasState(mode); - }, - }; -} diff --git a/src/lib/site.ts b/src/lib/site.ts index a0f578a..713bd0d 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -28,15 +28,17 @@ export function renderFooter() { const mirror = getMirrorLink(); footer.innerHTML = ` -
-