mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
130 lines
5.2 KiB
Markdown
130 lines
5.2 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.
|
|
|
|
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`
|