← back to Consulting Intake
public/portal.html
105 lines
<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Consulting Intake · Deliverable</title>
<style>
:root{--ink:#14110e;--paper:#f6f2ec;--gold:#a9884f;--line:#dcd3c5}
*{box-sizing:border-box}
body{margin:0;font-family:Georgia,serif;background:var(--paper);color:var(--ink)}
nav{display:flex;justify-content:space-between;align-items:center;padding:16px 28px;background:#fff;border-bottom:1px solid var(--line);font-family:system-ui,sans-serif}
nav .brand{letter-spacing:.06em}
nav a{color:var(--gold);text-decoration:none;font-size:13px;letter-spacing:.1em;text-transform:uppercase;margin-left:18px}
.hero{padding:56px 28px 34px;text-align:center}
.hero .eyebrow{letter-spacing:.32em;text-transform:uppercase;font-size:11px;color:var(--gold);font-family:system-ui,sans-serif}
.hero h1{font-weight:400;font-size:44px;margin:.2em 0}
.hero p{font-style:italic;color:#6b6357;max-width:640px;margin:0 auto}
section{max-width:1080px;margin:0 auto;padding:28px}
h2{font-weight:400;font-size:26px;border-bottom:1px solid var(--line);padding-bottom:8px}
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:18px;margin-top:18px}
.vcard{background:#fff;border:1px solid var(--line);text-decoration:none;color:inherit;transition:.2s}
.vcard:hover{box-shadow:0 16px 40px rgba(20,17,14,.1);transform:translateY(-2px)}
.vcard .swatch{height:120px;background:var(--gold)}
.vcard .body{padding:14px 16px}
.vcard b{font-family:system-ui,sans-serif;font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:#9a9082}
.list{font-family:system-ui,sans-serif;font-size:15px}
.list li{margin:8px 0}
.badge{display:inline-block;font-family:system-ui,sans-serif;font-size:11px;background:#faf7f1;border:1px solid var(--gold);color:var(--gold);padding:2px 9px;border-radius:20px;margin-left:8px}
</style>
</head>
<body>
<nav>
<div class="brand" id="brand">Consulting Intake</div>
<div><a href="/portal">Deliverable</a><a href="/admin">Growth & Social</a><a href="/">Sign-in</a></div>
</nav>
<div class="hero">
<div class="eyebrow">Consulting Deliverable</div>
<h1 id="name">Consulting Intake</h1>
<p id="tag">Tell us about your business — we'll build your new website and social presence.</p>
</div>
<section>
<h2>Website concept directions <span class="badge" id="vcount">—</span></h2>
<p class="list" style="color:#6b6357">Several high-end redesign concepts of your new site — pick the direction that feels right. (Generate/refresh with <code>node build.mjs</code>.)</p>
<div class="cards" id="versions"></div>
</section>
<section>
<h2>Priority growth moves</h2>
<ul class="list" id="sugs"></ul>
</section>
<section>
<h2>Integrated social snapshot</h2>
<ul class="list" id="social"></ul>
</section>
<script>
async function j(u){ const r = await fetch(u); return r.ok ? r.json() : null; }
(async () => {
const c = await j('/api/client'); if (c && c.business_name){ document.getElementById('name').textContent = c.business_name; document.getElementById('brand').textContent = c.business_name; if(c.tagline) document.getElementById('tag').textContent = c.tagline; }
// version concepts (generated into /versions by build.mjs)
const known = ['editorial','modern','bold','warm','classic'];
const vc = document.getElementById('versions'); let found = 0;
for (const v of known){ const ok = await fetch('/versions/'+v+'.html',{method:'HEAD'}).then(r=>r.ok).catch(()=>false);
if(ok){ found++; const a=document.createElement('a'); a.className='vcard'; a.href='/versions/'+v+'.html';
a.innerHTML='<div class="swatch"></div><div class="body"><b>'+v+'</b></div>'; vc.appendChild(a); } }
document.getElementById('vcount').textContent = found + ' concepts';
if(!found) vc.innerHTML = '<p style="font-family:system-ui;color:#9a9082">No concepts generated yet — run <code>node build.mjs</code>.</p>';
// suggestions
const s = await j('/api/admin/suggestions')||[]; const su=document.getElementById('sugs');
su.innerHTML = s.length ? s.map(x=>'<li><b>'+(x.title||'')+'</b> <span class="badge">impact '+(x.impact||'?')+' / effort '+(x.effort||'?')+'</span><br><span style="color:#6b6357">'+(x.rationale||'')+'</span></li>').join('') : '<li style="color:#9a9082">No growth moves seeded yet.</li>';
// socials
const so = await j('/api/admin/socials')||{}; const el=document.getElementById('social');
const rows = Object.entries(so).filter(([k])=>!['connected','notes'].includes(k)).map(([k,v])=>'<li><b>'+k+'</b> — '+(v?('<a href="'+(String(v).startsWith('http')?v:'#')+'">'+v+'</a>'):'<span style="color:#c66">not set up yet</span>')+'</li>');
el.innerHTML = rows.join('') + (so.notes?'<li style="color:#9a9082;font-size:13px">'+so.notes+'</li>':'');
})();
</script>
</body>
</html>