← back to Wallco Ai
auto-save: 2026-06-26T06:12:56 (65 files) — public/marketplace/_layout.css public/marketplace/_partials.js public/marketplace/admin.html public/marketplace/api.html public/marketplace/apply.html
3189159b9185aa11287fd874fc815467ebccf38f · 2026-06-26 06:13:05 -0700 · Steve Abrams
Files touched
A public/marketplaceD public/marketplace/_layout.cssD public/marketplace/_partials.jsD public/marketplace/admin.htmlD public/marketplace/api.htmlD public/marketplace/apply.htmlD public/marketplace/challenge.htmlD public/marketplace/challenges.htmlD public/marketplace/claim.htmlD public/marketplace/dashboard.htmlD public/marketplace/designer-profile.htmlD public/marketplace/designers.htmlD public/marketplace/index.htmlD public/marketplace/leaderboard.htmlD public/marketplace/licensing.htmlD public/marketplace/og-default.svgD public/marketplace/onboard.htmlD public/marketplace/pattern.htmlD public/marketplace/patterns.htmlD public/marketplace/projects.htmlD public/marketplace/status.htmlD public/marketplace/storefront.htmlD public/marketplace/uploads/plaid-gingham-1778710320184-zvvd2Q.pngA public/uploadsD public/uploads/studio/abc_envD public/uploads/studio/abc_escape.pngD public/uploads/studio/anon-1778593186660_x.pngD public/uploads/studio/anon-1778593188756_clean.pngD public/uploads/studio/anon-1778593202294_x.pngD public/uploads/studio/anon-1778593204319_clean.pngD public/uploads/studio/anon-1778593228883_x.pngD public/uploads/studio/anon-1778593230815_clean.pngD public/uploads/studio/anon-1778593245262_x.pngD public/uploads/studio/anon-1778593475377_x.pngD public/uploads/studio/anon-1778593477424_clean.pngD public/uploads/studio/anon-1778593601608_x.pngD public/uploads/studio/anon-1778593603667_clean.pngD public/uploads/studio/anon-1778594239612_x.pngD public/uploads/studio/anon-1778594241645_clean.pngD public/uploads/studio/anon-1778595929905_x.pngD public/uploads/studio/anon-1778595932493_clean.pngD public/uploads/studio/anon-1778615141686_x.pngD public/uploads/studio/anon-1778615143838_clean.pngD public/uploads/studio/anon-1778621049454_x.pngD public/uploads/studio/anon-1778621051395_clean.pngD public/uploads/studio/anon-1778621261864_x.pngD public/uploads/studio/anon-1778621263848_clean.pngD public/uploads/studio/anon-1780269618715_x.pngD public/uploads/studio/anon-1780269621633_clean.pngD public/uploads/studio/anon-1780269677996_x.pngD public/uploads/studio/anon-1780269680307_clean.pngD public/uploads/studio/test-1778593188275_tiny.pngD public/uploads/studio/test-1778593203822_tiny.pngD public/uploads/studio/test-1778593230349_tiny.pngD public/uploads/studio/test-1778593476944_tiny.pngD public/uploads/studio/test-1778593603188_tiny.pngD public/uploads/studio/test-1778594241189_tiny.pngD public/uploads/studio/test-1778595931840_tiny.pngD public/uploads/studio/test-1778615143370_tiny.pngD public/uploads/studio/test-1778621050946_tiny.pngD public/uploads/studio/test-1778621263377_tiny.pngD public/uploads/studio/test-1780269621132_tiny.pngD public/uploads/studio/test-1780269679833_tiny.pngD public/uploads/studio/test123_tiny.pngD public/uploads/studio/test_tiny.png
Diff
commit 3189159b9185aa11287fd874fc815467ebccf38f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 06:13:05 2026 -0700
auto-save: 2026-06-26T06:12:56 (65 files) — public/marketplace/_layout.css public/marketplace/_partials.js public/marketplace/admin.html public/marketplace/api.html public/marketplace/apply.html
---
public/marketplace | 1 +
public/marketplace/_layout.css | 143 -------
public/marketplace/_partials.js | 53 ---
public/marketplace/admin.html | 214 -----------
public/marketplace/api.html | 417 ---------------------
public/marketplace/apply.html | 99 -----
public/marketplace/challenge.html | 115 ------
public/marketplace/challenges.html | 114 ------
public/marketplace/claim.html | 59 ---
public/marketplace/dashboard.html | 222 -----------
public/marketplace/designer-profile.html | 224 -----------
public/marketplace/designers.html | 133 -------
public/marketplace/index.html | 156 --------
public/marketplace/leaderboard.html | 185 ---------
public/marketplace/licensing.html | 79 ----
public/marketplace/og-default.svg | 22 --
public/marketplace/onboard.html | 312 ---------------
public/marketplace/pattern.html | 396 -------------------
public/marketplace/patterns.html | 151 --------
public/marketplace/projects.html | 174 ---------
public/marketplace/status.html | 271 -------------
public/marketplace/storefront.html | 132 -------
.../uploads/plaid-gingham-1778710320184-zvvd2Q.png | Bin 2286743 -> 0 bytes
public/uploads | 1 +
public/uploads/studio/abc_env | Bin 73 -> 0 bytes
public/uploads/studio/abc_escape.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593186660_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593188756_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593202294_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593204319_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593228883_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593230815_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593245262_x.png | 1 -
public/uploads/studio/anon-1778593475377_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593477424_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593601608_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778593603667_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778594239612_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778594241645_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778595929905_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778595932493_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778615141686_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778615143838_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778621049454_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778621051395_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778621261864_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1778621263848_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1780269618715_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1780269621633_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1780269677996_x.png | Bin 73 -> 0 bytes
public/uploads/studio/anon-1780269680307_clean.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778593188275_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778593203822_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778593230349_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778593476944_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778593603188_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778594241189_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778595931840_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778615143370_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778621050946_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1778621263377_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1780269621132_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test-1780269679833_tiny.png | Bin 73 -> 0 bytes
public/uploads/studio/test123_tiny.png | 0
public/uploads/studio/test_tiny.png | 1 -
65 files changed, 2 insertions(+), 3673 deletions(-)
diff --git a/public/marketplace b/public/marketplace
new file mode 120000
index 0000000..f924870
--- /dev/null
+++ b/public/marketplace
@@ -0,0 +1 @@
+/Volumes/Henry/wallco-ai-archive/public/marketplace
\ No newline at end of file
diff --git a/public/marketplace/_layout.css b/public/marketplace/_layout.css
deleted file mode 100644
index 4f6a842..0000000
--- a/public/marketplace/_layout.css
+++ /dev/null
@@ -1,143 +0,0 @@
-/* Wallco Marketplace shared style — Apple-glass aesthetic, neutral, designer-celebratory */
-:root {
- --mp-bg: #faf8f4;
- --mp-fg: #111;
- --mp-muted: #6b6b6b;
- --mp-border: rgba(20,20,20,.08);
- --mp-card: rgba(255,255,255,.78);
- --mp-accent: #111;
- --mp-accent-bg: #111;
- --mp-accent-fg: #fff;
- --mp-glass-shadow: 0 6px 30px rgba(20,20,20,.06), 0 1px 0 rgba(255,255,255,.6) inset;
- --mp-pad: clamp(12px, 2vw, 28px);
- --mp-radius: 20px;
-}
-* { box-sizing: border-box; }
-html, body { margin:0; background: var(--mp-bg); color: var(--mp-fg); font-family: ui-sans-serif, -apple-system, system-ui, "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; }
-/* Reserve space for the fixed top nav so page content never slides under it. */
-body { padding-top: var(--mp-nav-h, 56px); }
-a { color: inherit; text-decoration: none; }
-a.under { text-decoration: underline; text-underline-offset: 4px; }
-
-/* Top nav: pinned to viewport top on every marketplace page, never scrolls.
- `fixed` (not `sticky`) so transformed/overflow parents can't release the pin. */
-.mp-nav {
- position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
- display: flex; align-items: center; justify-content: space-between;
- padding: 14px var(--mp-pad);
- backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
- background: rgba(250,248,244,.82);
- border-bottom: 1px solid var(--mp-border);
-}
-.mp-nav .brand { font-weight: 700; letter-spacing: .02em; font-size: 17px; }
-.mp-nav .links { display: flex; gap: 18px; font-size: 14px; color: var(--mp-muted); }
-.mp-nav .links a:hover { color: #000; }
-.mp-nav .menu-btn { display: none; }
-
-.mp-hero {
- padding: clamp(48px, 7vw, 120px) var(--mp-pad);
- display: grid; gap: 18px; max-width: 1280px; margin: 0 auto;
-}
-.mp-hero h1 { font-size: clamp(36px, 5vw, 72px); margin: 0; letter-spacing: -.02em; line-height: 1.02; }
-.mp-hero p.lede { font-size: clamp(16px, 1.4vw, 20px); max-width: 720px; color: var(--mp-muted); line-height: 1.55; }
-.mp-hero .cta-row { display:flex; gap:12px; flex-wrap:wrap; margin-top: 8px; }
-
-.mp-btn {
- display: inline-flex; align-items: center; gap: 8px;
- background: var(--mp-accent-bg); color: var(--mp-accent-fg);
- padding: 12px 22px; border-radius: 999px; font-weight: 600; font-size: 14px;
- border: 0; cursor: pointer; transition: transform .15s ease, opacity .15s ease;
-}
-.mp-btn:hover { transform: translateY(-1px); }
-.mp-btn.ghost { background: transparent; color: var(--mp-fg); border: 1px solid var(--mp-border); }
-.mp-btn.tiny { padding: 6px 12px; font-size: 12px; }
-
-.mp-wrap { max-width: 1280px; margin: 0 auto; padding: var(--mp-pad); }
-
-.mp-grid { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
-.mp-grid.lg { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
-.mp-grid.xl { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
-
-.mp-card {
- background: var(--mp-card);
- border: 1px solid var(--mp-border);
- border-radius: var(--mp-radius);
- box-shadow: var(--mp-glass-shadow);
- overflow: hidden;
- backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
- transition: transform .2s ease, box-shadow .2s ease;
-}
-.mp-card:hover { transform: translateY(-3px); box-shadow: 0 12px 40px rgba(20,20,20,.1); }
-.mp-card .img {
- aspect-ratio: 1/1; background: #eee; background-size: cover; background-position: center;
-}
-.mp-card .body { padding: 14px 16px; display:flex; flex-direction:column; gap: 6px; }
-.mp-card .title { font-weight: 600; font-size: 15px; }
-.mp-card .sub { font-size: 13px; color: var(--mp-muted); }
-.mp-card .row { display:flex; align-items:center; justify-content:space-between; gap: 8px; margin-top: 6px; }
-
-.mp-pill { display: inline-block; padding: 4px 10px; border-radius: 999px; background: rgba(20,20,20,.06); font-size: 11px; color: #2a2a2a; }
-.mp-pill.accent { background: #111; color: #fff; }
-.mp-pill.gold { background: linear-gradient(135deg,#cda86e,#f2dca5); color: #2c2110; }
-.mp-pill.verified { background: #0a7; color: #fff; }
-
-.mp-section { padding: clamp(28px, 4vw, 56px) 0; border-top: 1px solid var(--mp-border); }
-.mp-section h2 { font-size: clamp(22px, 2.4vw, 32px); margin: 0 0 18px; letter-spacing: -.01em; }
-.mp-section .sub { color: var(--mp-muted); margin-bottom: 18px; }
-
-.mp-stat { display:flex; flex-direction: column; gap: 4px; padding: 18px 20px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; box-shadow: var(--mp-glass-shadow); }
-.mp-stat .label { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--mp-muted); }
-.mp-stat .value { font-size: 28px; font-weight: 700; letter-spacing: -.01em; }
-.mp-stat.dim .value { color: var(--mp-muted); font-weight: 600; }
-
-.mp-form { display:flex; flex-direction:column; gap: 14px; max-width: 560px; }
-.mp-form label { display:flex; flex-direction:column; gap: 6px; font-size: 13px; color: #333; }
-.mp-form input[type=text], .mp-form input[type=email], .mp-form input[type=url], .mp-form input[type=number], .mp-form textarea, .mp-form select {
- border: 1px solid var(--mp-border); padding: 10px 12px; border-radius: 12px; font-size: 14px; font-family: inherit; background: #fff;
-}
-.mp-form textarea { min-height: 100px; resize: vertical; }
-.mp-form .row { display: flex; gap: 10px; }
-.mp-form .row label { flex: 1; }
-.mp-form .check { flex-direction: row; align-items: center; gap: 8px; font-size: 13px; }
-.mp-form .err { color: #c33; font-size: 13px; }
-.mp-form .ok { color: #060; font-size: 13px; }
-
-.mp-claim-banner {
- background: linear-gradient(135deg, rgba(255,255,255,.92), rgba(255,255,255,.78));
- border: 1px solid var(--mp-border); border-radius: 22px; padding: 22px 24px; margin: 22px 0;
- box-shadow: var(--mp-glass-shadow); backdrop-filter: blur(14px);
-}
-.mp-claim-banner h3 { margin: 0 0 6px; font-size: 17px; }
-.mp-claim-banner .small { color: var(--mp-muted); font-size: 13px; margin-bottom: 14px; line-height: 1.5; }
-
-.mp-footer { padding: 40px var(--mp-pad); text-align: center; color: var(--mp-muted); font-size: 13px; border-top: 1px solid var(--mp-border); margin-top: 60px; }
-
-/* Hamburger (Gucci-style ≡ MENU) */
-.mp-hamburger { display:none; }
-@media (max-width: 720px) {
- .mp-nav .links { display:none; }
- .mp-nav .menu-btn { display: inline-flex; align-items:center; gap:6px; background: transparent; border: 0; cursor: pointer; font-size: 12px; letter-spacing: .12em; text-transform: uppercase; }
- .mp-nav .menu-btn .bars { display: inline-flex; flex-direction: column; gap: 3px; }
- .mp-nav .menu-btn .bars span { display:block; width: 18px; height: 1px; background: #111; }
-}
-
-/* Designer Profile / Storefront hero */
-.mp-storefront-hero {
- padding: clamp(40px, 6vw, 90px) var(--mp-pad);
- display: grid; gap: 16px; grid-template-columns: minmax(180px, 220px) 1fr;
- align-items: center; max-width: 1280px; margin: 0 auto;
-}
-.mp-storefront-hero .avatar { width: 220px; height: 220px; border-radius: 22px; background: #ddd; background-size: cover; background-position: center; box-shadow: var(--mp-glass-shadow); }
-.mp-storefront-hero h1 { margin: 0; font-size: clamp(34px, 4vw, 56px); letter-spacing: -.01em; }
-.mp-storefront-hero .studio { color: var(--mp-muted); font-size: 16px; }
-.mp-storefront-hero .badges { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0; }
-.mp-storefront-hero .meta { display: flex; gap: 14px; color: var(--mp-muted); font-size: 13px; }
-
-/* Status board */
-.mp-status-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
-.mp-status-board { padding: 24px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 24px; box-shadow: var(--mp-glass-shadow); }
-
-@media (max-width: 720px) {
- .mp-storefront-hero { grid-template-columns: 1fr; }
- .mp-storefront-hero .avatar { width: 140px; height: 140px; }
-}
diff --git a/public/marketplace/_partials.js b/public/marketplace/_partials.js
deleted file mode 100644
index 1220db6..0000000
--- a/public/marketplace/_partials.js
+++ /dev/null
@@ -1,53 +0,0 @@
-// Shared header/footer + tiny helpers. Loaded as a plain script.
-(function(){
- function el(tag, attrs={}, children=[]){ const e=document.createElement(tag); for(const k in attrs){ if(k==='style') Object.assign(e.style, attrs[k]); else if(k==='dataset') Object.assign(e.dataset, attrs[k]); else if(k==='html') e.innerHTML = attrs[k]; else e.setAttribute(k, attrs[k]); } (children||[]).forEach(c=> e.appendChild(typeof c==='string'? document.createTextNode(c): c)); return e; }
-
- function buildNav() {
- const links = [
- ['/marketplace', 'Marketplace'],
- ['/designers', 'Designers'],
- ['/patterns', 'Patterns'],
- ['/marketplace/leaderboard', 'Leaderboard'],
- ['/marketplace/challenges', 'Challenges'],
- ['/marketplace/apply', 'Become a Designer'],
- ['/marketplace/onboard', 'Onboard'],
- ['/marketplace/dashboard', 'Dashboard'],
- ['/marketplace/status', 'Status'],
- ];
- const nav = el('nav', { class: 'mp-nav' }, [
- el('a', { class: 'brand', href: '/marketplace' }, ['Wallco · Marketplace']),
- el('div', { class: 'links' }, links.map(([href,t]) => el('a', { href }, [t]))),
- el('button', { class: 'menu-btn', 'aria-label': 'Menu' }, [
- el('span', { class: 'bars' }, [ el('span'), el('span'), el('span') ]),
- document.createTextNode(' MENU')
- ])
- ]);
- document.body.prepend(nav);
- const mb = nav.querySelector('.menu-btn');
- if (mb) mb.addEventListener('click', () => {
- const links = nav.querySelector('.links');
- links.style.display = (links.style.display === 'flex') ? '' : 'flex';
- links.style.flexDirection = 'column';
- links.style.position = 'absolute';
- links.style.right = '12px';
- links.style.top = '52px';
- links.style.background = '#fff';
- links.style.padding = '10px 14px';
- links.style.borderRadius = '12px';
- links.style.boxShadow = '0 12px 40px rgba(0,0,0,.1)';
- });
- }
- function buildFooter() {
- const f = el('footer', { class: 'mp-footer' }, [
- el('div', {}, ['Wallco · Marketplace — designer-led wallcoverings, AI colorways, samples, licensing.']),
- el('div', { style: { marginTop: '6px' } }, ['Built on wallco.ai — every pattern attributable, every commission auditable.']),
- ]);
- document.body.appendChild(f);
- }
- function fmt$(n){ return '$' + Number(n||0).toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:2}); }
- function fmtN(n){ return Number(n||0).toLocaleString(); }
- async function api(path, opts={}){ const r = await fetch(path, Object.assign({ credentials:'include', headers:{'content-type':'application/json'} }, opts)); if(!r.ok && opts.throwOnError !== false){ let err = ''; try{ err = (await r.json()).error || r.statusText; } catch(_){ err = r.statusText; } throw new Error(err); } return r.json(); }
-
- window.MP = { el, buildNav, buildFooter, fmt$, fmtN, api };
- document.addEventListener('DOMContentLoaded', () => { buildNav(); buildFooter(); });
-})();
diff --git a/public/marketplace/admin.html b/public/marketplace/admin.html
deleted file mode 100644
index 5962c58..0000000
--- a/public/marketplace/admin.html
+++ /dev/null
@@ -1,214 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Marketplace Admin · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- body { background: #f6f4ee; }
- .admin-grid { display: grid; gap: 22px; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
- .panel { padding: 18px; background: var(--mp-card); border:1px solid var(--mp-border); border-radius: 18px; box-shadow: var(--mp-glass-shadow); }
- .panel h3 { margin-top: 0; }
- .panel table { width: 100%; border-collapse: collapse; font-size: 13px; }
- .panel th, .panel td { padding: 7px 8px; text-align: left; border-bottom: 1px solid var(--mp-border); }
- .admin-token { padding: 6px 10px; border-radius: 999px; font-size: 12px; background: #111; color: #fff; }
- .row-act { display: flex; gap: 6px; }
- .mp-btn.danger { background: #c33; }
- .mp-btn.good { background: #0a7; }
-</style>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Marketplace admin</h1>
- <p class="lede">Approve designers, moderate patterns, review claim requests, payout exports. All actions audited to <code>mp_moderation_events</code>.</p>
- <div>
- <input id="adm" placeholder="ADMIN_TOKEN" style="padding:8px 12px;border-radius:999px;border:1px solid var(--mp-border);font-size:13px;width:280px"/>
- <button class="mp-btn tiny" onclick="setTok()">Save token</button>
- <span id="tokOk" class="sub" style="margin-left:8px"></span>
- </div>
- </header>
- <main class="mp-wrap">
- <div class="admin-grid">
-
- <section class="panel">
- <h3>Pending designers</h3>
- <table id="dTable"><thead><tr><th>Name</th><th>Slug</th><th>Status</th><th></th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel">
- <h3>Pending patterns</h3>
- <table id="pTable"><thead><tr><th>Title</th><th>Designer</th><th>Source</th><th></th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel">
- <h3>Claim requests</h3>
- <table id="cTable"><thead><tr><th>Name</th><th>Slug</th><th>Status</th><th>Created</th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel">
- <h3>Licensing inquiries</h3>
- <table id="lTable"><thead><tr><th>Pattern</th><th>Buyer</th><th>Type</th><th>Status</th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel">
- <h3>Commission payable</h3>
- <p class="sub">Entries currently payable; click to mark paid.</p>
- <table id="payTable"><thead><tr><th>When</th><th>Designer</th><th>Type</th><th>Amount</th><th></th></tr></thead><tbody></tbody></table>
- <div style="display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap">
- <a class="mp-btn ghost tiny" href="#" onclick="exportPayouts(event)">Export CSV</a>
- <span id="payoutMsg" class="sub"></span>
- </div>
- <h4 style="margin:14px 0 6px">Recent payout batches</h4>
- <table id="batchTable"><thead><tr><th>Label</th><th>Designers</th><th>Total</th><th>Status</th><th></th></tr></thead><tbody></tbody></table>
- </section>
-
- </div>
- </main>
-
-<script>
-const TOK_KEY = 'wallco.mp.admin_token';
-let TOK = localStorage.getItem(TOK_KEY) || '';
-document.getElementById('adm').value = TOK;
-document.getElementById('tokOk').textContent = TOK ? '✓ token loaded' : '';
-
-function setTok() {
- TOK = document.getElementById('adm').value.trim();
- localStorage.setItem(TOK_KEY, TOK);
- document.getElementById('tokOk').textContent = '✓ saved';
- loadAll();
-}
-
-function hdr() { return TOK ? { 'x-admin-token': TOK } : {}; }
-async function api(p, opts={}) {
- opts.headers = Object.assign({}, opts.headers || {}, hdr(), {'content-type':'application/json'});
- opts.credentials = 'include';
- const r = await fetch(p, opts);
- const j = await r.json().catch(() => ({}));
- if (!r.ok) throw new Error(j.error || r.statusText);
- return j;
-}
-
-async function loadDesigners() {
- // we need a designers-pending endpoint; fall back to /api/marketplace/designers all
- try {
- const r = await fetch('/api/marketplace/designers?limit=500&sort=newest');
- const j = await r.json();
- const tb = document.getElementById('dTable').querySelector('tbody'); tb.innerHTML='';
- (j.designers || []).forEach(d => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${d.display_name}</td><td><a class="under" href="/designers/${d.slug}" target="_blank" rel="noopener noreferrer">${d.slug}</a></td><td><span class="mp-pill">${d.level}</span></td>
- <td class="row-act">
- <button class="mp-btn tiny good" onclick="approveDesigner(${d.id})">Approve</button>
- <button class="mp-btn tiny" onclick="verifyDesigner(${d.id})">Verify</button>
- <button class="mp-btn tiny" onclick="foundingDesigner(${d.id})">Founding</button>
- </td>`;
- tb.appendChild(tr);
- });
- } catch (err) { console.error(err); }
-}
-
-async function loadPatterns() {
- // public endpoint lists approved only; fetch full status by direct admin query (build new endpoint? for now just show all)
- try {
- const r = await fetch('/api/marketplace/patterns?limit=200');
- const j = await r.json();
- const tb = document.getElementById('pTable').querySelector('tbody'); tb.innerHTML='';
- (j.patterns || []).forEach(p => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${p.title}</td><td>${p.designer_name}</td><td>internal</td>
- <td class="row-act">
- <button class="mp-btn tiny good" onclick="approvePattern(${p.id})">Approve</button>
- <button class="mp-btn tiny danger" onclick="rejectPattern(${p.id})">Reject</button>
- <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank" rel="noopener noreferrer">View</a>
- </td>`;
- tb.appendChild(tr);
- });
- } catch (err) { console.error(err); }
-}
-
-async function loadClaims() {
- try {
- const j = await api('/api/marketplace/admin/claims');
- const tb = document.getElementById('cTable').querySelector('tbody'); tb.innerHTML='';
- (j.claims || []).forEach(c => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${c.public_name}</td><td>${c.slug}</td><td><span class="mp-pill">${c.outreach_status}</span></td><td>${new Date(c.created_at).toLocaleDateString()}</td>`;
- tb.appendChild(tr);
- });
- } catch (err) { document.getElementById('cTable').querySelector('tbody').innerHTML = `<tr><td colspan="4" class="sub">${err.message} — set ADMIN_TOKEN above.</td></tr>`; }
-}
-
-async function loadInquiries() {
- // No public endpoint yet; show "(coming soon)"
- const tb = document.getElementById('lTable').querySelector('tbody');
- tb.innerHTML = `<tr><td colspan="4" class="sub">Add /api/marketplace/admin/inquiries to surface here.</td></tr>`;
-}
-
-async function loadPayables() {
- try {
- const j = await api('/api/marketplace/commissions/ledger');
- const tb = document.getElementById('payTable').querySelector('tbody'); tb.innerHTML='';
- (j.ledger || []).filter(c => ['pending','approved','payable'].includes(c.status)).forEach(c => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${new Date(c.created_at).toLocaleDateString()}</td><td>#${c.designer_id}</td><td>${c.entry_type}</td><td>$${Number(c.commission_amount).toFixed(2)}</td>
- <td><span class="mp-pill">${c.status}</span></td>`;
- tb.appendChild(tr);
- });
- } catch (err) { document.getElementById('payTable').querySelector('tbody').innerHTML = `<tr><td colspan="5" class="sub">${err.message} — set ADMIN_TOKEN above.</td></tr>`; }
-}
-
-async function approveDesigner(id) { await api(`/api/marketplace/designers/${id}/approve`, {method:'POST'}); loadDesigners(); }
-async function verifyDesigner(id) { await api(`/api/marketplace/designers/${id}/verify`, {method:'POST'}); loadDesigners(); }
-async function foundingDesigner(id){ await api(`/api/marketplace/designers/${id}/founding`,{method:'POST'}); loadDesigners(); }
-async function approvePattern(id) { await api(`/api/marketplace/patterns/${id}/approve`, {method:'POST'}); loadPatterns(); }
-async function rejectPattern(id) { const reason = prompt('Reason:'); if (!reason) return; await api(`/api/marketplace/patterns/${id}/reject`, {method:'POST', body: JSON.stringify({reason})}); loadPatterns(); }
-
-async function exportPayouts(e){
- e.preventDefault();
- const msg = document.getElementById('payoutMsg');
- msg.textContent = 'Exporting…';
- try {
- const j = await api('/api/marketplace/admin/payouts/export', { method: 'POST', body: JSON.stringify({}) });
- if (j.empty) {
- msg.textContent = 'No payable entries to export.';
- return;
- }
- msg.innerHTML = `✓ Batch <code>${j.batch.batch_label}</code> — $${Number(j.total_amount).toFixed(2)} across ${j.designer_count} designer${j.designer_count===1?'':'s'} (${j.ledger_rows_paid} ledger rows).`;
- // Trigger CSV download.
- if (j.download_url) {
- const u = new URL(j.download_url, window.location.origin);
- if (TOK) u.searchParams.set('admin', TOK);
- window.location.href = u.toString();
- }
- loadPayables();
- loadBatches();
- } catch (err) {
- msg.textContent = 'Export failed: ' + err.message;
- }
-}
-
-async function loadBatches() {
- try {
- const j = await api('/api/marketplace/admin/payouts');
- const tb = document.getElementById('batchTable').querySelector('tbody'); tb.innerHTML='';
- (j.batches || []).forEach(b => {
- const tr = document.createElement('tr');
- const dlUrl = `/api/marketplace/admin/payouts/${b.id}/download${TOK ? '?admin=' + encodeURIComponent(TOK) : ''}`;
- tr.innerHTML = `<td>${b.batch_label}</td><td>${b.designer_count}</td><td>$${Number(b.total_amount).toFixed(2)}</td><td><span class="mp-pill">${b.status}</span></td><td><a class="mp-btn tiny ghost" href="${dlUrl}">Download</a></td>`;
- tb.appendChild(tr);
- });
- if (!(j.batches || []).length) {
- tb.innerHTML = `<tr><td colspan="5" class="sub">No payout batches yet.</td></tr>`;
- }
- } catch (err) {
- document.getElementById('batchTable').querySelector('tbody').innerHTML = `<tr><td colspan="5" class="sub">${err.message} — set ADMIN_TOKEN above.</td></tr>`;
- }
-}
-
-function loadAll(){ loadDesigners(); loadPatterns(); loadClaims(); loadInquiries(); loadPayables(); loadBatches(); }
-loadAll();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/api.html b/public/marketplace/api.html
deleted file mode 100644
index 68fca6a..0000000
--- a/public/marketplace/api.html
+++ /dev/null
@@ -1,417 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Agent API</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<style>
- .api-wrap { max-width: 980px; margin: 0 auto; padding: 32px 20px 96px; }
- .api-wrap h1 { font-family: 'Cormorant Garamond', serif; font-weight: 300; font-size: 44px; margin: 0 0 6px; letter-spacing: -0.01em; }
- .api-wrap .api-lede { color: #555; font-size: 16px; line-height: 1.55; max-width: 720px; margin: 0 0 24px; }
- .api-wrap h2 { font-family: 'Cormorant Garamond', serif; font-weight: 400; font-size: 26px; margin: 36px 0 8px; padding-bottom: 6px; border-bottom: 1px solid #e8e6e1; }
- .api-wrap h3 { font: 600 14px/1.4 system-ui, sans-serif; letter-spacing: 0.04em; text-transform: uppercase; color: #222; margin: 22px 0 6px; }
- .api-wrap p, .api-wrap li { font: 14px/1.6 system-ui, sans-serif; color: #333; }
- .api-wrap ul { padding-left: 22px; }
- .api-wrap code, .api-wrap .endpoint { font: 13px/1.4 ui-monospace, "SF Mono", Menlo, monospace; }
- .api-wrap pre { background: #1a1a1a; color: #f4f1ea; padding: 14px 16px; border-radius: 6px; overflow-x: auto; font: 12.5px/1.55 ui-monospace, "SF Mono", Menlo, monospace; }
- .api-wrap pre.lang-shell::before { content: "shell"; display:block; color:#888; font-size:10.5px; letter-spacing:.1em; margin-bottom:6px; text-transform:uppercase; }
- .api-wrap pre.lang-js::before { content: "node.js"; display:block; color:#888; font-size:10.5px; letter-spacing:.1em; margin-bottom:6px; text-transform:uppercase; }
- .api-wrap pre.lang-python::before { content: "python"; display:block; color:#888; font-size:10.5px; letter-spacing:.1em; margin-bottom:6px; text-transform:uppercase; }
- .api-wrap pre.lang-json::before { content: "json"; display:block; color:#888; font-size:10.5px; letter-spacing:.1em; margin-bottom:6px; text-transform:uppercase; }
- .endpoint-card { border: 1px solid #e8e6e1; border-radius: 8px; padding: 16px 18px; margin: 14px 0; background: #fafaf7; }
- .endpoint-card .endpoint { font-weight: 600; font-size: 14px; }
- .endpoint-card .method { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; margin-right: 8px; }
- .method.get { background: #eaf4ed; color: #2a6e3e; }
- .method.post { background: #e9eef8; color: #2a4e8e; }
- .endpoint-card .desc { margin: 8px 0 6px; color: #555; font-size: 13.5px; line-height: 1.5; }
- .endpoint-card .meta { font-size: 12px; color: #777; }
- .pill { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 11px; background: #eee; color: #444; margin-left: 6px; vertical-align: middle; }
- .pill.auth-public { background: #eef7ee; color: #2a6e3e; }
- .pill.auth-designer { background: #fef2da; color: #7a5a14; }
- .pill.auth-admin { background: #f7e0e0; color: #8a2a2a; }
- .toc { background: #fafaf7; border: 1px solid #e8e6e1; border-radius: 8px; padding: 14px 18px; margin-bottom: 24px; }
- .toc a { color: #2a4e8e; text-decoration: none; }
- .toc a:hover { text-decoration: underline; }
- .toc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 6px 18px; margin-top: 6px; font-size: 13.5px; }
- .callout { background: #fff8e5; border-left: 3px solid #d4a533; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 14px 0; font-size: 13.5px; line-height: 1.55; }
- .callout strong { color: #6a4d10; }
- .callout.warn { background: #fdecec; border-left-color: #c04040; }
- .callout.warn strong { color: #7a1a1a; }
-</style>
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <header class="mp-hero">
- <h1>The Wallco Agent API</h1>
- <p class="lede">Qualified AI agents — acting on behalf of real designers — can onboard, upload patterns, generate colorways, write copy, and read commission ledgers via the same HTTP API the dashboards use. Bots scraping or automating without a verified designer behind them will get nothing useful here and may be blocked. This page is the full contract.</p>
- <div class="cta-row">
- <a class="mp-btn" href="/marketplace/apply">Apply as Agent-Of-Record</a>
- <a class="mp-btn ghost" href="/marketplace">Back to marketplace</a>
- </div>
- </header>
-
- <main class="api-wrap">
-
- <div class="toc">
- <strong>Contents</strong>
- <div class="toc-grid">
- <a href="#qualification">1. Who qualifies</a>
- <a href="#rules">2. Hard rules</a>
- <a href="#auth">3. Authentication</a>
- <a href="#base">4. Base URL · versioning</a>
- <a href="#onboard">5. Onboarding endpoints</a>
- <a href="#profile">6. Profile endpoints</a>
- <a href="#patterns">7. Pattern endpoints</a>
- <a href="#ai">8. AI helper endpoints</a>
- <a href="#licensing">9. Licensing endpoints</a>
- <a href="#commission">10. Commission endpoints</a>
- <a href="#projects">11. Project / trade endpoints</a>
- <a href="#discovery">12. Discovery endpoints</a>
- <a href="#status">13. Status / health</a>
- <a href="#errors">14. Errors</a>
- <a href="#limits">15. Rate limits</a>
- <a href="#examples">16. End-to-end examples</a>
- <a href="#terms">17. Terms for agents</a>
- </div>
- </div>
-
- <section>
- <h2 id="qualification">1. Who qualifies</h2>
- <p>An <em>agent</em> is an AI-driven autonomous actor (Claude, GPT, custom LLM workflows, headless scripts orchestrated by an LLM) that performs marketplace actions on behalf of a human designer, studio, or brand. Examples:</p>
- <ul>
- <li>A designer's own Claude session that uploads new patterns, auto-tags them, generates colorways, and answers license inquiries.</li>
- <li>A studio's MCP server that mirrors a Figma library into Wallco and keeps the storefront in sync.</li>
- <li>A brand's automation that converts approved exclusive licenses into Shopify SKUs and watches the commission ledger.</li>
- </ul>
- <p>To qualify, the agent must:</p>
- <ul>
- <li>Be tied to a single verified <code>mp_designer_profile</code>. No anonymous traffic.</li>
- <li>Accept the same <strong>rights warranty</strong> per asset as a human uploader (you own or control all rights, you grant Wallco the rights in the designer agreement).</li>
- <li>Pass <strong>manual review on first 3 patterns.</strong> After that, the designer can opt into auto-approval for that agent.</li>
- <li>Have a signed <strong>agent-of-record</strong> agreement on file (a one-line attestation in the application body is enough — see §5).</li>
- </ul>
-
- <h2 id="rules">2. Hard rules — what gets blocked</h2>
- <div class="callout warn">
- <strong>Block list.</strong> Violating any of these voids the designer's account, not just the agent's session.
- <ul style="margin:6px 0 0 18px">
- <li>Scraping <code>/api/marketplace/*</code> for buyer info or competitive intel.</li>
- <li>Bulk pattern upload without per-asset rights warranty.</li>
- <li>Automated price undercutting, dynamic price probing.</li>
- <li>Generating fake license inquiries, fake project saves, fake follows.</li>
- <li>Uploading patterns that fail the DW Settlement Agreement screen (banana leaves, banana pods, grapes, birds, butterflies in tropical foliage compositions — auto-checked, blocked at <code>POST /patterns/upload</code>).</li>
- <li>Acting on behalf of designers other than the one whose JWT cookie you hold.</li>
- </ul>
- </div>
-
- <h2 id="auth">3. Authentication</h2>
- <p>Wallco uses a signed HMAC cookie called <code>mp_designer</code>. The cookie is issued by <code>POST /api/marketplace/designer/apply</code> and is valid for <strong>30 days</strong>. Treat it like a bearer token — keep it secret, rotate by re-applying.</p>
- <p>For headless agents, capture the <code>Set-Cookie</code> header on the apply response and replay it on every subsequent call as a <code>Cookie</code> header.</p>
-<pre class="lang-shell">curl -sS -i -X POST https://wallco.ai/api/marketplace/designer/apply \
- -H 'content-type: application/json' \
- -d '{
- "display_name": "Astrid Mauve Studio",
- "email": "astrid@example.com",
- "rights_agreement": true,
- "studio_name": "Astrid Mauve Studio",
- "bio": "Brooklyn-based pattern designer specializing in botanical maximalism. Agent-of-record: a Claude session managed by astrid@example.com.",
- "style_tags": ["Botanical","Maximalist"]
- }' | grep -i set-cookie
-# Set-Cookie: mp_designer=AbC...Xyz.dEf...; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly</pre>
- <p>Replay the cookie on every authenticated call:</p>
-<pre class="lang-shell">curl -sS https://wallco.ai/api/marketplace/designer/me \
- -H "cookie: mp_designer=AbC...Xyz.dEf..."</pre>
- <div class="callout">
- <strong>Admin endpoints.</strong> A small set of endpoints (approve, reject, founding, payouts) require admin credentials, not the designer cookie. Agents will never have these. They're listed for completeness but always return <code>403</code> for designer-cookie callers.
- </div>
-
- <h2 id="base">4. Base URL · versioning</h2>
- <ul>
- <li><strong>Production:</strong> <code>https://wallco.ai</code></li>
- <li><strong>Local dev:</strong> <code>http://127.0.0.1:9792</code></li>
- <li><strong>Versioning:</strong> v1, implicit (no <code>/v1/</code> prefix yet). Breaking changes will move under <code>/api/marketplace/v2/</code> and be announced 30 days in advance.</li>
- <li><strong>Content types:</strong> JSON in/out (<code>application/json</code>) except <code>POST /patterns/upload</code> which is <code>multipart/form-data</code>.</li>
- </ul>
- </section>
-
- <section>
- <h2 id="onboard">5. Onboarding endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/designer/apply</span><span class="pill auth-public">public</span></div>
- <div class="desc">Create a designer profile (status: <code>pending</code>) and receive the <code>mp_designer</code> cookie. The designer is auto-logged-in but cannot upload patterns until status flips to <code>approved</code>.</div>
- <div class="meta"><strong>Body:</strong> <code>display_name</code> (req), <code>email</code> (req, valid), <code>rights_agreement</code> (req, must be <code>true</code>), <code>studio_name</code>, <code>bio</code>, <code>website_url</code>, <code>instagram_url</code>, <code>location</code>, <code>style_tags</code> (string[]).</div>
- <div class="meta"><strong>Returns:</strong> <code>{ ok: true, designer: { id, slug, status } }</code> + <code>Set-Cookie</code>.</div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/designer/claim</span><span class="pill auth-public">public</span></div>
- <div class="desc">Submit a claim against an existing unclaimed designer page (e.g. someone Wallco curated into the directory). Routed to admin review.</div>
- <div class="meta"><strong>Body:</strong> <code>slug</code>, <code>name</code>, <code>email</code>, <code>website_url</code>, <code>instagram_url</code>, <code>portfolio_url</code>, <code>proof</code>.</div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/designer/logout</span><span class="pill auth-public">public</span></div>
- <div class="desc">Clears the <code>mp_designer</code> cookie. Useful when rotating an agent's credentials.</div>
- </div>
-
- <h2 id="profile">6. Profile endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/designer/me</span><span class="pill auth-designer">designer cookie</span></div>
- <div class="desc">Returns the full designer record + badges + patterns + points ledger + commission ledger (last 50) + next-level threshold.</div>
- <div class="meta"><strong>Returns:</strong> <code>{ designer, badges, patterns, points_ledger, commission_ledger, next_level }</code></div>
- </div>
-
- <h2 id="patterns">7. Pattern endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/patterns/upload</span><span class="pill auth-designer">designer cookie</span></div>
- <div class="desc">Upload one pattern. Multipart. Required field <code>image</code> (file, ≤50MB, jpg/png/webp/tiff/avif) + <code>rights_warranty=true</code>. The pattern starts in <code>status='pending'</code> and is reviewed by admin.</div>
- <div class="meta"><strong>Body fields:</strong> <code>image</code> (file, req), <code>title</code>, <code>description</code>, <code>rights_warranty</code> (req, "true"), <code>repeat_type</code>, <code>scale_notes</code>, <code>motif_tags</code>, <code>style_tags</code>, <code>room_tags</code>, <code>color_tags</code>, <code>mood_tags</code>, <code>commercial_suitable</code> ("true"/"false"), <code>hospitality_ready</code>, <code>rights_warranty_text</code>.</div>
- <div class="meta"><strong>Returns:</strong> <code>{ ok: true, pattern: { id, slug, status } }</code></div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/patterns</span><span class="pill auth-public">public</span></div>
- <div class="desc">List approved patterns. Filterable by <code>?designer=<slug></code>. Paginated via <code>?limit=<1..200>&offset=<n></code>.</div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/patterns/:slug</span><span class="pill auth-public">public</span></div>
- <div class="desc">Pattern detail + colorways. Side effect: increments <code>view_count</code>.</div>
- </div>
-
- <h2 id="ai">8. AI helper endpoints</h2>
- <p>Three live-LLM endpoints. The tag + copy endpoints run on Mac1 qwen3:14b (~30–90s/call). The colorways endpoint runs on Gemini 2.5 Flash Image (~5–15s/call × 6 in parallel).</p>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/ai/tag-pattern</span><span class="pill auth-public">public</span></div>
- <div class="desc">Given a <code>patternId</code> or <code>patternSlug</code>, returns style/room/color/motif/mood tag families + scale + mood sentence. Persists tags back to <code>mp_patterns</code> when a row is resolved. Falls back to a mock shape if qwen3:14b is unreachable.</div>
- <div class="meta"><strong>Body:</strong> <code>{ patternId? | patternSlug? | imageUrl? }</code></div>
- <div class="meta"><strong>Returns:</strong> <code>{ status: "live"|"mock", model: "qwen3:14b", pattern_id, pattern_slug, persisted, styleTags, roomTags, colorTags, motifTags, moodTags, scale, mood }</code></div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/ai/write-pattern-description</span><span class="pill auth-public">public</span></div>
- <div class="desc">Writes title suggestion, short description, long description, SEO title, SEO meta. Persists short + long descriptions to <code>mp_patterns</code> when a row is resolved.</div>
- <div class="meta"><strong>Body:</strong> <code>{ patternId? | patternSlug? | title? }</code></div>
- <div class="meta"><strong>Returns:</strong> <code>{ status, model, pattern_id, persisted, titleSuggestion, shortDescription, longDescription, seoTitle, seoDescription }</code></div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/ai/generate-colorways</span><span class="pill auth-public">public</span></div>
- <div class="desc">Generates 6 photo-real colorway variants via Gemini 2.5 Flash Image (Warm Clay, Moss & Linen, Ink Blue, Rose Plaster, Gold Smoke, Pearl Frost). Persists each into <code>mp_pattern_colorways</code> with <code>ai_generated=true</code>. Cost-logged via the cost tracker.</div>
- <div class="meta"><strong>Body:</strong> <code>{ patternId? | patternSlug? | imageUrl? }</code> + optional <code>{ mock: true, skipPersist: true }</code></div>
- <div class="meta"><strong>Returns:</strong> <code>{ status, pattern, colorways: [{ id, name, image_url, hex_palette, description }], errors? }</code></div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/ai/create-room-mockup</span><span class="pill auth-public">public</span></div>
- <div class="desc">Stub. Returns prompt/shape only. Will wire to ComfyUI / Replicate / Gemini Image when budget is approved.</div>
- </div>
-
- <h2 id="licensing">9. Licensing endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/licensing/inquiry</span><span class="pill auth-public">public</span></div>
- <div class="desc">Submit a license inquiry against a pattern. Routed to the designer (and admin). Awards points to the designer; flags <code>LICENSING_READY</code> badge on first inquiry.</div>
- <div class="meta"><strong>Body:</strong> <code>pattern_slug</code>, <code>buyer_name</code>, <code>buyer_email</code> (req), <code>buyer_company</code>, <code>license_type</code> (<code>non_exclusive | exclusive | exclusive_category</code>), <code>usage_description</code>, <code>territory</code>, <code>duration_months</code>, <code>budget_range</code>.</div>
- <div class="meta"><strong>Returns:</strong> <code>{ ok: true, inquiry_id }</code></div>
- <div class="meta"><strong>Anti-abuse:</strong> automated fake inquiries are explicit grounds for account termination — see <a href="#rules">§2</a>.</div>
- </div>
-
- <h2 id="commission">10. Commission endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/commissions/calculate</span><span class="pill auth-public">public</span></div>
- <div class="desc">Pure calculator. Given a line-item price + commission rate + optional adjustments, returns net basis + commission. Useful for previewing payouts before a sale clears.</div>
- <div class="meta"><strong>Body:</strong> <code>{ lineItemPrice, commissionRate, discount?, refund?, shippingCost?, ... }</code></div>
- <div class="meta"><strong>Returns:</strong> <code>{ net_basis, commission }</code></div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/commissions/ledger</span><span class="pill auth-designer">designer cookie or admin</span></div>
- <div class="desc">Returns up to 500 commission ledger rows for the authenticated designer (or all rows for admin).</div>
- <div class="meta"><strong>Returns:</strong> <code>{ ledger: [{ id, entry_type, basis_amount, commission_rate, commission_amount, status, created_at }, ...] }</code></div>
- </div>
-
- <h2 id="projects">11. Project / trade endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/projects</span><span class="pill auth-designer">designer cookie</span></div>
- <div class="desc">Create a project workspace (trade-buyer pattern; works with a designer cookie as a stand-in user today).</div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method post">POST</span><span class="endpoint">/api/marketplace/projects/:id/save</span><span class="pill auth-designer">designer cookie</span></div>
- <div class="desc">Save a pattern + colorway combo into a project. Increments <code>save_count</code> on the pattern; awards points to the pattern's designer.</div>
- </div>
-
- <h2 id="discovery">12. Discovery endpoints</h2>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/designers</span><span class="pill auth-public">public</span></div>
- <div class="desc">List approved designers. Sort: <code>?sort=newest|points|featured</code>. Paginate: <code>?limit&offset</code>.</div>
- </div>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/designers/:slug</span><span class="pill auth-public">public</span></div>
- <div class="desc">Designer detail + their approved patterns + badges + collections. If the slug is an unclaimed page, returns the claim record instead.</div>
- </div>
-
- <h2 id="status">13. Status / health</h2>
-
- <div class="endpoint-card">
- <div><span class="method get">GET</span><span class="endpoint">/api/marketplace/status</span><span class="pill auth-public">public</span></div>
- <div class="desc">Health check + count snapshot (designers, patterns, collections, commission entries, pending $, paid $, open inquiries, open takedowns).</div>
- </div>
-
- <h2 id="errors">14. Errors</h2>
- <p>All errors return JSON of the shape:</p>
-<pre class="lang-json">{ "error": "<short human-readable message>" }</pre>
- <ul>
- <li><strong>400</strong> — malformed body, missing required field, bad pattern_slug.</li>
- <li><strong>401</strong> — designer cookie missing or expired.</li>
- <li><strong>403</strong> — admin endpoint hit without admin auth, or designer account suspended/rejected.</li>
- <li><strong>404</strong> — resource not found, or endpoint is admin-gated and you're not admin (intentional 404, not 401).</li>
- <li><strong>413</strong> — body too large.</li>
- <li><strong>500</strong> — server error. Retry with exponential backoff; if it persists, contact <code>marketplace@wallco.ai</code>.</li>
- </ul>
-
- <h2 id="limits">15. Rate limits</h2>
- <ul>
- <li><strong>Read endpoints:</strong> 120 req / minute / cookie or IP (whichever is higher).</li>
- <li><strong>Write endpoints (apply, upload, inquiry):</strong> 30 req / minute / cookie. Patterns: max 50 uploads / 24h / designer.</li>
- <li><strong>LLM endpoints (tag, write, colorways):</strong> 6 concurrent calls / cookie. Each call takes 5–90s; queue accordingly.</li>
- <li><strong>Backoff:</strong> 429 responses include <code>Retry-After</code> in seconds. Honor it.</li>
- </ul>
-
- <h2 id="examples">16. End-to-end examples</h2>
-
- <h3>Apply, upload, auto-tag, auto-copy, auto-colorway (Node.js)</h3>
-<pre class="lang-js">import fs from 'node:fs';
-
-const BASE = 'https://wallco.ai';
-let COOKIE = '';
-
-async function api(path, opts = {}) {
- const headers = { 'content-type': 'application/json', ...opts.headers };
- if (COOKIE) headers.cookie = COOKIE;
- const res = await fetch(BASE + path, { ...opts, headers });
- const setCookie = res.headers.get('set-cookie');
- if (setCookie && setCookie.includes('mp_designer=')) {
- COOKIE = setCookie.split(';')[0];
- }
- if (!res.ok) throw new Error(`${res.status} ${path}: ${await res.text()}`);
- return res.json();
-}
-
-// 1. Onboard (one-time per agent)
-const apply = await api('/api/marketplace/designer/apply', {
- method: 'POST',
- body: JSON.stringify({
- display_name: 'Astrid Mauve Studio',
- email: 'astrid@example.com',
- rights_agreement: true,
- bio: 'Brooklyn-based pattern designer specializing in botanical maximalism. ' +
- 'Agent-of-record: a Claude session managed by astrid@example.com.',
- style_tags: ['Botanical', 'Maximalist'],
- }),
-});
-// → { ok: true, designer: { id, slug, status: 'pending' } }
-
-// 2. Upload a pattern (multipart — use FormData)
-const fd = new FormData();
-fd.append('image', new Blob([fs.readFileSync('./wildflower-reverie.png')]), 'wildflower-reverie.png');
-fd.append('title', 'Wildflower Reverie');
-fd.append('rights_warranty', 'true');
-fd.append('repeat_type', 'half-drop');
-fd.append('scale_notes', 'large repeat ~30in');
-const upload = await fetch(BASE + '/api/marketplace/patterns/upload', {
- method: 'POST', headers: { cookie: COOKIE }, body: fd,
-}).then(r => r.json());
-const slug = upload.pattern.slug;
-
-// 3. Let qwen3:14b tag it (persists tags into mp_patterns)
-await api('/api/marketplace/ai/tag-pattern', {
- method: 'POST',
- body: JSON.stringify({ patternSlug: slug }),
-});
-
-// 4. Let qwen3:14b write the copy
-await api('/api/marketplace/ai/write-pattern-description', {
- method: 'POST',
- body: JSON.stringify({ patternSlug: slug }),
-});
-
-// 5. Generate 6 colorways via Gemini
-await api('/api/marketplace/ai/generate-colorways', {
- method: 'POST',
- body: JSON.stringify({ patternSlug: slug }),
-});
-
-// 6. Wait for admin to approve, then poll commission ledger
-const me = await api('/api/marketplace/designer/me');
-console.log('points:', me.designer.points, 'commission rows:', me.commission_ledger.length);</pre>
-
- <h3>Same flow in Python</h3>
-<pre class="lang-python">import requests, json
-
-BASE = 'https://wallco.ai'
-s = requests.Session()
-
-# 1. Onboard
-r = s.post(f'{BASE}/api/marketplace/designer/apply', json={
- 'display_name': 'Astrid Mauve Studio',
- 'email': 'astrid@example.com',
- 'rights_agreement': True,
- 'bio': 'Brooklyn-based pattern designer. Agent-of-record: a Python script run by astrid@example.com.',
- 'style_tags': ['Botanical', 'Maximalist'],
-})
-r.raise_for_status()
-# session.cookies now contains mp_designer automatically
-
-# 2. Upload a pattern
-with open('wildflower-reverie.png', 'rb') as f:
- r = s.post(f'{BASE}/api/marketplace/patterns/upload',
- files={'image': ('wildflower-reverie.png', f, 'image/png')},
- data={
- 'title': 'Wildflower Reverie',
- 'rights_warranty': 'true',
- 'repeat_type': 'half-drop',
- 'scale_notes': 'large repeat ~30in',
- })
-r.raise_for_status()
-slug = r.json()['pattern']['slug']
-
-# 3. Auto-tag
-s.post(f'{BASE}/api/marketplace/ai/tag-pattern', json={'patternSlug': slug}).raise_for_status()
-# 4. Auto-copy
-s.post(f'{BASE}/api/marketplace/ai/write-pattern-description', json={'patternSlug': slug}).raise_for_status()
-# 5. Colorways
-s.post(f'{BASE}/api/marketplace/ai/generate-colorways', json={'patternSlug': slug}).raise_for_status()
-
-# 6. Read commission ledger
-print(s.get(f'{BASE}/api/marketplace/commissions/ledger').json())</pre>
-
- <h2 id="terms">17. Terms for agents</h2>
- <p>By using this API on behalf of a designer, the agent's operator agrees that:</p>
- <ul>
- <li>The designer retains full liability for every uploaded asset's rights and warranty. The agent is a tool, not a legal shield.</li>
- <li>Wallco may suspend the <code>mp_designer</code> cookie at any time without notice if abuse is detected.</li>
- <li>All API responses may be cached for up to 60s on the edge; do not rely on real-time round-trip consistency.</li>
- <li>Agent traffic is logged with cookie ID, IP, and User-Agent for fraud review. Use a distinctive <code>User-Agent</code> (e.g. <code>AstridMauveAgent/0.3 (claude-sonnet-4-6)</code>) so support can identify your traffic when reaching out.</li>
- <li>This page is the authoritative spec. The accompanying source of truth is <code>src/marketplace/index.js</code> in the wallco-ai repo — if the page and the code disagree, the code wins until the page is corrected.</li>
- </ul>
-
- <p style="margin-top:32px; color:#888; font-size:13px">
- Questions, scope expansion, agent-of-record agreements: <a href="mailto:marketplace@wallco.ai">marketplace@wallco.ai</a>.
- Last updated 2026-05-13.
- </p>
- </section>
-
- </main>
-</body>
-</html>
diff --git a/public/marketplace/apply.html b/public/marketplace/apply.html
deleted file mode 100644
index e5f4256..0000000
--- a/public/marketplace/apply.html
+++ /dev/null
@@ -1,99 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Become a Wallco Designer</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Become a Wallco Designer.</h1>
- <p class="lede">Apply to publish patterns, get AI colorways and room mockups, build your storefront, and earn commission from wallpaper sales, samples, and licensing. Founding designers get 20% royalty on wallpaper and 60–70% on licensing.</p>
- </header>
-
- <main class="mp-wrap">
- <div class="mp-grid lg" style="grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);">
- <section class="mp-card" style="padding: 22px;">
- <h2 style="margin-top:0">Apply</h2>
- <form id="form" class="mp-form">
- <label>Designer / studio name *
- <input type="text" name="display_name" required maxlength="80" placeholder="Your designer name (will appear on your public profile)" />
- </label>
- <label>Email *
- <input type="email" name="email" required maxlength="200" placeholder="hi@yourstudio.com" />
- </label>
- <div class="row">
- <label>Studio name <input type="text" name="studio_name" maxlength="100" placeholder="Optional" /></label>
- <label>Location <input type="text" name="location" maxlength="150" placeholder="City, Country" /></label>
- </div>
- <label>Bio
- <textarea name="bio" maxlength="2000" placeholder="Two short paragraphs about your work — style, training, what you’re known for."></textarea>
- </label>
- <div class="row">
- <label>Website <input type="url" name="website_url" placeholder="https://yourstudio.com" /></label>
- <label>Instagram <input type="url" name="instagram_url" placeholder="https://instagram.com/your-handle" /></label>
- </div>
- <label>Style tags (comma separated)
- <input type="text" name="style_tags" placeholder="Botanical, Maximalist, Hospitality" />
- </label>
-
- <div style="border-top: 1px solid var(--mp-border); padding-top: 14px; margin-top: 6px;">
- <label class="check">
- <input type="checkbox" name="rights_agreement" required />
- I confirm I own or control all rights to the patterns I will upload to Wallco, and I agree to the Wallco Designer Agreement (commission, licensing, royalty terms).
- </label>
- </div>
-
- <button type="submit" class="mp-btn">Submit application</button>
- <div id="msg" class="err" style="display:none"></div>
- </form>
- </section>
-
- <aside>
- <h3 style="margin-top:0">Founding designer benefits</h3>
- <ul style="color: var(--mp-muted); line-height: 1.6; padding-left: 18px;">
- <li>Free public storefront at <code>wallco.ai/store/your-slug</code></li>
- <li>Founding Designer badge</li>
- <li>20% royalty on eligible wallpaper sales</li>
- <li>60% share of non-exclusive license revenue</li>
- <li>70% share of exclusive license revenue</li>
- <li>Free AI colorway generation for first 10 patterns</li>
- <li>Featured launch placement</li>
- <li>Early trade-buyer exposure</li>
- </ul>
- <h3>Commission rules</h3>
- <p style="color: var(--mp-muted); line-height: 1.6; font-size: 14px;">Commission is calculated on <strong>net product revenue</strong> — line item minus discounts, refunds, and sample credits. Tax and shipping are never included. Status moves from pending → approved → payable → paid, with a 30-day return window before payout.</p>
- <h3>Claim page disclaimer</h3>
- <p style="color: var(--mp-muted); line-height: 1.6; font-size: 14px;">Wallco may publish an unclaimed public stub for known pattern designers. Those pages explicitly state they are <em>not affiliated with or endorsed by</em> the designer, and contain no copied artwork or bios. If you’re the designer, you can claim that page to convert it into your official storefront.</p>
- </aside>
- </div>
- </main>
-
-<script>
-document.getElementById('form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const msg = document.getElementById('msg');
- msg.style.display='none'; msg.className='err';
- const f = new FormData(e.target);
- const body = Object.fromEntries(f.entries());
- body.rights_agreement = body.rights_agreement === 'on';
- try {
- const r = await fetch('/api/marketplace/designer/apply', {
- method: 'POST', credentials: 'include',
- headers: {'content-type':'application/json'}, body: JSON.stringify(body)
- });
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'failed');
- msg.className = 'ok';
- msg.style.display = 'block';
- msg.textContent = 'Application received. Redirecting to your dashboard…';
- setTimeout(() => { location.href = '/marketplace/dashboard'; }, 1200);
- } catch (err) {
- msg.style.display='block'; msg.className='err'; msg.textContent = err.message;
- }
-});
-</script>
-</body>
-</html>
diff --git a/public/marketplace/challenge.html b/public/marketplace/challenge.html
deleted file mode 100644
index dfef8a5..0000000
--- a/public/marketplace/challenge.html
+++ /dev/null
@@ -1,115 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Challenge</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .cd-wrap { max-width: 1000px; margin: 0 auto; padding: var(--mp-pad); }
- .cd-hero { padding: clamp(36px,5vw,80px) var(--mp-pad) 12px; }
- .cd-hero h1 { font-size: clamp(30px,3.6vw,48px); margin: 0 0 8px; letter-spacing: -.02em; }
- .cd-hero .meta { color: var(--mp-muted); font-size: 14px; line-height: 1.55; }
- .cd-hero .pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
- .cd-hero .pills .metric { background: #111; color: #fff; padding: 4px 12px; border-radius: 999px; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
- .cd-hero .pills .status { padding: 4px 12px; border-radius: 999px; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
- .cd-hero .pills .status.active { background: #0a7; color: #fff; }
- .cd-hero .pills .status.scheduled { background: rgba(20,20,20,.08); color: #444; }
- .cd-hero .pills .status.closed { background: #cda86e; color: #2c2110; }
- .cd-table { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 22px; overflow: hidden; box-shadow: var(--mp-glass-shadow); margin-top: 24px; }
- .cd-row { display: grid; grid-template-columns: 56px 1fr auto; gap: 12px; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--mp-border); }
- .cd-row:last-child { border-bottom: 0; }
- .cd-row .rank { font-weight: 700; font-size: 18px; color: #555; text-align: center; }
- .cd-row .rank.top1 { color: #b78a2a; }
- .cd-row .rank.top2 { color: #888; }
- .cd-row .rank.top3 { color: #a66; }
- .cd-row .designer { display: flex; align-items: center; gap: 12px; min-width: 0; }
- .cd-row .designer .avatar { width: 38px; height: 38px; border-radius: 50%; background: #ddd; background-size: cover; background-position: center; flex: 0 0 38px; }
- .cd-row .designer .name { font-weight: 600; font-size: 15px; }
- .cd-row .designer .level { font-size: 12px; color: var(--mp-muted); }
- .cd-row .score { font-weight: 700; font-variant-numeric: tabular-nums; }
- .cd-winner { padding: 22px 24px; background: linear-gradient(135deg,#fdf6e1,#fcedc6); border: 1px solid #e8d6a0; border-radius: 22px; margin-top: 16px; display: flex; align-items: center; gap: 18px; }
- .cd-winner .avatar { width: 64px; height: 64px; border-radius: 50%; background: #ddd; background-size: cover; background-position: center; flex: 0 0 64px; }
- .cd-winner .label { text-transform: uppercase; letter-spacing: .08em; font-size: 11px; color: #715a1f; }
- .cd-winner .name { font-weight: 700; font-size: 20px; color: #2c2110; }
- .cd-winner .score { color: #715a1f; font-size: 14px; }
-</style>
-</head>
-<body>
- <main class="cd-wrap">
- <header class="cd-hero">
- <div style="font-size:13px;margin-bottom:10px"><a class="under" href="/marketplace/challenges">← All challenges</a> · <a class="under" href="/marketplace/leaderboard">Leaderboard</a></div>
- <h1 id="title">Loading…</h1>
- <div class="pills" id="pills"></div>
- <div class="meta" id="desc"></div>
- <div class="meta" id="window"></div>
- </header>
-
- <div id="winner"></div>
-
- <section style="margin-top:24px">
- <h2 style="font-size:20px;letter-spacing:-.01em;margin:0 0 12px">Top 50</h2>
- <div id="board" class="cd-table"></div>
- </section>
- </main>
-
-<script>
-function rankClass(rank) { if (rank===1) return 'top1'; if (rank===2) return 'top2'; if (rank===3) return 'top3'; return ''; }
-function fmtDate(iso) { return iso ? new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}) : ''; }
-
-async function load() {
- const id = location.pathname.split('/').pop();
- let json;
- try { json = await MP.api('/api/marketplace/challenges/' + encodeURIComponent(id)); }
- catch (e) { document.getElementById('title').textContent = 'Not found'; return; }
- const c = json.challenge;
- document.title = 'Wallco · ' + c.title;
- document.getElementById('title').textContent = c.title;
- document.getElementById('desc').textContent = c.description || '';
- document.getElementById('window').textContent = fmtDate(c.starts_at) + ' → ' + fmtDate(c.ends_at) + ' · metric: ' + c.metric;
- document.getElementById('pills').innerHTML = `<span class="metric">${c.metric}</span> <span class="status ${c.status}">${c.status}</span>`;
-
- if (c.status === 'closed' && c.winner_designer_id) {
- const w = document.getElementById('winner');
- const avatarStyle = c.winner_avatar ? `background-image:url(${c.winner_avatar.replace(/"/g,'%22')})` : 'background:linear-gradient(135deg,#e9e3d8,#cdbfa6)';
- w.className = 'cd-winner';
- w.innerHTML = `
- <div class="avatar" style="${avatarStyle}"></div>
- <div style="flex:1;min-width:0">
- <div class="label">Winner · +1000 pts · Top Seller badge</div>
- <div class="name"><a href="/designers/${c.winner_slug}">${c.winner_name}</a></div>
- <div class="score">Score: ${Number(c.winner_score||0).toLocaleString()} ${c.metric === 'points' ? 'points' : c.metric}</div>
- </div>`;
- }
-
- const board = document.getElementById('board');
- const rows = json.leaderboard || [];
- if (!rows.length) {
- board.innerHTML = '<div style="padding:24px;text-align:center;color:var(--mp-muted)">No entries yet.</div>';
- return;
- }
- rows.forEach((r, i) => {
- const rank = r.rank || (i + 1);
- const avatarStyle = r.avatar_url ? `background-image:url(${r.avatar_url.replace(/"/g,'%22')})` : 'background:linear-gradient(135deg,#e9e3d8,#cdbfa6)';
- const row = document.createElement('div');
- row.className = 'cd-row';
- row.innerHTML = `
- <div class="rank ${rankClass(rank)}">#${rank}</div>
- <div class="designer">
- <div class="avatar" style="${avatarStyle}"></div>
- <div>
- <div class="name"><a href="/designers/${r.slug}">${r.display_name}</a></div>
- <div class="level">${r.level || ''}</div>
- </div>
- </div>
- <div class="score">${Number(r.score || 0).toLocaleString()}</div>
- `;
- board.appendChild(row);
- });
-}
-
-document.addEventListener('DOMContentLoaded', load);
-</script>
-</body>
-</html>
diff --git a/public/marketplace/challenges.html b/public/marketplace/challenges.html
deleted file mode 100644
index a112370..0000000
--- a/public/marketplace/challenges.html
+++ /dev/null
@@ -1,114 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Monthly Challenges</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .ch-wrap { max-width: 1100px; margin: 0 auto; padding: var(--mp-pad); }
- .ch-hero { padding: clamp(36px,5vw,80px) var(--mp-pad) 12px; }
- .ch-hero h1 { font-size: clamp(34px,4vw,56px); margin: 0 0 10px; letter-spacing: -.02em; }
- .ch-hero p { color: var(--mp-muted); max-width: 720px; margin: 0; line-height: 1.55; }
- .ch-section h2 { font-size: 22px; letter-spacing: -.01em; margin: 28px 0 14px; }
- .ch-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(280px,1fr)); }
- .ch-card { padding: 18px 20px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 20px; box-shadow: var(--mp-glass-shadow); display: grid; gap: 10px; transition: transform .15s ease, box-shadow .15s ease; }
- .ch-card:hover { transform: translateY(-2px); box-shadow: 0 12px 36px rgba(20,20,20,.08); }
- .ch-card .ttl { font-weight: 700; font-size: 17px; letter-spacing: -.01em; }
- .ch-card .sub { font-size: 13px; color: var(--mp-muted); line-height: 1.5; }
- .ch-card .metric { display: inline-block; padding: 3px 9px; border-radius: 999px; background: #111; color: #fff; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; width: max-content; }
- .ch-card .status { display:inline-block; padding: 3px 9px; border-radius: 999px; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; width: max-content; }
- .ch-card .status.active { background: #0a7; color: #fff; }
- .ch-card .status.scheduled { background: rgba(20,20,20,.08); color: #444; }
- .ch-card .status.closed { background: #cda86e; color: #2c2110; }
- .ch-card .winner { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: rgba(20,20,20,.04); border-radius: 14px; }
- .ch-card .winner .avatar { width: 32px; height: 32px; border-radius: 50%; background: #ddd; background-size: cover; background-position: center; flex: 0 0 32px; }
- .ch-card .winner .name { font-weight: 600; font-size: 14px; }
- .ch-card .winner .label { font-size: 11px; color: var(--mp-muted); text-transform: uppercase; letter-spacing: .04em; }
- .ch-empty { padding: 28px; text-align: center; color: var(--mp-muted); }
-</style>
-</head>
-<body>
- <header class="ch-hero ch-wrap">
- <h1>Monthly Challenges</h1>
- <p>Compete with the Wallco designer community. Each challenge is a time-boxed window — top designers by the challenge metric earn 1,000 bonus points and the Top Seller badge.</p>
- </header>
-
- <main class="ch-wrap">
- <section class="ch-section">
- <h2>Active</h2>
- <div id="active" class="ch-grid"></div>
- </section>
-
- <section class="ch-section">
- <h2>Past winners</h2>
- <div id="closed" class="ch-grid"></div>
- </section>
-
- <section class="ch-section">
- <h2>Coming soon</h2>
- <div id="scheduled" class="ch-grid"></div>
- </section>
- </main>
-
-<script>
-function fmtDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}); }
-function dayRange(c) {
- return fmtDate(c.starts_at) + ' → ' + fmtDate(c.ends_at);
-}
-function challengeCard(c) {
- const card = document.createElement('a');
- card.className = 'ch-card';
- card.href = '/marketplace/challenges/' + c.id;
- let winnerHtml = '';
- if (c.status === 'closed' && c.winner_designer_id) {
- const avatarStyle = c.winner_avatar ? `background-image:url(${c.winner_avatar.replace(/"/g,'%22')})` : 'background:linear-gradient(135deg,#e9e3d8,#cdbfa6)';
- winnerHtml = `
- <div class="winner">
- <div class="avatar" style="${avatarStyle}"></div>
- <div>
- <div class="label">Winner</div>
- <div class="name">${c.winner_name || ''}</div>
- </div>
- </div>`;
- }
- card.innerHTML = `
- <div style="display:flex;gap:8px;flex-wrap:wrap">
- <span class="metric">${c.metric}</span>
- <span class="status ${c.status}">${c.status}</span>
- </div>
- <div class="ttl">${c.title}</div>
- <div class="sub">${c.description || ''}</div>
- <div class="sub">${dayRange(c)}</div>
- ${winnerHtml}
- `;
- return card;
-}
-
-async function load() {
- let json;
- try { json = await MP.api('/api/marketplace/challenges'); } catch (e) { return; }
- const all = json.challenges || [];
- const buckets = { active: [], scheduled: [], closed: [], cancelled: [] };
- all.forEach(c => { (buckets[c.status] || (buckets[c.status]=[])).push(c); });
- const sections = [
- ['active', 'No active challenges right now. Check back soon.'],
- ['closed', 'No closed challenges yet.'],
- ['scheduled', 'Nothing scheduled. Admins can create one from the admin console.'],
- ];
- sections.forEach(([k, emptyMsg]) => {
- const wrap = document.getElementById(k);
- const rows = buckets[k] || [];
- if (!rows.length) {
- wrap.innerHTML = `<div class="ch-empty">${emptyMsg}</div>`;
- return;
- }
- rows.forEach(c => wrap.appendChild(challengeCard(c)));
- });
-}
-
-document.addEventListener('DOMContentLoaded', load);
-</script>
-</body>
-</html>
diff --git a/public/marketplace/claim.html b/public/marketplace/claim.html
deleted file mode 100644
index 027edfb..0000000
--- a/public/marketplace/claim.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Claim your designer page · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Claim your designer page.</h1>
- <p class="lede">Wallco verifies every claim before publishing. If a public stub already exists for you, claiming it converts it into your official storefront, portfolio, and commission account.</p>
- </header>
- <main class="mp-wrap">
- <div class="mp-claim-banner">
- <h3>Disclaimer</h3>
- <p class="small">Any unclaimed public designer profile on Wallco is <strong>not affiliated with or endorsed by the designer</strong>. Those pages contain no copied artwork, no copied bios, and no listings from other platforms. Claiming a page is how a designer adds their official portfolio, storefront, and commission settings.</p>
- </div>
-
- <section class="mp-card" style="padding: 22px;">
- <h2 style="margin-top:0">Claim form</h2>
- <form id="claim" class="mp-form">
- <label>Your name *<input type="text" name="name" required maxlength="100"/></label>
- <label>Email *<input type="email" name="email" required maxlength="200"/></label>
- <label>Unclaimed page slug (if known) <input type="text" name="slug" placeholder="e.g. pattern-studio-one"/></label>
- <div class="row">
- <label>Website <input type="url" name="website_url"/></label>
- <label>Instagram <input type="url" name="instagram_url"/></label>
- </div>
- <label>Portfolio URL <input type="url" name="portfolio_url"/></label>
- <label>Proof / context (a sentence or two so we can verify it's you)
- <textarea name="proof" maxlength="2000"></textarea>
- </label>
- <button type="submit" class="mp-btn">Submit claim</button>
- <div id="msg" class="err" style="display:none"></div>
- </form>
- </section>
- </main>
-
-<script>
-document.getElementById('claim').addEventListener('submit', async (e) => {
- e.preventDefault();
- const msg = document.getElementById('msg'); msg.style.display='none';
- const body = Object.fromEntries(new FormData(e.target).entries());
- try {
- const r = await fetch('/api/marketplace/designer/claim', { method:'POST', credentials:'include', headers:{'content-type':'application/json'}, body: JSON.stringify(body) });
- const j = await r.json();
- if(!r.ok) throw new Error(j.error || 'failed');
- msg.className='ok'; msg.style.display='block';
- msg.textContent = 'Claim submitted. Wallco will verify and reach out — typically within 48 hours.';
- e.target.reset();
- } catch (err) {
- msg.className='err'; msg.style.display='block'; msg.textContent = err.message;
- }
-});
-</script>
-</body>
-</html>
diff --git a/public/marketplace/dashboard.html b/public/marketplace/dashboard.html
deleted file mode 100644
index 77ceaf3..0000000
--- a/public/marketplace/dashboard.html
+++ /dev/null
@@ -1,222 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Designer Dashboard · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .tabs { display:flex; gap: 8px; margin: 18px 0; flex-wrap: wrap; }
- .tab-btn { padding: 8px 16px; border-radius: 999px; border: 1px solid var(--mp-border); background: rgba(255,255,255,.7); cursor: pointer; font-size: 14px; }
- .tab-btn.active { background: #111; color: #fff; }
- .panel { display:none; }
- .panel.active { display: block; }
- table { width: 100%; border-collapse: collapse; font-size: 14px; }
- th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--mp-border); }
- th { color: var(--mp-muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; }
- .level-bar { height: 10px; background: rgba(20,20,20,.06); border-radius: 999px; overflow: hidden; margin-top:8px; }
- .level-bar > div { height: 100%; background: linear-gradient(90deg,#111,#bfa76a); transition: width .4s ease; }
- .upload-card { padding: 20px; background: rgba(255,255,255,.7); border:1px solid var(--mp-border); border-radius: 18px; box-shadow: var(--mp-glass-shadow); }
- .badges-row { display: flex; flex-wrap: wrap; gap: 8px; }
- .badge-tile { display:flex; align-items:center; gap:8px; padding:8px 14px; border-radius: 999px; background: rgba(255,255,255,.7); border: 1px solid var(--mp-border); font-size: 13px; }
-</style>
-</head>
-<body>
- <main class="mp-wrap" id="root">
- <div id="unauth" style="display:none; padding: 60px 0; text-align:center;">
- <h2>Not signed in</h2>
- <p>Apply as a designer or claim your profile to access the dashboard.</p>
- <a class="mp-btn" href="/marketplace/apply">Apply now</a>
- </div>
-
- <section id="authed" style="display:none;">
- <header style="display:flex; align-items:center; gap: 20px; margin-bottom: 22px;">
- <div style="flex:1">
- <h1 id="hello" style="margin: 0">Welcome back</h1>
- <div style="color: var(--mp-muted)" id="meta"></div>
- </div>
- <button class="mp-btn ghost" id="logoutBtn">Log out</button>
- </header>
-
- <div class="mp-status-grid" id="kpi"></div>
-
- <section style="margin: 22px 0;">
- <strong>Next level</strong>
- <div id="nextLevel" style="color: var(--mp-muted); margin-top: 4px;"></div>
- <div class="level-bar"><div id="levelFill" style="width: 0%"></div></div>
- </section>
-
- <section class="badges-row" id="badges"></section>
-
- <div class="tabs" id="tabs">
- <button class="tab-btn active" data-tab="patterns">Patterns</button>
- <button class="tab-btn" data-tab="upload">Upload</button>
- <button class="tab-btn" data-tab="commissions">Commissions</button>
- <button class="tab-btn" data-tab="points">Points ledger</button>
- <button class="tab-btn" data-tab="storefront">Storefront</button>
- </div>
-
- <section class="panel active" id="panel-patterns">
- <h3>Your patterns</h3>
- <div class="mp-grid lg" id="patternsGrid"></div>
- </section>
-
- <section class="panel" id="panel-upload">
- <div class="upload-card">
- <h3 style="margin-top:0">Upload a pattern</h3>
- <form id="uploadForm" class="mp-form" enctype="multipart/form-data">
- <label>Pattern title *<input type="text" name="title" required maxlength="120"/></label>
- <label>Description<textarea name="description" maxlength="4000"></textarea></label>
- <div class="row">
- <label>Repeat type<select name="repeat_type"><option>straight</option><option>half-drop</option><option>mirror</option><option>random</option></select></label>
- <label>Scale notes<input type="text" name="scale_notes"/></label>
- </div>
- <label>Style tags <input type="text" name="style_tags" placeholder="Botanical, Maximalist"/></label>
- <label>Room tags <input type="text" name="room_tags" placeholder="Powder Room, Bedroom"/></label>
- <label>Color tags <input type="text" name="color_tags" placeholder="Olive, Plaster"/></label>
- <div class="row">
- <label class="check"><input type="checkbox" name="commercial_suitable"/> Commercial-suitable</label>
- <label class="check"><input type="checkbox" name="hospitality_ready"/> Hospitality-ready</label>
- </div>
- <label>Image (≤50MB, JPG/PNG/WEBP) * <input type="file" name="image" required accept="image/*"/></label>
- <label class="check">
- <input type="checkbox" name="rights_warranty" required/>
- I warrant I own or control all rights to this artwork.
- </label>
- <button type="submit" class="mp-btn">Submit for review</button>
- <div id="uploadMsg" class="err" style="display:none"></div>
- </form>
- </div>
- </section>
-
- <section class="panel" id="panel-commissions">
- <h3>Commission ledger</h3>
- <table id="commissionTable"><thead><tr><th>When</th><th>Type</th><th>Basis</th><th>Rate</th><th>Amount</th><th>Status</th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel" id="panel-points">
- <h3>Points history</h3>
- <table id="pointsTable"><thead><tr><th>When</th><th>Action</th><th>Points</th><th>Notes</th></tr></thead><tbody></tbody></table>
- </section>
-
- <section class="panel" id="panel-storefront">
- <h3>Your public storefront</h3>
- <p style="color: var(--mp-muted)">Your storefront lives at <code id="storeUrl"></code>. Share this link with clients and trade buyers.</p>
- <a class="mp-btn" id="visitStore" target="_blank" rel="noopener noreferrer">Visit storefront →</a>
- </section>
- </section>
- </main>
-
-<script>
-async function load() {
- try {
- const r = await fetch('/api/marketplace/designer/me', { credentials: 'include' });
- if (r.status === 401) {
- document.getElementById('unauth').style.display='block';
- return;
- }
- const j = await r.json();
- document.getElementById('authed').style.display='block';
- const d = j.designer;
- document.getElementById('hello').textContent = `Welcome back, ${d.display_name}`;
- document.getElementById('meta').textContent = `${d.studio_name || ''} · ${d.level} · ${Number(d.points||0).toLocaleString()} pts`;
-
- const next = j.next_level;
- document.getElementById('nextLevel').textContent = next ? `Next level: ${next.name} at ${next.points.toLocaleString()} pts (you’re at ${(d.points||0).toLocaleString()})` : 'Top level reached — Hall of Pattern.';
- if (next) document.getElementById('levelFill').style.width = (Math.min(100, (d.points||0) / next.points * 100)) + '%';
- else document.getElementById('levelFill').style.width = '100%';
-
- const kpi = document.getElementById('kpi');
- const pending = j.commission_ledger.filter(c => ['pending','approved','payable'].includes(c.status)).reduce((s,c) => s + Number(c.commission_amount), 0);
- const paid = j.commission_ledger.filter(c => c.status === 'paid').reduce((s,c) => s + Number(c.commission_amount), 0);
- const items = [
- ['Patterns', j.patterns.length],
- ['Approved', j.patterns.filter(p => p.status==='approved').length],
- ['Pending', j.patterns.filter(p => p.status==='pending').length],
- ['Points', Number(d.points||0).toLocaleString()],
- ['Pending $', '$'+pending.toFixed(2)],
- ['Paid $', '$'+paid.toFixed(2)],
- ];
- kpi.innerHTML='';
- items.forEach(([l,v]) => { const c = document.createElement('div'); c.className='mp-stat'; c.innerHTML=`<div class="label">${l}</div><div class="value">${v}</div>`; kpi.appendChild(c); });
-
- const badges = document.getElementById('badges'); badges.innerHTML='';
- j.badges.forEach(b => { const e = document.createElement('div'); e.className='badge-tile'; e.innerHTML=`<span>${b.icon || '★'}</span> ${b.badge_name}`; badges.appendChild(e); });
-
- const pg = document.getElementById('patternsGrid'); pg.innerHTML='';
- j.patterns.forEach(p => {
- const card = document.createElement('div'); card.className='mp-card';
- card.innerHTML=`<div class="img" style="background-image:url('${p.thumbnail_url||''}')"></div>
- <div class="body">
- <div class="title">${p.title}</div>
- <div class="sub">${p.status} · ${p.view_count} views · ${p.save_count} saves</div>
- <div class="row">
- <span class="mp-pill ${p.status==='approved'?'verified': (p.status==='pending'?'':'')}">${p.status}</span>
- <a class="mp-btn tiny ghost" href="/patterns/${p.slug}" target="_blank" rel="noopener noreferrer">View</a>
- </div>
- </div>`;
- pg.appendChild(card);
- });
-
- const ct = document.getElementById('commissionTable').querySelector('tbody'); ct.innerHTML='';
- j.commission_ledger.forEach(c => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${new Date(c.created_at).toLocaleDateString()}</td><td>${c.entry_type}</td><td>$${Number(c.basis_amount).toFixed(2)}</td><td>${c.commission_rate}%</td><td><strong>$${Number(c.commission_amount).toFixed(2)}</strong></td><td><span class="mp-pill ${c.status==='paid'?'verified':''}">${c.status}</span></td>`;
- ct.appendChild(tr);
- });
-
- const pt = document.getElementById('pointsTable').querySelector('tbody'); pt.innerHTML='';
- j.points_ledger.forEach(p => {
- const tr = document.createElement('tr');
- tr.innerHTML = `<td>${new Date(p.created_at).toLocaleDateString()}</td><td>${p.action}</td><td>+${p.points}</td><td>${p.notes || ''}</td>`;
- pt.appendChild(tr);
- });
-
- const storeUrl = `${location.origin}/store/${d.slug}`;
- document.getElementById('storeUrl').textContent = storeUrl;
- document.getElementById('visitStore').href = `/store/${d.slug}`;
-
- } catch (err) {
- document.getElementById('unauth').style.display='block';
- console.error(err);
- }
-}
-
-// tabs
-document.addEventListener('click', (e) => {
- const t = e.target.closest('.tab-btn'); if(!t) return;
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
- document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
- t.classList.add('active');
- document.getElementById('panel-' + t.dataset.tab).classList.add('active');
-});
-
-document.getElementById('logoutBtn').addEventListener('click', async () => {
- await fetch('/api/marketplace/designer/logout', { method:'POST', credentials:'include' });
- location.reload();
-});
-
-document.getElementById('uploadForm').addEventListener('submit', async (e) => {
- e.preventDefault();
- const msg = document.getElementById('uploadMsg'); msg.style.display='none';
- const fd = new FormData(e.target);
- fd.set('commercial_suitable', e.target.commercial_suitable.checked ? 'true' : 'false');
- fd.set('hospitality_ready', e.target.hospitality_ready.checked ? 'true' : 'false');
- fd.set('rights_warranty', e.target.rights_warranty.checked ? 'true' : 'false');
- try {
- const r = await fetch('/api/marketplace/patterns/upload', { method:'POST', credentials:'include', body: fd });
- const j = await r.json();
- if(!r.ok) throw new Error(j.error || 'upload failed');
- msg.className='ok'; msg.style.display='block'; msg.textContent = 'Uploaded · pending admin approval.';
- setTimeout(load, 500);
- e.target.reset();
- } catch (err) {
- msg.className='err'; msg.style.display='block'; msg.textContent = err.message;
- }
-});
-
-load();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/designer-profile.html b/public/marketplace/designer-profile.html
deleted file mode 100644
index e2ca721..0000000
--- a/public/marketplace/designer-profile.html
+++ /dev/null
@@ -1,224 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Designer · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <main id="root"><div class="mp-wrap"><p>Loading…</p></div></main>
-
-<style>
- .contact-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); margin-top: 12px; }
- .contact-cell { padding: 14px 16px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; }
- .contact-cell .label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--mp-muted); margin-bottom: 4px; }
- .contact-cell .value { font-size: 14px; word-break: break-all; }
- .contact-cell .value a { color: #0a7; text-decoration: none; }
- .contact-cell .value a:hover { text-decoration: underline; }
-
- .social-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
- .social-row a { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; background: rgba(20,20,20,.06); color: #222; font-size: 13px; }
- .social-row a:hover { background: rgba(20,20,20,.12); }
-
- .video-gallery { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(320px,1fr)); }
- .video-tile { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; overflow: hidden; box-shadow: var(--mp-glass-shadow); }
- .video-tile .frame { position: relative; aspect-ratio: 16/9; background: #000; }
- .video-tile .frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
- .video-tile .frame a { display:flex; align-items:center; justify-content:center; height:100%; color:#fff; font-size:14px; background-size: cover; background-position: center; }
- .video-tile .meta { padding: 12px 14px; }
- .video-tile .meta .ttl { font-weight: 600; font-size: 14px; }
- .video-tile .meta .desc { font-size: 13px; color: var(--mp-muted); margin-top: 4px; line-height: 1.45; }
-</style>
-<script>
-const slug = location.pathname.split('/').filter(Boolean).pop();
-
-function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
-function escapeAttr(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<'); }
-
-function contactSection(d) {
- const cells = [];
- // Website is always public.
- if (d.website_url) cells.push({ label: 'Website', html: `<a href="${escapeAttr(d.website_url)}" target="_blank" rel="noopener">${escapeHtml(d.website_url.replace(/^https?:\/\//,''))}</a>` });
- // City/state always public.
- const cityState = [d.city || d.derived_city, d.state_region || d.derived_state, d.country].filter(Boolean).join(', ');
- if (cityState) cells.push({ label: 'Location', html: escapeHtml(cityState) });
- // Contact PII only if consented (server already redacted otherwise).
- if (d.contact_email) cells.push({ label: 'Email', html: `<a href="mailto:${escapeAttr(d.contact_email)}">${escapeHtml(d.contact_email)}</a>` });
- if (d.phone) cells.push({ label: 'Phone', html: `<a href="tel:${escapeAttr(d.phone.replace(/[^+\d]/g,''))}">${escapeHtml(d.phone)}</a>` });
- if (d.address_line1) {
- const addr = [d.address_line1, d.address_line2, [d.city, d.state_region, d.postal_code].filter(Boolean).join(' ')].filter(Boolean).join('\n');
- cells.push({ label: 'Studio address', html: escapeHtml(addr).replace(/\n/g,'<br/>') });
- }
-
- const socials = [
- ['instagram_url','Instagram','📸'],
- ['tiktok_url','TikTok','🎵'],
- ['pinterest_url','Pinterest','📌'],
- ['youtube_url','YouTube','▶︎'],
- ['facebook_url','Facebook','f'],
- ['twitter_url','X','𝕏'],
- ['linkedin_url','LinkedIn','in'],
- ].filter(([key]) => d[key]);
-
- if (!cells.length && !socials.length) return '';
-
- const cellsHtml = cells.length
- ? `<div class="contact-grid">${cells.map(c => `<div class="contact-cell"><div class="label">${c.label}</div><div class="value">${c.html}</div></div>`).join('')}</div>`
- : '';
- const socialsHtml = socials.length
- ? `<div class="social-row">${socials.map(([key,name,icon]) => `<a href="${escapeAttr(d[key])}" target="_blank" rel="noopener"><span style="font-weight:700">${icon}</span> ${name}</a>`).join('')}</div>`
- : '';
-
- return `<section class="mp-section"><h2>Contact + social</h2>${cellsHtml}${socialsHtml}</section>`;
-}
-
-function videoSection(videos) {
- if (!videos || !videos.length) return '';
- const tiles = videos.map(v => {
- let frame;
- if (v.provider === 'youtube' && v.embed_id) {
- frame = `<iframe src="https://www.youtube.com/embed/${escapeAttr(v.embed_id)}" allowfullscreen allow="autoplay; encrypted-media"></iframe>`;
- } else if (v.provider === 'vimeo' && v.embed_id) {
- frame = `<iframe src="https://player.vimeo.com/video/${escapeAttr(v.embed_id)}" allowfullscreen></iframe>`;
- } else {
- const thumb = v.thumbnail_url ? `background-image:url(${escapeAttr(v.thumbnail_url)})` : '';
- frame = `<a href="${escapeAttr(v.url)}" target="_blank" rel="noopener" style="${thumb}">▶︎ Watch on ${escapeHtml(v.provider)}</a>`;
- }
- return `<div class="video-tile">
- <div class="frame">${frame}</div>
- <div class="meta">
- <div class="ttl">${escapeHtml(v.title || v.provider + ' video')}</div>
- ${v.description ? `<div class="desc">${escapeHtml(v.description)}</div>` : ''}
- </div>
- </div>`;
- }).join('');
- return `<section class="mp-section"><h2>Videos</h2><div class="video-gallery">${tiles}</div></section>`;
-}
-
-async function load() {
- const j = await fetch('/api/marketplace/designers/' + encodeURIComponent(slug)).then(r => r.json());
- const root = document.getElementById('root');
- root.innerHTML = '';
- if (j.claim && !j.designer) {
- // unclaimed page
- root.innerHTML = `
- <header class="mp-storefront-hero">
- <div class="avatar" style="background: linear-gradient(135deg,#ddd,#bbb);"></div>
- <div>
- <h1>${j.claim.public_name}</h1>
- <div class="studio">Unclaimed designer profile</div>
- </div>
- </header>
- <div class="mp-wrap">
- <div class="mp-claim-banner">
- <h3>Are you ${j.claim.public_name}?</h3>
- <div class="small">${j.claim.disclaimer}</div>
- <a class="mp-btn" href="/marketplace/claim?slug=${j.claim.slug}">Claim this page</a>
- </div>
- <section class="mp-section">
- <h2>What you'll get by claiming</h2>
- <div class="mp-grid lg">
- <div class="mp-card"><div class="body"><strong>Storefront</strong><div class="sub">/store/${j.claim.slug}</div></div></div>
- <div class="mp-card"><div class="body"><strong>Pattern uploads</strong><div class="sub">With AI colorways and room mockups</div></div></div>
- <div class="mp-card"><div class="body"><strong>Commissions</strong><div class="sub">15–20% wallpaper royalty, 60–70% licensing</div></div></div>
- <div class="mp-card"><div class="body"><strong>Levels & badges</strong><div class="sub">Founding Designer, Verified, Hospitality Ready, more</div></div></div>
- </div>
- </section>
- </div>`;
- return;
- }
- if (!j.designer) { root.innerHTML = `<div class="mp-wrap"><h2>Not found</h2></div>`; return; }
- const d = j.designer;
- const badgePills = (j.badges || []).map(b => `<span class="mp-pill">${b.icon || '★'} ${b.badge_name}</span>`).join(' ');
- root.innerHTML = `
- <header class="mp-storefront-hero">
- <div class="avatar" style="background-image: linear-gradient(135deg,#ddd,#bbb); background-size: cover;"></div>
- <div>
- <h1>${d.display_name}</h1>
- <div class="studio">${d.studio_name || ''} · ${d.location || ''}</div>
- <div class="badges">${badgePills}</div>
- <div class="meta">
- <span class="mp-pill ${d.is_founding ? 'gold' : ''}">${d.level}</span>
- ${d.is_verified ? '<span class="mp-pill verified">Verified</span>' : ''}
- <span>${Number(d.points||0).toLocaleString()} pts</span>
- <span>${(j.patterns || []).length} patterns</span>
- <button class="mp-btn tiny" id="mp-follow-btn" data-designer-id="${d.id}" type="button" style="display:none">Follow</button>
- <span id="mp-follow-count" class="mp-pill" style="display:none"></span>
- <a class="mp-btn tiny" href="/store/${d.slug}">Visit storefront →</a>
- </div>
- </div>
- </header>
-
- <div class="mp-wrap">
- ${d.bio ? `<section class="mp-section"><h2>About</h2><p style="max-width: 720px; line-height: 1.6; color: var(--mp-muted);">${d.bio}</p></section>` : ''}
- ${contactSection(d)}
- ${videoSection(j.videos)}
- ${(j.collections||[]).length ? `<section class="mp-section">
- <h2>Collections</h2>
- <div class="mp-grid lg">
- ${j.collections.map(c => `<a class="mp-card" href="#"><div class="img" style="background: linear-gradient(135deg,#eee,#ddd)"></div><div class="body"><div class="title">${c.title}</div></div></a>`).join('')}
- </div>
- </section>` : ''}
- <section class="mp-section">
- <h2>Patterns</h2>
- <div class="mp-grid lg" id="pat"></div>
- </section>
- </div>`;
-
- const pat = document.getElementById('pat');
- (j.patterns || []).forEach(p => {
- const a = document.createElement('a'); a.className='mp-card'; a.href=`/patterns/${p.slug}`;
- a.innerHTML = `
- <div class="img" style="background-image:url('${p.thumbnail_url || p.original_image_url}')"></div>
- <div class="body">
- <div class="title">${p.title}</div>
- <div class="sub">${(p.style_tags||[]).slice(0,2).join(' · ')}</div>
- </div>`;
- pat.appendChild(a);
- });
-
- setupFollow(d.id, d.slug);
-}
-
-async function setupFollow(designerId, designerSlug) {
- const btn = document.getElementById('mp-follow-btn');
- const cntEl = document.getElementById('mp-follow-count');
- if (!btn) return;
- let st;
- try {
- st = await (await fetch('/api/marketplace/designers/' + encodeURIComponent(designerSlug) + '/follow', { credentials: 'include' })).json();
- } catch (_) { return; }
- btn.style.display = '';
- cntEl.style.display = '';
- render(st);
- btn.addEventListener('click', async () => {
- const next = !btn.dataset.following || btn.dataset.following === 'false';
- btn.disabled = true;
- try {
- const r = await fetch('/api/marketplace/designers/' + designerId + '/follow', {
- method: next ? 'POST' : 'DELETE',
- credentials: 'include',
- });
- if (r.status === 401) { window.location = '/marketplace/apply'; return; }
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'Follow failed');
- render(j);
- } catch (err) {
- alert(err.message);
- } finally {
- btn.disabled = false;
- }
- });
- function render(s) {
- btn.dataset.following = String(!!s.following);
- btn.textContent = s.following ? 'Following ✓' : 'Follow';
- btn.classList.toggle('ghost', !!s.following);
- cntEl.textContent = (Number(s.count) || 0).toLocaleString() + ' follower' + (Number(s.count) === 1 ? '' : 's');
- }
-}
-load();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/designers.html b/public/marketplace/designers.html
deleted file mode 100644
index e08f33d..0000000
--- a/public/marketplace/designers.html
+++ /dev/null
@@ -1,133 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Designers · Wallco Marketplace</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .controls { display:flex; gap: 10px; flex-wrap:wrap; margin: 14px 0 22px; align-items:center; }
- .controls select { padding: 8px 12px; border-radius: 999px; border: 1px solid var(--mp-border); background: rgba(255,255,255,.8); font-size: 14px; }
- .controls input[type=range] { flex: 0 0 200px; }
- .mp-card .loc { font-size: 12px; color: var(--mp-muted); display:inline-flex; align-items:center; gap:4px; }
- .mp-card .loc.weblink { color: #0a7; }
- .mp-card .loc:not(.weblink)::before { content: '📍'; font-size: 11px; }
-</style>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Designers</h1>
- <p class="lede">Independent creators publishing on Wallco. Each designer has a storefront, collections, and AI-generated colorways. Founding designers earn 20% royalty on wallpaper.</p>
- </header>
- <main class="mp-wrap">
- <div class="controls">
- <span class="sub">Sort</span>
- <select id="sort">
- <option value="featured">Featured first</option>
- <option value="points">Highest points</option>
- <option value="newest">Newest</option>
- <option value="location">Location (A→Z)</option>
- </select>
- <span class="sub" style="margin-left: 14px">Location</span>
- <select id="locFilter">
- <option value="">All locations</option>
- </select>
- <span class="sub" style="margin-left: 14px">Density</span>
- <input type="range" id="density" min="180" max="380" value="280" step="10"/>
- </div>
- <div class="mp-grid lg" id="grid"></div>
- </main>
-
-<script>
-const grid = document.getElementById('grid');
-const sortSel = document.getElementById('sort');
-const locFilter = document.getElementById('locFilter');
-const density = document.getElementById('density');
-
-const KEY = 'wallco.marketplace.designers';
-const saved = JSON.parse(localStorage.getItem(KEY) || '{}');
-if (saved.sort) sortSel.value = saved.sort;
-if (saved.location) {
- // Will set after locations load; cached for now.
- locFilter.dataset.pending = saved.location;
-}
-if (saved.density) density.value = saved.density;
-applyDensity();
-
-function applyDensity() {
- grid.style.gridTemplateColumns = `repeat(auto-fill, minmax(${density.value}px, 1fr))`;
-}
-
-function persist() {
- localStorage.setItem(KEY, JSON.stringify({
- sort: sortSel.value,
- location: locFilter.value,
- density: density.value,
- }));
-}
-
-async function loadLocations() {
- let j;
- try { j = await fetch('/api/marketplace/designers/locations').then(r => r.json()); }
- catch (_) { return; }
- const locs = j.locations || [];
- locs.forEach(loc => {
- const opt = document.createElement('option');
- opt.value = loc.location;
- opt.textContent = `${loc.location} (${loc.designer_count})`;
- locFilter.appendChild(opt);
- });
- if (locFilter.dataset.pending) {
- locFilter.value = locFilter.dataset.pending;
- delete locFilter.dataset.pending;
- }
-}
-
-async function load() {
- const sort = sortSel.value;
- const loc = locFilter.value;
- persist();
- const params = new URLSearchParams({ sort, limit: '120' });
- if (loc) params.set('location', loc);
- const j = await fetch('/api/marketplace/designers?' + params.toString()).then(r => r.json());
- grid.innerHTML = '';
- const rows = j.designers || [];
- if (!rows.length) {
- grid.innerHTML = '<div class="sub" style="padding:24px;grid-column:1/-1">No designers match this filter yet.</div>';
- return;
- }
- rows.forEach(d => {
- const a = document.createElement('a'); a.className='mp-card'; a.href=`/designers/${d.slug}`;
- const locBits = [];
- if (d.city) locBits.push(d.city);
- if (d.state && d.state !== d.city) locBits.push(d.state);
- const locPill = locBits.length ? `<span class="loc">${locBits.join(', ')}</span>` : '';
- const webHtml = d.website_url ? ` · <span class="loc weblink">🔗 Has site</span>` : '';
- a.innerHTML = `
- <div class="img" style="background: linear-gradient(135deg,#eee,#ddd);"></div>
- <div class="body">
- <div class="title">${d.display_name}</div>
- <div class="sub">${d.studio_name || ''}${d.studio_name && (d.style_tags||[]).length ? ' · ' : ''}${(d.style_tags||[]).slice(0,2).join(' · ')}</div>
- <div>${locPill}${webHtml}</div>
- <div class="row">
- <span class="mp-pill ${d.is_founding ? 'gold' : ''}">${d.level}</span>
- ${d.is_verified ? '<span class="mp-pill verified">Verified</span>' : ''}
- <span class="sub">${Number(d.points||0).toLocaleString()} pts</span>
- </div>
- </div>`;
- grid.appendChild(a);
- });
-}
-
-sortSel.addEventListener('change', load);
-locFilter.addEventListener('change', load);
-density.addEventListener('input', () => { applyDensity(); persist(); });
-
-(async () => {
- await loadLocations();
- load();
-})();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/index.html b/public/marketplace/index.html
deleted file mode 100644
index 32ddd7a..0000000
--- a/public/marketplace/index.html
+++ /dev/null
@@ -1,156 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Designer Marketplace</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Where pattern designers become brands.</h1>
- <p class="lede">Wallco is the AI-powered marketplace for designer-led wallcoverings. Independent creators upload patterns, get instant colorways, build storefronts, and earn commissions from samples, full orders, and licensing — all backed by Designer Wallcoverings production.</p>
- <div class="cta-row">
- <a class="mp-btn" href="/marketplace/apply">Become a Wallco Designer</a>
- <a class="mp-btn ghost" href="/designers">Browse Designers</a>
- <a class="mp-btn ghost" href="/patterns">Browse Patterns</a>
- <a class="mp-btn ghost" href="/marketplace/api">For Agents · API</a>
- </div>
- </header>
-
- <main class="mp-wrap">
-
- <section class="mp-section">
- <h2>The marketplace, by the numbers</h2>
- <div class="mp-status-grid" id="stats"></div>
- </section>
-
- <section class="mp-section">
- <h2>Featured designers</h2>
- <div class="sub">Founding creators publishing collections, colorways, and licensing inventory on Wallco.</div>
- <div class="mp-grid lg" id="designers"></div>
- </section>
-
- <section class="mp-section">
- <h2>Featured patterns</h2>
- <div class="sub">Designer-led wallcoverings. Sample first, license, or specify for trade.</div>
- <div class="mp-grid lg" id="patterns"></div>
- </section>
-
- <section class="mp-section">
- <h2>What designers get</h2>
- <div class="mp-grid lg">
- <div class="mp-card"><div class="body"><strong>Your storefront</strong><div class="sub">wallco.ai/store/your-slug. Hero, collections, samples, licensing inquiries, custom-design CTA.</div></div></div>
- <div class="mp-card"><div class="body"><strong>AI colorways</strong><div class="sub">Six instant palettes per pattern — warm clay, moss & linen, ink blue, rose plaster, gold smoke, original.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Transparent commissions</strong><div class="sub">Net-revenue royalty, ledger-tracked. Founding designers earn 20% on wallpaper, 60–70% on licensing.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Levels & badges</strong><div class="sub">From New Creator → Pattern Maker → Featured → Collection Artist → Trade Favorite → Wallco Signature → Hall of Pattern.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Trade exposure</strong><div class="sub">Interior designers save patterns to project boards, order samples, request custom recolors and murals.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Production-grade</strong><div class="sub">Backed by Designer Wallcoverings — Type II commercial, peel-and-stick, non-woven, grasscloth substrates.</div></div></div>
- </div>
- </section>
-
- <section class="mp-section" id="agents">
- <h2>Agents welcome. Bots are not.</h2>
- <div class="sub">
- Wallco accepts <strong>qualified AI agents</strong> that represent a real designer, studio,
- or brand — agents that upload patterns, set pricing, respond to license inquiries, and
- earn commission on behalf of the human they represent. We do <strong>not</strong> accept
- scraping bots, spam uploaders, price-arbitrage automations, or anything that doesn't
- round-trip back to a verified human creator.
- </div>
- <div class="mp-grid lg" style="margin-top:14px">
- <div class="mp-card"><div class="body"><strong>What "qualified" means</strong><div class="sub">
- (1) tied to a verified designer profile, (2) accepts the same rights warranty as a human
- uploader, (3) first 3 patterns reviewed manually before auto-approval, (4) signed
- agent-of-record agreement on file.</div></div></div>
- <div class="mp-card"><div class="body"><strong>What you can automate</strong><div class="sub">
- Pattern upload, AI colorway generation, auto-tag, auto-copy, license-inquiry triage,
- commission ledger reads, project saves for trade buyers, sample-order fulfillment hooks.</div></div></div>
- <div class="mp-card"><div class="body"><strong>What we block</strong><div class="sub">
- Mass uploads without rights-warranty per-asset, scraping the marketplace itself,
- automated price undercutting, fake license inquiries, anything violating the DW
- Settlement Agreement (auto-checked at upload).</div></div></div>
- <div class="mp-card"><div class="body"><strong>How to qualify</strong><div class="sub">
- Apply at <a href="/marketplace/apply">/marketplace/apply</a> with your designer profile
- + a one-line statement that an agent will be acting on your behalf. Read the
- <a href="/marketplace/api">API docs</a> for endpoints, auth, and rate limits.</div></div></div>
- </div>
- <div style="margin-top:18px">
- <a class="mp-btn" href="/marketplace/api">Read the Agent API docs →</a>
- </div>
- </section>
-
- <section class="mp-section">
- <h2>The model, in plain language</h2>
- <div class="mp-grid lg">
- <div class="mp-card"><div class="body"><strong>Patternbank-style licensing</strong><div class="sub">Designers earn from non-exclusive and exclusive licensing deals. Every inquiry tracked, every contract attributable.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Material Bank-style sampling</strong><div class="sub">Trade buyers order samples, save to project boards, get client approval — Wallco fulfills.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Designer storefronts</strong><div class="sub">Every approved designer gets a personal storefront, collections, and follow-button discovery.</div></div></div>
- <div class="mp-card"><div class="body"><strong>Wallco × AI</strong><div class="sub">Auto-tag, auto-colorway, auto-mockup. One pattern becomes a sellable family of SKUs.</div></div></div>
- </div>
- </section>
-
- </main>
-
-<script>
-async function loadHome() {
- const status = await fetch('/api/marketplace/status').then(r => r.json());
- const grid = document.getElementById('stats');
- const STATS = [
- ['designers_approved', 'Approved designers'],
- ['patterns_approved', 'Approved patterns'],
- ['collections', 'Collections'],
- ['commission_entries', 'Commission events'],
- ['pending_commission', 'Pending $ to designers', '$'],
- ['unclaimed', 'Awaiting claim'],
- ];
- STATS.forEach(([k,label,prefix]) => {
- const v = status.counts[k] || 0;
- const card = document.createElement('div'); card.className = 'mp-stat';
- const display = (prefix === '$') ? ('$' + Number(v).toLocaleString(undefined,{maximumFractionDigits:2})) : Number(v).toLocaleString();
- card.innerHTML = `<div class="label">${label}</div><div class="value">${display}</div>`;
- grid.appendChild(card);
- });
-
- const designers = await fetch('/api/marketplace/designers?sort=featured&limit=6').then(r => r.json());
- const dWrap = document.getElementById('designers');
- (designers.designers || []).forEach(d => {
- const card = document.createElement('a');
- card.className = 'mp-card'; card.href = '/designers/' + d.slug;
- card.innerHTML = `
- <div class="img" style="background:linear-gradient(135deg,#eee,#ddd)"></div>
- <div class="body">
- <div class="title">${d.display_name}</div>
- <div class="sub">${d.studio_name || ''}</div>
- <div class="row">
- <span class="mp-pill ${d.is_founding ? 'gold' : ''}">${d.level}</span>
- ${d.is_verified ? '<span class="mp-pill verified">Verified</span>' : ''}
- </div>
- <div class="sub" style="margin-top:4px">${(d.style_tags||[]).slice(0,3).join(' · ')}</div>
- </div>`;
- dWrap.appendChild(card);
- });
-
- const patterns = await fetch('/api/marketplace/patterns?limit=12').then(r => r.json());
- const pWrap = document.getElementById('patterns');
- (patterns.patterns || []).forEach(p => {
- const card = document.createElement('a');
- card.className = 'mp-card'; card.href = '/patterns/' + p.slug;
- card.innerHTML = `
- <div class="img" style="background-image:url('${p.thumbnail_url}')"></div>
- <div class="body">
- <div class="title">${p.title}</div>
- <div class="sub">by ${p.designer_name}</div>
- <div class="row">
- ${(p.style_tags||[]).slice(0,2).map(t => `<span class="mp-pill">${t}</span>`).join(' ')}
- ${p.featured ? '<span class="mp-pill gold">Featured</span>' : ''}
- </div>
- </div>`;
- pWrap.appendChild(card);
- });
-}
-loadHome();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/leaderboard.html b/public/marketplace/leaderboard.html
deleted file mode 100644
index ee8649e..0000000
--- a/public/marketplace/leaderboard.html
+++ /dev/null
@@ -1,185 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Designer Leaderboard</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .lb-wrap { max-width: 1100px; margin: 0 auto; padding: var(--mp-pad); }
- .lb-hero { padding: clamp(36px,5vw,80px) var(--mp-pad) 12px; }
- .lb-hero h1 { font-size: clamp(34px,4vw,56px); margin: 0 0 10px; letter-spacing: -.02em; }
- .lb-hero p { color: var(--mp-muted); max-width: 720px; margin: 0; line-height: 1.55; }
-
- .lb-tabs { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 999px; box-shadow: var(--mp-glass-shadow); width: max-content; max-width: 100%; margin-bottom: 22px; }
- .lb-tab { background: transparent; border: 0; padding: 9px 18px; border-radius: 999px; font-size: 13px; font-weight: 600; color: #444; cursor: pointer; transition: background .15s ease, color .15s ease; }
- .lb-tab:hover { background: rgba(20,20,20,.04); }
- .lb-tab.active { background: #111; color: #fff; }
-
- .lb-table { width: 100%; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 22px; overflow: hidden; box-shadow: var(--mp-glass-shadow); }
- .lb-row { display: grid; grid-template-columns: 56px 1fr auto; gap: 12px; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--mp-border); transition: background .15s ease; }
- .lb-row:last-child { border-bottom: 0; }
- .lb-row:hover { background: rgba(20,20,20,.02); }
- .lb-row .rank { font-size: 18px; font-weight: 700; color: #444; min-width: 34px; text-align: center; }
- .lb-row .rank.top1 { color: #b78a2a; }
- .lb-row .rank.top2 { color: #888; }
- .lb-row .rank.top3 { color: #a66; }
- .lb-row .designer { display: flex; align-items: center; gap: 14px; min-width: 0; }
- .lb-row .designer .avatar { width: 40px; height: 40px; border-radius: 50%; background: #ddd; background-size: cover; background-position: center; flex: 0 0 40px; }
- .lb-row .designer .meta { display: flex; flex-direction: column; min-width: 0; }
- .lb-row .designer .name { font-weight: 600; font-size: 15px; }
- .lb-row .designer .name a { color: inherit; }
- .lb-row .designer .name a:hover { text-decoration: underline; text-underline-offset: 3px; }
- .lb-row .designer .level { font-size: 12px; color: var(--mp-muted); }
- .lb-row .weblink { color: #0a7; text-decoration: none; }
- .lb-row .weblink:hover { text-decoration: underline; }
- .lb-row .score { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 16px; }
- .lb-row .pills { display: flex; gap: 6px; margin-left: 8px; }
- .lb-empty { padding: 28px; text-align: center; color: var(--mp-muted); }
-
- .lb-challenges { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); margin: 24px 0 36px; }
- .lb-challenge { padding: 16px 18px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 18px; box-shadow: var(--mp-glass-shadow); display: grid; gap: 6px; }
- .lb-challenge .ttl { font-weight: 700; font-size: 15px; }
- .lb-challenge .sub { font-size: 12px; color: var(--mp-muted); }
- .lb-challenge .metric { display: inline-block; padding: 3px 9px; border-radius: 999px; background: #111; color: #fff; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; width: max-content; }
- .lb-meta { color: var(--mp-muted); font-size: 13px; margin-bottom: 12px; }
-
- @media (max-width: 560px) {
- .lb-row { grid-template-columns: 40px 1fr auto; padding: 12px; }
- .lb-row .designer .avatar { width: 32px; height: 32px; flex-basis: 32px; }
- .lb-tab { padding: 8px 14px; font-size: 12px; }
- }
-</style>
-</head>
-<body>
- <header class="lb-hero lb-wrap">
- <h1>Designer Leaderboard</h1>
- <p>The top 50 Wallco designers — by points earned, wallpaper sales, trade saves, and published collections. Updated continuously from the marketplace ledger.</p>
- </header>
-
- <main class="lb-wrap">
-
- <section id="active-section" style="display:none">
- <h2 style="font-size:20px;letter-spacing:-.01em;margin:0 0 12px">Active challenges</h2>
- <div id="active-challenges" class="lb-challenges"></div>
- </section>
-
- <div class="lb-tabs" role="tablist" aria-label="Leaderboard metric">
- <button class="lb-tab active" data-metric="points" role="tab">Points</button>
- <button class="lb-tab" data-metric="sales" role="tab">Sales</button>
- <button class="lb-tab" data-metric="saves" role="tab">Trade saves</button>
- <button class="lb-tab" data-metric="collections" role="tab">Collections</button>
- </div>
-
- <div id="meta" class="lb-meta">Loading…</div>
- <div id="board" class="lb-table" aria-live="polite"></div>
-
- </main>
-
-<script>
-const METRIC_LABEL = { points: 'lifetime points', sales: 'paid + fulfilled wallpaper orders', saves: 'trade project saves', collections: 'published collections' };
-const METRIC_UNIT = { points: 'pts', sales: 'orders', saves: 'saves', collections: 'collections' };
-
-function rankClass(rank) {
- if (rank === 1) return 'top1';
- if (rank === 2) return 'top2';
- if (rank === 3) return 'top3';
- return '';
-}
-
-function fmtScore(metric, score) {
- const n = Number(score || 0);
- return n.toLocaleString(undefined, { maximumFractionDigits: metric === 'points' ? 0 : 0 });
-}
-
-function escapeAttr(s) {
- return String(s || '').replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<');
-}
-
-async function loadBoard(metric) {
- const meta = document.getElementById('meta');
- const board = document.getElementById('board');
- meta.textContent = 'Loading…';
- board.innerHTML = '';
- let json;
- try { json = await MP.api('/api/marketplace/leaderboard?metric=' + encodeURIComponent(metric) + '&limit=50'); }
- catch (e) { meta.textContent = 'Could not load leaderboard: ' + e.message; return; }
- const rows = json.leaderboard || [];
- meta.textContent = 'Top ' + rows.length + ' designers by ' + (METRIC_LABEL[metric] || metric) + '.';
- if (!rows.length) {
- board.innerHTML = '<div class="lb-empty">No designers yet. Be the first — <a class="under" href="/marketplace/apply">apply to become a Wallco designer</a>.</div>';
- return;
- }
- rows.forEach(r => {
- const row = document.createElement('div');
- row.className = 'lb-row';
- const avatarStyle = r.avatar_url ? `background-image:url(${r.avatar_url.replace(/"/g,'%22')})` : 'background:linear-gradient(135deg,#e9e3d8,#cdbfa6)';
- const pills = [];
- if (r.is_founding) pills.push('<span class="mp-pill gold">Founding</span>');
- if (r.is_verified) pills.push('<span class="mp-pill verified">✓</span>');
- const locBits = [];
- if (r.city) locBits.push(r.city);
- if (r.state && r.state !== r.city) locBits.push(r.state);
- const locLine = locBits.join(', ');
- const webLink = r.website_url
- ? `<a href="${escapeAttr(r.website_url)}" target="_blank" rel="noopener" class="weblink" title="${escapeAttr(r.website_url)}">🔗 Site</a>`
- : '';
- row.innerHTML = `
- <div class="rank ${rankClass(r.rank)}">#${r.rank}</div>
- <div class="designer">
- <div class="avatar" style="${avatarStyle}"></div>
- <div class="meta">
- <div class="name"><a href="/designers/${r.slug}">${r.display_name || r.slug}</a></div>
- <div class="level">${r.level || ''}${locLine ? ' · ' + locLine : ''}${webLink ? ' · ' + webLink : ''}</div>
- </div>
- <div class="pills">${pills.join('')}</div>
- </div>
- <div class="score">${fmtScore(metric, r.score)} <span style="font-size:12px;color:var(--mp-muted);font-weight:500">${METRIC_UNIT[metric]||''}</span></div>
- `;
- board.appendChild(row);
- });
-
- const active = (json.active_challenges || []);
- const activeWrap = document.getElementById('active-challenges');
- const activeSection = document.getElementById('active-section');
- activeWrap.innerHTML = '';
- if (active.length) {
- activeSection.style.display = '';
- active.forEach(c => {
- const card = document.createElement('a');
- card.className = 'lb-challenge';
- card.href = '/marketplace/challenges/' + c.id;
- const ends = c.ends_at ? new Date(c.ends_at).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}) : '';
- card.innerHTML = `
- <span class="metric">${c.metric}</span>
- <div class="ttl">${c.title}</div>
- <div class="sub">${c.description ? c.description : ''}</div>
- <div class="sub">Ends ${ends}</div>
- `;
- activeWrap.appendChild(card);
- });
- } else {
- activeSection.style.display = 'none';
- }
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- const tabs = document.querySelectorAll('.lb-tab');
- tabs.forEach(t => t.addEventListener('click', () => {
- tabs.forEach(x => x.classList.toggle('active', x === t));
- const metric = t.dataset.metric;
- history.replaceState(null,'','?metric=' + metric);
- loadBoard(metric);
- }));
- const initial = new URL(location.href).searchParams.get('metric');
- if (initial && ['points','sales','saves','collections'].includes(initial)) {
- tabs.forEach(x => x.classList.toggle('active', x.dataset.metric === initial));
- loadBoard(initial);
- } else {
- loadBoard('points');
- }
-});
-</script>
-</body>
-</html>
diff --git a/public/marketplace/licensing.html b/public/marketplace/licensing.html
deleted file mode 100644
index b6f2e85..0000000
--- a/public/marketplace/licensing.html
+++ /dev/null
@@ -1,79 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>License artwork · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-</head>
-<body>
- <header class="mp-hero">
- <h1>License artwork.</h1>
- <p class="lede">Non-exclusive, exclusive, editorial, or print-on-demand. Wallco routes inquiries to the designer and handles contract + payment.</p>
- </header>
- <main class="mp-wrap">
- <div id="info" class="sub" style="margin-bottom: 14px;"></div>
- <section class="mp-card" style="padding: 22px;">
- <form id="form" class="mp-form">
- <div class="row">
- <label>Your name *<input type="text" name="buyer_name" required maxlength="100"/></label>
- <label>Email *<input type="email" name="buyer_email" required maxlength="200"/></label>
- </div>
- <label>Company <input type="text" name="buyer_company" maxlength="200"/></label>
- <div class="row">
- <label>License type *
- <select name="license_type" required>
- <option value="non_exclusive">Non-exclusive</option>
- <option value="exclusive">Exclusive</option>
- <option value="editorial">Editorial</option>
- <option value="print_on_demand">Print on demand</option>
- <option value="custom">Custom</option>
- </select>
- </label>
- <label>Duration (months) <input type="number" name="duration_months" min="1" max="120"/></label>
- </div>
- <label>Territory <input type="text" name="territory" placeholder="US, EU, Worldwide"/></label>
- <label>Budget range <input type="text" name="budget_range" placeholder="e.g. $5,000–$10,000"/></label>
- <label>Usage description *
- <textarea name="usage_description" required maxlength="4000" placeholder="How will the artwork be used? Hospitality wallcovering, packaging, hotel suite collection, editorial spread, etc."></textarea>
- </label>
- <button type="submit" class="mp-btn">Send inquiry</button>
- <div id="msg" class="err" style="display:none"></div>
- </form>
- </section>
- </main>
-
-<script>
-const slug = location.pathname.split('/').filter(Boolean).pop();
-(async () => {
- try {
- const j = await fetch('/api/marketplace/patterns/' + encodeURIComponent(slug)).then(r => r.json());
- if (j.pattern) {
- document.getElementById('info').innerHTML = `Inquiring about <strong>${j.pattern.title}</strong> by <a class="under" href="/designers/${j.pattern.designer_slug}">${j.pattern.designer_name}</a>`;
- } else {
- // it's a designer slug; show their info
- const d = await fetch('/api/marketplace/designers/' + encodeURIComponent(slug)).then(r => r.json());
- if (d.designer) document.getElementById('info').innerHTML = `Inquiring about <strong>${d.designer.display_name}</strong>'s catalog`;
- }
- } catch(_){}
-})();
-
-document.getElementById('form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const msg = document.getElementById('msg'); msg.style.display='none';
- const body = Object.fromEntries(new FormData(e.target).entries());
- body.pattern_slug = slug;
- try {
- const r = await fetch('/api/marketplace/licensing/inquiry', { method:'POST', credentials:'include', headers:{'content-type':'application/json'}, body: JSON.stringify(body) });
- const j = await r.json();
- if(!r.ok) throw new Error(j.error || 'failed');
- msg.className='ok'; msg.style.display='block'; msg.textContent = `Inquiry #${j.inquiry_id} sent. The designer + Wallco team will respond within 2 business days.`;
- e.target.reset();
- } catch (err) {
- msg.className='err'; msg.style.display='block'; msg.textContent = err.message;
- }
-});
-</script>
-</body>
-</html>
diff --git a/public/marketplace/og-default.svg b/public/marketplace/og-default.svg
deleted file mode 100644
index 5cbf928..0000000
--- a/public/marketplace/og-default.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="Wallco Designer Marketplace">
- <defs>
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
- <stop offset="0" stop-color="#1f1a14"/>
- <stop offset="1" stop-color="#3a2f24"/>
- </linearGradient>
- <pattern id="dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
- <circle cx="20" cy="20" r="1.2" fill="#d2b15c" opacity="0.18"/>
- </pattern>
- </defs>
- <rect width="1200" height="630" fill="url(#bg)"/>
- <rect width="1200" height="630" fill="url(#dots)"/>
- <g fill="#f5efe2" font-family="Georgia, 'Times New Roman', serif">
- <text x="80" y="240" font-size="92" font-weight="400" letter-spacing="-2">Wallco</text>
- <text x="80" y="340" font-size="38" font-style="italic" fill="#d2b15c" opacity="0.95">Designer-warranted wallcoverings.</text>
- <text x="80" y="392" font-size="28" opacity="0.75">Patterns from independent artists, sampled in your room before you commit.</text>
- </g>
- <g transform="translate(80,500)" fill="#f5efe2" font-family="Helvetica, Arial, sans-serif" font-size="22" opacity="0.7">
- <text>wallco.ai · designerwallcoverings.com</text>
- </g>
-</svg>
diff --git a/public/marketplace/onboard.html b/public/marketplace/onboard.html
deleted file mode 100644
index 440c306..0000000
--- a/public/marketplace/onboard.html
+++ /dev/null
@@ -1,312 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco · Designer Onboarding</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .ob-wrap { max-width: 880px; margin: 0 auto; padding: var(--mp-pad); }
- .ob-hero { padding: clamp(32px,4vw,72px) var(--mp-pad) 12px; }
- .ob-hero h1 { font-size: clamp(30px,3.6vw,48px); margin: 0 0 8px; letter-spacing: -.02em; }
- .ob-hero .lede { color: var(--mp-muted); max-width: 700px; line-height: 1.55; }
-
- .ob-progress { display: flex; align-items: center; gap: 14px; margin: 20px 0 18px; }
- .ob-progress .bar { flex: 1; height: 6px; background: rgba(20,20,20,.08); border-radius: 999px; overflow: hidden; }
- .ob-progress .bar > div { height: 100%; background: linear-gradient(90deg,#0a7,#27c97a); border-radius: 999px; transition: width .25s ease; }
- .ob-progress .pct { font-weight: 700; font-size: 14px; min-width: 48px; text-align: right; }
-
- .ob-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 18px; }
- .ob-tab { padding: 8px 14px; border-radius: 999px; background: rgba(20,20,20,.06); color: #444; cursor: pointer; font-size: 13px; font-weight: 600; border: 0; }
- .ob-tab.active { background: #111; color: #fff; }
- .ob-tab.done::after { content: ' ✓'; color: #0a7; }
- .ob-tab.active.done::after { color: #5fe39a; }
-
- .ob-card { padding: 22px 24px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 22px; box-shadow: var(--mp-glass-shadow); }
- .ob-card h2 { font-size: 22px; margin: 0 0 6px; letter-spacing: -.01em; }
- .ob-card .sub { color: var(--mp-muted); font-size: 14px; margin-bottom: 18px; line-height: 1.5; }
-
- .field-grid { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }
- .field-grid label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: #333; }
- .field-grid label.full { grid-column: 1 / -1; }
- .field-grid input, .field-grid textarea, .field-grid select {
- border: 1px solid var(--mp-border); padding: 10px 12px; border-radius: 12px; font-size: 14px; font-family: inherit; background: #fff;
- }
- .field-grid textarea { min-height: 96px; resize: vertical; }
- @media (max-width: 600px) { .field-grid { grid-template-columns: 1fr; } }
-
- .ob-row { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; gap: 10px; flex-wrap: wrap; }
- .ob-row .msg { font-size: 13px; color: var(--mp-muted); }
- .ob-row .msg.ok { color: #060; }
- .ob-row .msg.err { color: #c33; }
-
- .video-list { display: grid; gap: 12px; margin-top: 16px; }
- .video-row { display: grid; grid-template-columns: 120px 1fr auto; gap: 12px; padding: 12px; background: rgba(20,20,20,.03); border-radius: 12px; align-items: center; }
- .video-row .thumb { aspect-ratio: 16/9; background: #ccc; background-size: cover; background-position: center; border-radius: 8px; }
- .video-row .vmeta .title { font-weight: 600; font-size: 14px; }
- .video-row .vmeta .url { font-size: 12px; color: var(--mp-muted); word-break: break-all; }
- .video-row .vmeta .badges { margin-top: 4px; }
- .video-row .vmeta .badge { display:inline-block; padding: 2px 8px; border-radius: 999px; background: rgba(20,20,20,.06); font-size: 11px; }
- .video-row .vmeta .badge.pending { background: #fee; color: #944; }
- .video-row .vmeta .badge.approved { background: #def; color: #084; }
- .video-row .del { background: transparent; border: 1px solid var(--mp-border); padding: 6px 10px; border-radius: 8px; cursor: pointer; font-size: 12px; }
- .video-row .del:hover { background: #fee; border-color: #f99; color: #944; }
-</style>
-</head>
-<body>
- <header class="ob-hero ob-wrap">
- <h1>Onboard your designer profile</h1>
- <p class="lede">Four short steps to publish your storefront. You can save progress and come back any time — your work auto-saves after each step.</p>
- <div class="ob-progress">
- <div class="bar"><div id="bar-fill" style="width:0%"></div></div>
- <div class="pct" id="pct">0%</div>
- </div>
- </header>
-
- <main class="ob-wrap">
- <div class="ob-tabs" id="tabs"></div>
- <div id="step-host"></div>
- </main>
-
-<template id="step-profile">
- <div class="ob-card">
- <h2>1 · Profile basics</h2>
- <p class="sub">Make your storefront recognisable. Your studio name and bio show on every product page and at the top of your storefront.</p>
- <form class="field-grid" data-step="profile">
- <label class="full">Studio name<input name="studio_name" type="text" maxlength="100" placeholder="e.g. Mauve Atelier"/></label>
- <label class="full">Bio (2-3 sentences)<textarea name="bio" maxlength="2000" placeholder="What kind of patterns do you create? What inspires you?"></textarea></label>
- <label>Avatar URL<input name="avatar_url" type="url" maxlength="500" placeholder="https://…"/></label>
- <label>Cover image URL<input name="cover_url" type="url" maxlength="500" placeholder="https://…"/></label>
- </form>
- </div>
-</template>
-
-<template id="step-web">
- <div class="ob-card">
- <h2>2 · Website + social</h2>
- <p class="sub">Designers with a public website get a ranking boost on the Stars of Design leaderboard. Add anything you'd like buyers to discover.</p>
- <form class="field-grid" data-step="web">
- <label class="full">Website<input name="website_url" type="url" maxlength="300" placeholder="https://yourstudio.com"/></label>
- <label>Instagram<input name="instagram_url" type="url" maxlength="300" placeholder="https://instagram.com/…"/></label>
- <label>TikTok<input name="tiktok_url" type="url" maxlength="300" placeholder="https://tiktok.com/@…"/></label>
- <label>Pinterest<input name="pinterest_url" type="url" maxlength="300" placeholder="https://pinterest.com/…"/></label>
- <label>YouTube<input name="youtube_url" type="url" maxlength="300" placeholder="https://youtube.com/@…"/></label>
- <label>Facebook<input name="facebook_url" type="url" maxlength="300" placeholder="https://facebook.com/…"/></label>
- <label>Twitter / X<input name="twitter_url" type="url" maxlength="300" placeholder="https://x.com/…"/></label>
- <label>LinkedIn<input name="linkedin_url" type="url" maxlength="300" placeholder="https://linkedin.com/in/…"/></label>
- </form>
- </div>
-</template>
-
-<template id="step-contact">
- <div class="ob-card">
- <h2>3 · Contact + address</h2>
- <p class="sub">Trade buyers, licensees, and our team use this to reach you about commissions and orders. Toggle whether to show this publicly on your profile.</p>
- <form class="field-grid" data-step="contact">
- <label>Public contact email<input name="contact_email" type="email" maxlength="200" placeholder="hello@yourstudio.com"/></label>
- <label>Phone<input name="phone" type="tel" maxlength="50" placeholder="+1 555 555 5555"/></label>
- <label class="full">Address line 1<input name="address_line1" type="text" maxlength="200" placeholder="123 Studio Lane"/></label>
- <label class="full">Address line 2<input name="address_line2" type="text" maxlength="200" placeholder="Suite 4 (optional)"/></label>
- <label>City<input name="city" type="text" maxlength="100"/></label>
- <label>State / Region<input name="state_region" type="text" maxlength="100"/></label>
- <label>Postal code<input name="postal_code" type="text" maxlength="30"/></label>
- <label>Country<input name="country" type="text" maxlength="100" placeholder="USA"/></label>
- <label class="full" style="flex-direction:row;align-items:center;gap:10px">
- <input name="public_contact_consent" type="checkbox"/>
- <span style="font-size:13px">Show my contact info publicly on my designer profile</span>
- </label>
- </form>
- </div>
-</template>
-
-<template id="step-videos">
- <div class="ob-card">
- <h2>4 · Videos</h2>
- <p class="sub">Paste a YouTube, Vimeo, TikTok, or Instagram link. We auto-detect the embed. Videos go through quick review before showing publicly.</p>
- <form class="field-grid" data-step="add-video">
- <label class="full">Video URL<input name="url" type="url" maxlength="500" placeholder="https://youtube.com/watch?v=…" required/></label>
- <label class="full">Title (optional)<input name="title" type="text" maxlength="200" placeholder="Studio tour, pattern process, brand intro…"/></label>
- <label class="full">Description (optional)<textarea name="description" maxlength="2000"></textarea></label>
- <label class="full" style="flex-direction:row;align-items:center;gap:10px">
- <input name="is_featured" type="checkbox"/>
- <span style="font-size:13px">Feature this video on my profile</span>
- </label>
- </form>
- <div class="ob-row" style="margin-top:14px">
- <button class="mp-btn" id="add-video-btn">Add video</button>
- </div>
- <div class="video-list" id="video-list"></div>
- </div>
-</template>
-
-<script>
-const STEPS = ['profile','web','contact','videos'];
-const LABELS = { profile: 'Profile', web: 'Website + social', contact: 'Contact', videos: 'Videos' };
-let me = null;
-let completeness = null;
-
-function tabsHtml(activeStep, doneMap) {
- const wrap = document.getElementById('tabs');
- wrap.innerHTML = '';
- STEPS.forEach((step, i) => {
- const btn = document.createElement('button');
- btn.className = 'ob-tab' + (step === activeStep ? ' active' : '') + (doneMap[step] ? ' done' : '');
- btn.textContent = `${i+1} · ${LABELS[step]}`;
- btn.addEventListener('click', () => renderStep(step));
- wrap.appendChild(btn);
- });
-}
-
-function setPct(pct) {
- document.getElementById('bar-fill').style.width = pct + '%';
- document.getElementById('pct').textContent = pct + '%';
-}
-
-async function loadMe() {
- try {
- const r = await fetch('/api/marketplace/designer/me', { credentials: 'include' });
- if (r.status === 401) { location.href = '/marketplace/apply?return=/marketplace/onboard'; return; }
- const j = await r.json();
- me = j.designer;
- completeness = await fetch('/api/marketplace/designer/me/completeness', { credentials: 'include' }).then(r => r.json());
- setPct(completeness.pct || 0);
- const doneMap = {};
- (completeness.steps || []).forEach(s => { doneMap[s.key] = s.done; });
- // Map onboarding STEPS (server-side) onto wizard STEPS (web/contact/videos).
- // server returns: profile, web, contact, videos, rights → we map directly except rights (auto on apply).
- tabsHtml(STEPS[0], doneMap);
- renderStep(STEPS[0]);
- } catch (e) {
- document.getElementById('step-host').innerHTML = '<div class="ob-card"><p>Could not load your profile. <a class="under" href="/marketplace/apply">Apply first</a> or refresh.</p></div>';
- }
-}
-
-function fillFormFromMe(form, fields) {
- fields.forEach(name => {
- const el = form.elements[name];
- if (!el || !me) return;
- if (el.type === 'checkbox') el.checked = !!me[name];
- else el.value = me[name] || '';
- });
-}
-
-function renderStep(step) {
- const host = document.getElementById('step-host');
- host.innerHTML = '';
- const tmpl = document.getElementById('step-' + step);
- host.appendChild(tmpl.content.cloneNode(true));
- const form = host.querySelector('form');
- if (step === 'profile') fillFormFromMe(form, ['studio_name','bio','avatar_url','cover_url']);
- if (step === 'web') fillFormFromMe(form, ['website_url','instagram_url','tiktok_url','pinterest_url','youtube_url','facebook_url','twitter_url','linkedin_url']);
- if (step === 'contact') fillFormFromMe(form, ['contact_email','phone','address_line1','address_line2','city','state_region','postal_code','country','public_contact_consent']);
-
- if (step !== 'videos') {
- // Auto-save handler
- const ob = document.createElement('div');
- ob.className = 'ob-row';
- const idx = STEPS.indexOf(step);
- const nextBtn = `<button class="mp-btn" id="save-next">${idx < STEPS.length - 1 ? 'Save & continue →' : 'Save'}</button>`;
- ob.innerHTML = `<div class="msg" id="msg"></div>${nextBtn}`;
- host.querySelector('.ob-card').appendChild(ob);
- document.getElementById('save-next').addEventListener('click', async (e) => {
- e.preventDefault();
- const payload = {};
- Array.from(form.elements).forEach(el => {
- if (!el.name) return;
- if (el.type === 'checkbox') payload[el.name] = el.checked;
- else payload[el.name] = el.value;
- });
- const msg = document.getElementById('msg');
- msg.textContent = 'Saving…'; msg.className = 'msg';
- try {
- const r = await fetch('/api/marketplace/designer/me', {
- method: 'PATCH', credentials: 'include',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify(payload),
- });
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'save failed');
- me = j.designer;
- setPct(j.completeness?.pct || 0);
- const doneMap = {};
- (j.completeness?.steps || []).forEach(s => { doneMap[s.key] = s.done; });
- msg.textContent = 'Saved ✓'; msg.className = 'msg ok';
- if (idx < STEPS.length - 1) {
- setTimeout(() => { tabsHtml(STEPS[idx+1], doneMap); renderStep(STEPS[idx+1]); }, 350);
- } else {
- tabsHtml(step, doneMap);
- }
- } catch (err) {
- msg.textContent = 'Could not save: ' + err.message; msg.className = 'msg err';
- }
- });
- } else {
- // Videos step
- document.getElementById('add-video-btn').addEventListener('click', addVideo);
- refreshVideos();
- }
-}
-
-async function refreshVideos() {
- const list = document.getElementById('video-list');
- list.innerHTML = '<div class="sub">Loading…</div>';
- const j = await fetch('/api/marketplace/designer/me/videos', { credentials: 'include' }).then(r => r.json()).catch(() => ({ videos: [] }));
- list.innerHTML = '';
- if (!j.videos || !j.videos.length) {
- list.innerHTML = '<div class="sub">No videos yet. Paste a URL above to add your first.</div>';
- return;
- }
- j.videos.forEach(v => {
- const row = document.createElement('div');
- row.className = 'video-row';
- let thumb = '';
- if (v.provider === 'youtube' && v.embed_id) thumb = `background-image:url(https://i.ytimg.com/vi/${v.embed_id}/mqdefault.jpg)`;
- row.innerHTML = `
- <div class="thumb" style="${thumb}"></div>
- <div class="vmeta">
- <div class="title">${v.title || v.url}</div>
- <div class="url">${v.url}</div>
- <div class="badges"><span class="badge ${v.status}">${v.status}</span> ${v.is_featured ? '<span class="badge">Featured</span>' : ''} <span class="badge">${v.provider}</span></div>
- </div>
- <button class="del" data-id="${v.id}">Remove</button>`;
- row.querySelector('.del').addEventListener('click', async () => {
- if (!confirm('Remove this video?')) return;
- await fetch('/api/marketplace/designer/me/videos/' + v.id, { method: 'DELETE', credentials: 'include' });
- refreshVideos();
- });
- list.appendChild(row);
- });
-}
-
-async function addVideo() {
- const form = document.querySelector('form[data-step=add-video]');
- const url = form.elements.url.value.trim();
- if (!url) { alert('Paste a video URL first'); return; }
- const payload = {
- url, title: form.elements.title.value, description: form.elements.description.value,
- is_featured: form.elements.is_featured.checked,
- };
- const r = await fetch('/api/marketplace/designer/me/videos', {
- method: 'POST', credentials: 'include',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify(payload),
- });
- const j = await r.json();
- if (!r.ok) { alert(j.error || 'Could not add video'); return; }
- form.reset();
- refreshVideos();
- // refresh completeness
- const c = await fetch('/api/marketplace/designer/me/completeness', { credentials: 'include' }).then(r => r.json()).catch(() => null);
- if (c) {
- setPct(c.pct || 0);
- const doneMap = {};
- (c.steps || []).forEach(s => { doneMap[s.key] = s.done; });
- tabsHtml('videos', doneMap);
- }
-}
-
-document.addEventListener('DOMContentLoaded', loadMe);
-</script>
-</body>
-</html>
diff --git a/public/marketplace/pattern.html b/public/marketplace/pattern.html
deleted file mode 100644
index 89d40e0..0000000
--- a/public/marketplace/pattern.html
+++ /dev/null
@@ -1,396 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Pattern · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .pat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
- @media (max-width: 800px){ .pat-grid { grid-template-columns: 1fr; } }
- .hero-pat { aspect-ratio: 1/1; border-radius: 24px; background: #eee; background-size: cover; background-position: center; box-shadow: var(--mp-glass-shadow); }
- .swatch-row { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 14px; }
- .swatch { width: 36px; height: 36px; border-radius: 8px; border: 1px solid var(--mp-border); box-shadow: 0 0 0 2px #fff inset; }
- .colorway-card { padding: 14px; background: rgba(255,255,255,.7); border: 1px solid var(--mp-border); border-radius: 16px; }
- .colorway-card .name { font-weight: 600; margin-bottom: 6px; }
- .specs { display:grid; grid-template-columns: 130px 1fr; gap: 4px 18px; font-size: 14px; color: var(--mp-muted); margin: 14px 0 22px; }
- .specs .k { color: #222; }
-
- /* Texture picker */
- .tex-section { margin-top: 36px; padding: 22px; background: rgba(255,255,255,.5); border: 1px solid var(--mp-border); border-radius: 18px; }
- .tex-section h3 { margin: 0 0 4px; }
- .tex-section p.help { color: var(--mp-muted); margin: 0 0 14px; font-size: 14px; }
- .tex-category { margin-top: 14px; }
- .tex-category .cat-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
- .tex-category .cat-label { font-weight: 600; }
- .tex-category .cat-blurb { color: var(--mp-muted); font-size: 13px; }
- .tex-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scroll-snap-type: x mandatory; }
- .tex-strip::-webkit-scrollbar { height: 6px; }
- .tex-strip::-webkit-scrollbar-thumb { background: rgba(0,0,0,.15); border-radius: 3px; }
- .tex-chip { flex: 0 0 96px; aspect-ratio: 1/1; border-radius: 10px; background-size: cover; background-position: center; cursor: pointer; border: 2px solid transparent; scroll-snap-align: start; position: relative; transition: transform .12s ease, border-color .12s ease; }
- .tex-chip:hover { transform: scale(1.04); }
- .tex-chip.selected { border-color: #000; box-shadow: 0 0 0 3px rgba(0,0,0,.06); }
- .tex-chip .tex-title { position: absolute; bottom: 0; left: 0; right: 0; padding: 4px 6px; font-size: 10px; color: #fff; background: linear-gradient(to top, rgba(0,0,0,.65), transparent); border-radius: 0 0 8px 8px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- .tex-selected-banner { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: rgba(0,0,0,.04); border-radius: 12px; margin-top: 14px; font-size: 14px; }
- .tex-selected-banner .pip { width: 28px; height: 28px; border-radius: 6px; background-size: cover; }
- .tex-clear { color: var(--mp-muted); cursor: pointer; text-decoration: underline; font-size: 13px; }
-
- /* Plaid generator */
- .pl-section { margin-top: 36px; padding: 22px; background: rgba(255,255,255,.5); border: 1px solid var(--mp-border); border-radius: 18px; }
- .pl-section h3 { margin: 0 0 4px; }
- .pl-section p.help { color: var(--mp-muted); margin: 0 0 14px; font-size: 14px; }
- .pl-row { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; margin-top: 12px; }
- .pl-block { flex: 1 1 240px; }
- .pl-block label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 6px; }
- .color-dots { display: flex; flex-wrap: wrap; gap: 8px; }
- .color-dot { width: 32px; height: 32px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; box-shadow: 0 0 0 1px rgba(0,0,0,.12) inset; transition: transform .1s ease; position: relative; }
- .color-dot:hover { transform: scale(1.1); }
- .color-dot.selected { border-color: #000; box-shadow: 0 0 0 2px #fff inset, 0 0 0 4px rgba(0,0,0,.12); }
- .color-dot.selected::after { content: '✓'; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: #fff; font-size: 16px; text-shadow: 0 0 2px rgba(0,0,0,.6); font-weight: 700; }
- .pl-tip { font-size: 12px; color: var(--mp-muted); margin-top: 6px; }
- .pl-actions { margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
- .pl-multi { display: flex; flex-wrap: wrap; gap: 6px; max-width: 100%; }
- .pl-chip { font-size: 13px; padding: 6px 10px; background: rgba(0,0,0,.05); border: 1px solid var(--mp-border); border-radius: 999px; cursor: pointer; user-select: none; }
- .pl-chip.selected { background: #111; color: #fff; border-color: #111; }
-</style>
-</head>
-<body>
- <main id="root"><div class="mp-wrap"><p>Loading…</p></div></main>
-
-<script>
-const slug = location.pathname.split('/').filter(Boolean).pop();
-
-// ── pattern-page state ────────────────────────────────────────────────────
-const state = {
- pattern: null,
- selectedTexture: null, // { sku, title, image_url, category } | null
- patternColors: [], // extracted hex strings from existing colorways
- pickedColors: new Set(), // user-selected hex colors for stripe/plaid
- pickedPlaidTypes: new Set(), // user-selected plaid type keys
-};
-
-// 10 canonical plaid + stripe families.
-const PLAID_TYPES = [
- { key: 'gingham', label: 'Gingham', blurb: 'Even-check 2-color woven.' },
- { key: 'tartan', label: 'Tartan', blurb: 'Scottish multi-stripe twill.' },
- { key: 'buffalo', label: 'Buffalo', blurb: 'Bold 2-color large check.' },
- { key: 'windowpane', label: 'Windowpane', blurb: 'Thin grid on solid ground.' },
- { key: 'houndstooth',label: 'Houndstooth', blurb: 'Broken-check geometric.' },
- { key: 'madras', label: 'Madras', blurb: 'Lightweight bright cotton check.' },
- { key: 'glen', label: 'Glen Plaid', blurb: 'Prince of Wales — woven crowfoot.' },
- { key: 'argyle', label: 'Argyle', blurb: 'Diamond-on-diamond with overlay.' },
- { key: 'tattersall', label: 'Tattersall', blurb: 'Two-color thin-line grid on light ground.' },
- { key: 'pinstripe', label: 'Pinstripe', blurb: 'Vertical thin parallel stripes.' },
-];
-
-// ── page render ────────────────────────────────────────────────────────────
-async function load() {
- const j = await fetch('/api/marketplace/patterns/' + encodeURIComponent(slug)).then(r => r.json());
- const root = document.getElementById('root');
- if (!j.pattern) { root.innerHTML = `<div class="mp-wrap"><h2>Pattern not found</h2></div>`; return; }
- const p = j.pattern;
- state.pattern = p;
-
- // Extract a deduped color list from existing colorways to seed the plaid picker
- const colorSet = new Set();
- (j.colorways || []).forEach(c => {
- let pal = c.hex_palette;
- if (typeof pal === 'string') { try { pal = JSON.parse(pal); } catch { pal = []; } }
- (pal || []).forEach(h => { if (/^#[0-9a-f]{6}$/i.test(h)) colorSet.add(h.toLowerCase()); });
- });
- // Fall back to the 6 editorial palettes if no colorways exist yet
- if (colorSet.size < 4) {
- ['#B46A55','#E8C7A9','#69785A','#2F3A2D','#1F2E46','#7E9AB8','#C98986','#80514F','#BFA76A','#151515','#F4EFE8','#FFF8F5'].forEach(h => colorSet.add(h));
- }
- state.patternColors = Array.from(colorSet);
-
- root.innerHTML = `
- <main class="mp-wrap">
- <div class="pat-grid">
- <div>
- <div class="hero-pat" style="background-image:url('${p.original_image_url}')"></div>
- <div style="display:flex; gap: 6px; margin-top: 10px; flex-wrap: wrap;" id="thumbs"></div>
- </div>
- <div>
- <div class="sub">by <a class="under" href="/designers/${p.designer_slug}">${p.designer_name}</a></div>
- <h1 style="margin: 6px 0;">${p.title}</h1>
- <p style="color: var(--mp-muted); line-height: 1.6">${p.description || ''}</p>
-
- <div class="swatch-row" id="swatches"></div>
-
- <div class="specs">
- <div class="k">Repeat type</div><div>${p.repeat_type || '—'}</div>
- <div class="k">Style</div><div>${(p.style_tags||[]).join(' · ') || '—'}</div>
- <div class="k">Best for</div><div>${(p.room_tags||[]).join(' · ') || '—'}</div>
- <div class="k">Color family</div><div>${(p.color_tags||[]).join(' · ') || '—'}</div>
- <div class="k">Commercial</div><div>${p.commercial_suitable ? 'Yes (Type II contract grade available)' : 'Residential first'}</div>
- <div class="k">Rights</div><div>${p.rights_confirmed ? 'Designer-warranted' : 'Pending'}</div>
- </div>
-
- <div style="display:flex; gap: 10px; flex-wrap: wrap;">
- <button class="mp-btn" onclick="orderSample()">Order sample · $${(p.sample_price||5).toFixed(2)}</button>
- <button class="mp-btn ghost" onclick="orderRoll()">Order full roll · $${(p.base_price||248).toFixed(2)}</button>
- <a class="mp-btn ghost" href="/licensing/${p.slug}">License artwork</a>
- <button class="mp-btn ghost" id="btn-ai-colorways" onclick="aiColorways()">Generate AI colorways</button>
- </div>
-
- <h3 style="margin-top: 30px">Colorways</h3>
- <div class="mp-grid" id="cw"></div>
- </div>
- </div>
-
- <!-- ── Real-product texture picker ─────────────────────────────────── -->
- <section class="tex-section" id="texSection">
- <h3>Choose a real-product background texture</h3>
- <p class="help">Pick any natural wallcovering we sell — your colorways will be regenerated on that exact texture (grasscloth, sisal, cork, silk, raffia, linen, glass-bead, flocked, jute, seagrass, paperweave). Available to all users, no login required.</p>
- <div id="texCategories">Loading textures…</div>
- <div id="texSelectedBanner" style="display:none;"></div>
- </section>
-
- <!-- ── Stripe / plaid generator ────────────────────────────────────── -->
- <section class="pl-section" id="plSection">
- <h3>Create a stripe or plaid in these colors</h3>
- <p class="help">Click any color dot to include it. Pick one or all the plaid/stripe types — we'll generate a seamless variant for each type you select.</p>
-
- <div class="pl-row">
- <div class="pl-block">
- <label>Colors <span style="font-weight:400;color:var(--mp-muted)" id="plColorCount">(0 selected)</span></label>
- <div class="color-dots" id="colorDots"></div>
- <div class="pl-tip">Tap a dot to toggle it. We use these as the literal yarns in the woven plaid.</div>
- </div>
- <div class="pl-block">
- <label>Plaid / stripe types <span style="font-weight:400;color:var(--mp-muted)" id="plTypeCount">(0 selected)</span></label>
- <div class="pl-multi" id="plTypes"></div>
- <div class="pl-actions">
- <a class="tex-clear" onclick="selectAllPlaids()">Select all</a>
- <span style="color:var(--mp-muted)">·</span>
- <a class="tex-clear" onclick="clearPlaids()">Clear</a>
- </div>
- </div>
- </div>
-
- <div class="pl-actions" style="margin-top:18px">
- <button class="mp-btn" id="btn-gen-plaid" onclick="generatePlaids()">Generate selected stripe/plaid variants</button>
- <span style="color:var(--mp-muted);font-size:13px" id="plSummary"></span>
- </div>
-
- <div class="mp-grid" id="plResults" style="margin-top:18px"></div>
- </section>
- </main>`;
-
- // initial render — existing DB colorways
- const cw = document.getElementById('cw');
- (j.colorways || []).forEach(c => {
- const palette = Array.isArray(c.hex_palette) ? c.hex_palette : (c.hex_palette ? JSON.parse(c.hex_palette) : []);
- const card = document.createElement('div'); card.className = 'colorway-card';
- const imgHtml = c.image_url ? `<div style="aspect-ratio:1/1;border-radius:12px;background:#eee url('${c.image_url}') center/cover;margin-bottom:10px;"></div>` : '';
- card.innerHTML = imgHtml +
- `<div class="name">${c.name}${c.is_primary ? ' · primary' : ''}</div>
- <div class="swatch-row">${palette.map(h => `<div class="swatch" style="background:${h}" title="${h}"></div>`).join('')}</div>`;
- cw.appendChild(card);
- });
-
- renderColorDots();
- renderPlaidTypes();
- loadTextures();
-}
-
-// ── Texture picker ─────────────────────────────────────────────────────────
-async function loadTextures() {
- try {
- const r = await fetch('/api/marketplace/textures');
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'failed to load');
- const root = document.getElementById('texCategories');
- if (!j.groups || !j.groups.length) { root.textContent = 'No textures available.'; return; }
- root.innerHTML = j.groups.map(g => `
- <div class="tex-category">
- <div class="cat-head">
- <span class="cat-label">${g.label} <span style="color:var(--mp-muted);font-weight:400;">· ${g.count}</span></span>
- <span class="cat-blurb">${g.blurb || ''}</span>
- </div>
- <div class="tex-strip" data-cat="${g.key}">
- ${g.textures.map(t => `
- <div class="tex-chip" data-sku="${t.sku}" data-img="${t.image_url}" data-title="${(t.title||'').replace(/"/g,'"')}" data-category="${g.key}"
- style="background-image:url('${t.image_url}')"
- title="${(t.full_title || t.title || '').replace(/"/g,'"')}">
- <div class="tex-title">${t.title || 'Untitled'}</div>
- </div>
- `).join('')}
- </div>
- </div>
- `).join('');
- root.querySelectorAll('.tex-chip').forEach(el => {
- el.addEventListener('click', () => selectTexture({
- sku: el.dataset.sku,
- image_url: el.dataset.img,
- title: el.dataset.title,
- category: el.dataset.category,
- }, el));
- });
- restoreTextureFromStorage();
- } catch (err) {
- document.getElementById('texCategories').textContent = 'Could not load textures: ' + err.message;
- }
-}
-
-const TEX_LS_KEY = 'wallco.mp.selectedTexture';
-
-function selectTexture(t, el) {
- state.selectedTexture = t;
- document.querySelectorAll('.tex-chip.selected').forEach(x => x.classList.remove('selected'));
- if (el) el.classList.add('selected');
- const banner = document.getElementById('texSelectedBanner');
- banner.style.display = 'flex';
- banner.className = 'tex-selected-banner';
- banner.innerHTML = `
- <div class="pip" style="background-image:url('${t.image_url}')"></div>
- <div><strong>${t.title}</strong> <span style="color:var(--mp-muted);">· ${t.category}</span></div>
- <a class="tex-clear" onclick="clearTexture()" style="margin-left:auto">Remove</a>
- `;
- try { localStorage.setItem(TEX_LS_KEY, JSON.stringify(t)); } catch (_) {}
-}
-window.clearTexture = function() {
- state.selectedTexture = null;
- document.querySelectorAll('.tex-chip.selected').forEach(x => x.classList.remove('selected'));
- document.getElementById('texSelectedBanner').style.display = 'none';
- try { localStorage.removeItem(TEX_LS_KEY); } catch (_) {}
-};
-
-// Re-select the user's last-used texture if it's still in the catalog.
-function restoreTextureFromStorage() {
- let saved = null;
- try { saved = JSON.parse(localStorage.getItem(TEX_LS_KEY) || 'null'); } catch (_) {}
- if (!saved || !saved.sku) return;
- const chip = document.querySelector(`.tex-chip[data-sku="${CSS.escape(saved.sku)}"]`);
- if (chip) {
- selectTexture({
- sku: chip.dataset.sku,
- image_url: chip.dataset.img,
- title: chip.dataset.title,
- category: chip.dataset.category,
- }, chip);
- // Scroll the strip so the selected chip is visible.
- try { chip.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'center' }); } catch (_) {}
- }
-}
-
-// ── Plaid / stripe color picker + type chooser ─────────────────────────────
-function renderColorDots() {
- const root = document.getElementById('colorDots');
- root.innerHTML = state.patternColors.map(h => `
- <div class="color-dot" data-hex="${h}" style="background:${h}" title="${h}"></div>
- `).join('');
- root.querySelectorAll('.color-dot').forEach(el => {
- el.addEventListener('click', () => {
- const hex = el.dataset.hex;
- if (state.pickedColors.has(hex)) { state.pickedColors.delete(hex); el.classList.remove('selected'); }
- else { state.pickedColors.add(hex); el.classList.add('selected'); }
- document.getElementById('plColorCount').textContent = `(${state.pickedColors.size} selected)`;
- });
- });
-}
-
-function renderPlaidTypes() {
- const root = document.getElementById('plTypes');
- root.innerHTML = PLAID_TYPES.map(t => `
- <span class="pl-chip" data-key="${t.key}" title="${t.blurb}">${t.label}</span>
- `).join('');
- root.querySelectorAll('.pl-chip').forEach(el => {
- el.addEventListener('click', () => {
- const k = el.dataset.key;
- if (state.pickedPlaidTypes.has(k)) { state.pickedPlaidTypes.delete(k); el.classList.remove('selected'); }
- else { state.pickedPlaidTypes.add(k); el.classList.add('selected'); }
- document.getElementById('plTypeCount').textContent = `(${state.pickedPlaidTypes.size} selected)`;
- });
- });
-}
-
-window.selectAllPlaids = function() {
- state.pickedPlaidTypes = new Set(PLAID_TYPES.map(t => t.key));
- document.querySelectorAll('#plTypes .pl-chip').forEach(el => el.classList.add('selected'));
- document.getElementById('plTypeCount').textContent = `(${state.pickedPlaidTypes.size} selected)`;
-};
-window.clearPlaids = function() {
- state.pickedPlaidTypes.clear();
- document.querySelectorAll('#plTypes .pl-chip').forEach(el => el.classList.remove('selected'));
- document.getElementById('plTypeCount').textContent = '(0 selected)';
-};
-
-window.generatePlaids = async function() {
- if (!state.pickedColors.size) { alert('Pick at least one color.'); return; }
- if (!state.pickedPlaidTypes.size) { alert('Pick at least one plaid/stripe type.'); return; }
- const colors = Array.from(state.pickedColors);
- const types = Array.from(state.pickedPlaidTypes);
- const btn = document.getElementById('btn-gen-plaid');
- const summary = document.getElementById('plSummary');
- const results = document.getElementById('plResults');
- btn.disabled = true;
- btn.textContent = `Generating ${types.length} variant${types.length>1?'s':''}…`;
- summary.textContent = `${colors.length} color${colors.length>1?'s':''} · ${types.length} variant${types.length>1?'s':''} · ~${Math.ceil(types.length * 10)}s`;
- try {
- const r = await fetch('/api/marketplace/ai/generate-stripe-plaid', {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ patternSlug: slug, colors, plaidTypes: types, textureSku: state.selectedTexture?.sku || null }),
- });
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status));
- (j.variants || []).forEach(v => {
- const card = document.createElement('div'); card.className = 'colorway-card';
- const imgHtml = v.image_url ? `<div style="aspect-ratio:1/1;border-radius:12px;background:#eee url('${v.image_url}') center/cover;margin-bottom:10px;"></div>` : '';
- card.innerHTML = imgHtml +
- `<div class="name">${v.label || v.type} <span style="color:var(--mp-muted);font-weight:400;">· AI</span></div>` +
- `<div class="swatch-row">${(v.hex_palette||[]).map(h => `<div class="swatch" style="background:${h}" title="${h}"></div>`).join('')}</div>`;
- results.appendChild(card);
- });
- btn.textContent = `Generated ${j.variants?.length || 0}/${types.length}`;
- if (j.errors && j.errors.length) summary.textContent += ` · ${j.errors.length} failed`;
- } catch (err) {
- alert('Stripe/plaid generation failed: ' + err.message);
- btn.disabled = false;
- btn.textContent = 'Generate selected stripe/plaid variants';
- }
-};
-
-// ── colorway generator (texture-aware) ─────────────────────────────────────
-window.orderSample = function() { alert('Sample order — wires to Designer Wallcoverings fulfillment ($5 ships in 3–5 days).'); };
-window.orderRoll = function() { alert('Full-roll order — wires to Shopify checkout.'); };
-window.aiColorways = async function() {
- const btn = document.getElementById('btn-ai-colorways');
- const cwEl = document.getElementById('cw');
- const hasTexture = !!state.selectedTexture;
- if (btn) { btn.disabled = true; btn.textContent = hasTexture ? `Generating on ${state.selectedTexture.title}… (~30s)` : 'Generating 6 colorways… (~30s)'; }
- try {
- const r = await fetch('/api/marketplace/ai/generate-colorways', {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ patternSlug: slug, textureSku: state.selectedTexture?.sku || null }),
- });
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || ('HTTP ' + r.status));
- if (cwEl && Array.isArray(j.colorways)) {
- j.colorways.forEach(c => {
- const palette = Array.isArray(c.hex_palette) ? c.hex_palette : [];
- const card = document.createElement('div'); card.className = 'colorway-card';
- const imgHtml = c.image_url
- ? `<div style="aspect-ratio:1/1;border-radius:12px;background:#eee url('${c.image_url}') center/cover;margin-bottom:10px;"></div>`
- : '';
- const texLabel = hasTexture ? ` <span style="color:var(--mp-muted);font-weight:400;">· on ${state.selectedTexture.category}</span>` : '';
- card.innerHTML = imgHtml +
- `<div class="name">${c.name} <span style="color:var(--mp-muted);font-weight:400;">· AI</span>${texLabel}</div>` +
- `<div class="swatch-row">${palette.map(h => `<div class="swatch" style="background:${h}" title="${h}"></div>`).join('')}</div>`;
- cwEl.appendChild(card);
- });
- }
- if (btn) btn.textContent = `Generated ${j.colorways?.length || 0} colorways${hasTexture ? ' on ' + state.selectedTexture.category : ''}`;
- } catch (err) {
- alert('Colorway generation failed: ' + err.message);
- if (btn) { btn.disabled = false; btn.textContent = 'Generate AI colorways'; }
- }
-};
-load();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/patterns.html b/public/marketplace/patterns.html
deleted file mode 100644
index 6a800a8..0000000
--- a/public/marketplace/patterns.html
+++ /dev/null
@@ -1,151 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Patterns · Wallco Marketplace</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .controls { display:flex; gap: 10px; flex-wrap:wrap; margin: 14px 0 22px; align-items:center; }
- .controls select, .controls input[type="search"], .controls input[type="text"] {
- padding: 8px 12px; border-radius: 999px; border: 1px solid var(--mp-border);
- background: rgba(255,255,255,.8); font-size: 14px;
- }
- .controls input[type="search"] { min-width: 240px; }
- .controls label.chk { display:inline-flex; gap:6px; align-items:center; font-size:14px; color:var(--mp-ink-2); }
- .meta { color: var(--mp-ink-2); font-size: 13px; margin: 0 0 12px; }
-</style>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Patterns</h1>
- <p class="lede">Every pattern is uploaded by an independent designer, rights-confirmed, and produced through Designer Wallcoverings. Search, sort, sample.</p>
- </header>
- <main class="mp-wrap">
- <div class="controls">
- <input type="search" id="q" placeholder="Search title, description, tags…" autocomplete="off" />
- <span class="sub">Style</span>
- <input type="text" id="style" placeholder="e.g. floral, modern" size="14" />
- <span class="sub">Room</span>
- <input type="text" id="room" placeholder="e.g. bedroom" size="12" />
- <span class="sub">Color</span>
- <input type="text" id="color" placeholder="e.g. blue, gold" size="14" />
- <label class="chk"><input type="checkbox" id="commercial"/> Commercial only</label>
- <span class="sub">Sort</span>
- <select id="sort">
- <option value="featured">Featured first</option>
- <option value="newest">Newest</option>
- </select>
- <span class="sub">Designer</span>
- <select id="designer"><option value="">All</option></select>
- <span class="sub" style="margin-left:14px">Density</span>
- <input type="range" id="density" min="200" max="380" value="280" step="10"/>
- </div>
- <p class="meta" id="meta"></p>
- <div class="mp-grid lg" id="grid"></div>
- </main>
-
-<script>
-const grid = document.getElementById('grid');
-const qInput = document.getElementById('q');
-const styleInput = document.getElementById('style');
-const roomInput = document.getElementById('room');
-const colorInput = document.getElementById('color');
-const commercialInput = document.getElementById('commercial');
-const sortSel = document.getElementById('sort');
-const dSel = document.getElementById('designer');
-const density = document.getElementById('density');
-const meta = document.getElementById('meta');
-
-const KEY = 'wallco.marketplace.patterns';
-const saved = JSON.parse(localStorage.getItem(KEY) || '{}');
-if (saved.density) density.value = saved.density;
-if (saved.q) qInput.value = saved.q;
-if (saved.style) styleInput.value = saved.style;
-if (saved.room) roomInput.value = saved.room;
-if (saved.color) colorInput.value = saved.color;
-if (saved.commercial) commercialInput.checked = !!saved.commercial;
-if (saved.sort) sortSel.value = saved.sort;
-applyDensity();
-
-function applyDensity() { grid.style.gridTemplateColumns = `repeat(auto-fill, minmax(${density.value}px, 1fr))`; }
-
-function saveState() {
- localStorage.setItem(KEY, JSON.stringify({
- density: density.value,
- q: qInput.value,
- style: styleInput.value,
- room: roomInput.value,
- color: colorInput.value,
- commercial: commercialInput.checked,
- sort: sortSel.value,
- }));
-}
-
-async function loadDesigners() {
- const j = await fetch('/api/marketplace/designers?limit=120').then(r => r.json());
- (j.designers || []).forEach(d => {
- const o = document.createElement('option'); o.value=d.slug; o.textContent=d.display_name; dSel.appendChild(o);
- });
-}
-
-function hasFilters() {
- return !!(qInput.value.trim() || styleInput.value.trim() || roomInput.value.trim() || colorInput.value.trim() || commercialInput.checked);
-}
-
-async function load() {
- saveState();
- let url, j;
- if (hasFilters()) {
- const params = new URLSearchParams({ limit: '120' });
- if (qInput.value.trim()) params.set('q', qInput.value.trim());
- if (styleInput.value.trim()) params.set('style', styleInput.value.trim());
- if (roomInput.value.trim()) params.set('room', roomInput.value.trim());
- if (colorInput.value.trim()) params.set('color', colorInput.value.trim());
- if (commercialInput.checked) params.set('commercial', 'true');
- url = '/api/marketplace/search?' + params;
- } else {
- const params = new URLSearchParams({ limit: '120' });
- if (dSel.value) params.set('designer', dSel.value);
- url = '/api/marketplace/patterns?' + params;
- }
- j = await fetch(url).then(r => r.json());
- let patterns = j.patterns || [];
- if (sortSel.value === 'newest' && hasFilters()) {
- // ranked results — re-sort by id desc as a proxy for newest
- patterns = patterns.slice().sort((a,b) => (b.id||0) - (a.id||0));
- }
- grid.innerHTML = '';
- patterns.forEach(p => {
- const a = document.createElement('a'); a.className='mp-card'; a.href=`/patterns/${p.slug}`;
- a.innerHTML = `
- <div class="img" style="background-image:url('${p.thumbnail_url || p.original_image_url || ''}')"></div>
- <div class="body">
- <div class="title">${p.title}</div>
- <div class="sub">by ${p.designer_name}</div>
- <div class="row">
- ${(p.style_tags||[]).slice(0,2).map(t => `<span class="mp-pill">${t}</span>`).join(' ')}
- ${p.featured ? '<span class="mp-pill gold">Featured</span>' : ''}
- </div>
- </div>`;
- grid.appendChild(a);
- });
- meta.textContent = patterns.length
- ? `${patterns.length} pattern${patterns.length===1?'':'s'}${hasFilters() ? ' · filtered' : ''}`
- : (hasFilters() ? 'No patterns match those filters.' : 'No patterns yet.');
-}
-
-let qTimer = null;
-function debounce(fn, ms){ return () => { clearTimeout(qTimer); qTimer = setTimeout(fn, ms); }; }
-const debouncedLoad = debounce(load, 250);
-
-[qInput, styleInput, roomInput, colorInput].forEach(el => el.addEventListener('input', debouncedLoad));
-commercialInput.addEventListener('change', load);
-sortSel.addEventListener('change', load);
-dSel.addEventListener('change', load);
-density.addEventListener('input', () => { applyDensity(); saveState(); });
-loadDesigners().then(load);
-</script>
-</body>
-</html>
diff --git a/public/marketplace/projects.html b/public/marketplace/projects.html
deleted file mode 100644
index ed41a25..0000000
--- a/public/marketplace/projects.html
+++ /dev/null
@@ -1,174 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Trade Projects · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .mp-proj-head { display:flex; align-items:flex-end; justify-content:space-between; gap: 16px; flex-wrap: wrap; padding: 24px 0; border-bottom: 1px solid var(--mp-border); margin-bottom: 18px; }
- .mp-proj-head h1 { font-size: clamp(28px, 3.4vw, 42px); margin: 0 0 4px; letter-spacing: -.02em; }
- .mp-proj-head .lede { color: var(--mp-muted); font-size: 14px; max-width: 540px; }
- .mp-proj-card { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: var(--mp-radius); box-shadow: var(--mp-glass-shadow); padding: 18px 20px; margin-bottom: 18px; }
- .mp-proj-card .head { display:flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
- .mp-proj-card .head h2 { margin: 0; font-size: 20px; letter-spacing: -.01em; }
- .mp-proj-card .meta { font-size: 13px; color: var(--mp-muted); }
- .mp-proj-saves { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; margin-top: 14px; }
- .mp-save { background: #fff; border: 1px solid var(--mp-border); border-radius: 14px; overflow: hidden; transition: transform .15s ease; }
- .mp-save:hover { transform: translateY(-2px); }
- .mp-save .img { aspect-ratio: 1/1; background: #eee; background-size: cover; background-position: center; }
- .mp-save .body { padding: 8px 10px; }
- .mp-save .t { font-size: 13px; font-weight: 600; line-height: 1.2; }
- .mp-save .s { font-size: 11px; color: var(--mp-muted); margin-top: 2px; }
- .mp-empty-saves { color: var(--mp-muted); font-size: 13px; font-style: italic; }
- .mp-empty { padding: 60px 0; text-align: center; color: var(--mp-muted); }
- .mp-empty h2 { font-size: 22px; color: var(--mp-fg); margin: 0 0 8px; font-weight: 600; }
- .mp-newproj { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 24px; }
- .mp-newproj input { border: 1px solid var(--mp-border); padding: 10px 12px; border-radius: 12px; font-size: 14px; background: #fff; min-width: 220px; }
- .mp-err { color: #c33; font-size: 13px; margin: 8px 0; }
-</style>
-</head>
-<body>
- <main id="root"><div class="mp-wrap"><p>Loading…</p></div></main>
-
-<script>
-async function load() {
- const root = document.getElementById('root');
- let res;
- try {
- res = await fetch('/api/marketplace/projects', { credentials: 'include' });
- } catch (err) {
- root.innerHTML = `<div class="mp-wrap"><p class="mp-err">Network error: ${err.message}</p></div>`;
- return;
- }
-
- if (res.status === 401) {
- root.innerHTML = `
- <div class="mp-wrap">
- <div class="mp-proj-head">
- <div>
- <h1>Trade · Project Boards</h1>
- <div class="lede">Save patterns to project boards, share with clients, and order samples in bulk. Sign in to start a project.</div>
- </div>
- </div>
- <div class="mp-empty">
- <h2>Sign in to view your project boards</h2>
- <p>You'll need a designer or trade account to save patterns.</p>
- <a class="mp-btn" href="/marketplace/apply">Apply for an account</a>
- </div>
- </div>`;
- return;
- }
- if (!res.ok) {
- let msg = 'Failed to load projects';
- try { msg = (await res.json()).error || msg; } catch (_) {}
- root.innerHTML = `<div class="mp-wrap"><p class="mp-err">${msg}</p></div>`;
- return;
- }
-
- const j = await res.json();
- const projects = j.projects || [];
- const head = `
- <div class="mp-proj-head">
- <div>
- <h1>Trade · Project Boards</h1>
- <div class="lede">Patterns you've saved, organized by project. Use boards to assemble specs for clients, share scope, and queue sample orders.</div>
- </div>
- <div class="meta">${projects.length} project${projects.length === 1 ? '' : 's'} · ${
- projects.reduce((n, p) => n + (Number(p.save_count) || 0), 0)
- } saves</div>
- </div>
- <form class="mp-newproj" id="np">
- <input type="text" name="title" placeholder="New project title" required />
- <input type="text" name="client_name" placeholder="Client (optional)" />
- <input type="text" name="room_type" placeholder="Room type (optional)" />
- <button class="mp-btn" type="submit">Create board</button>
- <span class="mp-err" id="np-err" style="display:none"></span>
- </form>`;
-
- if (!projects.length) {
- root.innerHTML = `
- <div class="mp-wrap">
- ${head}
- <div class="mp-empty">
- <h2>No project boards yet</h2>
- <p>Create your first board above, then browse <a class="under" href="/patterns">patterns</a> and click <em>Save to project</em>.</p>
- </div>
- </div>`;
- bindNewProj();
- return;
- }
-
- const cards = projects.map((p) => {
- const meta = [
- p.client_name ? p.client_name : null,
- p.room_type ? p.room_type : null,
- `${Number(p.save_count) || 0} pattern${(Number(p.save_count) || 0) === 1 ? '' : 's'}`,
- ].filter(Boolean).join(' · ');
- const saves = (p.saves || []).map((s) => {
- const img = (s.colorway && s.colorway.image_url) || s.pattern.thumbnail_url || s.pattern.original_image_url || '';
- return `
- <a class="mp-save" href="/patterns/${encodeURIComponent(s.pattern.slug)}">
- <div class="img" style="background-image:url('${img}')"></div>
- <div class="body">
- <div class="t">${escapeHtml(s.pattern.title || 'Untitled')}</div>
- <div class="s">${escapeHtml(s.designer.name || '')}${s.colorway ? ' · ' + escapeHtml(s.colorway.name || '') : ''}</div>
- </div>
- </a>`;
- }).join('');
- return `
- <article class="mp-proj-card">
- <div class="head">
- <div>
- <h2>${escapeHtml(p.title)}</h2>
- <div class="meta">${escapeHtml(meta)}</div>
- </div>
- <div class="meta">Created ${new Date(p.created_at).toLocaleDateString()}</div>
- </div>
- ${p.notes ? `<p style="margin: 10px 0 0; color: var(--mp-muted); font-size: 13px;">${escapeHtml(p.notes)}</p>` : ''}
- ${saves ? `<div class="mp-proj-saves">${saves}</div>` : `<div class="mp-empty-saves" style="margin-top: 10px">No patterns saved yet — browse the <a class="under" href="/patterns">pattern library</a> and save a few to this board.</div>`}
- </article>`;
- }).join('');
-
- root.innerHTML = `<div class="mp-wrap">${head}${cards}</div>`;
- bindNewProj();
-}
-
-function escapeHtml(s) {
- return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
-}
-
-function bindNewProj() {
- const form = document.getElementById('np');
- if (!form) return;
- form.addEventListener('submit', async (e) => {
- e.preventDefault();
- const fd = new FormData(form);
- const errEl = document.getElementById('np-err');
- errEl.style.display = 'none';
- try {
- const r = await fetch('/api/marketplace/projects', {
- method: 'POST',
- credentials: 'include',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({
- title: fd.get('title'),
- client_name: fd.get('client_name'),
- room_type: fd.get('room_type'),
- }),
- });
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'Create failed');
- load();
- } catch (err) {
- errEl.textContent = err.message;
- errEl.style.display = '';
- }
- });
-}
-
-load();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/status.html b/public/marketplace/status.html
deleted file mode 100644
index 83bf1e0..0000000
--- a/public/marketplace/status.html
+++ /dev/null
@@ -1,271 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Wallco Marketplace · Live Build Status</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .status-meta { display:flex; gap:12px; align-items:center; font-size: 13px; color: var(--mp-muted); margin-top: 6px; }
- .dot { width:10px; height:10px; border-radius:50%; background:#0a7; display:inline-block; animation: pulse 1.4s infinite; }
- @keyframes pulse { 50% { opacity: .35; } }
- .latest { display: grid; gap: 12px; margin-top: 16px; }
- .row-item { display:flex; gap: 14px; align-items:center; padding: 10px 14px; background: rgba(255,255,255,.6); border:1px solid var(--mp-border); border-radius: 14px; }
- .row-item .thumb { width: 56px; height: 56px; border-radius: 10px; background: #eee; background-size: cover; background-position: center; flex: 0 0 56px; }
- .row-item .meta-r { font-size: 12px; color: var(--mp-muted); }
- pre.json { background: #111; color: #cfe; padding: 14px; border-radius: 14px; font-size: 12px; overflow:auto; max-height: 300px; }
- .build-log { padding: 14px 16px; background: #0c0c0e; color: #d8d8d8; border-radius: 16px; font: 12px/1.55 ui-monospace, Menlo, monospace; height: 360px; overflow: auto; }
- .build-log .ok { color: #6ce892; }
- .build-log .warn{ color: #f3c969; }
- .build-log .err { color: #f08a8a; }
- .checks { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 10px; margin-top: 10px; }
- .check-row { display:flex; align-items:center; gap:8px; font-size:13px; }
- .check-row .pill-ok { background:#0a7; color:#fff; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
- .check-row .pill-fail{ background:#c33; color:#fff; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
-</style>
-</head>
-<body>
- <header class="mp-hero">
- <h1>Live build status</h1>
- <p class="lede">What's running, what's seeded, what's earning. Auto-refreshing every 4 seconds. This is the "show what's happening" board Steve asked for.</p>
- <div class="status-meta"><span class="dot" id="dot"></span><span id="liveText">Live • polling /api/marketplace/status</span></div>
- </header>
-
- <main class="mp-wrap">
-
- <section class="mp-status-board">
- <h2 style="margin-top:0">Counts</h2>
- <div class="mp-status-grid" id="stats"></div>
- </section>
-
- <section class="mp-section">
- <h2>Endpoint health</h2>
- <div class="sub">Smoke-test every marketplace endpoint and show green/red live.</div>
- <div class="checks" id="checks"></div>
- </section>
-
- <section class="mp-section">
- <h2>Deltas (rolling window)</h2>
- <div class="sub">Change since 5m / 30m / 60m / 24h ago — populated by pm2 service <code>wallco-mp-watcher</code>.</div>
- <div class="mp-status-grid" id="deltas"></div>
- </section>
-
- <section class="mp-section">
- <h2>Recent events</h2>
- <div class="sub">Append-only feed of meaningful changes (new designer, new pattern, new licensing inquiry, watcher startup, etc.).</div>
- <div class="latest" id="events"></div>
- </section>
-
- <section class="mp-section">
- <h2>Latest activity</h2>
- <div class="sub">Most-recent designer signups, patterns, commission events, and points earned.</div>
- <div class="latest" id="latest"></div>
- </section>
-
- <section class="mp-section">
- <h2>Raw status payload</h2>
- <pre class="json" id="raw"></pre>
- </section>
-
- <section class="mp-section">
- <h2>Build log</h2>
- <div class="sub">Append-only stream of what just got built. Refreshes from server's pm2 log when available.</div>
- <div class="build-log" id="log"></div>
- </section>
-
- </main>
-
-<script>
-const ENDPOINTS = [
- ['GET', '/api/marketplace/status'],
- ['GET', '/api/marketplace/designers'],
- ['GET', '/api/marketplace/patterns'],
- ['GET', '/api/marketplace/designers/astrid-mauve'],
- ['POST', '/api/marketplace/commissions/calculate', { lineItemPrice: 496, commissionRate: 15 }],
- ['POST', '/api/marketplace/ai/generate-colorways', { patternSlug: 'wildflower-reverie-astrid-mauve' }],
- ['POST', '/api/marketplace/ai/tag-pattern', {}],
- ['POST', '/api/marketplace/ai/write-pattern-description', { title: 'Wildflower Reverie' }],
-];
-
-const STATS = [
- ['designers', 'Designers'],
- ['designers_approved', 'Approved'],
- ['founding', 'Founding'],
- ['unclaimed', 'Unclaimed pages'],
- ['patterns', 'Patterns'],
- ['patterns_approved', 'Approved patterns'],
- ['patterns_pending', 'Pending review'],
- ['collections', 'Published collections'],
- ['commission_entries', 'Commission events'],
- ['pending_commission', 'Pending $ owed', '$'],
- ['paid_commission', 'Paid $ to date', '$'],
- ['open_inquiries', 'Open license inquiries'],
- ['open_takedowns', 'Open takedowns'],
-];
-
-function logLine(text, cls) {
- const log = document.getElementById('log');
- const line = document.createElement('div');
- const t = new Date().toLocaleTimeString();
- line.innerHTML = `<span style="color:#888">${t}</span> <span class="${cls||''}">${text}</span>`;
- log.appendChild(line);
- log.scrollTop = log.scrollHeight;
-}
-
-async function refreshStats() {
- try {
- const r = await fetch('/api/marketplace/status', { credentials: 'include' });
- const j = await r.json();
- document.getElementById('raw').textContent = JSON.stringify(j, null, 2);
- const grid = document.getElementById('stats');
- grid.innerHTML = '';
- for (const [k,label,prefix] of STATS) {
- const v = j.counts && j.counts[k] != null ? j.counts[k] : '–';
- const card = document.createElement('div');
- card.className = 'mp-stat';
- const display = (prefix === '$') ? ('$' + Number(v).toLocaleString(undefined,{maximumFractionDigits:2})) : Number(v).toLocaleString();
- card.innerHTML = `<div class="label">${label}</div><div class="value">${display}</div>`;
- grid.appendChild(card);
- }
- document.getElementById('liveText').textContent = 'Live • db ' + (j.db ? 'up' : 'DOWN') + ' • ' + new Date(j.ts).toLocaleTimeString();
- document.getElementById('dot').style.background = j.db ? '#0a7' : '#c33';
- } catch (err) {
- logLine('status fetch failed: ' + err.message, 'err');
- document.getElementById('dot').style.background = '#c33';
- }
-}
-
-async function refreshChecks() {
- const grid = document.getElementById('checks');
- grid.innerHTML = '';
- for (const ep of ENDPOINTS) {
- const [method, url, body] = ep;
- const row = document.createElement('div');
- row.className = 'check-row';
- row.innerHTML = `<code>${method} ${url}</code>`;
- grid.appendChild(row);
- try {
- const opts = { method, credentials: 'include', headers: { 'content-type': 'application/json' } };
- if (body) opts.body = JSON.stringify(body);
- const r = await fetch(url, opts);
- const ok = r.ok;
- const pill = document.createElement('span');
- pill.className = ok ? 'pill-ok' : 'pill-fail';
- pill.textContent = ok ? ('OK ' + r.status) : ('FAIL ' + r.status);
- row.appendChild(pill);
- logLine(`${method} ${url} → ${r.status}`, ok ? 'ok' : 'err');
- } catch (err) {
- const pill = document.createElement('span');
- pill.className = 'pill-fail';
- pill.textContent = 'ERR';
- row.appendChild(pill);
- logLine(`${method} ${url} → ${err.message}`, 'err');
- }
- }
-}
-
-async function refreshLatest() {
- try {
- const [designers, patterns] = await Promise.all([
- fetch('/api/marketplace/designers?sort=newest&limit=4').then(r => r.json()),
- fetch('/api/marketplace/patterns?limit=4').then(r => r.json()),
- ]);
- const out = document.getElementById('latest');
- out.innerHTML = '';
- (designers.designers || []).forEach(d => {
- const row = document.createElement('div'); row.className = 'row-item';
- row.innerHTML = `<div class="thumb" style="background-image:url('${d.avatar_url || ''}')"></div>
- <div style="flex:1">
- <div><strong>${d.display_name}</strong> · <a class="under" href="/designers/${d.slug}">/designers/${d.slug}</a></div>
- <div class="meta-r">${d.level} · ${Number(d.points||0).toLocaleString()} pts · ${d.is_founding ? 'Founding' : ''} ${d.is_verified ? '· Verified' : ''}</div>
- </div>`;
- out.appendChild(row);
- });
- (patterns.patterns || []).forEach(p => {
- const row = document.createElement('div'); row.className = 'row-item';
- row.innerHTML = `<div class="thumb" style="background-image:url('${p.thumbnail_url || ''}')"></div>
- <div style="flex:1">
- <div><strong>${p.title}</strong> · <a class="under" href="/patterns/${p.slug}">/patterns/${p.slug}</a></div>
- <div class="meta-r">by ${p.designer_name} · ${(p.style_tags||[]).slice(0,3).join(' · ')}</div>
- </div>`;
- out.appendChild(row);
- });
- } catch (err) {
- logLine('latest fetch failed: ' + err.message, 'err');
- }
-}
-
-async function refreshDeltas() {
- try {
- const w = await fetch('/api/marketplace/watcher').then(r => r.json());
- const grid = document.getElementById('deltas');
- if (!w.deltas) { grid.innerHTML = '<div class="sub">Watcher warming up… give it 60s after pm2 start.</div>'; return; }
- grid.innerHTML = '';
- const KEYS = [
- ['designers', 'Designers'],
- ['patterns', 'Patterns'],
- ['collections', 'Collections'],
- ['commission_entries', 'Commission events'],
- ['open_inquiries', 'Licensing inquiries'],
- ['unclaimed', 'Claim requests'],
- ];
- KEYS.forEach(([k,label]) => {
- const card = document.createElement('div'); card.className = 'mp-stat';
- const d5 = w.deltas['5m'][k] || 0;
- const d30 = w.deltas['30m'][k] || 0;
- const d60 = w.deltas['60m'][k] || 0;
- const d24 = w.deltas['24h'][k] || 0;
- const sign = (n) => n > 0 ? ('+' + n) : (n < 0 ? String(n) : '0');
- const color = (n) => n > 0 ? 'color:#0a7' : (n < 0 ? 'color:#c33' : 'color:#aaa');
- card.innerHTML = `<div class="label">${label}</div>
- <div style="display:flex;gap:8px;font-size:14px;font-weight:600;margin-top:4px;flex-wrap:wrap">
- <span style="${color(d5)}">5m ${sign(d5)}</span>
- <span style="${color(d30)}">30m ${sign(d30)}</span>
- <span style="${color(d60)}">1h ${sign(d60)}</span>
- <span style="${color(d24)}">24h ${sign(d24)}</span>
- </div>`;
- grid.appendChild(card);
- });
- } catch (err) {
- document.getElementById('deltas').innerHTML = `<div class="sub">watcher offline · ${err.message}</div>`;
- }
-}
-
-async function refreshEvents() {
- try {
- const j = await fetch('/api/marketplace/events?limit=20').then(r => r.json());
- const out = document.getElementById('events');
- out.innerHTML = '';
- (j.events || []).forEach(e => {
- const row = document.createElement('div'); row.className = 'row-item';
- const t = new Date(e.ts).toLocaleString();
- let summary = '';
- if (e.type === 'startup') summary = `🟢 watcher online (${e.host}:${e.port})`;
- else if (e.type === 'delta') summary = `+${e.change} ${e.label} (total ${e.total})`;
- else if (e.type === 'poll_fail') summary = `❌ poll failed: ${e.error}`;
- else summary = `${e.type}: ${JSON.stringify(e).slice(0,140)}`;
- row.innerHTML = `<div style="flex:1"><strong>${summary}</strong><div class="meta-r">${t}</div></div>`;
- out.appendChild(row);
- });
- if (!j.events || !j.events.length) out.innerHTML = '<div class="sub">No events yet.</div>';
- } catch (err) {
- document.getElementById('events').innerHTML = `<div class="sub">${err.message}</div>`;
- }
-}
-
-let firstRun = true;
-async function tick() {
- if (firstRun) { logLine('▶ status board live, polling every 4s', 'ok'); firstRun = false; }
- await refreshStats();
- await refreshDeltas();
- await refreshEvents();
- await refreshLatest();
- if (Math.random() < 0.25) await refreshChecks(); // re-smoke every ~16s
-}
-tick();
-refreshChecks();
-setInterval(tick, 4000);
-</script>
-</body>
-</html>
diff --git a/public/marketplace/storefront.html b/public/marketplace/storefront.html
deleted file mode 100644
index e6184af..0000000
--- a/public/marketplace/storefront.html
+++ /dev/null
@@ -1,132 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<title>Storefront · Wallco</title>
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="stylesheet" href="/marketplace/_layout.css" />
-<script src="/marketplace/_partials.js" defer></script>
-<style>
- .hero-wide { padding: clamp(60px, 8vw, 140px) var(--mp-pad); background: linear-gradient(135deg,#1a1a1a 0%,#222 50%,#0d0d0d 100%); color: #fff; }
- .hero-wide h1 { font-size: clamp(40px, 5vw, 80px); margin: 0; letter-spacing: -.02em; }
- .hero-wide .meta-w { color: rgba(255,255,255,.7); }
- .hero-wide .badges { margin: 12px 0; display:flex; gap:6px; flex-wrap:wrap; }
- .hero-wide .mp-pill { background: rgba(255,255,255,.12); color:#fff; }
- .hero-wide .mp-pill.gold { background: linear-gradient(135deg,#cda86e,#f2dca5); color: #2c2110; }
-</style>
-</head>
-<body>
- <main id="root"><div class="mp-wrap"><p>Loading…</p></div></main>
-
-<script>
-const slug = location.pathname.split('/').filter(Boolean).pop();
-async function load() {
- const j = await fetch('/api/marketplace/designers/' + encodeURIComponent(slug)).then(r => r.json());
- const root = document.getElementById('root');
- if (!j.designer) {
- root.innerHTML = `<div class="mp-wrap"><h2>This designer hasn't published a storefront yet.</h2><a class="mp-btn" href="/designers">Browse designers</a></div>`;
- return;
- }
- const d = j.designer;
- root.innerHTML = `
- <section class="hero-wide">
- <div class="mp-wrap">
- <div class="meta-w" style="margin-bottom: 10px">Wallco · Designer Storefront</div>
- <h1>${d.display_name}</h1>
- <div class="meta-w" style="margin-top: 8px; max-width: 720px; line-height: 1.6;">${d.bio || ''}</div>
- <div class="badges">
- <span class="mp-pill ${d.is_founding ? 'gold' : ''}">${d.level}</span>
- ${d.is_verified ? '<span class="mp-pill verified">Verified</span>' : ''}
- ${(j.badges||[]).map(b => `<span class="mp-pill">${b.icon||'★'} ${b.badge_name}</span>`).join('')}
- </div>
- <div style="margin-top: 18px; display:flex; gap:10px; flex-wrap:wrap; align-items: center;">
- <a class="mp-btn" href="#patterns">Shop patterns</a>
- <button class="mp-btn ghost" id="mp-follow-btn" data-designer-id="${d.id}" type="button" style="display:none; background:rgba(255,255,255,.08); color:#fff; border-color: rgba(255,255,255,.2);">Follow</button>
- <span id="mp-follow-count" class="mp-pill" style="display:none"></span>
- <a class="mp-btn ghost" style="background:rgba(255,255,255,.08); color:#fff; border-color: rgba(255,255,255,.2);" href="/licensing/${slug}">License artwork</a>
- <a class="mp-btn ghost" style="background:rgba(255,255,255,.08); color:#fff; border-color: rgba(255,255,255,.2);" href="#custom">Request custom design</a>
- </div>
- </div>
- </section>
- <main class="mp-wrap">
- ${(j.collections||[]).length ? `<section class="mp-section">
- <h2>Collections</h2>
- <div class="mp-grid lg">
- ${j.collections.map(c => `<div class="mp-card"><div class="img" style="background: linear-gradient(135deg,#eee,#ddd)"></div><div class="body"><div class="title">${c.title}</div></div></div>`).join('')}
- </div>
- </section>` : ''}
- <section class="mp-section" id="patterns">
- <h2>Patterns</h2>
- <div class="mp-grid lg" id="pat"></div>
- </section>
- <section class="mp-section" id="custom">
- <h2>Custom & licensing</h2>
- <div class="mp-grid lg">
- <div class="mp-card"><div class="body"><strong>Order samples</strong><div class="sub">Any pattern, $5 per sample, ships in 3–5 business days.</div><a class="mp-btn tiny" style="margin-top:8px" href="/patterns">Browse patterns</a></div></div>
- <div class="mp-card"><div class="body"><strong>License artwork</strong><div class="sub">Non-exclusive or exclusive. Hospitality, commercial print, packaging.</div><a class="mp-btn tiny" style="margin-top:8px" href="/licensing/${slug}">Inquire</a></div></div>
- <div class="mp-card"><div class="body"><strong>Custom commissions</strong><div class="sub">Custom recolors, scale changes, murals, or new artwork.</div><a class="mp-btn tiny" style="margin-top:8px" href="/licensing/${slug}">Request</a></div></div>
- <div class="mp-card"><div class="body"><strong>Trade accounts</strong><div class="sub">Interior designers + architects — save to project boards, free samples, payment terms.</div><a class="mp-btn tiny" style="margin-top:8px" href="/marketplace/apply">Apply</a></div></div>
- </div>
- </section>
- </main>`;
-
- const pat = document.getElementById('pat');
- (j.patterns || []).forEach(p => {
- const a = document.createElement('a'); a.className='mp-card'; a.href=`/patterns/${p.slug}`;
- a.innerHTML = `
- <div class="img" style="background-image:url('${p.thumbnail_url || p.original_image_url}')"></div>
- <div class="body">
- <div class="title">${p.title}</div>
- <div class="sub">${(p.style_tags||[]).slice(0,2).join(' · ')}</div>
- <div class="row">
- <a class="mp-btn tiny">Order sample</a>
- <a class="mp-btn tiny ghost">License</a>
- </div>
- </div>`;
- pat.appendChild(a);
- });
-
- setupFollow(d.id, d.slug);
-}
-
-async function setupFollow(designerId, designerSlug) {
- const btn = document.getElementById('mp-follow-btn');
- const cntEl = document.getElementById('mp-follow-count');
- if (!btn) return;
- let st;
- try {
- st = await (await fetch('/api/marketplace/designers/' + encodeURIComponent(designerSlug) + '/follow', { credentials: 'include' })).json();
- } catch (_) { return; }
- btn.style.display = '';
- cntEl.style.display = '';
- render(st);
- btn.addEventListener('click', async () => {
- const next = !btn.dataset.following || btn.dataset.following === 'false';
- btn.disabled = true;
- try {
- const r = await fetch('/api/marketplace/designers/' + designerId + '/follow', {
- method: next ? 'POST' : 'DELETE',
- credentials: 'include',
- });
- if (r.status === 401) { window.location = '/marketplace/apply'; return; }
- const j = await r.json();
- if (!r.ok) throw new Error(j.error || 'Follow failed');
- render(j);
- } catch (err) {
- alert(err.message);
- } finally {
- btn.disabled = false;
- }
- });
- function render(s) {
- btn.dataset.following = String(!!s.following);
- btn.textContent = s.following ? 'Following ✓' : 'Follow';
- cntEl.textContent = (Number(s.count) || 0).toLocaleString() + ' follower' + (Number(s.count) === 1 ? '' : 's');
- cntEl.style.background = 'rgba(255,255,255,.12)';
- cntEl.style.color = '#fff';
- }
-}
-load();
-</script>
-</body>
-</html>
diff --git a/public/marketplace/uploads/plaid-gingham-1778710320184-zvvd2Q.png b/public/marketplace/uploads/plaid-gingham-1778710320184-zvvd2Q.png
deleted file mode 100644
index fb4cac2..0000000
Binary files a/public/marketplace/uploads/plaid-gingham-1778710320184-zvvd2Q.png and /dev/null differ
diff --git a/public/uploads b/public/uploads
new file mode 120000
index 0000000..bdd37a8
--- /dev/null
+++ b/public/uploads
@@ -0,0 +1 @@
+/Volumes/Henry/wallco-ai-archive/public/uploads
\ No newline at end of file
diff --git a/public/uploads/studio/abc_env b/public/uploads/studio/abc_env
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/abc_env and /dev/null differ
diff --git a/public/uploads/studio/abc_escape.png b/public/uploads/studio/abc_escape.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/abc_escape.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593186660_x.png b/public/uploads/studio/anon-1778593186660_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593186660_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593188756_clean.png b/public/uploads/studio/anon-1778593188756_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593188756_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593202294_x.png b/public/uploads/studio/anon-1778593202294_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593202294_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593204319_clean.png b/public/uploads/studio/anon-1778593204319_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593204319_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593228883_x.png b/public/uploads/studio/anon-1778593228883_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593228883_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593230815_clean.png b/public/uploads/studio/anon-1778593230815_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593230815_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593245262_x.png b/public/uploads/studio/anon-1778593245262_x.png
deleted file mode 100644
index e92f2fb..0000000
--- a/public/uploads/studio/anon-1778593245262_x.png
+++ /dev/null
@@ -1 +0,0 @@
-exploit
\ No newline at end of file
diff --git a/public/uploads/studio/anon-1778593475377_x.png b/public/uploads/studio/anon-1778593475377_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593475377_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593477424_clean.png b/public/uploads/studio/anon-1778593477424_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593477424_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593601608_x.png b/public/uploads/studio/anon-1778593601608_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593601608_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778593603667_clean.png b/public/uploads/studio/anon-1778593603667_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778593603667_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778594239612_x.png b/public/uploads/studio/anon-1778594239612_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778594239612_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778594241645_clean.png b/public/uploads/studio/anon-1778594241645_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778594241645_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778595929905_x.png b/public/uploads/studio/anon-1778595929905_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778595929905_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778595932493_clean.png b/public/uploads/studio/anon-1778595932493_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778595932493_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778615141686_x.png b/public/uploads/studio/anon-1778615141686_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778615141686_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778615143838_clean.png b/public/uploads/studio/anon-1778615143838_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778615143838_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778621049454_x.png b/public/uploads/studio/anon-1778621049454_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778621049454_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778621051395_clean.png b/public/uploads/studio/anon-1778621051395_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778621051395_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778621261864_x.png b/public/uploads/studio/anon-1778621261864_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778621261864_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1778621263848_clean.png b/public/uploads/studio/anon-1778621263848_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1778621263848_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1780269618715_x.png b/public/uploads/studio/anon-1780269618715_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1780269618715_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1780269621633_clean.png b/public/uploads/studio/anon-1780269621633_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1780269621633_clean.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1780269677996_x.png b/public/uploads/studio/anon-1780269677996_x.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1780269677996_x.png and /dev/null differ
diff --git a/public/uploads/studio/anon-1780269680307_clean.png b/public/uploads/studio/anon-1780269680307_clean.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/anon-1780269680307_clean.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778593188275_tiny.png b/public/uploads/studio/test-1778593188275_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778593188275_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778593203822_tiny.png b/public/uploads/studio/test-1778593203822_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778593203822_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778593230349_tiny.png b/public/uploads/studio/test-1778593230349_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778593230349_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778593476944_tiny.png b/public/uploads/studio/test-1778593476944_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778593476944_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778593603188_tiny.png b/public/uploads/studio/test-1778593603188_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778593603188_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778594241189_tiny.png b/public/uploads/studio/test-1778594241189_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778594241189_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778595931840_tiny.png b/public/uploads/studio/test-1778595931840_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778595931840_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778615143370_tiny.png b/public/uploads/studio/test-1778615143370_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778615143370_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778621050946_tiny.png b/public/uploads/studio/test-1778621050946_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778621050946_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1778621263377_tiny.png b/public/uploads/studio/test-1778621263377_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1778621263377_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1780269621132_tiny.png b/public/uploads/studio/test-1780269621132_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1780269621132_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test-1780269679833_tiny.png b/public/uploads/studio/test-1780269679833_tiny.png
deleted file mode 100644
index c443d5c..0000000
Binary files a/public/uploads/studio/test-1780269679833_tiny.png and /dev/null differ
diff --git a/public/uploads/studio/test123_tiny.png b/public/uploads/studio/test123_tiny.png
deleted file mode 100644
index e69de29..0000000
diff --git a/public/uploads/studio/test_tiny.png b/public/uploads/studio/test_tiny.png
deleted file mode 100644
index 2371c64..0000000
--- a/public/uploads/studio/test_tiny.png
+++ /dev/null
@@ -1 +0,0 @@
-�PNG
\ No newline at end of file
← ce0520d auto-save: 2026-06-26T04:09:16 (1 files) — data/designs.json
·
back to Wallco Ai
·
chore: macstudio3 migration — reconcile from mac2 + repoint ce1283b →