Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling

A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.

What is here:
  storefront/  the Next.js app
  backend/     ONLY the custom Spree code. Spree itself is a prebuilt image
               that mounts no app code, so every file here needs an explicit
               volume entry in docker-compose.yml or it is silently ignored.
  catalogue/   the scripts that loaded and maintain the products
  docs/        manifest, mock registry, napkins

Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.

The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:09:55 +08:00
co-authored by Claude Opus 5
commit a58a61b98c
98 changed files with 15524 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import json, os, re, urllib.request, time
base = os.path.dirname(os.path.abspath(__file__))
out_root = os.path.join(base, "products")
os.makedirs(out_root, exist_ok=True)
def slugify(name):
s = re.sub(r'[^a-zA-Z0-9]+', '-', name).strip('-').lower()
return s[:60]
catalogue = []
with open(os.path.join(base, "products.jsonl")) as f:
for line in f:
line = line.strip()
if not line:
continue
catalogue.append(json.loads(line))
for i, p in enumerate(catalogue, 1):
dirname = f"{i:02d}-{slugify(p['brand'])}-{slugify(p['name'])}"
pdir = os.path.join(out_root, dirname)
os.makedirs(pdir, exist_ok=True)
with open(os.path.join(pdir, "info.json"), "w") as f:
json.dump({"name": p["name"], "brand": p["brand"], "price_myr": p["price"], "source_code": p["code"]}, f, indent=2)
for j, url in enumerate(p["imgs"], 1):
dest = os.path.join(pdir, f"image-{j}.jpg")
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=15) as r, open(dest, "wb") as out:
out.write(r.read())
except Exception as e:
print(f"FAIL {url}: {e}")
print(f"[{i}/{len(catalogue)}] {p['brand']} - {p['name']} -> {len(p['imgs'])} imgs")