Files
com.gregkilmer.www/docs/STORE.md
T
2026-08-06 17:29:26 -04:00

152 lines
6.1 KiB
Markdown

# 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`