diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..4e1828a --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "recordgod", + "runtimeExecutable": ".venv/bin/uvicorn", + "runtimeArgs": ["app.main:app", "--port", "8010"], + "port": 8010 + } + ] +} diff --git a/app/layout_routes.py b/app/layout_routes.py new file mode 100644 index 0000000..d54846c --- /dev/null +++ b/app/layout_routes.py @@ -0,0 +1,108 @@ +import json +import re +from collections import Counter +from urllib.parse import urljoin + +import httpx +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy import text + +from .auth import require_token +from .db import get_db + +# Storefront customizer — per-store theme + header menu + which elements appear on cards / +# product pages, in what order. Lets RecordGod dress ANY shop without touching code. +router = APIRouter(prefix="/layout", tags=["layout"]) + +DEFAULTS = { + "theme": {"primary": "#ff5db1", "accent": "#46d18a", "bg": "#0c0c0e", + "panel": "#141418", "text": "#f0f0f2", "font": "system-ui", "logo": "", + "radius": 12, "cardCols": 4}, + "menu": [{"label": "Home", "href": "/"}, {"label": "Vinyl", "href": "/vinyl"}, + {"label": "Genres", "href": "/genres"}, {"label": "New in", "href": "/new"}, + {"label": "Cart", "href": "/cart"}], + "card": ["cover", "title", "artist", "price", "condition", "cart"], + "product_page": ["cover", "title", "artist", "price", "condition", "genre", + "tracklist", "cart", "related"], +} +# Every element the editor can place, per surface. +PALETTE = { + "card": ["cover", "title", "artist", "price", "condition", "cart", "genre", + "format", "year", "stock_badge", "wishlist"], + "product_page": ["cover", "gallery", "title", "artist", "label", "price", "condition", + "genre", "format", "year", "country", "tracklist", "cart", "wishlist", + "description", "related", "dealgod_value"], +} + + +class ConfigIn(BaseModel): + config: dict + + +class UrlIn(BaseModel): + url: str + + +@router.get("") +async def get_layout(ident=Depends(require_token), db=Depends(get_db)): + row = (await db.execute(text("SELECT config FROM store_config WHERE store_id=:s"), + {"s": ident["store_id"]})).first() + cfg = row[0] if row else None + if isinstance(cfg, str): + cfg = json.loads(cfg) + return {"ok": True, "config": cfg or DEFAULTS, "defaults": DEFAULTS, "palette": PALETTE} + + +@router.post("") +async def save_layout(body: ConfigIn, ident=Depends(require_token), db=Depends(get_db)): + await db.execute(text(""" + INSERT INTO store_config (store_id, config, updated_at) + VALUES (:s, CAST(:c AS jsonb), now()) + ON CONFLICT (store_id) DO UPDATE SET config = EXCLUDED.config, updated_at = now() + """), {"s": ident["store_id"], "c": json.dumps(body.config)}) + await db.commit() + return {"ok": True} + + +@router.post("/import-brand") +async def import_brand(body: UrlIn, ident=Depends(require_token)): + """Pull a store's brand from its website — theme colour, logo, font — the 'bam, adapt + colourway' move (same spirit as DealGod store-intel). Lightweight HTTP scrape.""" + url = body.url.strip() + if not url.startswith("http"): + url = "https://" + url + try: + async with httpx.AsyncClient(timeout=15, follow_redirects=True, + headers={"User-Agent": "RecordGod/0.1 brand-import"}) as c: + html = (await c.get(url)).text + except Exception as e: + raise HTTPException(400, f"could not fetch {url}: {e}") + + def meta(prop): + m = (re.search(rf']+(?:name|property)=["\']{re.escape(prop)}["\'][^>]+content=["\']([^"\']+)', html, re.I) + or re.search(rf']+content=["\']([^"\']+)["\'][^>]+(?:name|property)=["\']{re.escape(prop)}["\']', html, re.I)) + return m.group(1) if m else None + + theme_color = meta("theme-color") + logo = meta("og:image") + if not logo: + m = re.search(r']+rel=["\'][^"\']*icon[^"\']*["\'][^>]+href=["\']([^"\']+)', html, re.I) + logo = m.group(1) if m else None + if logo and not logo.startswith("http"): + logo = urljoin(url, logo) + + counts = Counter(h.lower() for h in re.findall(r'#[0-9a-fA-F]{6}', html)) + common = [h for h, _ in counts.most_common(24) if h not in ('#ffffff', '#000000')][:6] + + gf = re.search(r'fonts\.googleapis\.com/css2?\?family=([A-Za-z0-9+]+)', html) + if gf: + font = gf.group(1).replace('+', ' ') + else: + fm = re.search(r'font-family:\s*["\']?([A-Za-z0-9 \-]+)', html, re.I) + font = fm.group(1).strip() if fm else None + + return {"ok": True, "palette": { + "primary": theme_color or (common[0] if common else "#ff5db1"), + "colors": common, "logo": logo or "", "font": font or "system-ui", + }} diff --git a/app/main.py b/app/main.py index e0bd140..9437691 100644 --- a/app/main.py +++ b/app/main.py @@ -18,12 +18,26 @@ from .wowplatter_v1 import router as wowplatter_v1_router # noqa: E402 from .virtual import router as virtual_router # noqa: E402 from .settings_routes import router as settings_router # noqa: E402 from .admin_routes import router as admin_router # noqa: E402 +from .layout_routes import router as layout_router # noqa: E402 +from .db import engine # noqa: E402 +from sqlalchemy import text as _sqltext # noqa: E402 app = FastAPI(title="RECORDGOD", version=__version__) app.include_router(wowplatter_v1_router) app.include_router(virtual_router) app.include_router(settings_router) app.include_router(admin_router) +app.include_router(layout_router) + + +@app.on_event("startup") +async def _ensure_tables(): + # new tables that aren't part of the original schema.sql load — create lazily on deploy + async with engine.begin() as conn: + await conn.execute(_sqltext( + "CREATE TABLE IF NOT EXISTS store_config " + "(store_id int PRIMARY KEY, config jsonb NOT NULL, " + "updated_at timestamptz NOT NULL DEFAULT now())")) @app.get("/healthz") diff --git a/site/builder.html b/site/builder.html new file mode 100644 index 0000000..8973bad --- /dev/null +++ b/site/builder.html @@ -0,0 +1,206 @@ + + + + + +RecordGod — storefront builder + + + +

RecordGod

storefront builder

+
+
+ +
+
+
RecordGod · storefront builder
+
+ + + +
+
+
+
+
+
+ + + +