← back to Pattern Vault
public/trends.html
113 lines
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Trend Board — The Pattern Vault</title>
<style>
:root{--bg:#0d0b10;--card:#17141d;--ink:#efe9f7;--mut:#9a8fb0;--gold:#d4af6a;--line:#2a2436;--cols:3;--gap-green:#7fd18a;--covered:#9a8fb0}
*{box-sizing:border-box;margin:0}
body{background:var(--bg);color:var(--ink);font:15px/1.55 "Avenir Next",system-ui,sans-serif}
header{padding:56px 6vw 24px;text-align:center;border-bottom:1px solid var(--line)}
h1{font-size:clamp(26px,4.2vw,48px);font-weight:600;letter-spacing:.12em;text-transform:uppercase}
h1 em{color:var(--gold);font-style:normal}
.tag{color:var(--mut);font-style:italic;margin-top:8px}
.back{display:inline-block;margin-top:14px;color:var(--gold);text-decoration:none;font-size:13px;letter-spacing:.06em}
.chip{display:inline-block;margin-top:14px;margin-left:10px;padding:4px 14px;border:1px solid var(--line);border-radius:99px;color:var(--mut);font-size:12px;letter-spacing:.04em}
.controls{display:flex;gap:14px;align-items:center;justify-content:center;padding:18px 6vw 4px;flex-wrap:wrap}
select,input[type=range]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px}
.wrap{padding:20px 6vw 60px;max-width:1400px;margin:0 auto}
.lane{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:22px 24px;margin-bottom:22px}
.lane.gap{border-color:var(--gold)}
.laneHead{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
.laneName{font-size:20px;font-weight:600;letter-spacing:.04em}
.gapbadge{white-space:nowrap;padding:5px 13px;border-radius:99px;font-size:12.5px;font-weight:600;letter-spacing:.03em}
.gapbadge.g{background:rgba(127,209,138,.14);color:var(--gap-green);border:1px solid var(--gap-green)}
.gapbadge.c{background:transparent;color:var(--covered);border:1px solid var(--line)}
.why{color:var(--ink);margin:12px 0 8px;max-width:70ch}
.meta{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0 4px}
.pill{font-size:12px;color:var(--mut);border:1px solid var(--line);border-radius:99px;padding:3px 11px}
.pill b{color:var(--gold);font-weight:600}
.ev{color:var(--mut);font-size:12.5px;font-style:italic;margin-top:6px}
.briefs{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;margin-top:16px}
.brief{background:var(--bg);border:1px solid var(--line);border-radius:12px;padding:14px 15px}
.brief .bt{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--gold);margin-bottom:8px}
.brief .prompt{font-size:13.5px;color:var(--ink);margin-bottom:10px}
.brief .row{font-size:12.5px;color:var(--mut);margin:3px 0}
.brief .row b{color:var(--ink);font-weight:600}
.brief .price{color:var(--gold);font-weight:600}
.briefHdr{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);margin-top:18px}
.empty{text-align:center;padding:80px 6vw;color:var(--mut)}
.empty .big{font-size:22px;color:var(--ink);margin-bottom:12px;letter-spacing:.04em}
.empty code{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:4px 10px;color:var(--gold)}
footer{border-top:1px solid var(--line);color:var(--mut);text-align:center;padding:26px;font-size:13px}
footer a{color:var(--gold)}
</style></head><body>
<header>
<h1>Trend <em>Board</em></h1>
<div class="tag">Where the market is hot — and the gaps we can own with original designs</div>
<div><a class="back" href="/">← Pattern Vault</a><span class="chip" id="scanChip"></span></div>
</header>
<div class="controls" id="controls" style="display:none">
<label>Sort <select id="sortSel">
<option value="gap">Gaps first</option>
<option value="lane">By lane A→Z</option>
</select></label>
<label>Density <input type="range" id="density" min="1" max="4" value="3"></label>
</div>
<div class="wrap" id="wrap"></div>
<footer>Original designs only — the board identifies demand; every brief is generated in our own hand, settlement-gated. · <a href="/">Back to the Vault</a></footer>
<script>
let trends=[],scannedAt=null;
const wrap=document.getElementById('wrap'),controls=document.getElementById('controls');
const sortSel=document.getElementById('sortSel'),density=document.getElementById('density');
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
sortSel.value=localStorage.pvTrendSort||'gap';density.value=localStorage.pvTrendCols||'3';
document.documentElement.style.setProperty('--cols',density.value);
sortSel.onchange=()=>{localStorage.pvTrendSort=sortSel.value;render()};
density.oninput=()=>{localStorage.pvTrendCols=density.value;document.documentElement.style.setProperty('--cols',density.value)};
function sortedTrends(){const a=[...trends];
if(sortSel.value==='lane')a.sort((x,y)=>(x.lane||'').localeCompare(y.lane||''));
else a.sort((x,y)=>((y.gap?1:0)-(x.gap?1:0))||((x.lane||'').localeCompare(y.lane||'')));
return a}
function briefCard(b){return `<div class="brief">
<div class="bt">Original design brief</div>
<div class="prompt">${esc(b.prompt)}</div>
${b.suggestedColorway?`<div class="row"><b>Colorway:</b> ${esc(b.suggestedColorway)}</div>`:''}
${b.targetTier?`<div class="row"><b>Tier:</b> ${esc(b.targetTier)}</div>`:''}
${b.targetPriceUsd!=null?`<div class="row price">$${esc(b.targetPriceUsd)}</div>`:''}
</div>`}
function laneCard(t){const briefs=Array.isArray(t.briefs)?t.briefs:[];
const isGap=t.gap===true||(t.gap==null&&t.haveCoverage===false);
return `<div class="lane ${isGap?'gap':''}">
<div class="laneHead">
<div class="laneName">${esc(t.lane||'Untitled lane')}</div>
<div class="gapbadge ${isGap?'g':'c'}">${isGap?'🟢 GAP — opportunity':'✓ covered'}</div>
</div>
${t.why?`<div class="why">${esc(t.why)}</div>`:''}
<div class="meta">
${t.priceBand?`<span class="pill">Price band <b>${esc(t.priceBand)}</b></span>`:''}
<span class="pill">${t.haveCoverage?'We have coverage':'No coverage yet'}</span>
</div>
${t.evidence?`<div class="ev">${esc(t.evidence)}</div>`:''}
${briefs.length?`<div class="briefHdr">Design briefs (${briefs.length})</div>
<div class="briefs">${briefs.map(briefCard).join('')}</div>`:''}
</div>`}
function render(){
wrap.innerHTML=sortedTrends().map(laneCard).join('');
}
(async()=>{
let data;
try{data=await(await fetch('/api/trends')).json()}catch(e){data={trends:[],scannedAt:null,empty:true}}
trends=Array.isArray(data.trends)?data.trends:[];scannedAt=data.scannedAt;
const chip=document.getElementById('scanChip');
if(scannedAt){const d=new Date(scannedAt);chip.textContent='🕓 scanned '+(isNaN(d)?scannedAt:d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}))}
if(!trends.length){
wrap.innerHTML=`<div class="empty"><div class="big">Trend scout hasn't run yet</div>
<div>Run <code>/design-trend-scout</code> to research the top-selling designs on Spoonflower, Etsy, Society6 & friends, find the gaps in our catalog, and brief originals to fill them.</div></div>`;
return;
}
const totalBriefs=trends.reduce((n,t)=>n+((Array.isArray(t.briefs)?t.briefs.length:0)),0);
if(totalBriefs>12)controls.style.display='flex';
render();
})();
</script></body></html>