314 lines
14 KiB
HTML
314 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Postcard designer · Portal · MKDRealtor.com</title>
|
||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Work+Sans:300,400,500,700%7CPoppins:400,600,700">
|
||
<link rel="stylesheet" href="proposal.css">
|
||
</head>
|
||
<body class="portal">
|
||
<div class="proposal-banner">
|
||
<div><span class="tag">Proposal mockup</span> MKDRealtor.com — UX examples (not production)</div>
|
||
<div><a href="index.html">All screens</a></div>
|
||
</div>
|
||
<div class="portal-shell">
|
||
<aside class="portal-sidebar">
|
||
<a class="portal-brand" href="portal-dashboard.html"><img src="brand/exit_logo.png" alt="EXIT Realty"><span>MKDRealtor.com</span></a>
|
||
<nav class="portal-nav">
|
||
<div class="nav-section">Overview</div>
|
||
<a class="" href="portal-dashboard.html">Dashboard</a>
|
||
<a class="" href="portal-leads.html">Leads</a>
|
||
<a class="" href="portal-analytics.html">Analytics</a>
|
||
<div class="nav-section">Outreach</div>
|
||
<a class="" href="portal-contacts.html">Mailing list</a>
|
||
<a class="" href="portal-campaign.html">Campaigns</a>
|
||
<a class="active" href="portal-postcard-designer.html">Postcard design</a>
|
||
<div class="nav-section">Social</div>
|
||
<a class="" href="portal-social.html">Compose & preview</a>
|
||
<a class="" href="portal-social-accounts.html">Accounts</a>
|
||
</nav>
|
||
</aside>
|
||
<div class="portal-main">
|
||
<div class="portal-topbar">
|
||
<h1>Postcard designer</h1>
|
||
<div class="portal-user">Signed in as <strong>Monica Dhillon</strong> · <a href="portal-login.html" style="color:#00626c">Sign out</a></div>
|
||
</div>
|
||
<div class="portal-content">
|
||
<div class="proposal-note"><strong>UX note:</strong> Pick print size (4×6 / 6×9 / 6×11 / square). Upload images once into a reusable media library, then apply to any design. Save as template → attach to postcard campaign.</div>
|
||
|
||
<div class="designer-layout">
|
||
<div class="designer-controls">
|
||
<div class="panel">
|
||
<div class="panel-h"><h2>Card size</h2><span class="muted" style="font-size:12px" id="size-label">4×6 in</span></div>
|
||
<div class="panel-b">
|
||
<div class="size-grid" id="size-grid">
|
||
<button type="button" class="size-option active" data-size="4x6" data-label="4×6 in" data-ratio="6/4" onclick="setSize(this)">
|
||
<div class="sz-name">4 × 6</div>
|
||
<div class="sz-meta">Standard · lowest postage</div>
|
||
<span class="sz-badge">Most common</span>
|
||
</button>
|
||
<button type="button" class="size-option" data-size="6x9" data-label="6×9 in" data-ratio="9/6" onclick="setSize(this)">
|
||
<div class="sz-name">6 × 9</div>
|
||
<div class="sz-meta">More room for photo + copy</div>
|
||
</button>
|
||
<button type="button" class="size-option" data-size="6x11" data-label="6×11 in" data-ratio="11/6" onclick="setSize(this)">
|
||
<div class="sz-name">6 × 11</div>
|
||
<div class="sz-meta">Jumbo · stands out in mail</div>
|
||
</button>
|
||
<button type="button" class="size-option" data-size="square" data-label="6×6 in square" data-ratio="1/1" onclick="setSize(this)">
|
||
<div class="sz-name">6 × 6</div>
|
||
<div class="sz-meta">Square · social-ready crop</div>
|
||
</button>
|
||
</div>
|
||
<p class="library-hint">Size locked when campaign is queued. Provider pricing varies by size.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-h"><h2>Media library</h2><span class="muted" style="font-size:12px">Reusable</span></div>
|
||
<div class="panel-b">
|
||
<input type="file" id="pc-upload" accept="image/*" multiple hidden onchange="handleUpload(event)">
|
||
<div class="dropzone compact" id="pc-drop" onclick="document.getElementById('pc-upload').click()"
|
||
ondragover="event.preventDefault();this.style.borderColor='#00626c'"
|
||
ondragleave="this.style.borderColor=''"
|
||
ondrop="event.preventDefault();this.style.borderColor=''; handleDrop(event)">
|
||
Drop images here or <strong>click to upload</strong><br>
|
||
<span class="muted">Saved to your library — reuse across postcards & campaigns</span>
|
||
</div>
|
||
<div class="media-library" id="media-library" style="margin-top:12px"></div>
|
||
<p class="library-hint" id="library-count">0 images in library</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-h"><h2>Side</h2></div>
|
||
<div class="panel-b">
|
||
<div class="channel-tabs" style="margin:0">
|
||
<a href="#" class="active" id="tab-front" onclick="showSide('front');return false">Front</a>
|
||
<a href="#" id="tab-back" onclick="showSide('back');return false">Back</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel" id="front-controls">
|
||
<div class="panel-h"><h2>Front artwork</h2></div>
|
||
<div class="panel-b form-grid">
|
||
<div class="field"><label>Headline</label>
|
||
<input id="pc-headline" value="JUST LISTED" oninput="syncPostcard()">
|
||
</div>
|
||
<div class="field"><label>Subhead</label>
|
||
<input id="pc-sub" value="412 Willow Lane · Open Sat 11–1" oninput="syncPostcard()">
|
||
</div>
|
||
<div class="field"><label>Accent color</label>
|
||
<input id="pc-color" type="color" value="#00626c" oninput="syncPostcard()">
|
||
</div>
|
||
<div class="field"><label>Overlay</label>
|
||
<select id="pc-overlay" onchange="syncPostcard()">
|
||
<option value="dark">Dark gradient</option>
|
||
<option value="light">Light band</option>
|
||
<option value="none">None</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel" id="back-controls" style="display:none">
|
||
<div class="panel-h"><h2>Back copy</h2></div>
|
||
<div class="panel-b form-grid">
|
||
<div class="field"><label>Body text</label>
|
||
<textarea id="pc-body" style="min-height:100px" oninput="syncPostcard()">Quiet street, updated kitchen, walkable to Oakridge schools. Asking $449,000.
|
||
|
||
Call or text Monica for a private showing.</textarea>
|
||
</div>
|
||
<div class="field"><label>Agent line</label>
|
||
<input id="pc-agent" value="Monica Dhillon · (555) 123-4567 · mkdrealtor.com" oninput="syncPostcard()">
|
||
</div>
|
||
<div class="field"><label>Include</label>
|
||
<label class="check-row"><input type="checkbox" id="pc-qr" checked onchange="syncPostcard()"> QR to listing / contact</label>
|
||
<label class="check-row"><input type="checkbox" id="pc-addr" checked onchange="syncPostcard()"> Recipient address block ( Lob )</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel">
|
||
<div class="panel-b" style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn btn-primary" type="button" onclick="alert('Mockup: save front/back assets + size + library refs → MessageTemplate')">Save design</button>
|
||
<a class="btn btn-ghost" href="portal-campaign.html">Use in campaign</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="designer-preview-col">
|
||
<div class="panel">
|
||
<div class="panel-h">
|
||
<h2>Live preview · <span id="preview-size">4×6</span></h2>
|
||
<span class="muted" style="font-size:12px" id="side-label">Front</span>
|
||
</div>
|
||
<div class="panel-b" style="background:#e8edf3;display:flex;justify-content:center;padding:28px">
|
||
<div class="postcard size-4x6" id="postcard-front">
|
||
<div class="postcard-media" id="pc-media"></div>
|
||
<div class="postcard-overlay" id="pc-ov"></div>
|
||
<div class="postcard-copy">
|
||
<div class="postcard-headline" id="pc-h-out">JUST LISTED</div>
|
||
<div class="postcard-sub" id="pc-s-out">412 Willow Lane · Open Sat 11–1</div>
|
||
</div>
|
||
</div>
|
||
<div class="postcard postcard-back size-4x6" id="postcard-back" style="display:none">
|
||
<div class="postcard-back-grid">
|
||
<div class="postcard-back-msg">
|
||
<div class="postcard-back-body" id="pc-b-out"></div>
|
||
<div class="postcard-agent" id="pc-a-out"></div>
|
||
<div class="postcard-qr" id="pc-qr-out">QR</div>
|
||
</div>
|
||
<div class="postcard-back-mail" id="pc-mail-out">
|
||
<div class="stamp">Stamp</div>
|
||
<div class="addr-block">
|
||
{{first_name}} {{last_name}}<br>
|
||
{{street}}<br>
|
||
{{city}}, {{state}} {{zip}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="muted" style="font-size:13px;margin:0">Print provider receives front + back images at selected size. Address verified before batch send (Lob).</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const LIBRARY_KEY = 'monica_postcard_library_v1';
|
||
let selectedBg = 'images/grid-layout-1-370x256.jpg';
|
||
let currentSize = '4x6';
|
||
|
||
const SEED_LIBRARY = [
|
||
{ id: 'seed-1', name: 'Willow exterior', src: 'images/grid-layout-1-370x256.jpg', reused: 3 },
|
||
{ id: 'seed-2', name: 'Kitchen', src: 'images/grid-layout-2-370x256.jpg', reused: 1 },
|
||
{ id: 'seed-3', name: 'Living room', src: 'images/grid-layout-3-370x256.jpg', reused: 0 },
|
||
{ id: 'seed-4', name: 'Neighborhood', src: 'images/bg-image-1.jpg', reused: 2 },
|
||
];
|
||
|
||
function loadLibrary() {
|
||
try {
|
||
const raw = localStorage.getItem(LIBRARY_KEY);
|
||
if (raw) return JSON.parse(raw);
|
||
} catch (e) {}
|
||
return SEED_LIBRARY.slice();
|
||
}
|
||
|
||
function saveLibrary(items) {
|
||
try { localStorage.setItem(LIBRARY_KEY, JSON.stringify(items)); } catch (e) {}
|
||
}
|
||
|
||
let library = loadLibrary();
|
||
|
||
function renderLibrary() {
|
||
const root = document.getElementById('media-library');
|
||
root.innerHTML = '';
|
||
const upload = document.createElement('button');
|
||
upload.type = 'button';
|
||
upload.className = 'media-tile upload-tile';
|
||
upload.innerHTML = '<span>+</span>Upload';
|
||
upload.onclick = () => document.getElementById('pc-upload').click();
|
||
root.appendChild(upload);
|
||
|
||
library.forEach(item => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'media-tile' + (item.src === selectedBg ? ' active' : '');
|
||
btn.style.backgroundImage = 'url(' + JSON.stringify(item.src).slice(1, -1) + ')';
|
||
btn.title = item.name + (item.reused ? ' · used ' + item.reused + '×' : '');
|
||
btn.innerHTML = '<span class="media-label">' + escapeHtml(item.name) + '</span>';
|
||
btn.onclick = () => selectMedia(item.id);
|
||
root.appendChild(btn);
|
||
});
|
||
document.getElementById('library-count').textContent =
|
||
library.length + ' image' + (library.length === 1 ? '' : 's') + ' in library · click to use on this card';
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
function selectMedia(id) {
|
||
const item = library.find(x => x.id === id);
|
||
if (!item) return;
|
||
selectedBg = item.src;
|
||
item.reused = (item.reused || 0) + 1;
|
||
saveLibrary(library);
|
||
renderLibrary();
|
||
syncPostcard();
|
||
}
|
||
|
||
function addFiles(files) {
|
||
Array.from(files || []).forEach(file => {
|
||
if (!file.type.startsWith('image/')) return;
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
const item = {
|
||
id: 'up-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7),
|
||
name: file.name.replace(/\.[^.]+$/, '') || 'Upload',
|
||
src: reader.result,
|
||
reused: 0,
|
||
};
|
||
library.unshift(item);
|
||
saveLibrary(library);
|
||
selectedBg = item.src;
|
||
renderLibrary();
|
||
syncPostcard();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
function handleUpload(e) { addFiles(e.target.files); e.target.value = ''; }
|
||
function handleDrop(e) { addFiles(e.dataTransfer.files); }
|
||
|
||
function setSize(btn) {
|
||
currentSize = btn.dataset.size;
|
||
document.querySelectorAll('.size-option').forEach(el => el.classList.toggle('active', el === btn));
|
||
document.getElementById('size-label').textContent = btn.dataset.label;
|
||
document.getElementById('preview-size').textContent = btn.dataset.label.replace(' in', '');
|
||
['postcard-front', 'postcard-back'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
el.className = el.className.replace(/size-\S+/g, '').trim() + ' size-' + currentSize;
|
||
if (id === 'postcard-back') el.classList.add('postcard-back');
|
||
});
|
||
}
|
||
|
||
function showSide(side) {
|
||
const front = side === 'front';
|
||
document.getElementById('postcard-front').style.display = front ? 'block' : 'none';
|
||
document.getElementById('postcard-back').style.display = front ? 'none' : 'block';
|
||
document.getElementById('front-controls').style.display = front ? 'block' : 'none';
|
||
document.getElementById('back-controls').style.display = front ? 'none' : 'block';
|
||
document.getElementById('tab-front').classList.toggle('active', front);
|
||
document.getElementById('tab-back').classList.toggle('active', !front);
|
||
document.getElementById('side-label').textContent = front ? 'Front' : 'Back';
|
||
}
|
||
|
||
function syncPostcard() {
|
||
const color = document.getElementById('pc-color').value;
|
||
const ov = document.getElementById('pc-overlay').value;
|
||
document.getElementById('pc-media').style.backgroundImage = 'url(' + JSON.stringify(selectedBg).slice(1, -1) + ')';
|
||
document.getElementById('pc-h-out').textContent = document.getElementById('pc-headline').value;
|
||
document.getElementById('pc-s-out').textContent = document.getElementById('pc-sub').value;
|
||
const textColor = ov === 'light' ? '#111' : '#fff';
|
||
document.getElementById('pc-h-out').style.color = textColor;
|
||
document.getElementById('pc-s-out').style.color = textColor;
|
||
document.getElementById('pc-h-out').style.borderColor = color;
|
||
document.getElementById('pc-ov').className = 'postcard-overlay ov-' + ov;
|
||
document.getElementById('pc-b-out').textContent = document.getElementById('pc-body').value;
|
||
document.getElementById('pc-a-out').textContent = document.getElementById('pc-agent').value;
|
||
document.getElementById('pc-qr-out').style.display = document.getElementById('pc-qr').checked ? 'flex' : 'none';
|
||
document.getElementById('pc-mail-out').style.display = document.getElementById('pc-addr').checked ? 'block' : 'none';
|
||
}
|
||
|
||
renderLibrary();
|
||
syncPostcard();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|