← back to Small Business Builder
src/render/edit.js
145 lines
// Per-business edit view — left panel of stacked modals, right panel shows the
// 5 template previews (full size, scrollable).
import { esc } from '../lib/escape.js';
import { templateMeta } from './templates/index.js';
export function renderEdit({ business: b, socials }) {
const tpls = templateMeta();
const sBy = Object.fromEntries((socials || []).map(s => [s.platform, s]));
return `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Edit · ${esc(b.name)}</title>
<style>${EDIT_CSS}</style>
</head><body>
<div class="layout">
<aside class="panel">
<a class="back" href="/">← All businesses</a>
<h1>${esc(b.name)}</h1>
<p class="meta">${esc(b.category || '')} · ${esc(b.city || '')}${b.state ? ', ' + esc(b.state) : ''}</p>
<p class="tier">Tier: <strong>${esc((b.tier || 'free').toUpperCase())}</strong></p>
<details open class="mf"><summary>Basics</summary>
<form onsubmit="return saveJson(event,'/api/biz/update')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Name <input name="name" value="${esc(b.name)}"></label>
<label>Category
<select name="category">
${['salon','barbershop','nail_salon','generic'].map(c =>
`<option ${b.category===c?'selected':''}>${c}</option>`).join('')}
</select>
</label>
<label>Phone <input name="phone" value="${esc(b.phone || '')}"></label>
<label>Email <input name="email" value="${esc(b.email || '')}"></label>
<label>Website <input name="website" value="${esc(b.website || '')}"></label>
<label>Owner <input name="owner_name" value="${esc(b.owner_name || '')}"></label>
<label>Years in business <input type="number" name="years_in_business" value="${b.years_in_business ?? ''}"></label>
<button>Save</button><output></output>
</form>
</details>
<details class="mf"><summary>Address</summary>
<form onsubmit="return saveJson(event,'/api/biz/update')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Address <input name="address" value="${esc(b.address || '')}"></label>
<label>City <input name="city" value="${esc(b.city || '')}"></label>
<label>State <input name="state" maxlength="2" value="${esc(b.state || '')}"></label>
<label>ZIP <input name="zip" value="${esc(b.zip || '')}"></label>
<button>Save</button><output></output>
</form>
</details>
<details class="mf"><summary>About</summary>
<form onsubmit="return saveJson(event,'/api/biz/update')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Anything clients should know
<textarea name="about_text" rows="8">${esc(b.about_text || '')}</textarea>
</label>
<button>Save</button><output></output>
</form>
</details>
<details class="mf"><summary>Colors & hero</summary>
<form onsubmit="return saveJson(event,'/api/biz/update')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Primary color <input name="color_primary" type="color" value="${esc(b.color_primary || '#1f2937')}"></label>
<label>Accent color <input name="color_accent" type="color" value="${esc(b.color_accent || '#d97706')}"></label>
<label>Hero image URL <input name="hero_image_url" value="${esc(b.hero_image_url || '')}"></label>
<button>Save</button><output></output>
</form>
</details>
<details class="mf"><summary>Socials</summary>
<form onsubmit="return saveJson(event,'/api/biz/socials')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Instagram <input name="instagram" value="${esc(sBy.instagram?.handle || '')}"></label>
<label>TikTok <input name="tiktok" value="${esc(sBy.tiktok?.handle || '')}"></label>
<label>X / Twitter <input name="twitter" value="${esc(sBy.twitter?.handle || '')}"></label>
<label>LinkedIn <input name="linkedin" value="${esc(sBy.linkedin?.handle || '')}"></label>
<label>Facebook <input name="facebook" value="${esc(sBy.facebook?.handle || '')}"></label>
<button>Save</button><output></output>
</form>
</details>
<details class="mf"><summary>Tier</summary>
<form onsubmit="return saveJson(event,'/api/biz/update')">
<input type="hidden" name="slug" value="${esc(b.slug)}">
<label>Plan
<select name="tier">
${['free','plus','premium'].map(t => `<option ${b.tier===t?'selected':''}>${t}</option>`).join('')}
</select>
</label>
<button>Save</button><output></output>
</form>
</details>
</aside>
<main class="grid">
<h2>5 template variants</h2>
<div class="cols">
${tpls.map(m => `
<section class="tpl">
<header><h3>${esc(m.name)}</h3><a target="_blank" rel="noopener" href="/biz/${esc(b.slug)}/template/${m.id}">Open ↗</a></header>
<iframe loading="lazy" src="/biz/${esc(b.slug)}/template/${m.id}/preview" title="${esc(m.name)} preview" aria-hidden="true" tabindex="-1"></iframe>
<p class="blurb">${esc(m.blurb)}</p>
</section>`).join('')}
</div>
</main>
</div>
<script>${EDIT_JS}</script>
</body></html>`;
}
const EDIT_CSS = `
*{box-sizing:border-box}html,body{margin:0;font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#fafafa;color:#0b0b0b}
.layout{display:grid;grid-template-columns:380px 1fr;min-height:100vh}
.panel{background:#0b0b0b;color:#f6f4f0;padding:24px;position:sticky;top:0;height:100vh;overflow-y:auto}
.back{color:#999;font-size:13px;text-decoration:none}.back:hover{color:#fff}
.panel h1{font-family:Georgia,serif;margin:12px 0 4px;font-size:26px}
.panel .meta{color:#aaa;font-size:13px;margin:0 0 6px}
.panel .tier{font-size:13px;color:#aaa;margin:0 0 18px}
.mf{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:10px;margin-bottom:8px}
.mf>summary{padding:12px 14px;cursor:pointer;font-weight:600;font-size:14px;list-style:none;display:flex;justify-content:space-between}
.mf>summary::after{content:'+';opacity:.5}.mf[open]>summary::after{content:'−'}
.mf form{padding:6px 14px 16px;display:flex;flex-direction:column;gap:8px}
.mf label{display:flex;flex-direction:column;font-size:12px;color:#aaa;gap:4px}
.mf input,.mf select,.mf textarea{background:#0b0b0b;border:1px solid #333;color:#f6f4f0;padding:8px 10px;border-radius:6px;font-size:13px;font-family:inherit}
.mf button{background:#d97706;color:#fff;border:0;padding:9px 12px;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px;align-self:flex-start}
.mf output{font-size:12px;color:#aaa}
.grid{padding:32px}
.grid h2{font-family:Georgia,serif;margin:0 0 20px}
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:18px}
.tpl{background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.05)}
.tpl header{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid #eee}
.tpl header h3{margin:0;font-size:15px}
.tpl header a{font-size:12px;color:#d97706;text-decoration:none}
.tpl iframe{width:100%;height:520px;border:0;display:block;background:#fff}
.tpl .blurb{padding:10px 14px;color:#666;font-size:12px;margin:0;border-top:1px solid #eee}
@media (max-width:880px){.layout{grid-template-columns:1fr}.panel{position:static;height:auto}.cols{grid-template-columns:1fr}}
`;
const EDIT_JS = `
async function saveJson(ev,url){ev.preventDefault();const f=ev.currentTarget;const fd=new FormData(f);const o=f.querySelector('output');o.textContent='Saving…';
try{const r=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(fd.entries()))});const j=await r.json();if(!r.ok)throw new Error(j.error||r.status);o.textContent='Saved.';
document.querySelectorAll('iframe').forEach(i=>i.src=i.src);}catch(e){o.textContent=String(e.message||e);} return false;}
`;