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": "Records", "href": "/records"}, {"label": "Wanted", "href": "/wantlist"}], "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", }}