From a2d8e2e8069bcb66c729be16261b409dddf511de Mon Sep 17 00:00:00 2001 From: Pablo Stanley Date: Sun, 17 May 2026 17:04:22 -0700 Subject: [PATCH] =?UTF-8?q?Add=20Gumroad=20free=20pack=20=E2=80=94=20site?= =?UTF-8?q?=20nav=20dialog=20+=20generator=20scripts=20+=20campaign=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things in one branch: 1. **Site nav "pack" entry → in-app dialog.** Tapping "pack" in the header (desktop nav or mobile menu) opens a `PackDialog` modal that previews the free pack and links out to pablostanley.gumroad.com/l/pixabots — keeps people on the site for the pitch instead of bouncing them to Gumroad cold. Dialog is mounted once in the root layout and triggered via a tiny custom-event-based `openPackDialog()` helper so there's no prop drilling. 2. **Pack generator scripts (`scripts/`).** Idempotent, seeded, reproducible: - `generate-gumroad-pack.mjs` 2,000 PNGs × 4 sizes + 2,000 GIFs @ 480 - `generate-pack-webp.mjs` 2,000 animated WebPs @ 480 - `generate-gumroad-campaign.mjs` 6 animated 16:9 banners + animated square thumbnail + 5 social-size variants All output lands in the gitignored `pixabots-gumroad/` tree. 3. **Campaign HTML (`pixabots-gumroad/campaign/`).** Master dashboard (`index.html`) with click-to-copy buttons for titles, tags, captions, four format-variant Name/Description pairs, and Additional Details key/value pairs. Plus `gumroad-body.html` (Gumroad product description in pasteable HTML) and `email.html` (subscriber blast with hero image, four subject-line options, direct product URL). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 7 + ROADMAP.md | 3 +- app/src/app/layout.tsx | 2 + app/src/components/header-menu.tsx | 38 ++- app/src/components/pack-dialog.tsx | 93 ++++++ app/src/components/site-header.tsx | 8 + pixabots-gumroad/campaign/email.html | 71 +++++ pixabots-gumroad/campaign/gumroad-body.html | 52 +++ pixabots-gumroad/campaign/index.html | 310 ++++++++++++++++++ scripts/generate-gumroad-campaign.mjs | 314 ++++++++++++++++++ scripts/generate-gumroad-pack.mjs | 333 ++++++++++++++++++++ scripts/generate-pack-webp.mjs | 192 +++++++++++ 12 files changed, 1413 insertions(+), 10 deletions(-) create mode 100644 app/src/components/pack-dialog.tsx create mode 100644 pixabots-gumroad/campaign/email.html create mode 100644 pixabots-gumroad/campaign/gumroad-body.html create mode 100644 pixabots-gumroad/campaign/index.html create mode 100644 scripts/generate-gumroad-campaign.mjs create mode 100644 scripts/generate-gumroad-pack.mjs create mode 100644 scripts/generate-pack-webp.mjs diff --git a/.gitignore b/.gitignore index 5e1f2bc..a79a68e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,10 @@ pnpm-debug.log* *.psd .source .claude/ + +# Gumroad pack — large binaries, regen via scripts/generate-gumroad-*.mjs +/pixabots-gumroad/pixabots-pack/ +/pixabots-gumroad/upload/zips/ +/pixabots-gumroad/upload/gallery/ +/pixabots-gumroad/upload/thumbnail.gif +/pixabots-gumroad/campaign/social/ diff --git a/ROADMAP.md b/ROADMAP.md index 2a01765..09e470a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -167,7 +167,8 @@ When reviewing code, use the Before/After markdown table format from the skill. - [x] **Cache header revalidation.** `Cache-Control: immutable` was wrong once PR #156 made same-ID sprite art mutable. Swapped for 1-day fresh + 7-day stale-while-revalidate — art updates propagate within ~1 day without a manual dashboard purge. Manual purge still works for urgent rollout. (PR #166, #168, #169) - [x] **Animated `cheeky-terminal` (16-frame sequence) + `terminal-round` + `glasses` (blink).** Upgraded three previously-static eye parts to animated. `cheeky-terminal` is the first 16-frame sequence (fills one full super-loop). `ANIM_VERSION` 1→2 to bust CDN caches. Parts.ts docstring updated to include N=16 as valid sequence length. - [x] **Client PNG download baked full sprite sheet.** Creator `download()` used 5-arg `drawImage(img, 0, 0, size, size)` which stretched whole sheet — animated faces showed both blink frames side-by-side. Switched to 9-arg form with source rect `(0, 0, NATIVE, NATIVE)` so only frame 0 is sampled. Works for every layer (eyes/heads/body/top), every kind (static/blink/sequence). (PR #175) -- [x] **Perf — lazy-load Inspector + CommandPalette, swap `setInterval` for `requestAnimationFrame`.** Inspector pulled `react-colorful` (~16KB chunk) into main bundle even though only used when FX panel opens — now `next/dynamic`. CommandPalette was mounted in root layout for every page — also dynamic. Creator animation loop ran `setInterval(FRAME_MS)` regardless of tab visibility (battery drain on hidden tabs); swapped for rAF with FRAME_MS throttle. Build chunks now show Inspector + react-colorful as a separate split. +- [x] **Perf — lazy-load Inspector + CommandPalette, swap `setInterval` for `requestAnimationFrame`.** Inspector pulled `react-colorful` (~16KB chunk) into main bundle even though only used when FX panel opens — now `next/dynamic`. CommandPalette was mounted in root layout for every page — also dynamic. Creator animation loop ran `setInterval(FRAME_MS)` regardless of tab visibility (battery drain on hidden tabs); swapped for rAF with FRAME_MS throttle. Build chunks now show Inspector + react-colorful as a separate split. (PR #176) +- [x] **Gumroad free pack launch.** Shipped a 2,000-pixabot Gumroad pack ([pablostanley.gumroad.com/l/pixabots](https://pablostanley.gumroad.com/l/pixabots)) — 4 zip variants (all formats / PNG / GIF / WebP). New scripts in `scripts/` regenerate the pack + 6 animated 16:9 banner GIFs + 5 social-size variants + animated square thumbnail. Campaign HTML dashboard at `pixabots-gumroad/campaign/index.html` with click-to-copy titles, captions, tags, and variant Name/Description pairs. Site nav gets a new "pack" entry — opens an in-app `PackDialog` modal (hero image + CTA to Gumroad) rather than navigating away. ## Improve GIFs / Animated API diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 06a9282..32f101e 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -11,6 +11,7 @@ import { SITE_URL, TOTAL_COMBOS_LABEL } from "@/lib/constants"; import "./globals.css"; const CommandPalette = dynamic(() => import("@/components/command-palette").then((m) => m.CommandPalette)); +const PackDialog = dynamic(() => import("@/components/pack-dialog").then((m) => m.PackDialog)); const pixelify = Pixelify_Sans({ subsets: ["latin"], @@ -105,6 +106,7 @@ export default function RootLayout({ + diff --git a/app/src/components/header-menu.tsx b/app/src/components/header-menu.tsx index f26e8d9..4f0b401 100644 --- a/app/src/components/header-menu.tsx +++ b/app/src/components/header-menu.tsx @@ -11,16 +11,21 @@ import { import { PixelIcon } from "@/components/ui/pixel-icon"; import { useSfx } from "@/lib/use-sfx"; import { useTheme } from "@/lib/use-theme"; +import { openPackDialog } from "@/components/pack-dialog"; -type Item = { href: string; label: string; external?: boolean }; +type Item = + | { kind: "internal"; href: string; label: string } + | { kind: "external"; href: string; label: string } + | { kind: "pack"; label: string }; const ITEMS: readonly Item[] = [ - { href: "/", label: "Create" }, - { href: "/browse", label: "Browse" }, - { href: "/favorites", label: "Stars" }, - { href: "/docs", label: "Docs" }, - { href: "/docs/api", label: "API" }, - { href: "https://github.com/pablostanley/pixabots", label: "GitHub", external: true }, + { kind: "internal", href: "/", label: "Create" }, + { kind: "internal", href: "/browse", label: "Browse" }, + { kind: "internal", href: "/favorites", label: "Stars" }, + { kind: "internal", href: "/docs", label: "Docs" }, + { kind: "internal", href: "/docs/api", label: "API" }, + { kind: "pack", label: "Pack" }, + { kind: "external", href: "https://github.com/pablostanley/pixabots", label: "GitHub" }, ]; /** @@ -49,13 +54,28 @@ export function HeaderMenu() { Menu