# Store (`store.gregkilmer.com`) A small storefront for Greg's **proxy MTG card** and **3D-printed deck box / counter** services. It lives at `/store` inside the same app, reuses the main site's aesthetic, and is reachable directly or via the `store.gregkilmer.com` subdomain. It takes **no payment** — the order form only emails a request — and the form is **hard-locked** until you choose to accept orders. ## Pages | Route | What it is | | ------------------ | ------------------------------------------------ | | `/store` | Storefront: intro, products, live slots, policies | | `/store/cards` | Proxy MTG cards specs + pricing | | `/store/deck-boxes`| 3D-printed deck boxes & counters specs + pricing | | `/store/order` | Order request form (gated) or "closed" notice | The store uses its own nav (`StoreNav.astro`) and has **no links back to the main site**. The two sites are intentionally not connected from the UI. ## Opening and closing orders Everything customer-facing about availability is driven by **one file**: ``` src/data/store-availability.json ``` ```json { "ordersOpen": false, "lastUpdated": "2026-08-04", "turnaround": "Estimated turnaround: ~2 weeks once a list is confirmed.", "maxNote": "I take a limited number of orders at a time...", "slots": { "cards": { "open": 0, "total": 5 }, "deckBoxes": { "open": 0, "total": 4 } } } ``` - **`ordersOpen`** is the master switch. - `false` → the order page shows a "closed" notice and renders **no form**. - `true` → the order form appears. - **`slots.cards` / `slots.deckBoxes`** drive the "X of Y open" meters on the storefront and order page. Set `open` to 0 to show a category as Closed. - **`turnaround`**, **`maxNote`**, and **`lastUpdated`** are shown verbatim. Edit the file, commit, and redeploy. That's it. ### The lock is enforced in two places 1. **UI**: `/store/order` reads the file at render and hides the form when `ordersOpen` is false. 2. **Server**: `/api/order` reads the file again and returns **403** if `ordersOpen` is false — *before* doing any validation or delivery. So even a direct POST can't bypass the lock. ## Editing products, specs, and pricing Those live in version-controlled TypeScript (not the availability JSON): ``` src/data/store-products.ts ``` Each product has an `icon`, `title`, `summary`, example `image`, a `specs` list (size, thickness, finishes, white back, etc.), and `pricing` tiers. Search for `[Greg to fill]` to find the placeholder prices and concrete specs to replace. ### Available print colors (deck boxes) The deck-box product has a `colors` array — this drives the **Available print colors** swatch grid on `/store/deck-boxes` and the color dropdown in the order form. Each entry is `{ name, hex, inStock }`: ```ts colors: [ { name: 'Midnight Black', hex: '#1b1730', inStock: true }, { name: 'Nebula Teal', hex: '#4f8c84', inStock: false }, // shows "Out of stock" ], ``` - **Add a color** → append a new `{ name, hex, inStock }` entry. - **Mark a color sold out** → set `inStock: false`. It shows an "Out of stock" label on the grid and drops out of the order-form dropdown automatically. - **Remove a color** → delete the entry. Only in-stock colors are offered in the order form; the chosen color is included in the order email. Colors are optional — products with no `colors` array (e.g. proxy cards) simply don't show the grid or the dropdown. The shared **requirements / policies** list is also in this file (`requirements`) and is rendered on the storefront, the order page, and required as a checkbox on the order form. ## Order delivery (email) Order requests are emailed using the **same SMTP/webhook pipeline as the contact form** (see `docs/DEPLOYMENT.md`). The only addition is the inbox: ``` ORDER_TO=orders@gregkilmer.com ``` If `ORDER_TO` is blank, it falls back to `CONTACT_TO`. Configure `SMTP_*` vars as usual; if neither SMTP nor `CONTACT_WEBHOOK_URL` is set, `/api/order` returns 503 (delivery not configured) so it's visibly a config gap. Rate limiting reuses `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_MIN` unless you set `ORDER_RATE_LIMIT` / `ORDER_RATE_WINDOW_MIN` to give orders a separate budget. Bot protection reuses the Cloudflare **Turnstile** keys (`TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY`). Leave both blank to skip Turnstile (e.g. local dev). ## Pointing `store.gregkilmer.com` at the store The store is part of the same app — no separate server. The subdomain is purely a reverse-proxy concern (Cosmos / Traefik / Nginx Proxy Manager): 1. Create a DNS record for `store.gregkilmer.com`. 2. In your reverse proxy, point `store.gregkilmer.com` at the same container on port 4321, with TLS. 3. (Optional) Add a redirect or rewrite so `store.gregkilmer.com/` maps to `/store`. The store pages are reachable at `/store*` on the main domain too, so a redirect isn't strictly required — visitors who know the path can reach it either way. No code change is needed to enable the subdomain. ## Search visibility Store pages are **`noindex` by default** (set in `StoreLayout.astro`). When you're ready for the store to be indexed, change `noindex` to `false` there (or override it per page via the `noindex` prop). ## Files added / changed - `src/data/store-availability.json` — **the file you edit** - `src/data/store-availability.ts` — typed reader (used by pages + API) - `src/data/store-products.ts` — products, specs, pricing, requirements - `src/components/store/` — `StoreNav`, `ProductCard`, `PricingTable`, `SlotMeter`, `RequirementList`, `OrderForm.tsx` - `src/layouts/StoreLayout.astro` — store wrapper (surface, store nav, noindex) - `src/pages/store/` — `index`, `cards`, `deck-boxes`, `order` - `src/pages/api/order.ts` — order endpoint with hard gate - `src/layouts/BaseLayout.astro` — added optional `storeNav` prop (renders `StoreNav` instead of the main `NavBar` on store pages) - `src/components/nav/MobileMenu.tsx` — added store icon paths - `src/lib/env.ts` — added `orderTo()` helper - `.env.example` — added `ORDER_TO`