mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
fix(lint): make the linter run again, and finish the em dash sweep
The linter had two faults stacked on top of each other. First, the global `ajv: ^8.17.1` override starved @eslint/eslintrc, which needs ajv 6 and passes it `missingRefs`; ajv 8 rejects that option, so ESLint died before loading a single rule. Scoped overrides give eslint and @eslint/eslintrc ajv 6.12.6 and leave the audit override in place everywhere else. Second, underneath that, `extends: ["@repo/config/eslint-preset.js"]` could never resolve: ESLint's shareable-config naming convention rewrites it to `@repo/eslint-config-config/...`. require.resolve hands ESLint a real path. With the linter running it reported 35 react/no-unescaped-entities errors across 7 files. Those are escaped rather than silenced, using ESLint's own coordinates so only flagged text nodes were touched. Lint now exits 0. Two @next/next/no-img-element warnings remain on the splash logos, which are deliberate: theme-paired and fetchPriority="high". Em dashes are also gone from the six blog posts and the README (105 occurrences), same rules as the site copy: colons for definition labels, commas for appositives, semicolons for independent clauses.
This commit is contained in:
@@ -57,7 +57,7 @@ pnpm test:ui # Run tests with interactive UI
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **WCAG 2.2 AA** — engineered to meet, audited against (Lighthouse measured 100%)
|
||||
- **WCAG 2.2 AA**: engineered to meet, audited against (Lighthouse measured 100%)
|
||||
- Skip-to-main link, semantic HTML5, ARIA labels
|
||||
- Proper heading hierarchy, color contrast (4.5:1+)
|
||||
- `prefers-reduced-motion` support
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// require.resolve, not the bare specifier: ESLint's shareable-config naming convention
|
||||
// rewrites "@repo/config/eslint-preset.js" to "@repo/eslint-config-config/eslint-preset.js"
|
||||
// and then cannot find it. Resolving through Node hands ESLint a real path instead.
|
||||
module.exports = {
|
||||
extends: ["@repo/config/eslint-preset.js"],
|
||||
extends: [require.resolve("@repo/config/eslint-preset.js")],
|
||||
};
|
||||
@@ -18,14 +18,14 @@ It started as a simple prototype: a 3x3 grid, a shuffle, and a timer. But the Ba
|
||||
|
||||
The tile collection features over 8,000 geometric designs from the [Bauhaus Blocks collection by art101](https://art101.io/), with 500 hand-curated core tiles bundled in the app and additional DLC palette packs available for download. Each tile is a 256x256 pixel sprite drawn in the Bauhaus tradition: bold primary shapes, clean geometry, and striking color contrasts. The tiles are packed into a libGDX TextureAtlas spread across PNG sprite sheets for efficient GPU batching.
|
||||
|
||||
We chose art101's Bauhaus Blocks because they capture the movement's aesthetic perfectly — circles, triangles, and rectangles in red, blue, yellow, black, and white — while providing enough variety that the game rarely repeats tiles in a session. DLC packs introduce new palettes and styles — darkly, pop, pastel, and randomized variants — unlocked automatically with the ad-free purchase.
|
||||
We chose art101's Bauhaus Blocks because they capture the movement's aesthetic perfectly: circles, triangles, and rectangles in red, blue, yellow, black, and white. They also provide enough variety that the game rarely repeats tiles in a session. DLC packs introduce new palettes and styles, including darkly, pop, pastel, and randomized variants, unlocked automatically with the ad-free purchase.
|
||||
|
||||
Each tile in-game has beveled 3D edges and the game board is styled like a wood-and-felt craft table, giving the abstract designs a physical, tactile quality.
|
||||
|
||||
## Five Game Modes
|
||||
|
||||
- **Classic**: the core experience. Memorize the full grid, watch the tiles scatter, then reconstruct the pattern before time runs out.
|
||||
- **Speed**: tiles appear one at a time in their correct position for a shrinking window of time, then drop to the tray. Place each before the next one arrives. The display time decays exponentially — starting at 28% of the Classic base time and shrinking with a 0.40 decay factor per tile.
|
||||
- **Speed**: tiles appear one at a time in their correct position for a shrinking window of time, then drop to the tray. Place each before the next one arrives. The display time decays exponentially, starting at 28% of the Classic base time and shrinking with a 0.40 decay factor per tile.
|
||||
- **Endless**: Speed mode that never ends. Placed tiles have a lifespan of three more spawns, after which they fade out and rejoin the queue. Your score is total correct placements before you run out of lives.
|
||||
- **Memory**: traditional card-matching. All tiles face down. Flip two to find pairs. Matched pairs spin away, mismatches cost a life.
|
||||
- **Zen**: no timer, no score, no pressure. A free-form sandbox where you compose with tiles at your own pace. Filter tiles by palette, format, or collection, and browse the full catalog including DLC packs.
|
||||
@@ -39,7 +39,7 @@ Each mode supports 2x2 through 5x5 grids and three difficulty levels (Easy at 1.
|
||||
Bauhaus Echo is built with [libGDX](https://libgdx.com/), a Java-based cross-platform game framework. A single shared codebase in the `core` module runs on all platforms, with thin launcher modules for each target:
|
||||
|
||||
- **Android** via the standard Android SDK (compileSdk 35, minSdk 21)
|
||||
- **iOS and iPad** via [RoboVM/MobiVM](https://mobivm.com/) — an ahead-of-time compiler that translates Java bytecode to native ARM
|
||||
- **iOS and iPad** via [RoboVM/MobiVM](https://mobivm.com/), an ahead-of-time compiler that translates Java bytecode to native ARM
|
||||
- **Desktop** via LWJGL3, packaged with [Construo](https://github.com/nicklaus-dev/construo) and bundled JRE via jlink for a self-contained native app
|
||||
- **Web** via [GWT](http://www.gwtproject.org/) (Google Web Toolkit), which compiles the Java source to JavaScript
|
||||
|
||||
@@ -51,7 +51,7 @@ The project started with a Scene2D UI menu and basic tile grid rendering. From t
|
||||
|
||||
1. **Core gameplay**: grid rendering, tile dragging, snap-to-grid placement, and the show-shuffle-reconstruct loop for Classic mode
|
||||
2. **Configuration and polish**: a config screen for grid size and difficulty, custom fonts, tile selection tooltips, and extended memorization timers
|
||||
3. **New animations**: tile spin, shrink, and slide animations using libGDX's Action system — parallel and sequential action compositions for smooth transitions
|
||||
3. **New animations**: tile spin, shrink, and slide animations using libGDX's Action system: parallel and sequential action compositions for smooth transitions
|
||||
4. **Speed Mode**: the conveyor-belt mechanic where tiles arrive one at a time with decaying display windows
|
||||
5. **Endless Mode**: building on Speed with tile cycling, lifespan tracking, and an in-game settings overlay for pausing mid-run
|
||||
6. **Memory/Pairs Mode**: face-down card-matching with flip animations and pair detection
|
||||
@@ -67,14 +67,14 @@ One of the more satisfying fixes was smart tile snapping. Early on, dragging a t
|
||||
Each tile is a `Tile` actor extending libGDX's `Image` class with custom drag handling. When you pick up a tile, it:
|
||||
|
||||
1. Plays a pickup sound effect
|
||||
2. Renders a procedural drop shadow — a 64x64 soft-edge texture drawn at 35% alpha with an (8, -12) pixel offset, shared across all tile instances
|
||||
2. Renders a procedural drop shadow, a 64x64 soft-edge texture drawn at 35% alpha with an (8, -12) pixel offset, shared across all tile instances
|
||||
3. Tracks your finger position and updates the tile's screen coordinates
|
||||
|
||||
When you release it above the split line (the boundary between the grid and the tray), the game checks `Level.getClosestOpenTargetIndex()` to find the nearest unoccupied grid cell. If one exists, the tile snaps into place with a 0.15-second MoveToAction and plays a drop sound. If all positions are occupied, the tile rejects — spinning 360 degrees, shrinking back to tray size, and sliding to the bottom in a 0.13-second parallel animation.
|
||||
When you release it above the split line (the boundary between the grid and the tray), the game checks `Level.getClosestOpenTargetIndex()` to find the nearest unoccupied grid cell. If one exists, the tile snaps into place with a 0.15-second MoveToAction and plays a drop sound. If all positions are occupied, the tile rejects: spinning 360 degrees, shrinking back to tray size, and sliding to the bottom in a 0.13-second parallel animation.
|
||||
|
||||
### The GWT Challenge
|
||||
|
||||
Getting the web build working was one of the trickier parts of the project. GWT compiles Java to JavaScript, but it only supports a subset of Java — no `javax.crypto`, no reflection, limited standard library. This became a problem when we added HMAC-SHA256 score signing for the leaderboard.
|
||||
Getting the web build working was one of the trickier parts of the project. GWT compiles Java to JavaScript, but it only supports a subset of Java: no `javax.crypto`, no reflection, limited standard library. This became a problem when we added HMAC-SHA256 score signing for the leaderboard.
|
||||
|
||||
The solution was a pure JavaScript SHA-256 implementation embedded via GWT's JSNI (JavaScript Native Interface). The GWT leaderboard client uses `RequestBuilder` instead of libGDX's `Gdx.net` API for proper CORS handling, and the HMAC secret is embedded at compile time so it doesn't appear in the distributed JavaScript source in plain text.
|
||||
|
||||
@@ -91,7 +91,7 @@ The leaderboard runs on a Flask API behind Nginx on a VPS, backed by PostgreSQL.
|
||||
- **Nginx** terminates SSL (Let's Encrypt) and enforces rate limits: 10 requests/second globally, 6 requests/minute on score submission endpoints
|
||||
- **flask-limiter** adds a secondary rate limit of 100 requests/hour per IP
|
||||
|
||||
No ORM — just parameterized SQL queries via `psycopg2`. The schema is straightforward: each score row stores the device UUID, nickname, mode, grid, difficulty, primary score value, an optional secondary tiebreaker, platform, and timestamp.
|
||||
No ORM, just parameterized SQL queries via `psycopg2`. The schema is straightforward: each score row stores the device UUID, nickname, mode, grid, difficulty, primary score value, an optional secondary tiebreaker, platform, and timestamp.
|
||||
|
||||
### Scoring Logic
|
||||
|
||||
@@ -99,7 +99,7 @@ Different modes rank differently:
|
||||
|
||||
| Mode | Primary Score | Tiebreaker | Best = |
|
||||
|------|--------------|------------|--------|
|
||||
| Classic | Completion time | — | Lowest |
|
||||
| Classic | Completion time | n/a | Lowest |
|
||||
| Speed | Completion time | Lives remaining | Lowest time, then highest lives |
|
||||
| Endless | Tiles placed | Survival time | Highest tiles, then longest time |
|
||||
| Memory | Lives remaining | Completion time | Highest lives, then fastest time |
|
||||
@@ -112,7 +112,7 @@ Score submissions are signed with HMAC-SHA256. The client computes `HMAC(secret,
|
||||
|
||||
Beyond signature verification, the server validates score bounds per mode:
|
||||
|
||||
- Classic mode times must fall within plausible ranges for the grid size — you can't clear a 5x5 in 0.5 seconds
|
||||
- Classic mode times must fall within plausible ranges for the grid size: you can't clear a 5x5 in 0.5 seconds
|
||||
- Speed mode lives are capped per grid and difficulty (1–6 depending on configuration)
|
||||
- Endless mode tile counts have per-grid maximums (e.g., 500 for 2x2, 200 for 5x5)
|
||||
- Memory mode lives can't exceed the configuration maximum (3–16 depending on grid and difficulty)
|
||||
@@ -127,8 +127,8 @@ Device UUIDs are never returned in API responses. When fetching a leaderboard, t
|
||||
|
||||
The game uses a `LeaderboardClient` interface with two implementations:
|
||||
|
||||
- **HttpLeaderboardClient** for Android, iOS, and Desktop — uses libGDX's `Gdx.net.sendHttpRequest()` with Java's `javax.crypto.Mac` for HMAC computation
|
||||
- **GwtLeaderboardClient** for the web build — uses GWT's `RequestBuilder` with the pure-JS SHA-256 implementation
|
||||
- **HttpLeaderboardClient** for Android, iOS, and Desktop: uses libGDX's `Gdx.net.sendHttpRequest()` with Java's `javax.crypto.Mac` for HMAC computation
|
||||
- **GwtLeaderboardClient** for the web build: uses GWT's `RequestBuilder` with the pure-JS SHA-256 implementation
|
||||
|
||||
All network calls are asynchronous with callbacks posted back to the rendering thread. Score submission is fire-and-forget: if the network call fails, the game continues normally. Local high scores always work regardless of connectivity.
|
||||
|
||||
@@ -138,6 +138,6 @@ The mobile versions (Android and iOS) are free with AdMob banner ads. A one-time
|
||||
|
||||
## What's Next
|
||||
|
||||
Since launch we've shipped Zen mode — a pressure-free creative sandbox — along with four DLC tile packs (darkly, pop, pastel, and rands) bringing the total to over 8,000 tiles. We also added a hint system for Classic mode, a milestone progression system, and expanded the soundtrack to 13 tracks with mode-specific playlists. We're continuing to explore new tile collections from [art101](https://art101.io/) and gameplay ideas.
|
||||
Since launch we've shipped Zen mode, a pressure-free creative sandbox, along with four DLC tile packs (darkly, pop, pastel, and rands) bringing the total to over 8,000 tiles. We also added a hint system for Classic mode, a milestone progression system, and expanded the soundtrack to 13 tracks with mode-specific playlists. We're continuing to explore new tile collections from [art101](https://art101.io/) and gameplay ideas.
|
||||
|
||||
See the full [Bauhaus Echo product page](/products/bauhaus-echo) for more details.
|
||||
|
||||
@@ -20,22 +20,22 @@ Hosted at [xmrcheckout.such.software](https://xmrcheckout.such.software).
|
||||
|
||||
[wowcheckout](https://github.com/Such-Software/wowcheckout) is a fork of xmrcheckout for [Wownero](https://wownero.org). File and folder structure is identical to upstream so merging fixes stays easy. The content-only differences:
|
||||
|
||||
- **Network bytes** — Wownero address prefixes (53, 63, 54), patched at runtime
|
||||
- **Pricing** — WOW-BTC from [Nonlogs](https://api.nonlogs.io/api/markets) multiplied by BTC/USD from Kraken (no direct WOW/USD pair exists)
|
||||
- **Daemon** — Wownero node, [binaries from Codeberg](https://codeberg.org/wownero/wownero/releases)
|
||||
- **Network bytes**: Wownero address prefixes (53, 63, 54), patched at runtime
|
||||
- **Pricing**: WOW-BTC from [Nonlogs](https://api.nonlogs.io/api/markets) multiplied by BTC/USD from Kraken (no direct WOW/USD pair exists)
|
||||
- **Daemon**: Wownero node, [binaries from Codeberg](https://codeberg.org/wownero/wownero/releases)
|
||||
|
||||
Hosted at [wowcheckout.such.software](https://wowcheckout.such.software).
|
||||
|
||||
## Grin via BTCPayServer
|
||||
|
||||
Grin's transaction protocol is interactive — sender and receiver exchange slates to build a transaction. The checkout system must hold a wallet with spend authority, making it semi-custodial by nature.
|
||||
Grin's transaction protocol is interactive: sender and receiver exchange slates to build a transaction. The checkout system must hold a wallet with spend authority, making it semi-custodial by nature.
|
||||
|
||||
We contributed a [Grin plugin for BTCPayServer](https://github.com/ArcticMining/btcpayserver-plugin-grin) that talks to grin-wallet's Owner API to issue invoices, finalize transactions, and track confirmations. It slots in alongside Bitcoin and Lightning with no extra Medusa integration work.
|
||||
|
||||
## Trust Model
|
||||
|
||||
- **XMR and WOW** — Fully non-custodial. View-only access. Funds go directly to the merchant wallet.
|
||||
- **Grin** — Semi-custodial. The server holds a Grin wallet with spend authority because the protocol requires it. Merchants trust the operator to forward funds. This is inherent to Grin, not a design choice.
|
||||
- **XMR and WOW**: Fully non-custodial. View-only access. Funds go directly to the merchant wallet.
|
||||
- **Grin**: Semi-custodial. The server holds a Grin wallet with spend authority because the protocol requires it. Merchants trust the operator to forward funds. This is inherent to Grin, not a design choice.
|
||||
|
||||
## Medusa Integration
|
||||
|
||||
@@ -49,4 +49,4 @@ MedusaJS already has a BTCPay payment plugin that handles invoices and webhook c
|
||||
|
||||
## Work With Us
|
||||
|
||||
If you want a managed webshop that accepts cryptocurrency — BTC, Lightning, XMR, WOW, GRIN, or a combination — we can set that up for you. [Get in touch](/#contact).
|
||||
If you want a managed webshop that accepts cryptocurrency, whether BTC, Lightning, XMR, WOW, GRIN, or a combination, we can set that up for you. [Get in touch](/#contact).
|
||||
|
||||
@@ -11,16 +11,16 @@ This post covers the technical journey: getting three_js and cannon_physics work
|
||||
|
||||
## The Stack
|
||||
|
||||
- **three_js** (v0.2.7) — Dart port of three.js for 3D rendering
|
||||
- **cannon_physics** (v0.0.3) — Dart port of cannon.js for rigid body physics
|
||||
- **flutter_angle** (v0.3.9) — OpenGL ES bridge between three_js and Flutter's texture system
|
||||
- **Blender** — 3D models (GLB) with baked number text geometry
|
||||
- **three_js** (v0.2.7): Dart port of three.js for 3D rendering
|
||||
- **cannon_physics** (v0.0.3): Dart port of cannon.js for rigid body physics
|
||||
- **flutter_angle** (v0.3.9): OpenGL ES bridge between three_js and Flutter's texture system
|
||||
- **Blender**: 3D models (GLB) with baked number text geometry
|
||||
|
||||
All three packages are early-stage. The three_js ecosystem is the only viable option for programmatic 3D in Flutter right now: alternatives like `flutter_cube` and `model_viewer` only load static files.
|
||||
|
||||
## Bug #1: Nothing Moves
|
||||
|
||||
The first build rendered the scene correctly — ground plane, lighting, a die sitting in the middle. But when physics stepped, the die didn't move. Not a pixel.
|
||||
The first build rendered the scene correctly: ground plane, lighting, a die sitting in the middle. But when physics stepped, the die didn't move. Not a pixel.
|
||||
|
||||
The physics engine was running. We confirmed with debug prints: the body position changed from y=25 to y=2 over 30 frames, bounced, settled. The mesh position was being set from the body position every frame, but the visual was frozen on the first frame.
|
||||
|
||||
@@ -55,13 +55,13 @@ Mesh copy(Object3D source, [bool? recursive]) {
|
||||
|
||||
## Physics Shapes from Blender
|
||||
|
||||
The Blender dice models aren't regular polyhedra — they're "Die Scaled D10" variants with beveled edges. Each GLB includes three nodes:
|
||||
The Blender dice models aren't regular polyhedra; they're "Die Scaled D10" variants with beveled edges. Each GLB includes three nodes:
|
||||
|
||||
1. **Body mesh** — beveled, smooth-shaded, for display
|
||||
2. **Numbers mesh** — 3D text geometry, child of the body
|
||||
3. **Primitive mesh** — clean polyhedron with exact vertex/face counts
|
||||
1. **Body mesh**: beveled, smooth-shaded, for display
|
||||
2. **Numbers mesh**: 3D text geometry, child of the body
|
||||
3. **Primitive mesh**: clean polyhedron with exact vertex/face counts
|
||||
|
||||
The primitive mesh is the collision shape. We extract its vertices and triangle indices and build `cannon.ConvexPolyhedron` shapes from them, scaled to match the visual model. This guarantees the physics shape matches the visual exactly — no sinking, no clipping.
|
||||
The primitive mesh is the collision shape. We extract its vertices and triangle indices and build `cannon.ConvexPolyhedron` shapes from them, scaled to match the visual model. This guarantees the physics shape matches the visual exactly: no sinking, no clipping.
|
||||
|
||||
```dart
|
||||
cannon.ConvexPolyhedron _d8Shape(double scale) {
|
||||
@@ -81,7 +81,7 @@ cannon.ConvexPolyhedron _d8Shape(double scale) {
|
||||
|
||||
With rendering and physics working, we needed to read which die face pointed up after settling. The approach: define a normal vector for each face, rotate it by the physics body's quaternion, find which rotated normal has the highest Y component.
|
||||
|
||||
Faces 1 and 6 (top and bottom, on the Y axis) always worked. But faces 2–5 (on the X/Z axes) appeared to give random results — the same visible number would report different values across rolls. We spent hours trying different face-to-number mappings, ASCII-rendering text vertices to identify numbers, and building calibration toggles. Nothing was consistent.
|
||||
Faces 1 and 6 (top and bottom, on the Y axis) always worked. But faces 2–5 (on the X/Z axes) appeared to give random results: the same visible number would report different values across rolls. We spent hours trying different face-to-number mappings, ASCII-rendering text vertices to identify numbers, and building calibration toggles. Nothing was consistent.
|
||||
|
||||
It turned out to be two separate bugs compounding into apparent randomness:
|
||||
|
||||
@@ -90,10 +90,10 @@ It turned out to be two separate bugs compounding into apparent randomness:
|
||||
**Bug 3b: Quaternion rotation convention.** `vector_math`'s `Quaternion.rotated()` implements the *inverse* rotation: `q_conjugate * v * q` instead of the standard `q * v * q_conjugate`. This meant rotating a face normal by the body quaternion gave the opposite direction on the X/Z axes. The Y axis appeared unaffected because the die falls along Y, where the inverse coincidentally gives similar results.
|
||||
|
||||
```dart
|
||||
// vector_math Quaternion.rotate() — INVERSE convention:
|
||||
// vector_math Quaternion.rotate() uses the INVERSE convention:
|
||||
// conjugate(this) * [v,0] * this → q_conj * v * q
|
||||
|
||||
// cannon_physics Quaternion.vmult() — CORRECT convention:
|
||||
// cannon_physics Quaternion.vmult() uses the CORRECT convention:
|
||||
// q * v * q_conj
|
||||
final worldPos = bodyQuaternion.vmult(faceNormal);
|
||||
```
|
||||
@@ -102,7 +102,7 @@ After fixing the offset bias, results became consistent (same face always mapped
|
||||
|
||||
## Reading the Top Face: Vertex Count Fingerprints
|
||||
|
||||
Even with correct rotation, we needed to know which number the Blender model painted on which face. The 3D text is a single merged mesh — no per-face metadata.
|
||||
Even with correct rotation, we needed to know which number the Blender model painted on which face. The 3D text is a single merged mesh: no per-face metadata.
|
||||
|
||||
The solution: cluster text vertices by direction and use vertex counts as fingerprints.
|
||||
|
||||
@@ -110,7 +110,7 @@ The solution: cluster text vertices by direction and use vertex counts as finger
|
||||
1. Transform all text mesh vertices to body-local space
|
||||
2. For each vertex, compute the normalized direction from the die center
|
||||
3. Assign to the face whose direction it most closely matches (dot product)
|
||||
4. Count vertices per face — each die number has a unique count determined by font complexity
|
||||
4. Count vertices per face: each die number has a unique count determined by font complexity
|
||||
|
||||
**At settle time:**
|
||||
1. Rotate each face direction by the body quaternion (using `vmult`)
|
||||
@@ -121,16 +121,16 @@ The calibration was done empirically: roll the die, note the visual top face and
|
||||
|
||||
```dart
|
||||
_vertCountToNumber = {
|
||||
32: 1, // "1" — simplest glyph, fewest vertices
|
||||
32: 1, // "1": simplest glyph, fewest vertices
|
||||
1316: 2,
|
||||
2336: 3,
|
||||
112: 4, // "4" — straight lines, few vertices
|
||||
112: 4, // "4": straight lines, few vertices
|
||||
1643: 5,
|
||||
2386: 6, // "6" — most complex glyph
|
||||
2386: 6, // "6": most complex glyph
|
||||
};
|
||||
```
|
||||
|
||||
For d8 and d20, the same approach works after centering the text vertices (subtracting the text mesh centroid) to remove the translation offset that biases the direction computation. The d6 calibration was done without centering — mixing the two caused a regression that took another round of debugging to find.
|
||||
For d8 and d20, the same approach works after centering the text vertices (subtracting the text mesh centroid) to remove the translation offset that biases the direction computation. The d6 calibration was done without centering; mixing the two caused a regression that took another round of debugging to find.
|
||||
|
||||
For d20, we calibrated 15 of 20 faces from user testing and derived the remaining 5 using the opposite-faces-sum-to-21 rule, verified across all 10 pairs.
|
||||
|
||||
@@ -138,8 +138,8 @@ For d20, we calibrated 15 of 20 faces from user testing and derived the remainin
|
||||
|
||||
Detecting when a die has "stopped rolling" is harder than it sounds. We went through several iterations:
|
||||
|
||||
- **Too eager:** `velocity < 0.3` for 30 frames — resolves while die is still rocking
|
||||
- **Too strict:** `velocity < 0.1` for 60 frames — never triggers for polyhedra with micro-vibrations
|
||||
- **Too eager:** `velocity < 0.3` for 30 frames; resolves while die is still rocking
|
||||
- **Too strict:** `velocity < 0.1` for 60 frames; never triggers for polyhedra with micro-vibrations
|
||||
- **Timing mismatch:** Safety timeout fires from a previous roll's `Future.delayed`, forcing an early result
|
||||
- **Final approach:** `velocity < 0.5 AND angularVelocity < 0.5 AND flatness > 0.95` for 40 frames (~0.67s), with a generation counter that invalidates stale timeouts
|
||||
|
||||
@@ -161,7 +161,7 @@ world.addBody(cannon.Body(
|
||||
|
||||
## Performance
|
||||
|
||||
The three_js + cannon_physics stack runs at 60fps on recent iOS devices (simulator and device). On a mid-range Android phone (Moto G Stylus 2025), debug builds are noticeably sluggish but release builds are smooth. The physics simulation is lightweight — one or two rigid bodies with simple collision shapes.
|
||||
The three_js + cannon_physics stack runs at 60fps on recent iOS devices (simulator and device). On a mid-range Android phone (Moto G Stylus 2025), debug builds are noticeably sluggish but release builds are smooth. The physics simulation is lightweight: one or two rigid bodies with simple collision shapes.
|
||||
|
||||
We considered driving the dice with real-time accelerometer input (shake the phone, die reacts in real-time) but deferred it to a future release. The current approach uses shake detection as a trigger: a sharp shake re-rolls the die with random velocity and spin.
|
||||
|
||||
@@ -187,6 +187,6 @@ We considered driving the dice with real-time accelerometer input (shake the pho
|
||||
|
||||
4. **Empirical calibration beats analytical derivation.** We spent hours trying to determine face-to-number mappings from vertex analysis and ASCII rendering. In the end, rolling the die 15 times and writing down what we saw was faster and more reliable.
|
||||
|
||||
5. **Settle detection is a state machine problem.** Simple velocity thresholds aren't enough — you need flatness checks, generation counters for timeout invalidation, and tuned frame counts. The "feels right" threshold took five iterations.
|
||||
5. **Settle detection is a state machine problem.** Simple velocity thresholds aren't enough: you need flatness checks, generation counters for timeout invalidation, and tuned frame counts. The "feels right" threshold took five iterations.
|
||||
|
||||
Suchoice is available on [Google Play](https://play.google.com/store/apps/details?id=com.suchsoftware.suchoice) and the [App Store](https://apps.apple.com/us/app/suchoice/id6759626658).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Neroswap: a Privacy-First DEX & CEX Aggregator"
|
||||
date: "2026-03-22"
|
||||
excerpt: "How we built a real-time orderbook aggregator across 6 decentralized and centralized exchanges — scraping atomic swap networks, gRPC observer nodes, and public APIs every 10 minutes. No KYC, no custody, no tracking."
|
||||
excerpt: "How we built a real-time orderbook aggregator across 6 decentralized and centralized exchanges: scraping atomic swap networks, gRPC observer nodes, and public APIs every 10 minutes. No KYC, no custody, no tracking."
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
If you want to buy Monero on a decentralized exchange today, you need to check at least three different platforms — each with its own node, its own UI, and its own pair format. Want to compare XMR/BTC on an atomic swap DEX versus a P2P exchange versus a no-KYC centralized platform? That's six browser tabs and a spreadsheet.
|
||||
If you want to buy Monero on a decentralized exchange today, you need to check at least three different platforms: each with its own node, its own UI, and its own pair format. Want to compare XMR/BTC on an atomic swap DEX versus a P2P exchange versus a no-KYC centralized platform? That's six browser tabs and a spreadsheet.
|
||||
|
||||
Wownero is even worse. Liquidity is split across a handful of small exchanges with no way to compare prices without visiting each one individually.
|
||||
|
||||
@@ -18,17 +18,17 @@ Neroswap tracks three decentralized exchanges and three centralized (no-KYC) exc
|
||||
|
||||
### BasicSwap DEX
|
||||
|
||||
[BasicSwap](https://basicswapdex.com/) is a decentralized exchange built on the Particl network's SMSG (SecureMessaging) protocol. Offers are broadcast across a peer-to-peer network and settled via cross-chain atomic swaps — no central server, no custody, no counterparty risk. It supports 14 coins including BTC, XMR, LTC, BCH, WOW, FIRO, DASH, PIVX, and Decred.
|
||||
[BasicSwap](https://basicswapdex.com/) is a decentralized exchange built on the Particl network's SMSG (SecureMessaging) protocol. Offers are broadcast across a peer-to-peer network and settled via cross-chain atomic swaps: no central server, no custody, no counterparty risk. It supports 14 coins including BTC, XMR, LTC, BCH, WOW, FIRO, DASH, PIVX, and Decred.
|
||||
|
||||
**How we scrape it:** We run a local BasicSwap full node with a custom "God Mode" patch. Normally, BasicSwap only shows offers for coins you have wallets configured for. Our patch injects a synthetic interface that bypasses the native coin client check, letting us query offers for all 14 supported coins without actually syncing all those blockchains. The scraper hits the node's local REST API at `127.0.0.1:12700/json/offers` and maps internal coin IDs to human-readable names.
|
||||
|
||||
BasicSwap currently has the widest pair coverage of any DEX we track — around 30 active trading pairs and 100+ offers at any given time.
|
||||
BasicSwap currently has the widest pair coverage of any DEX we track: around 30 active trading pairs and 100+ offers at any given time.
|
||||
|
||||
### Retoswap (Haveno)
|
||||
|
||||
[Retoswap](https://retoswap.com/) is a Haveno fork — a P2P exchange where all trades settle over Tor. It supports XMR against fiat currencies (USD, EUR, GBP, CHF, AUD, CAD) and crypto (BTC, ETH, LTC, BCH, and others). Offers include detailed payment method info, fee structures, and maker deposit requirements.
|
||||
[Retoswap](https://retoswap.com/) is a Haveno fork, a P2P exchange where all trades settle over Tor. It supports XMR against fiat currencies (USD, EUR, GBP, CHF, AUD, CAD) and crypto (BTC, ETH, LTC, BCH, and others). Offers include detailed payment method info, fee structures, and maker deposit requirements.
|
||||
|
||||
**How we scrape it:** We run a Haveno observer node locally and connect via gRPC on port 9998 with password authentication. The scraper auto-creates an observer account if needed, then pulls the full offer list including price, amount ranges, payment methods, and trade state. Haveno's gRPC interface gives us much richer data than a typical REST API — we get maker addresses, security deposits, and fee breakdowns for every offer.
|
||||
**How we scrape it:** We run a Haveno observer node locally and connect via gRPC on port 9998 with password authentication. The scraper auto-creates an observer account if needed, then pulls the full offer list including price, amount ranges, payment methods, and trade state. Haveno's gRPC interface gives us much richer data than a typical REST API: we get maker addresses, security deposits, and fee breakdowns for every offer.
|
||||
|
||||
Retoswap typically has 40-300+ active offers and is the primary source for XMR/fiat pricing on Neroswap.
|
||||
|
||||
@@ -36,7 +36,7 @@ Retoswap typically has 40-300+ active offers and is the primary source for XMR/f
|
||||
|
||||
[Eigenwallet](https://eigenwallet.org/) provides trustless BTC-to-XMR atomic swaps routed through Tor. Market makers run persistent onion services and publish quotes (price, min/max swap amounts, refund policy) to a set of rendezvous nodes.
|
||||
|
||||
**How we scrape it:** This was the most challenging integration. Eigenwallet has no public API — their dev team confirmed their internal liquidity scripts are private. So we built a custom Rust binary that connects directly to the Eigenwallet libp2p network.
|
||||
**How we scrape it:** This was the most challenging integration. Eigenwallet has no public API; their dev team confirmed their internal liquidity scripts are private. So we built a custom Rust binary that connects directly to the Eigenwallet libp2p network.
|
||||
|
||||
Our scraper spins up a libp2p node, bootstraps through Tor via an embedded Arti client, discovers market makers through 7 hardcoded rendezvous points, and negotiates the `/comit/xmr/btc/bid-quote/2.0.0` protocol to fetch `BidQuote` structs containing price (in satoshis), min/max quantities, and refund policy terms.
|
||||
|
||||
@@ -46,11 +46,11 @@ Eigenwallet typically has 3-6 active market makers with total XMR liquidity rang
|
||||
|
||||
### Nonlogs
|
||||
|
||||
[Nonlogs](https://nonlogs.io/) is a no-KYC centralized exchange focused on Wownero. It offers WOW/BTC, WOW/USDT, and several other privacy coin pairs (GRIN, BEAM, RYO, MWC). **Scraped via public REST API** — straightforward ticker data with 24h volume, bid/ask spreads, and price change percentages.
|
||||
[Nonlogs](https://nonlogs.io/) is a no-KYC centralized exchange focused on Wownero. It offers WOW/BTC, WOW/USDT, and several other privacy coin pairs (GRIN, BEAM, RYO, MWC). **Scraped via public REST API**: straightforward ticker data with 24h volume, bid/ask spreads, and price change percentages.
|
||||
|
||||
### CexSwap
|
||||
|
||||
[CexSwap](https://cexswap.cc/) is a Wownero-focused CEX with both spot markets and AMM liquidity pools. It supports 40+ assets across 100 spot pairs and 90+ AMM pools. **Scraped via two public API endpoints** — one for spot tickers (OHLCV data) and one for AMM pools (TVL, liquidity depth). CexSwap is the largest source of WOW trading pairs on Neroswap.
|
||||
[CexSwap](https://cexswap.cc/) is a Wownero-focused CEX with both spot markets and AMM liquidity pools. It supports 40+ assets across 100 spot pairs and 90+ AMM pools. **Scraped via two public API endpoints**: one for spot tickers (OHLCV data) and one for AMM pools (TVL, liquidity depth). CexSwap is the largest source of WOW trading pairs on Neroswap.
|
||||
|
||||
### AltQuick
|
||||
|
||||
@@ -62,7 +62,7 @@ The pipeline runs on a dedicated scraper server and pushes data to the web serve
|
||||
|
||||
### Scraping
|
||||
|
||||
An orchestrator script runs seven scrapers in a staggered sequence — Kraken reference prices first, then each exchange with 90-second gaps to avoid overwhelming any single source. The full cycle takes about 6 minutes.
|
||||
An orchestrator script runs seven scrapers in a staggered sequence: Kraken reference prices first, then each exchange with 90-second gaps to avoid overwhelming any single source. The full cycle takes about 6 minutes.
|
||||
|
||||
```
|
||||
T+0s Kraken (USD ref prices: BTC, XMR, LTC, BCH)
|
||||
@@ -80,22 +80,22 @@ Eigenwallet runs independently as a persistent daemon, writing its own JSON file
|
||||
|
||||
Every time the frontend revalidates (every 30 seconds), it records an aggregated snapshot to a SQLite database on the web server:
|
||||
|
||||
- **Price snapshots** — best/worst/last price per source per trading pair, with USD-converted volume
|
||||
- **Liquidity snapshots** — total available USD liquidity per DEX per coin (XMR and WOW), with outlier filtering to exclude garbage offers
|
||||
- **Price snapshots**: best/worst/last price per source per trading pair, with USD-converted volume
|
||||
- **Liquidity snapshots**: total available USD liquidity per DEX per coin (XMR and WOW), with outlier filtering to exclude garbage offers
|
||||
|
||||
This powers the sparkline charts on the dashboard and the DEX Liquidity overview. At roughly 80 price snapshots and 5 liquidity snapshots per cycle, the database grows at about 30-40 MB per month.
|
||||
|
||||
### Freshness Detection
|
||||
|
||||
Each JSON file carries a `fetched_at` timestamp. The frontend checks data age against a 30-minute threshold — if a source hasn't been updated in over 30 minutes, it's marked as "Stale" (orange badge) instead of "Live" (green). Stale data is excluded from the Best Prices summary, the DEX Liquidity calculations, and the history database to prevent polluting charts with old numbers.
|
||||
Each JSON file carries a `fetched_at` timestamp. The frontend checks data age against a 30-minute threshold: if a source hasn't been updated in over 30 minutes, it's marked as "Stale" (orange badge) instead of "Live" (green). Stale data is excluded from the Best Prices summary, the DEX Liquidity calculations, and the history database to prevent polluting charts with old numbers.
|
||||
|
||||
### Serving
|
||||
|
||||
The JSON files are synced via rsync over SSH to the production web server. The frontend is a Next.js app using ISR (Incremental Static Regeneration) with 30-second revalidation — so data is always at most 30 seconds behind the latest scrape.
|
||||
The JSON files are synced via rsync over SSH to the production web server. The frontend is a Next.js app using ISR (Incremental Static Regeneration) with 30-second revalidation, so data is always at most 30 seconds behind the latest scrape.
|
||||
|
||||
## Privacy by Design
|
||||
|
||||
Neroswap has no user accounts, no cookies, no tracking pixels, and no analytics. The site is a read-only dashboard — it never touches your funds or your identity. There is nothing to log and nothing to sell.
|
||||
Neroswap has no user accounts, no cookies, no tracking pixels, and no analytics. The site is a read-only dashboard; it never touches your funds or your identity. There is nothing to log and nothing to sell.
|
||||
|
||||
All three DEX sources (BasicSwap, Retoswap, Eigenwallet) are fully decentralized with on-chain or cross-chain atomic settlement. Retoswap trades happen entirely over Tor. Eigenwallet swaps are routed through Tor onion services. The three CEX sources (Nonlogs, CexSwap, AltQuick) are all no-KYC platforms.
|
||||
|
||||
@@ -109,7 +109,7 @@ All three DEX sources (BasicSwap, Retoswap, Eigenwallet) are fully decentralized
|
||||
|
||||
## WOW Price API
|
||||
|
||||
Neroswap also provides a public API endpoint for Wownero's USD price at [`/api/price/wow`](https://neroswap.com/api/price/wow). It computes a volume-weighted average price (VWAP) across all WOW trading pairs on Nonlogs, CexSwap, and AltQuick — using Kraken BTC and direct USDT prices as the USD reference. Pairs with zero volume are included at equal weight as a fallback.
|
||||
Neroswap also provides a public API endpoint for Wownero's USD price at [`/api/price/wow`](https://neroswap.com/api/price/wow). It computes a volume-weighted average price (VWAP) across all WOW trading pairs on Nonlogs, CexSwap, and AltQuick, using Kraken BTC and direct USDT prices as the USD reference. Pairs with zero volume are included at equal weight as a fallback.
|
||||
|
||||
## Try It
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Such Moon Launch — Rebuilding My First Game, Years Later"
|
||||
title: "Such Moon Launch: Rebuilding My First Game, Years Later"
|
||||
date: "2026-05-18"
|
||||
excerpt: "The expanded re-release of the very first game I ever wrote. 11 levels, 4 weapons, an endless mode, and proper tilt-to-steer controls. Wownero-themed pixel-art rocket arcade."
|
||||
draft: false
|
||||
@@ -11,9 +11,9 @@ draft: false
|
||||
|
||||
## The Backstory
|
||||
|
||||
This was **the very first game I ever wrote**. The original — *Wownero Moon Launch* — was a single-level rocket-landing prototype I shipped years ago as a fun side project. The we released a slightly upgraded version as a test app called "Meowlet Moon Launch" under our old **Tabby Labs** developer account. One level in the original and four levels in Tabby Labs version. Basic physics. No music. No juice. But it worked, and people played it.
|
||||
This was **the very first game I ever wrote**. The original, *Wownero Moon Launch*, was a single-level rocket-landing prototype I shipped years ago as a fun side project. The we released a slightly upgraded version as a test app called "Meowlet Moon Launch" under our old **Tabby Labs** developer account. One level in the original and four levels in Tabby Labs version. Basic physics. No music. No juice. But it worked, and people played it.
|
||||
|
||||
It sat there, mostly forgotten, for a long time. Every once in a while I'd open the project and think *there's something here.* The core loop — gravity, fuel, an unforgiving landing, is one of the all-time classics, going back to the 1969 [Lunar Lander mainframe game](https://en.wikipedia.org/wiki/Lunar_Lander_(video_game_genre)). Done right, it never gets old.
|
||||
It sat there, mostly forgotten, for a long time. Every once in a while I'd open the project and think *there's something here.* The core loop of gravity, fuel, and an unforgiving landing is one of the all-time classics, going back to the 1969 [Lunar Lander mainframe game](https://en.wikipedia.org/wiki/Lunar_Lander_(video_game_genre)). Done right, it never gets old.
|
||||
|
||||
So we rebuilt it. And really gave it some juice.
|
||||
|
||||
@@ -21,25 +21,25 @@ So we rebuilt it. And really gave it some juice.
|
||||
|
||||
What started as a single-level prototype is now a full game:
|
||||
|
||||
- **11 hand-tuned story levels** across the solar system — Moon, Mars, Venus, Io, Jupiter, Saturn, Neptune, Pluto, an asteroid belt, a rotating space station, and a Martian mothership boss fight
|
||||
- **Procedural endless mode** — escalating waves with random target planets, more enemies, tighter hazard timing
|
||||
- **8 ship upgrades** — thrust, fuel capacity, fuel efficiency, shield, armor, landing gear, rotation speed, crypto magnet
|
||||
- **4 weapons** — auto-aim cannon, homing missiles, continuous laser, area-effect EMP
|
||||
- **13 ship skins** — 8 purchasable cosmetics plus 4 achievement unlocks
|
||||
- **11 hand-tuned story levels** across the solar system: Moon, Mars, Venus, Io, Jupiter, Saturn, Neptune, Pluto, an asteroid belt, a rotating space station, and a Martian mothership boss fight
|
||||
- **Procedural endless mode**: escalating waves with random target planets, more enemies, tighter hazard timing
|
||||
- **8 ship upgrades**: thrust, fuel capacity, fuel efficiency, shield, armor, landing gear, rotation speed, crypto magnet
|
||||
- **4 weapons**: auto-aim cannon, homing missiles, continuous laser, area-effect EMP
|
||||
- **13 ship skins**: 8 purchasable cosmetics plus 4 achievement unlocks
|
||||
- **Tilt-to-steer controls** on mobile, calibrated properly (no baseline drift, no "wait, which way is up" problem on phones)
|
||||
- **3D landing mini-game** on final approach, with cockpit HUD, altimeter, and tilt indicator
|
||||
- **Warp tunnel** transitions between levels
|
||||
- **Global leaderboards** powered by our own backend at `api.such.software`
|
||||
- **Cloud save** — progress synced to our servers, restorable on any device
|
||||
- **13 achievements** — Google Play Games on Android, Game Center on iOS
|
||||
- **Crypto-themed collectibles** — WOW, XMR, BTC, and DOGE pickups as in-game currency (no real cryptocurrency, no blockchain, no wallets — just pixel-art coins)
|
||||
- **Cross-platform** — Android, iOS, Web, Windows, macOS, Linux, all from a single Godot 4 codebase
|
||||
- **Cloud save**: progress synced to our servers, restorable on any device
|
||||
- **13 achievements**: Google Play Games on Android, Game Center on iOS
|
||||
- **Crypto-themed collectibles**: WOW, XMR, BTC, and DOGE pickups as in-game currency (no real cryptocurrency, no blockchain, no wallets, just pixel-art coins)
|
||||
- **Cross-platform**: Android, iOS, Web, Windows, macOS, Linux, all from a single Godot 4 codebase
|
||||
|
||||
Same rocket. Same Moon. Almost everything else is new.
|
||||
|
||||
## Tilt Controls Done Right
|
||||
|
||||
If you've ever played a mobile tilt game and thought *why does this fight me?*, you're not imagining it. The mistake (as we learned) is tracking a moving baseline so the rest position can "drift" — but that same logic eats slow, intentional tilts before they ever register. Doodle Jump, Crossy Road, and Asphalt all use **single-shot calibration plus a low-pass filter**, never continuous baseline drift. So that's what we did:
|
||||
If you've ever played a mobile tilt game and thought *why does this fight me?*, you're not imagining it. The mistake (as we learned) is tracking a moving baseline so the rest position can "drift", but that same logic eats slow, intentional tilts before they ever register. Doodle Jump, Crossy Road, and Asphalt all use **single-shot calibration plus a low-pass filter**, never continuous baseline drift. So that's what we did:
|
||||
|
||||
- Calibrate the player's neutral hold once at level start
|
||||
- Low-pass filter the raw gravity vector to kill 60 Hz jitter without losing slow tilts
|
||||
@@ -50,13 +50,13 @@ Result: you can tilt 5° from neutral and the rocket responds immediately. Hold
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Built with [Godot 4](https://godotengine.org/) in pure GDScript. Leaderboards and cloud save run on our own backend at `api.such.software`. Ads use AdMob on mobile and AdSense on web. There's an optional **Remove Ads** in-app purchase ($1.99 once, ad-free forever), plus two optional Moonrock packs if you want to skip the grind. Every upgrade can still be earned by playing — there are no IAP-gated levels. Desktop builds are always ad-free.
|
||||
Built with [Godot 4](https://godotengine.org/) in pure GDScript. Leaderboards and cloud save run on our own backend at `api.such.software`. Ads use AdMob on mobile and AdSense on web. There's an optional **Remove Ads** in-app purchase ($1.99 once, ad-free forever), plus two optional Moonrock packs if you want to skip the grind. Every upgrade can still be earned by playing; there are no IAP-gated levels. Desktop builds are always ad-free.
|
||||
|
||||
## Get It
|
||||
|
||||
<SuchMoonLaunchStoreButtons />
|
||||
|
||||
- **Web (free)** — [moonlaunch.such.software](https://moonlaunch.such.software)
|
||||
- **Mobile** — Google Play and App Store listings going live as reviews clear
|
||||
- **Web (free)**: [moonlaunch.such.software](https://moonlaunch.such.software)
|
||||
- **Mobile**: Google Play and App Store listings going live as reviews clear
|
||||
|
||||
*Such moon. Many crypto. Wow.*
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
---
|
||||
title: "Building a Data-Driven Trivia Engine: No LLMs, Just Verified Facts"
|
||||
date: "2026-04-09"
|
||||
excerpt: "How we built Vegan IQ's question pipeline — deterministic generation from USDA, Oxford, and FAOSTAT data, quality filtering from 2,461 to 1,066 questions, and a greedy anti-repetition algorithm that makes every quiz feel fresh."
|
||||
excerpt: "How we built Vegan IQ's question pipeline: deterministic generation from USDA, Oxford, and FAOSTAT data, quality filtering from 2,461 to 1,066 questions, and a greedy anti-repetition algorithm that makes every quiz feel fresh."
|
||||
draft: true
|
||||
---
|
||||
|
||||
Vegan IQ ships with 1,066 trivia questions. None of them were written by an LLM. Every question traces back to a specific data source — USDA FoodData Central, Poore & Nemecek (Science, 2018), NIH, or FAOSTAT. The pipeline that produces them is a deterministic Python script that generates questions from structured data, filters them for quality, and outputs a single JSON file that the Flutter app bundles as an asset.
|
||||
Vegan IQ ships with 1,066 trivia questions. None of them were written by an LLM. Every question traces back to a specific data source: USDA FoodData Central, Poore & Nemecek (Science, 2018), NIH, or FAOSTAT. The pipeline that produces them is a deterministic Python script that generates questions from structured data, filters them for quality, and outputs a single JSON file that the Flutter app bundles as an asset.
|
||||
|
||||
This post covers the full pipeline: data sourcing, question generation, quality filtering, and the runtime pacing algorithm that prevents repetitive quizzes.
|
||||
|
||||
## The Data Layer
|
||||
|
||||
All source data lives in a single Python module (`food_data.py`, ~2,800 lines). No API calls at generation time — everything is embedded as Python dicts and lists, verified against the original sources.
|
||||
All source data lives in a single Python module (`food_data.py`, ~2,800 lines). No API calls at generation time; everything is embedded as Python dicts and lists, verified against the original sources.
|
||||
|
||||
### Nutritional Data (USDA FoodData Central)
|
||||
|
||||
69 foods (56 vegan, 13 animal products), each with per-100g values for calories, protein, fat, carbs, fiber, iron, calcium, and vitamin C. The animal products exist so the app can generate fair head-to-head comparisons — "Which has more iron per 100g: hemp seeds or ground beef?" — where the plant food wins on data.
|
||||
69 foods (56 vegan, 13 animal products), each with per-100g values for calories, protein, fat, carbs, fiber, iron, calcium, and vitamin C. The animal products exist so the app can generate fair head-to-head comparisons, such as "Which has more iron per 100g: hemp seeds or ground beef?", where the plant food wins on data.
|
||||
|
||||
### Environmental Impact (Poore & Nemecek, 2018)
|
||||
|
||||
@@ -23,12 +23,12 @@ The largest meta-analysis of food systems ever published: 38,700 farms across 11
|
||||
|
||||
### Other Sources
|
||||
|
||||
- **NIH Office of Dietary Supplements** — RDA values, bioavailability, deficiency risks for B12, vitamin D, omega-3, iron, calcium
|
||||
- **FAOSTAT** — top-3 producing countries and global production percentages for 45+ foods
|
||||
- **Oxford Companion to Food** — etymology and historical origins for 60+ foods
|
||||
- **FDA labeling guidelines** — 38 animal-derived ingredients (carmine, casein, confectioners glaze, etc.)
|
||||
- **Faunalytics + peer-reviewed studies** — animal cognition and behavior research
|
||||
- **USDA NASS** — retail prices for protein cost-efficiency comparisons
|
||||
- **NIH Office of Dietary Supplements**: RDA values, bioavailability, deficiency risks for B12, vitamin D, omega-3, iron, calcium
|
||||
- **FAOSTAT**: top-3 producing countries and global production percentages for 45+ foods
|
||||
- **Oxford Companion to Food**: etymology and historical origins for 60+ foods
|
||||
- **FDA labeling guidelines**: 38 animal-derived ingredients (carmine, casein, confectioners glaze, etc.)
|
||||
- **Faunalytics + peer-reviewed studies**: animal cognition and behavior research
|
||||
- **USDA NASS**: retail prices for protein cost-efficiency comparisons
|
||||
|
||||
## Question Generation
|
||||
|
||||
@@ -77,7 +77,7 @@ Eight phrasing variants are randomly selected. The `isAnimalCorrect` boolean on
|
||||
|
||||
## From 2,461 to 1,066: The Curation Step
|
||||
|
||||
Raw generation produces 2,461 questions. The quality filter cuts this to 1,066 — a 43% acceptance rate.
|
||||
Raw generation produces 2,461 questions. The quality filter cuts this to 1,066, a 43% acceptance rate.
|
||||
|
||||
### Subcategory Caps
|
||||
|
||||
@@ -107,7 +107,7 @@ Same underlying data, different framing. Unique question texts went from ~30 to
|
||||
|
||||
### Plant Milk Exclusion
|
||||
|
||||
Plant milks are ~90% water by weight. Per-kg environmental comparisons are misleading — oat milk looks absurdly efficient compared to almonds, but that's comparing a beverage to a solid food. A `_ENV_PER_KG_EXCLUDE` set filters all plant milks from the 5 environmental per-kg generators. Plant milks still appear in nutrition per-100g questions where the comparison is fair.
|
||||
Plant milks are ~90% water by weight. Per-kg environmental comparisons are misleading: oat milk looks absurdly efficient compared to almonds, but that's comparing a beverage to a solid food. A `_ENV_PER_KG_EXCLUDE` set filters all plant milks from the 5 environmental per-kg generators. Plant milks still appear in nutrition per-100g questions where the comparison is fair.
|
||||
|
||||
## Runtime: The Pacing Algorithm
|
||||
|
||||
@@ -117,7 +117,7 @@ Good questions don't matter if the quiz feels monotonous. The `QuestionService`
|
||||
|
||||
1. **Session-level (hard block):** Questions shown in the current app session are never repeated. `_sessionSeenIds` is a `Set<int>` reset on app restart.
|
||||
|
||||
2. **Cross-session (soft penalty):** The last 300 question IDs are persisted in `SharedPreferences`. Questions in this set receive a -15 penalty but aren't blocked — you'll eventually cycle through the full pool.
|
||||
2. **Cross-session (soft penalty):** The last 300 question IDs are persisted in `SharedPreferences`. Questions in this set receive a -15 penalty but aren't blocked; you'll eventually cycle through the full pool.
|
||||
|
||||
3. **Subcategory spacing:** No two adjacent questions can share a subcategory (-100 penalty). Two questions back: -60. Three back: -30.
|
||||
|
||||
@@ -153,7 +153,7 @@ Before scoring, the pool is split into three tiers:
|
||||
2. **Fallback:** In recent-300 (soft deprioritized via -15)
|
||||
3. **Emergency:** Seen this session (only used if pools are exhausted)
|
||||
|
||||
Preferred candidates are scored first. With 1,066 questions and 300 recent IDs tracked, the preferred pool stays above 700 for most users — enough for 40+ unique 15-question quizzes before any soft repeats.
|
||||
Preferred candidates are scored first. With 1,066 questions and 300 recent IDs tracked, the preferred pool stays above 700 for most users, enough for 40+ unique 15-question quizzes before any soft repeats.
|
||||
|
||||
### Spaced Repetition
|
||||
|
||||
@@ -161,7 +161,7 @@ Up to 3 questions the user previously answered incorrectly are pre-seeded into e
|
||||
|
||||
### Complexity
|
||||
|
||||
The greedy approach runs in O(count x min(poolSize, 60)) — well under a millisecond for 15-20 question quizzes on any modern phone. No noticeable latency between tapping a category and seeing the first question.
|
||||
The greedy approach runs in O(count x min(poolSize, 60)), well under a millisecond for 15-20 question quizzes on any modern phone. No noticeable latency between tapping a category and seeing the first question.
|
||||
|
||||
## Output
|
||||
|
||||
@@ -182,7 +182,7 @@ The pipeline outputs a single `questions.json` (623 KB, 1,066 entries). The Flut
|
||||
}
|
||||
```
|
||||
|
||||
Every question carries its citation in `data_source`. The app displays this in the explanation screen after the user answers. No trust-me-bro — you can verify any answer against the original dataset.
|
||||
Every question carries its citation in `data_source`. The app displays this in the explanation screen after the user answers. No trust-me-bro: you can verify any answer against the original dataset.
|
||||
|
||||
## What We Learned
|
||||
|
||||
@@ -190,10 +190,10 @@ Every question carries its citation in `data_source`. The app displays this in t
|
||||
|
||||
**Per-kg comparisons lie.** Comparing the environmental footprint of almonds (a solid) to oat milk (90% water) per kilogram is misleading. We had to exclude plant milks from per-kg environmental generators entirely.
|
||||
|
||||
**The pacing algorithm matters more than the questions.** A great pool of 1,066 questions can feel repetitive if the selection is random. The greedy scoring approach — with its layered penalties for subcategory, category, text, option overlap, and primary food — is what makes each quiz feel curated.
|
||||
**The pacing algorithm matters more than the questions.** A great pool of 1,066 questions can feel repetitive if the selection is random. The greedy scoring approach, with its layered penalties for subcategory, category, text, option overlap, and primary food, is what makes each quiz feel curated.
|
||||
|
||||
**Deterministic generation is underrated.** `seed(42)` means every team member, CI run, and release build produces the exact same question set. No drift, no surprises, no "the questions changed and I don't know why."
|
||||
|
||||
The full pipeline — data → generators → quality filter → JSON → pacing engine — runs in under 2 seconds on a laptop. The output is a single file that ships with the app. No servers, no APIs, no LLMs. Just data.
|
||||
The full pipeline, data → generators → quality filter → JSON → pacing engine, runs in under 2 seconds on a laptop. The output is a single file that ships with the app. No servers, no APIs, no LLMs. Just data.
|
||||
|
||||
See the [Vegan IQ product page](/products/vegan-iq) for more.
|
||||
|
||||
@@ -131,7 +131,7 @@ export default function AboutPage() {
|
||||
<div id="contact">
|
||||
<h2 className="text-3xl font-bold mb-3 tracking-tight">Get in Touch</h2>
|
||||
<p className="text-muted-foreground text-lg mb-12 max-w-2xl">
|
||||
Have a project in mind? We'd love to hear about it.
|
||||
Have a project in mind? We'd love to hear about it.
|
||||
</p>
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Home() {
|
||||
<div id="contact" className="w-full max-w-6xl text-left mb-12 section-container border-l-4 border-l-cyan-500">
|
||||
<h2 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Start a Project</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||
Have a project in mind? Let's talk about what you need and how we can help.
|
||||
Have a project in mind? Let's talk about what you need and how we can help.
|
||||
</p>
|
||||
</div>
|
||||
<ContactForm />
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function SmirkWalletPage() {
|
||||
<Card className="glass-card border-amber-500/20">
|
||||
<h3 className="text-xl font-bold mb-3">Encrypted Tips</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Tips are encrypted with the recipient's public key. Only they can claim.
|
||||
Tips are encrypted with the recipient's public key. Only they can claim.
|
||||
Senders can reclaim unclaimed tips (no lost funds).
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function SuchoicePrivacyPage() {
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">1. Introduction</h2>
|
||||
<p>
|
||||
Such Software ("we", "our", or "us") operates the Suchoice mobile and web application (the "App").
|
||||
Such Software ("we", "our", or "us") operates the Suchoice mobile and web application (the "App").
|
||||
This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our App.
|
||||
</p>
|
||||
</section>
|
||||
@@ -35,7 +35,7 @@ export default function SuchoicePrivacyPage() {
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">2. Information We Collect</h2>
|
||||
<h3 className="text-xl font-medium mt-4 mb-2">Camera and Photos</h3>
|
||||
<p>
|
||||
Our App requires access to your device's camera and photo library to function.
|
||||
Our App requires access to your device's camera and photo library to function.
|
||||
When you take a picture of a menu or select one from your gallery, the image is sent to our secure backend servers for processing by AI to extract menu items.
|
||||
<strong>We do not store these images.</strong> They are processed in memory and immediately discarded after the text is extracted.
|
||||
</p>
|
||||
@@ -45,7 +45,7 @@ export default function SuchoicePrivacyPage() {
|
||||
We use Google AdMob to display advertisements in the App. AdMob may collect and use data, including device identifiers (such as the Android Advertising ID or iOS IDFA), IP addresses, and usage data, to provide personalized advertising.
|
||||
</p>
|
||||
<p>
|
||||
You can learn more about how Google uses information from sites or apps that use their services by visiting: <a href="https://policies.google.com/technologies/partner-sites" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">Google's Privacy & Terms</a>.
|
||||
You can learn more about how Google uses information from sites or apps that use their services by visiting: <a href="https://policies.google.com/technologies/partner-sites" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">Google's Privacy & Terms</a>.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-medium mt-4 mb-2">In-App Purchases and Subscriptions</h3>
|
||||
@@ -79,7 +79,7 @@ export default function SuchoicePrivacyPage() {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">5. Children's Privacy</h2>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">5. Children's Privacy</h2>
|
||||
<p>
|
||||
These Services do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
|
||||
</p>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function VeganIqPrivacyPage() {
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">1. Introduction</h2>
|
||||
<p>
|
||||
Such Software LLC ("we", "our", or "us") operates the Vegan IQ mobile application (the "App").
|
||||
Such Software LLC ("we", "our", or "us") operates the Vegan IQ mobile application (the "App").
|
||||
This Privacy Policy explains what information the App collects, how we use it, and the choices you have.
|
||||
We collect as little as possible and we never sell your data.
|
||||
</p>
|
||||
@@ -72,7 +72,7 @@ export default function VeganIqPrivacyPage() {
|
||||
<p>
|
||||
<strong>The App does not currently display advertisements</strong> and does not use advertising identifiers such as the
|
||||
Android Advertising ID or iOS IDFA. If we introduce ads in the future, we will update this policy and, in regions that
|
||||
require it, ask for your consent through Google's User Messaging Platform before any ads are shown.
|
||||
require it, ask for your consent through Google's User Messaging Platform before any ads are shown.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function VeganIqPrivacyPage() {
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>Turn notifications on or off at any time in your device settings.</li>
|
||||
<li>Leave the leaderboard to remove your public display name.</li>
|
||||
<li>Reset the App's data from within the App, or delete the App to remove local data.</li>
|
||||
<li>Reset the App's data from within the App, or delete the App to remove local data.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function VeganIqPrivacyPage() {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">7. Children's Privacy</h2>
|
||||
<h2 className="text-2xl font-semibold mt-8 mb-4">7. Children's Privacy</h2>
|
||||
<p>
|
||||
These Services do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
|
||||
</p>
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function WebshopsPage() {
|
||||
We run a single Medusa v2 commerce platform that spins up a completely distinct,
|
||||
fully-branded storefront for every tenant, your colors, your typography, your layout,
|
||||
your dark mode. One battle-tested engine underneath; a store that looks and feels like
|
||||
nobody else's on top. No large setup fee, no monthly retainer: we build, host, and
|
||||
nobody else's on top. No large setup fee, no monthly retainer: we build, host, and
|
||||
maintain it on revenue share, so we only win when you make sales.
|
||||
</p>
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function WebshopsPage() {
|
||||
Per-tenant theming is the whole point
|
||||
</h2>
|
||||
<p className="text-muted-foreground leading-relaxed max-w-3xl mb-4">
|
||||
Most "multi-store" platforms give every shop the same skeleton with a swapped logo.
|
||||
Most "multi-store" platforms give every shop the same skeleton with a swapped logo.
|
||||
Ours derives an entire visual system per tenant from your theme configuration:
|
||||
palette, fonts, spacing, component styling, and a matched dark mode across both the
|
||||
storefront and checkout. Two shops on the same backend can look nothing alike.
|
||||
@@ -171,7 +171,7 @@ export default function WebshopsPage() {
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-8 max-w-3xl">
|
||||
Theming runs all the way down: product pages, catalog grids, cart, and checkout each
|
||||
carry the shop's palette, type, and layout. And the platform is more than a catalog,
|
||||
carry the shop's palette, type, and layout. And the platform is more than a catalog,
|
||||
service shops get a real{" "}
|
||||
<span className="font-medium text-foreground">booking calendar</span>: yoga classes
|
||||
and coaching calls are scheduled right on the product page, themed to match. Here is a
|
||||
@@ -183,7 +183,7 @@ export default function WebshopsPage() {
|
||||
{/* Feature grid, the real platform capabilities */}
|
||||
<section aria-labelledby="features-heading" className="mb-16">
|
||||
<h2 id="features-heading" className="text-2xl md:text-3xl font-bold mb-8">
|
||||
What's built in
|
||||
What's built in
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{features.map((f) => (
|
||||
|
||||
@@ -35,10 +35,10 @@ export const FounderSignature = () => {
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<p className="text-muted-foreground leading-relaxed italic border-l-4 border-primary/20 pl-6 text-lg">
|
||||
"Accessibility isn't bolted on. It's there from the first line of code."
|
||||
"Accessibility isn't bolted on. It's there from the first line of code."
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-6">
|
||||
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor devices for radiation detection. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's radioisotope battery program. He brings that same lab-bench rigor to software, and an insistence that it stay <span className="text-foreground font-semibold">accessible to everyone</span>, with front-ends engineered and audited against WCAG 2.2 AA.
|
||||
John Winter Murphy holds a <span className="text-foreground font-semibold">PhD in Materials Engineering from UT-Dallas (2014)</span>, focused on semiconductor devices for radiation detection. From <span className="text-foreground font-semibold">2015 to 2021 he was a scientist at Lawrence Livermore National Laboratory</span>, developing radiation detector systems and the lab's radioisotope battery program. He brings that same lab-bench rigor to software, and an insistence that it stay <span className="text-foreground font-semibold">accessible to everyone</span>, with front-ends engineered and audited against WCAG 2.2 AA.
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed mt-4">
|
||||
He previously co-founded <span className="text-foreground font-semibold">Patrn LLC</span> (<a href="https://art101.io" target="_blank" rel="noopener noreferrer" className="text-foreground font-semibold underline decoration-primary/50 hover:text-primary transition-colors">art101.io</a>), a series of art-history NFT drops, and <span className="text-foreground font-semibold">Tabby Labs</span>, which was developing a cryptocurrency wallet with an interactive world inside.
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"minimatch": "^9.0.7",
|
||||
"picomatch": "^4.0.4",
|
||||
"ajv": "^8.17.1",
|
||||
"eslint>ajv": "^6.12.6",
|
||||
"@eslint/eslintrc>ajv": "^6.12.6",
|
||||
"brace-expansion": "^2.0.3",
|
||||
"postcss": "^8.5.14",
|
||||
"fast-uri": "^3.1.2"
|
||||
|
||||
Generated
+33
-24
@@ -12,6 +12,8 @@ overrides:
|
||||
minimatch: ^9.0.7
|
||||
picomatch: ^4.0.4
|
||||
ajv: ^8.17.1
|
||||
eslint>ajv: ^6.12.6
|
||||
'@eslint/eslintrc>ajv': ^6.12.6
|
||||
brace-expansion: ^2.0.3
|
||||
postcss: ^8.5.14
|
||||
fast-uri: ^3.1.2
|
||||
@@ -823,8 +825,8 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
ajv@6.15.0:
|
||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
@@ -1360,12 +1362,12 @@ packages:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
fastq@1.20.1:
|
||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||
|
||||
@@ -1739,8 +1741,8 @@ packages:
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
@@ -2181,6 +2183,10 @@ packages:
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -2254,10 +2260,6 @@ packages:
|
||||
remark-rehype@11.1.2:
|
||||
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resend@4.8.0:
|
||||
resolution: {integrity: sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2594,6 +2596,9 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -2712,7 +2717,7 @@ snapshots:
|
||||
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
dependencies:
|
||||
ajv: 8.18.0
|
||||
ajv: 6.15.0
|
||||
debug: 4.4.3
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
@@ -2726,7 +2731,7 @@ snapshots:
|
||||
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
dependencies:
|
||||
ajv: 8.18.0
|
||||
ajv: 6.15.0
|
||||
debug: 4.4.3
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
@@ -3267,12 +3272,12 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
ajv@8.18.0:
|
||||
ajv@6.15.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.2
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
@@ -3867,7 +3872,7 @@ snapshots:
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
ajv: 8.18.0
|
||||
ajv: 6.15.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
@@ -3914,7 +3919,7 @@ snapshots:
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
ajv: 8.18.0
|
||||
ajv: 6.15.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
@@ -4026,9 +4031,9 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
fast-uri@3.1.2: {}
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fastq@1.20.1:
|
||||
dependencies:
|
||||
@@ -4460,7 +4465,7 @@ snapshots:
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
@@ -5082,6 +5087,8 @@ snapshots:
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
@@ -5200,8 +5207,6 @@ snapshots:
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resend@4.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@react-email/render': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -5680,6 +5685,10 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vfile-matter@5.0.1:
|
||||
|
||||
Reference in New Issue
Block a user