added first three mockups
This commit is contained in:
@@ -0,0 +1,872 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate planned UX mock pages for client proposals from features.md catalog."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
CREDIT_HTML = (
|
||||
'<p class="aiml-site-credit">Made by '
|
||||
'<a href="https://aimloperations.com" rel="noopener noreferrer" target="_blank">'
|
||||
"AI ML Operations, LLC</a></p>"
|
||||
)
|
||||
|
||||
CLIENTS = {
|
||||
"eds-plumbing-and-sewer": {
|
||||
"brand": "Ed's Plumbing & Sewer",
|
||||
"brand_html": "Ed's Plumbing & Sewer",
|
||||
"short": "Ed's Portal",
|
||||
"user": "Ed's Admin",
|
||||
"email": "info@thegreatestplumber.com",
|
||||
"phone": "(630) 663-1444",
|
||||
"area": "Darien & Chicagoland",
|
||||
"theme": "",
|
||||
"home": "index.html",
|
||||
"about": "about.html",
|
||||
"contact": "contact.html",
|
||||
"industry": "plumbing",
|
||||
"leads": [
|
||||
("Jordan Lee", "google / emergency", "New", "Clogged main line"),
|
||||
("Sam Ortiz", "facebook / spring-promo", "Contacted", "Water heater quote"),
|
||||
("Avery Chen", "direct / contact-form", "New", "Sump pump install"),
|
||||
("Morgan Diaz", "postcard / march-mailer", "Won", "Fixture upgrade"),
|
||||
("Riley Park", "google / brand", "Lost", "Gas line repair"),
|
||||
],
|
||||
"invoices": [
|
||||
("INV-1042", "Jordan Lee", "$480.00", "Open"),
|
||||
("INV-1041", "Morgan Diaz", "$1,250.00", "Paid"),
|
||||
("INV-1040", "Sam Ortiz", "$320.00", "Open"),
|
||||
],
|
||||
"blog_posts": [
|
||||
("When to call for a sewer backup", "Mar 12, 2026", "Signs your main line needs attention before it floods the basement."),
|
||||
("Water heater lifespan checklist", "Feb 28, 2026", "How to know if repair or replacement saves more money."),
|
||||
("Winter sump pump prep", "Jan 15, 2026", "Keep ejector and sump systems ready for melt season."),
|
||||
],
|
||||
"campaign": "Spring drain cleaning reminder",
|
||||
"social_caption": "Clogged drain? We clear it fast — 24/7 emergency service in Darien & Chicagoland.\n\nCall (630) 663-1444",
|
||||
"postcard_headline": "Drain trouble?",
|
||||
"postcard_sub": "Local plumbers · 24/7 emergency · (630) 663-1444",
|
||||
},
|
||||
"paint-to-please": {
|
||||
"brand": "Paint to Please",
|
||||
"brand_html": "Paint to Please",
|
||||
"short": "Bill's Portal",
|
||||
"user": "Bill",
|
||||
"email": "hello@painttoplease.example",
|
||||
"phone": "(630) XXX-XXXX",
|
||||
"area": "Wheaton, IL",
|
||||
"theme": " theme-paint",
|
||||
"home": "index.html",
|
||||
"about": "index.html#about",
|
||||
"contact": "index.html#contact",
|
||||
"industry": "painting",
|
||||
"leads": [
|
||||
("Ann Brooks", "yelp / profile", "New", "Living room + trim"),
|
||||
("Phyllis Voss", "google / brand", "Contacted", "Cabinet refinish"),
|
||||
("Luann Meyer", "facebook / before-after", "New", "Whole-home interior"),
|
||||
("Clara Hunt", "direct / estimate-form", "Won", "Fireplace + bookcase"),
|
||||
("Gina Delgado", "postcard / wheaton-mailer", "Lost", "Wallpaper removal"),
|
||||
],
|
||||
"invoices": [
|
||||
("INV-208", "Clara Hunt", "$2,400.00", "Paid"),
|
||||
("INV-209", "Ann Brooks", "$850.00", "Open"),
|
||||
("INV-207", "Phyllis Voss", "$1,100.00", "Open"),
|
||||
],
|
||||
"blog_posts": [
|
||||
("Trim painting tips that last", "Mar 10, 2026", "Prep and cut-in habits that keep baseboards crisp."),
|
||||
("Cabinet paint vs refinish", "Feb 20, 2026", "When a spray finish beats a full replace."),
|
||||
("Choosing white without regret", "Jan 8, 2026", "Warm vs cool whites for Wheaton interiors."),
|
||||
],
|
||||
"campaign": "Spring interior refresh offer",
|
||||
"social_caption": "Fresh walls, careful trim, zero shortcuts — interior painting in Wheaton.\n\nBook an estimate: (630) XXX-XXXX",
|
||||
"postcard_headline": "Walls tired?",
|
||||
"postcard_sub": "Interior painting · Wheaton · Paint to Please",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def banner(c: dict, extra: str = "") -> str:
|
||||
return f"""<div class="proposal-banner">
|
||||
<div><span class="tag">Proposal mockup</span> {c['brand_html']} — UX examples (not production){extra}</div>
|
||||
<div><a href="ux-gallery.html">All screens</a> · <a href="pricing.html">Pricing</a></div>
|
||||
</div>"""
|
||||
|
||||
|
||||
def nav(c: dict, active: str) -> str:
|
||||
def a(key: str, href: str, label: str) -> str:
|
||||
cls = "active" if key == active else ""
|
||||
return f'<a class="{cls}" href="{href}">{label}</a>'
|
||||
|
||||
return f"""<aside class="portal-sidebar">
|
||||
<a class="portal-brand" href="portal-dashboard.html">{c['brand_html']}<span>Client portal</span></a>
|
||||
<nav class="portal-nav">
|
||||
<div class="nav-section">Overview</div>
|
||||
{a('dashboard', 'portal-dashboard.html', 'Dashboard')}
|
||||
{a('leads', 'portal-leads.html', 'Leads')}
|
||||
{a('analytics', 'portal-analytics.html', 'UTM analytics')}
|
||||
<div class="nav-section">Email & SMS</div>
|
||||
{a('contacts', 'portal-contacts.html', 'Mailing list')}
|
||||
{a('campaign', 'portal-campaign.html', 'Campaigns')}
|
||||
{a('report', 'portal-campaign-report.html', 'Engagement')}
|
||||
<div class="nav-section">Direct mail</div>
|
||||
{a('postcard', 'portal-postcard-designer.html', 'Postcard design')}
|
||||
{a('directmail', 'portal-direct-mail.html', 'Mail campaigns')}
|
||||
<div class="nav-section">Blog</div>
|
||||
{a('blog', 'portal-blog.html', 'Posts')}
|
||||
<div class="nav-section">Payments</div>
|
||||
{a('invoices', 'portal-invoices.html', 'Invoices')}
|
||||
<div class="nav-section">Social</div>
|
||||
{a('social', 'portal-social.html', 'Compose')}
|
||||
{a('calendar', 'portal-social-calendar.html', 'Calendar')}
|
||||
{a('accounts', 'portal-social-accounts.html', 'Accounts')}
|
||||
{a('ai', 'portal-social-ai.html', 'AI generator')}
|
||||
</nav>
|
||||
</aside>"""
|
||||
|
||||
|
||||
def shell(c: dict, title: str, active: str, body: str, note: str = "") -> str:
|
||||
note_html = f'<div class="proposal-note"><strong>UX note:</strong> {note}</div>' if note else ""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title} · Portal · {c['brand_html']}</title>
|
||||
<link rel="stylesheet" href="assets/css/ux-hub.css">
|
||||
</head>
|
||||
<body class="portal{c['theme']}">
|
||||
{banner(c)}
|
||||
<div class="portal-shell">
|
||||
{nav(c, active)}
|
||||
<div class="portal-main">
|
||||
<div class="portal-topbar">
|
||||
<h1>{title}</h1>
|
||||
<div class="portal-user">Signed in as <strong>{c['user']}</strong> · <a href="portal-login.html">Sign out</a></div>
|
||||
</div>
|
||||
<div class="portal-content">
|
||||
{note_html}
|
||||
{body}
|
||||
</div>
|
||||
{CREDIT_HTML}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def public_shell(c: dict, title: str, body: str, note: str = "") -> str:
|
||||
note_html = f'<div class="proposal-note"><strong>UX note:</strong> {note}</div>' if note else ""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title} · {c['brand_html']}</title>
|
||||
<link rel="stylesheet" href="assets/css/ux-hub.css">
|
||||
</head>
|
||||
<body class="{c['theme'].strip()}">
|
||||
{banner(c)}
|
||||
<header class="public-nav">
|
||||
<a class="brand" href="{c['home']}">{c['brand_html']}</a>
|
||||
<div class="links">
|
||||
<a href="{c['home']}">Home</a>
|
||||
<a href="{c['about']}">About</a>
|
||||
<a href="{c['contact']}">Contact</a>
|
||||
<a href="public-blog.html">Blog</a>
|
||||
<a href="portal-login.html">Portal</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="public-simple">
|
||||
{note_html}
|
||||
{body}
|
||||
</div>
|
||||
{CREDIT_HTML}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def page_login(c: dict) -> str:
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · Portal · {c['brand_html']}</title>
|
||||
<link rel="stylesheet" href="assets/css/ux-hub.css">
|
||||
</head>
|
||||
<body class="portal{c['theme']}">
|
||||
{banner(c)}
|
||||
<div class="login-wrap">
|
||||
<div class="login-card">
|
||||
<div class="proposal-note" style="margin-bottom:20px"><strong>UX note:</strong> Private client login (Django auth). Not public.</div>
|
||||
<h1>{c['short']}</h1>
|
||||
<p class="sub">{c['brand_html']} · leads, campaigns, and billing in one place.</p>
|
||||
<form class="form-grid" action="portal-dashboard.html" method="get">
|
||||
<div class="field"><label for="email">Email</label><input id="email" type="email" value="{c['email']}"></div>
|
||||
<div class="field"><label for="password">Password</label><input id="password" type="password" value="••••••••"></div>
|
||||
<button class="btn btn-primary" type="submit">Sign in</button>
|
||||
</form>
|
||||
<p style="margin-top:16px;font-size:13px;color:#6b7280"><a href="ux-gallery.html">← Back to proposal gallery</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{CREDIT_HTML}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def page_dashboard(c: dict) -> str:
|
||||
rows = "".join(
|
||||
f'<tr><td><a href="portal-lead-detail.html">{n}</a></td><td>{s}</td><td><span class="badge badge-{st.lower()}">{st}</span></td></tr>'
|
||||
for n, s, st, _ in c["leads"][:3]
|
||||
)
|
||||
body = f"""
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><div class="label">New leads</div><div class="value">7</div><div class="delta">+3 this week</div></div>
|
||||
<div class="stat-card"><div class="label">Open pipeline</div><div class="value">18</div><div class="delta">4 contacted</div></div>
|
||||
<div class="stat-card"><div class="label">Mailing list</div><div class="value">412</div><div class="delta">8 opted out (30d)</div></div>
|
||||
<div class="stat-card"><div class="label">Open invoices</div><div class="value">2</div><div class="delta">Next due Fri</div></div>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Recent leads</h2><a class="btn btn-sm btn-ghost" href="portal-leads.html">View all</a></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table"><thead><tr><th>Name</th><th>Source</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Upcoming outreach</h2><a class="btn btn-sm btn-ghost" href="portal-campaign.html">Compose</a></div>
|
||||
<div class="panel-b">
|
||||
<p style="margin:0 0 12px;font-size:14px"><strong>{c['campaign']}</strong> — Email · scheduled Fri 10:00 · 186 recipients</p>
|
||||
<p style="margin:0 0 12px;font-size:14px"><strong>Job reminder SMS</strong> — Draft · 42 recipients</p>
|
||||
<p style="margin:0;font-size:14px"><strong>Neighborhood promo</strong> — Postcard · queued · 120 pieces</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Dashboard", "dashboard", body, "Portal home — glanceable widgets for leads, list health, and scheduled work.")
|
||||
|
||||
|
||||
def page_leads(c: dict) -> str:
|
||||
rows = "".join(
|
||||
f'<tr><td><a href="portal-lead-detail.html">{n}</a></td><td>{s}</td><td>{need}</td><td><span class="badge badge-{st.lower()}">{st}</span></td></tr>'
|
||||
for n, s, st, need in c["leads"]
|
||||
)
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h">
|
||||
<h2>Lead inbox</h2>
|
||||
<div>
|
||||
<select style="padding:6px 10px;border:1px solid var(--border);font:inherit;margin-right:8px">
|
||||
<option>All statuses</option><option>New</option><option>Contacted</option><option>Won</option>
|
||||
</select>
|
||||
<input placeholder="Filter…" style="padding:6px 10px;border:1px solid var(--border);font:inherit;width:160px">
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Name</th><th>Source / UTM</th><th>Need</th><th>Status</th></tr></thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Lead inbox", "leads", body, "Statuses, source attribution, quick filter. Contact form submissions land here.")
|
||||
|
||||
|
||||
def page_lead_detail(c: dict) -> str:
|
||||
n, s, st, need = c["leads"][0]
|
||||
body = f"""
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>{n}</h2><span class="badge badge-{st.lower()}">{st}</span></div>
|
||||
<div class="panel-b">
|
||||
<p><strong>Need:</strong> {need}</p>
|
||||
<p><strong>Email:</strong> jordan@example.com</p>
|
||||
<p><strong>Phone:</strong> (630) 555-0142</p>
|
||||
<p><strong>Area:</strong> {c['area']}</p>
|
||||
<div class="field" style="margin-top:16px">
|
||||
<label>Internal notes</label>
|
||||
<textarea>Called back — scheduling site visit Thursday AM.</textarea>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="button" style="margin-top:12px">Save notes</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>UTM attribution</h2></div>
|
||||
<div class="panel-b">
|
||||
<table class="table">
|
||||
<tr><td>Source</td><td><strong>{s.split(' / ')[0]}</strong></td></tr>
|
||||
<tr><td>Medium</td><td>cpc</td></tr>
|
||||
<tr><td>Campaign</td><td>{s.split(' / ')[-1]}</td></tr>
|
||||
<tr><td>Landing</td><td>/{c['contact'].split('#')[0] if '#' in c['contact'] else 'contact.html'}</td></tr>
|
||||
<tr><td>First touch</td><td>Mar 18, 2026 · 9:14am</td></tr>
|
||||
</table>
|
||||
<p class="muted" style="margin-top:12px">Linked contact consent: email ✓ · SMS ✓ · postcard —</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Lead detail", "leads", body, "Notes, UTM attribution, linked contact consent.")
|
||||
|
||||
|
||||
def page_analytics(c: dict) -> str:
|
||||
bars = "".join(f'<div class="bar" style="height:{h}%"></div>' for h in (40, 65, 35, 80, 55, 70, 90))
|
||||
body = f"""
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><div class="label">Leads (30d)</div><div class="value">42</div><div class="delta">+12% vs prior</div></div>
|
||||
<div class="stat-card"><div class="label">Top source</div><div class="value" style="font-size:18px;margin-top:10px">Google</div><div class="delta">18 leads</div></div>
|
||||
<div class="stat-card"><div class="label">Top campaign</div><div class="value" style="font-size:18px;margin-top:10px">spring-promo</div><div class="delta">11 leads</div></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Leads by week</h2></div>
|
||||
<div class="panel-b"><div class="chart-ph">{bars}</div></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>By source / campaign</h2></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Source</th><th>Campaign</th><th>Leads</th><th>Won</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>google</td><td>emergency</td><td>12</td><td>3</td></tr>
|
||||
<tr><td>facebook</td><td>spring-promo</td><td>11</td><td>2</td></tr>
|
||||
<tr><td>direct</td><td>(none)</td><td>9</td><td>4</td></tr>
|
||||
<tr><td>postcard</td><td>march-mailer</td><td>6</td><td>1</td></tr>
|
||||
<tr><td>yelp</td><td>profile</td><td>4</td><td>1</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "UTM analytics", "analytics", body, "Included with every client portal — leads by source/campaign over time.")
|
||||
|
||||
|
||||
def page_contacts(c: dict) -> str:
|
||||
body = """
|
||||
<div class="panel">
|
||||
<div class="panel-h">
|
||||
<h2>Mailing list</h2>
|
||||
<div>
|
||||
<a class="btn btn-sm btn-ghost" href="portal-contacts-import.html">Import</a>
|
||||
<a class="btn btn-sm btn-primary" href="portal-campaign.html">New campaign</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Name</th><th>Email</th><th>Email</th><th>SMS</th><th>Postcard</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Jordan Lee</td><td>jordan@example.com</td><td>✓</td><td>✓</td><td>—</td></tr>
|
||||
<tr><td>Sam Ortiz</td><td>sam@example.com</td><td>✓</td><td>—</td><td>✓</td></tr>
|
||||
<tr><td>Avery Chen</td><td>avery@example.com</td><td>✓</td><td>✓</td><td>✓</td></tr>
|
||||
<tr><td>Morgan Diaz</td><td>morgan@example.com</td><td>—</td><td>✓</td><td>✓</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Mailing list", "contacts", body, "Contacts with per-channel consent (email / SMS / postcard).")
|
||||
|
||||
|
||||
def page_contacts_import(c: dict) -> str:
|
||||
body = """
|
||||
<div class="steps">
|
||||
<div class="step active"><span>1</span> Upload</div>
|
||||
<div class="step"><span>2</span> Map columns</div>
|
||||
<div class="step"><span>3</span> Consent defaults</div>
|
||||
<div class="step"><span>4</span> Review duplicates</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Upload CSV / Excel</h2></div>
|
||||
<div class="panel-b">
|
||||
<div class="dropzone">Drop file here or <strong>browse</strong><div class="muted" style="margin-top:8px">.csv, .xlsx · max 5MB mock</div></div>
|
||||
<p class="hint-block muted" style="margin-top:16px">After upload: map name/email/phone, set default consent, merge duplicates.</p>
|
||||
<a class="btn btn-primary" href="portal-contacts.html" style="margin-top:16px">Continue (mock)</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Import contacts", "contacts", body, "CSV/Excel upload, column mapping, consent defaults, duplicate merge.")
|
||||
|
||||
|
||||
def page_campaign(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="channel-tabs">
|
||||
<a class="active" href="#">Email</a>
|
||||
<a href="#">SMS</a>
|
||||
<a href="portal-direct-mail.html">Postcard</a>
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Compose</h2></div>
|
||||
<div class="panel-b">
|
||||
<form class="form-grid">
|
||||
<div class="field"><label>Campaign name</label><input value="{c['campaign']}"></div>
|
||||
<div class="field"><label>Subject</label><input value="A quick note from {c['brand']}"></div>
|
||||
<div class="field"><label>Recipients</label><select><option>All email-consent contacts (186)</option><option>Tagged: customers</option></select></div>
|
||||
<div class="field"><label>Body</label><textarea>Hi {{{{first_name}}}},
|
||||
|
||||
Just a friendly reminder we're here when you need us in {c['area'].replace('&','&')}.
|
||||
|
||||
— {c['brand']}</textarea></div>
|
||||
<div class="field"><label>Send</label><select><option>Schedule for Fri 10:00am</option><option>Send now</option><option>Save draft</option></select></div>
|
||||
<button class="btn btn-primary" type="button">Schedule campaign</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Preview</h2></div>
|
||||
<div class="panel-b" style="background:#f8fafc;min-height:200px;font-size:14px">
|
||||
<strong>Subject:</strong> A quick note from {c['brand_html']}<br><br>
|
||||
Hi Jordan,<br><br>
|
||||
Just a friendly reminder we're here when you need us in {c['area']}.<br><br>
|
||||
— {c['brand_html']}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Campaign composer", "campaign", body, "Email, SMS, or postcard — select recipients, schedule, send.")
|
||||
|
||||
|
||||
def page_campaign_report(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><div class="label">Delivered</div><div class="value">178</div><div class="delta">of 186</div></div>
|
||||
<div class="stat-card"><div class="label">Opens</div><div class="value">64%</div><div class="delta">114 opens</div></div>
|
||||
<div class="stat-card"><div class="label">Clicks</div><div class="value">18%</div><div class="delta">32 clicks</div></div>
|
||||
<div class="stat-card"><div class="label">Unsubscribes</div><div class="value">2</div><div class="delta">1 bounce</div></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>{c['campaign']}</h2><span class="badge badge-sent">Sent</span></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table">
|
||||
<thead><tr><th>Recipient</th><th>Opened</th><th>Clicked</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>jordan@example.com</td><td>Yes</td><td>Yes</td><td>Engaged</td></tr>
|
||||
<tr><td>sam@example.com</td><td>Yes</td><td>—</td><td>Opened</td></tr>
|
||||
<tr><td>avery@example.com</td><td>—</td><td>—</td><td>Delivered</td></tr>
|
||||
<tr><td>bad@example.com</td><td>—</td><td>—</td><td>Bounced</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Email engagement", "report", body, "Opens, clicks, bounces, unsubscribes per campaign.")
|
||||
|
||||
|
||||
def page_postcard(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="designer-layout">
|
||||
<div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Size</h2></div>
|
||||
<div class="panel-b">
|
||||
<div class="size-grid">
|
||||
<button type="button" class="size-option active"><div class="sz-name">4×6</div><div class="muted">Standard</div></button>
|
||||
<button type="button" class="size-option"><div class="sz-name">6×9</div><div class="muted">Large</div></button>
|
||||
<button type="button" class="size-option"><div class="sz-name">6×11</div><div class="muted">Jumbo</div></button>
|
||||
<button type="button" class="size-option"><div class="sz-name">Square</div><div class="muted">6×6</div></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Copy</h2></div>
|
||||
<div class="panel-b form-grid">
|
||||
<div class="field"><label>Headline</label><input value="{c['postcard_headline']}"></div>
|
||||
<div class="field"><label>Subline</label><input value="{c['postcard_sub']}"></div>
|
||||
<a class="btn btn-primary" href="portal-direct-mail.html">Use in campaign</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Live preview — front</h2></div>
|
||||
<div class="panel-b" style="display:flex;justify-content:center">
|
||||
<div class="postcard">
|
||||
<div class="postcard-media"></div>
|
||||
<div class="postcard-copy">
|
||||
<div class="postcard-headline">{c['postcard_headline']}</div>
|
||||
<div class="postcard-sub">{c['postcard_sub']}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Postcard designer", "postcard", body, "Size picker, reusable media library, live front/back preview.")
|
||||
|
||||
|
||||
def page_direct_mail(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Direct mail campaign</h2><a class="btn btn-sm btn-ghost" href="portal-postcard-designer.html">Edit design</a></div>
|
||||
<div class="panel-b">
|
||||
<form class="form-grid cols-2">
|
||||
<div class="field"><label>Name</label><input value="Neighborhood just listed — March"></div>
|
||||
<div class="field"><label>Piece</label><select><option>{c['postcard_headline']} (4×6)</option></select></div>
|
||||
<div class="field"><label>Recipients</label><select><option>Postcard-consent contacts (120)</option></select></div>
|
||||
<div class="field"><label>Schedule</label><input type="date" value="2026-03-28"></div>
|
||||
</form>
|
||||
<p class="muted" style="margin:16px 0">Print + postage pass-through billed separately.</p>
|
||||
<button class="btn btn-primary" type="button">Queue for print</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Direct mail campaign", "directmail", body, "Recipient selection from contacts with postcard consent; vendor send.")
|
||||
|
||||
|
||||
def page_blog_portal(c: dict) -> str:
|
||||
rows = "".join(
|
||||
f'<tr><td><a href="portal-blog-edit.html">{t}</a></td><td>{d}</td><td><span class="badge badge-{"won" if i==0 else "draft"}">{"Published" if i==0 else "Draft"}</span></td></tr>'
|
||||
for i, (t, d, _) in enumerate(c["blog_posts"])
|
||||
)
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Blog posts</h2><a class="btn btn-sm btn-primary" href="portal-blog-edit.html">New post</a></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table"><thead><tr><th>Title</th><th>Date</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Blog list", "blog", body, "Client manages drafts and published posts.")
|
||||
|
||||
|
||||
def page_blog_edit(c: dict) -> str:
|
||||
t, _, excerpt = c["blog_posts"][0]
|
||||
slug = "-".join("".join(ch if ch.isalnum() else " " for ch in t.lower()).split()[:6])
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Edit post</h2>
|
||||
<div><button class="btn btn-sm btn-ghost" type="button">Save draft</button>
|
||||
<button class="btn btn-sm btn-primary" type="button">Publish</button></div>
|
||||
</div>
|
||||
<div class="panel-b form-grid">
|
||||
<div class="field"><label>Title</label><input value="{t}"></div>
|
||||
<div class="field"><label>Slug</label><input value="{slug}"></div>
|
||||
<div class="field"><label>Excerpt</label><textarea style="min-height:60px">{excerpt}</textarea></div>
|
||||
<div class="field"><label>Body</label><textarea style="min-height:180px">Customers often wait too long. Here's what to watch for…
|
||||
|
||||
(Mock editor — production uses a rich text field.)</textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Blog editor", "blog", body, "Create and edit posts; publish or keep as draft.")
|
||||
|
||||
|
||||
def page_invoices(c: dict) -> str:
|
||||
rows = "".join(
|
||||
f'<tr><td><a href="portal-invoice-detail.html">{num}</a></td><td>{who}</td><td>{amt}</td><td><span class="badge badge-{st.lower()}">{st}</span></td></tr>'
|
||||
for num, who, amt, st in c["invoices"]
|
||||
)
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Invoices</h2><a class="btn btn-sm btn-primary" href="portal-invoice-create.html">Create invoice</a></div>
|
||||
<div class="panel-b" style="padding:0">
|
||||
<table class="table"><thead><tr><th>Number</th><th>Customer</th><th>Amount</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Invoices", "invoices", body, "Paid / open / past due. Requires Email & SMS for pay-link notifications.")
|
||||
|
||||
|
||||
def page_invoice_create(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Create invoice</h2></div>
|
||||
<div class="panel-b">
|
||||
<form class="form-grid cols-2">
|
||||
<div class="field"><label>Customer name</label><input value="{c['leads'][0][0]}"></div>
|
||||
<div class="field"><label>Customer email</label><input type="email" value="jordan@example.com"></div>
|
||||
<div class="field"><label>Line item</label><input value="Service call — diagnosis"></div>
|
||||
<div class="field"><label>Amount</label><input value="480.00"></div>
|
||||
<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea style="min-height:70px">Due on receipt. Pay link emailed via Email add-on.</textarea></div>
|
||||
</form>
|
||||
<p class="muted" style="margin:12px 0">Stripe processing fees billed separately. Email notification requires Email & SMS add-on.</p>
|
||||
<a class="btn btn-primary" href="portal-invoice-detail.html">Create & send</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Create invoice", "invoices", body, "Amount, line items, customer email; pay link via email.")
|
||||
|
||||
|
||||
def page_invoice_detail(c: dict) -> str:
|
||||
num, who, amt, st = c["invoices"][0]
|
||||
body = f"""
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>{num}</h2><span class="badge badge-{st.lower()}">{st}</span></div>
|
||||
<div class="panel-b">
|
||||
<p><strong>Customer:</strong> {who}</p>
|
||||
<p><strong>Email:</strong> jordan@example.com</p>
|
||||
<p><strong>Amount:</strong> {amt}</p>
|
||||
<p><strong>Created:</strong> Mar 18, 2026</p>
|
||||
<p><strong>Pay link:</strong> <a href="public-pay.html">public-pay.html</a></p>
|
||||
<button class="btn btn-ghost btn-sm" type="button">Resend email</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Activity</h2></div>
|
||||
<div class="panel-b" style="font-size:14px">
|
||||
<p>Mar 18 · Invoice created</p>
|
||||
<p>Mar 18 · Pay-link email sent</p>
|
||||
<p>Mar 19 · Customer opened email</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Invoice detail", "invoices", body, "Status and payment history; resend pay link.")
|
||||
|
||||
|
||||
def page_social_accounts(c: dict) -> str:
|
||||
body = """
|
||||
<div class="connect-grid">
|
||||
<button type="button" class="connect-card">
|
||||
<div class="platform-icon meta">f</div>
|
||||
<div><strong>Facebook Page</strong><p class="muted" style="margin:4px 0 0">Connected · token healthy</p></div>
|
||||
</button>
|
||||
<button type="button" class="connect-card">
|
||||
<div class="platform-icon ig">IG</div>
|
||||
<div><strong>Instagram</strong><p class="muted" style="margin:4px 0 0">Reconnect required · token expired</p></div>
|
||||
</button>
|
||||
<button type="button" class="connect-card">
|
||||
<div class="platform-icon li">in</div>
|
||||
<div><strong>LinkedIn</strong><p class="muted" style="margin:4px 0 0">Not connected · Add via OAuth</p></div>
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Social accounts", "accounts", body, "OAuth connect / reconnect when tokens expire; health + disconnect.")
|
||||
|
||||
|
||||
def page_social(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="split">
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Compose</h2><a class="btn btn-sm btn-ghost" href="portal-social-ai.html">AI assist</a></div>
|
||||
<div class="panel-b form-grid">
|
||||
<div class="field"><label>Networks</label>
|
||||
<div style="display:flex;gap:12px;font-size:14px"><label><input type="checkbox" checked> Facebook</label>
|
||||
<label><input type="checkbox" checked> Instagram</label>
|
||||
<label><input type="checkbox"> LinkedIn</label></div>
|
||||
</div>
|
||||
<div class="field"><label>Caption</label><textarea id="cap">{c['social_caption'].replace('&','&')}</textarea></div>
|
||||
<div class="field"><label>Schedule</label><select><option>Post now</option><option>Tomorrow 9:00am</option><option>Add to calendar</option></select></div>
|
||||
<button class="btn btn-primary" type="button">Schedule post</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>Facebook preview</h2></div>
|
||||
<div class="panel-b" style="display:flex;justify-content:center">
|
||||
<div class="social-phone">
|
||||
<div class="soc-header">
|
||||
<div class="soc-avatar">{c['brand'][0]}</div>
|
||||
<div><div class="soc-name">{c['brand_html']}</div><div class="soc-meta">Just now · Public</div></div>
|
||||
</div>
|
||||
<div class="soc-caption">{c['social_caption']}</div>
|
||||
<div class="soc-image"></div>
|
||||
<div class="soc-actions">Like · Comment · Share</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Social composer", "social", body, "Compose once; live FB / IG / LinkedIn previews update as you type.")
|
||||
|
||||
|
||||
def page_social_calendar(c: dict) -> str:
|
||||
days = []
|
||||
for i in range(1, 36):
|
||||
has = i in (5, 12, 18, 24)
|
||||
cls = "day has-post" if has else "day"
|
||||
dot = '<div class="dot"></div>' if has else ""
|
||||
label = i if i <= 31 else ""
|
||||
days.append(f'<div class="{cls}">{label}{dot}</div>')
|
||||
body = f"""
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>March 2026</h2><a class="btn btn-sm btn-primary" href="portal-social.html">New post</a></div>
|
||||
<div class="panel-b">
|
||||
<div class="calendar-grid">
|
||||
<div class="dow">Sun</div><div class="dow">Mon</div><div class="dow">Tue</div><div class="dow">Wed</div><div class="dow">Thu</div><div class="dow">Fri</div><div class="dow">Sat</div>
|
||||
{''.join(days)}
|
||||
</div>
|
||||
<p class="muted" style="margin-top:16px">Dots = scheduled posts. Click through to composer in production.</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "Social calendar", "calendar", body, "Schedule and queue upcoming posts.")
|
||||
|
||||
|
||||
def page_social_ai(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="split">
|
||||
<div class="ai-chat">
|
||||
<div class="ai-chat-h"><h2>AI social generator</h2><span class="ai-pill">Ollama · local</span></div>
|
||||
<div class="ai-messages">
|
||||
<div class="ai-msg bot"><div class="who">Assistant</div><div class="bubble">Describe the post. I'll draft captions you can send to the composer.</div></div>
|
||||
<div class="ai-msg user"><div class="who">You</div><div class="bubble">Spring promo for local customers, friendly, include phone.</div></div>
|
||||
<div class="ai-msg bot"><div class="who">Assistant</div><div class="bubble">Draft A:
|
||||
<div class="ai-draft">{c['social_caption'].replace('&','&')}</div>
|
||||
<div style="margin-top:8px"><a class="btn btn-sm btn-primary" href="portal-social.html">Insert into composer</a></div>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="ai-chat-input">
|
||||
<textarea placeholder="Ask for another tone or length…"></textarea>
|
||||
<button class="btn btn-primary" type="button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-h"><h2>How it works</h2></div>
|
||||
<div class="panel-b" style="font-size:14px">
|
||||
<p>Runs against your <strong>local LLM via Ollama</strong> — no cloud LLM required.</p>
|
||||
<p>Requires <strong>Social consolidation</strong> so drafts hand off to compose → preview → schedule.</p>
|
||||
<p class="muted">Mock only — production streams from your Ollama host.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return shell(c, "AI social generator", "ai", body, "Prompt → draft variants → insert into social composer. Ollama local.")
|
||||
|
||||
|
||||
def page_public_blog(c: dict) -> str:
|
||||
items = "".join(
|
||||
f'<a class="blog-item" href="public-blog-post.html"><div class="meta">{d}</div><h3>{t}</h3><p>{ex}</p></a>'
|
||||
for t, d, ex in c["blog_posts"]
|
||||
)
|
||||
body = f'<h1 style="font-family:var(--display);margin:0 0 8px">Blog</h1><p class="muted" style="margin:0 0 24px;font-size:14px">Tips from {c["brand_html"]}</p><div class="blog-list">{items}</div>'
|
||||
return public_shell(c, "Blog", body, "Public readers view posts; client manages them in the portal.")
|
||||
|
||||
|
||||
def page_public_blog_post(c: dict) -> str:
|
||||
t, d, ex = c["blog_posts"][0]
|
||||
body = f"""
|
||||
<p class="muted">{d} · <a href="public-blog.html">All posts</a></p>
|
||||
<h1 style="font-family:var(--display);margin:8px 0 16px">{t}</h1>
|
||||
<div class="card" style="font-size:15px;line-height:1.6">
|
||||
<p>{ex}</p>
|
||||
<p>This is a mock article body for the proposal. In production, posts are managed from the client portal blog editor and rendered on the public site.</p>
|
||||
<p><a href="{c['contact']}">Contact {c['brand_html']}</a> if you need help sooner.</p>
|
||||
</div>
|
||||
"""
|
||||
return public_shell(c, t, body)
|
||||
|
||||
|
||||
def page_public_unsubscribe(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="card">
|
||||
<h1 style="font-family:var(--display);margin:0 0 8px">Email preferences</h1>
|
||||
<p class="muted" style="font-size:14px;margin:0 0 20px">Manage how {c['brand_html']} contacts you.</p>
|
||||
<form class="form-grid">
|
||||
<div class="field"><label><input type="checkbox"> Marketing email</label></div>
|
||||
<div class="field"><label><input type="checkbox" checked> Transactional / job updates</label></div>
|
||||
<div class="field"><label><input type="checkbox"> SMS</label></div>
|
||||
<div class="field"><label><input type="checkbox"> Postcard mail</label></div>
|
||||
<button class="btn btn-primary" type="button">Save preferences</button>
|
||||
</form>
|
||||
<p class="muted" style="margin-top:16px"><a href="#">Unsubscribe from all marketing</a></p>
|
||||
</div>
|
||||
"""
|
||||
return public_shell(c, "Unsubscribe", body, "One-click email unsubscribe and per-channel preference.")
|
||||
|
||||
|
||||
def page_public_pay(c: dict) -> str:
|
||||
num, who, amt, _ = c["invoices"][0]
|
||||
body = f"""
|
||||
<div class="pay-box">
|
||||
<div class="proposal-note"><strong>UX note:</strong> Stripe Checkout (hosted). Email add-on delivered this link.</div>
|
||||
<p class="muted">Invoice {num} · {who}</p>
|
||||
<h1>Pay {c['brand_html']}</h1>
|
||||
<div class="pay-amount">{amt}</div>
|
||||
<p style="font-size:14px;color:var(--muted)">Secure payment via Stripe. Card details never touch our servers in this mock.</p>
|
||||
<button class="btn btn-primary" type="button" style="width:100%;margin-top:16px" onclick="alert('Mock only — Stripe Checkout in production.')">Pay with card</button>
|
||||
<p class="muted" style="margin-top:16px;text-align:center"><a href="{c['home']}">Back to site</a></p>
|
||||
</div>
|
||||
"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pay invoice · {c['brand_html']}</title>
|
||||
<link rel="stylesheet" href="assets/css/ux-hub.css">
|
||||
</head>
|
||||
<body class="{c['theme'].strip()}">
|
||||
{banner(c)}
|
||||
{body}
|
||||
{CREDIT_HTML}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def page_public_404(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="error-hero">
|
||||
<h1>404</h1>
|
||||
<p style="font-size:18px">Page not found</p>
|
||||
<p class="muted">That URL doesn't exist on {c['brand_html']}.</p>
|
||||
<p style="margin-top:24px">
|
||||
<a class="btn btn-primary" href="{c['home']}">Go home</a>
|
||||
<a class="btn btn-ghost" href="{c['contact']}" style="margin-left:8px">Contact</a>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
return public_shell(c, "404", body, "Branded error page with path home or to contact.")
|
||||
|
||||
|
||||
def page_under_construction(c: dict) -> str:
|
||||
body = f"""
|
||||
<div class="card" style="text-align:center;padding:48px 32px">
|
||||
<h1 style="font-family:var(--display);margin:0 0 12px">Under construction</h1>
|
||||
<p style="color:var(--muted)">We're finishing something new for {c['brand_html']}. Leave your email and we'll notify you.</p>
|
||||
<form class="form-grid" style="max-width:360px;margin:24px auto 0" onsubmit="alert('Mock notify-me only');return false;">
|
||||
<div class="field"><input type="email" placeholder="you@example.com" required></div>
|
||||
<button class="btn btn-primary" type="submit">Notify me</button>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
return public_shell(c, "Under construction", body, "Pre-launch / maintenance holding page with notify-me email.")
|
||||
|
||||
|
||||
PAGES = {
|
||||
"portal-login.html": page_login,
|
||||
"portal-dashboard.html": page_dashboard,
|
||||
"portal-leads.html": page_leads,
|
||||
"portal-lead-detail.html": page_lead_detail,
|
||||
"portal-analytics.html": page_analytics,
|
||||
"portal-contacts.html": page_contacts,
|
||||
"portal-contacts-import.html": page_contacts_import,
|
||||
"portal-campaign.html": page_campaign,
|
||||
"portal-campaign-report.html": page_campaign_report,
|
||||
"portal-postcard-designer.html": page_postcard,
|
||||
"portal-direct-mail.html": page_direct_mail,
|
||||
"portal-blog.html": page_blog_portal,
|
||||
"portal-blog-edit.html": page_blog_edit,
|
||||
"portal-invoices.html": page_invoices,
|
||||
"portal-invoice-create.html": page_invoice_create,
|
||||
"portal-invoice-detail.html": page_invoice_detail,
|
||||
"portal-social-accounts.html": page_social_accounts,
|
||||
"portal-social.html": page_social,
|
||||
"portal-social-calendar.html": page_social_calendar,
|
||||
"portal-social-ai.html": page_social_ai,
|
||||
"public-blog.html": page_public_blog,
|
||||
"public-blog-post.html": page_public_blog_post,
|
||||
"public-unsubscribe.html": page_public_unsubscribe,
|
||||
"public-pay.html": page_public_pay,
|
||||
"public-404.html": page_public_404,
|
||||
"public-under-construction.html": page_under_construction,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for slug, cfg in CLIENTS.items():
|
||||
out = ROOT / "proposals" / slug / "mockup"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for name, fn in PAGES.items():
|
||||
path = out / name
|
||||
path.write_text(fn(cfg), encoding="utf-8")
|
||||
print("wrote", path.relative_to(ROOT))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rewrite ux-gallery.html for both clients — all screens Ready."""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
EDS = {
|
||||
"file": ROOT / "proposals/eds-plumbing-and-sewer/mockup/ux-gallery.html",
|
||||
"title": "Ed's Plumbing & Sewer — UX proposal gallery",
|
||||
"hero_class": "hub-hero",
|
||||
"h1": "Ed's Plumbing & Sewer — UX proposal",
|
||||
"card_class": "hub-card",
|
||||
"public_cards": """
|
||||
<a class="hub-card" href="index.html"><div class="eyebrow">Public · base</div><h3>Marketing home</h3><p>Brand-forward landing with services and CTA.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card" href="about.html"><div class="eyebrow">Public · base</div><h3>About</h3><p>Company story and trust signals.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card" href="contact.html"><div class="eyebrow">Public · base</div><h3>Contact form</h3><p>Lead capture → portal lead + contact.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card" href="services.html"><div class="eyebrow">Public · extras</div><h3>Services overview</h3><p>Plumbing & sewer service index.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card" href="public-404.html"><div class="eyebrow">Public · system</div><h3>404 not found</h3><p>Branded error page.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card" href="public-under-construction.html"><div class="eyebrow">Public · system</div><h3>Under construction</h3><p>Pre-launch holding page with notify-me.</p><span class="status">Ready</span></a>
|
||||
""",
|
||||
"blog_extra": "",
|
||||
"banner_brand": "Ed's Plumbing & Sewer",
|
||||
}
|
||||
|
||||
PTP = {
|
||||
"file": ROOT / "proposals/paint-to-please/mockup/ux-gallery.html",
|
||||
"title": "Paint to Please — UX proposal gallery",
|
||||
"hero_class": "hub-hero paint",
|
||||
"h1": "Paint to Please — UX proposal",
|
||||
"card_class": "hub-card paint",
|
||||
"public_cards": """
|
||||
<a class="hub-card paint" href="index.html"><div class="eyebrow">Public · base</div><h3>Marketing home</h3><p>Hero, services, gallery, reviews, CTA.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card paint" href="index.html#about"><div class="eyebrow">Public · base</div><h3>About</h3><p>Bill’s story and trust (section on landing).</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card paint" href="index.html#contact"><div class="eyebrow">Public · base</div><h3>Contact / estimate</h3><p>Lead capture form → portal lead.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card paint" href="public-404.html"><div class="eyebrow">Public · system</div><h3>404 not found</h3><p>Branded error page.</p><span class="status">Ready</span></a>
|
||||
<a class="hub-card paint" href="public-under-construction.html"><div class="eyebrow">Public · system</div><h3>Under construction</h3><p>Pre-launch holding page with notify-me.</p><span class="status">Ready</span></a>
|
||||
""",
|
||||
"blog_extra": '<a class="hub-card paint" href="index.html#blog"><div class="eyebrow">Public · Blog</div><h3>Tips (landing teaser)</h3><p>Current tips section on marketing home.</p><span class="status">Ready</span></a>\n',
|
||||
"banner_brand": "Paint to Please",
|
||||
}
|
||||
|
||||
|
||||
def gallery(cfg: dict) -> str:
|
||||
cc = cfg["card_class"]
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{cfg['title']}</title>
|
||||
<link rel="stylesheet" href="assets/css/ux-hub.css">
|
||||
</head>
|
||||
<body class="hub-page">
|
||||
<div class="proposal-banner">
|
||||
<div><span class="tag">Proposal mockup</span> {cfg['banner_brand']} — UX examples (not production)</div>
|
||||
<div>
|
||||
<a href="pricing.html">Pricing picker</a> ·
|
||||
<a href="index.html">Marketing home</a> ·
|
||||
<a href="ux-gallery.html">All screens</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="{cfg['hero_class']}">
|
||||
<h1>{cfg['h1']}</h1>
|
||||
<p>Click-through mocks for the public marketing site and Django client portal: leads, UTM analytics, and optional add-ons (email/SMS, direct mail, blog, Stripe payments, social, AI social).</p>
|
||||
</section>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Proposal</h2>
|
||||
<p class="sec-lead">Scope and interactive estimate.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="pricing.html">
|
||||
<div class="eyebrow">Proposal · Pricing</div>
|
||||
<h3>Feature pricing</h3>
|
||||
<p>Toggle add-ons; live build + monthly totals. Dependencies enforced.</p>
|
||||
<span class="status">Ready</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Public site · base</h2>
|
||||
<p class="sec-lead">What visitors see.</p>
|
||||
</div>
|
||||
<div class="hub-grid">{cfg['public_cards']}
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Client portal · base (+ UTM)</h2>
|
||||
<p class="sec-lead">Private Django UI. UTM tracking included with every portal.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="portal-login.html"><div class="eyebrow">Portal · base</div><h3>Login</h3><p>Client authentication.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-dashboard.html"><div class="eyebrow">Portal · base</div><h3>Dashboard</h3><p>New leads, campaign health, add-on widgets.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-leads.html"><div class="eyebrow">Portal · base</div><h3>Lead inbox</h3><p>Statuses, source attribution, filters.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-lead-detail.html"><div class="eyebrow">Portal · base</div><h3>Lead detail</h3><p>Notes, UTM, linked contact.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-analytics.html"><div class="eyebrow">Portal · base</div><h3>UTM analytics</h3><p>Leads by source/campaign over time.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Email & SMS</h2>
|
||||
<p class="sec-lead">Add-on. Required for payment notifications.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="portal-contacts.html"><div class="eyebrow">Portal · Email/SMS</div><h3>Mailing list</h3><p>Contacts with per-channel consent.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-contacts-import.html"><div class="eyebrow">Portal · Email/SMS</div><h3>Import contacts</h3><p>CSV upload, mapping, consent defaults.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-campaign.html"><div class="eyebrow">Portal · Email/SMS</div><h3>Campaign composer</h3><p>Email or SMS — recipients, schedule, send.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-campaign-report.html"><div class="eyebrow">Portal · Email/SMS</div><h3>Email engagement</h3><p>Opens, clicks, bounces, unsubscribes.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="public-unsubscribe.html"><div class="eyebrow">Public · Email/SMS</div><h3>Opt-out / unsubscribe</h3><p>One-click and per-channel preference.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Direct mail</h2>
|
||||
<p class="sec-lead">Add-on. Postcards / print vendor handoff.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="portal-postcard-designer.html"><div class="eyebrow">Portal · Direct mail</div><h3>Postcard designer</h3><p>Sizes, media library, front/back preview.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-direct-mail.html"><div class="eyebrow">Portal · Direct mail</div><h3>Direct mail campaign</h3><p>Recipients, schedule, send.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Blog</h2>
|
||||
<p class="sec-lead">Add-on. Public posts + portal management.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
{cfg['blog_extra']} <a class="{cc}" href="public-blog.html"><div class="eyebrow">Public · Blog</div><h3>Blog index</h3><p>Public post listing.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="public-blog-post.html"><div class="eyebrow">Public · Blog</div><h3>Blog post</h3><p>Single article view.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-blog.html"><div class="eyebrow">Portal · Blog</div><h3>Blog list</h3><p>Draft / published management.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-blog-edit.html"><div class="eyebrow">Portal · Blog</div><h3>Blog editor</h3><p>Create and edit posts.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Payments (Stripe)</h2>
|
||||
<p class="sec-lead">Add-on. Requires Email & SMS.</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="portal-invoices.html"><div class="eyebrow">Portal · Payments</div><h3>Invoices</h3><p>Open / paid / past due list.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-invoice-create.html"><div class="eyebrow">Portal · Payments</div><h3>Create invoice</h3><p>Line items, customer email, send.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-invoice-detail.html"><div class="eyebrow">Portal · Payments</div><h3>Invoice detail</h3><p>Status and payment history.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="public-pay.html"><div class="eyebrow">Public · Payments</div><h3>Pay invoice</h3><p>Stripe checkout / confirmation.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
|
||||
<div class="hub-section">
|
||||
<h2>Social & AI social</h2>
|
||||
<p class="sec-lead">Add-ons. AI generator requires social consolidation (Ollama).</p>
|
||||
</div>
|
||||
<div class="hub-grid">
|
||||
<a class="{cc}" href="portal-social-accounts.html"><div class="eyebrow">Portal · Social</div><h3>Social accounts</h3><p>OAuth connect, health, disconnect.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-social.html"><div class="eyebrow">Portal · Social</div><h3>Social composer</h3><p>Live FB / IG / LinkedIn previews.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-social-calendar.html"><div class="eyebrow">Portal · Social</div><h3>Social calendar</h3><p>Schedule and queue.</p><span class="status">Ready</span></a>
|
||||
<a class="{cc}" href="portal-social-ai.html"><div class="eyebrow">Portal · AI Social</div><h3>AI social generator</h3><p>Ollama drafts → insert into composer.</p><span class="status">Ready</span></a>
|
||||
</div>
|
||||
<p class="aiml-site-credit">Made by <a href="https://aimloperations.com" rel="noopener noreferrer" target="_blank">AI ML Operations, LLC</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
for cfg in (EDS, PTP):
|
||||
cfg["file"].write_text(gallery(cfg), encoding="utf-8")
|
||||
print("updated", cfg["file"])
|
||||
Reference in New Issue
Block a user