thedollarclientkit / share-image.html
thedollartoolkit's picture
Publish BOFU batch: 7 canonical pages, trimmed titles/metas, rails-compar redirect fix (46 pages, 41 redirects)
029c12c
Raw History Blame Contribute Delete
16.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WhatsApp Share Image Generator &amp; Spec | Dollar Client Kit</title>
<!--
═════════════════════════════════════════════════════════════════════
WHATSAPP SHARE-IMAGE — SPEC + WORKING PROTOTYPE
The Dollar Client Kit · verified October 2026
WHY: the current "Copy my result" button copies a text block. Text
links die in WhatsApp. An IMAGE with one huge number survives the
feed, works as a Status, and people tap the link out of curiosity.
HOW IT WORKS (all client-side, no server, no upload):
1. User finishes a calculation in calculator.html
2. They tap "Share image" (next to "Copy my result")
3. This file's canvas draws a 1080×1080 PNG from live values
4. The PNG downloads as dollar-leakage.png → they post it
TO SHIP IT: drop generateShareImage() + drawCard() into
calculator.html, add a "Share image" button beside #copyBtn, and
call generateShareImage(LAST.result) on click. No dependencies.
DIMENSIONS: 1080×1080 (WhatsApp Status + feed square). Safe zone
is the inner 920px — WhatsApp crops Status edges.
BRAND (exact tokens from calculator.html):
ink #111629 · ink-2 #1A2036 · lime #CDF74F · vellum #EFE8D9
graphite #5B6072 · graphite-2 #8A8F9E
═════════════════════════════════════════════════════════════════════
-->
<style>
:root{--ink:#111629;--ink2:#1A2036;--lime:#CDF74F;--vellum:#EFE8D9;--graphite:#5B6072;--g2:#8A8F9E;--line:rgba(255,255,255,.1)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Inter,system-ui,sans-serif;background:var(--ink);color:var(--vellum);padding:40px 24px 80px;line-height:1.6}
.wrap{max-width:960px;margin:0 auto}
h1{font-size:clamp(24px,4vw,34px);font-weight:600;letter-spacing:-.01em;margin-bottom:6px}
.sub{color:var(--g2);font-size:15px;margin-bottom:32px;max-width:640px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:28px;align-items:start}
@media(max-width:820px){.grid{grid-template-columns:1fr}}
.panel{background:var(--ink2);border:1px solid var(--line);border-radius:16px;padding:24px}
.panel h2{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:var(--g2);margin-bottom:16px;font-weight:700}
label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--g2);margin:14px 0 5px;font-weight:600}
input,select{width:100%;padding:11px 13px;background:var(--ink);border:1px solid var(--line);border-radius:9px;color:var(--vellum);font-size:15px;font-family:inherit}
input:focus,select:focus{outline:none;border-color:var(--lime)}
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.btn{display:inline-flex;align-items:center;gap:8px;background:var(--lime);color:var(--ink);border:none;padding:13px 24px;border-radius:100px;font-size:14px;font-weight:700;cursor:pointer;margin-top:20px;width:100%;justify-content:center;font-family:inherit;transition:transform .15s}
.btn:hover{transform:translateY(-1px)}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--vellum)}
canvas{width:100%;height:auto;display:block;border-radius:14px;border:1px solid var(--line)}
.hint{font-size:12.5px;color:var(--g2);margin-top:14px;line-height:1.55}
.copy{background:var(--ink2);border:1px solid var(--line);border-radius:16px;padding:24px;margin-top:28px}
.copy h2{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:var(--g2);margin-bottom:14px;font-weight:700}
table{width:100%;border-collapse:collapse;font-size:14px}
td{padding:11px 0;border-bottom:1px solid var(--line);vertical-align:top}
td:first-child{color:var(--g2);width:180px;font-size:12px;text-transform:uppercase;letter-spacing:.08em;font-weight:600;padding-right:16px}
code{font-family:ui-monospace,Menlo,monospace;background:var(--ink);padding:2px 7px;border-radius:5px;font-size:13px;color:var(--lime)}
.tierbox{margin-top:10px;font-size:13.5px;color:var(--g2)}
.tierbox b{color:var(--vellum)}
</style>
</head>
<body>
<div class="wrap">
<h1>WhatsApp share image — generator + spec</h1>
<p class="sub">Turns a fee-leakage result into a 1080×1080 PNG people can post as a WhatsApp Status or feed image. Runs entirely in the browser. Live numbers, one giant figure, one link.</p>
<div class="grid">
<!-- LEFT: controls -->
<div class="panel">
<h2>Inputs (mirrors calculator state)</h2>
<div class="row">
<div>
<label>Lost per year (USD)</label>
<input id="in-lost" type="number" value="2700" min="0">
</div>
<div>
<label>Recoverable (USD)</label>
<input id="in-gain" type="number" value="2436" min="0">
</div>
</div>
<div class="row">
<div>
<label>Monthly billing (USD)</label>
<input id="in-amount" type="number" value="1000" min="0">
</div>
<div>
<label>Effective cost (%)</label>
<input id="in-eff" type="number" value="22.5" step="0.1">
</div>
</div>
<div class="row">
<div>
<label>Country</label>
<select id="in-country">
<option value="Ghana">Ghana</option>
<option value="Nigeria">Nigeria</option>
<option value="Kenya">Kenya</option>
</select>
</div>
<div>
<label>Current route</label>
<select id="in-rail">
<option>Fiverr</option>
<option>Upwork</option>
<option>Payoneer</option>
<option>SWIFT wire</option>
</select>
</div>
</div>
<button class="btn" id="gen">Generate &amp; download PNG</button>
<p class="hint">In the real calculator these are read straight from the live result — no typing. This panel exists so you can test tiers and layouts before wiring it in.</p>
<div class="tierbox" id="tier-note"></div>
</div>
<!-- RIGHT: live preview -->
<div class="panel">
<h2>Live preview · 1080×1080</h2>
<canvas id="cv" width="1080" height="1080"></canvas>
<p class="hint">Statuses crop the outer edges — all critical text sits inside the inner 920px safe zone. The number is the hook; the link is the click.</p>
</div>
</div>
<!-- SPEC TABLE -->
<div class="copy">
<h2>Spec — implementation contract</h2>
<table>
<tr><td>Canvas</td><td>1080 × 1080 px (square). WhatsApp Status + feed safe. Export <code>image/png</code>.</td></tr>
<tr><td>Safe zone</td><td>Inner 920px (80px margin). WhatsApp Status crops left/right.</td></tr>
<tr><td>Background</td><td><code>#111629</code> (ink) with a subtle radial <code>#1A2036</code> glow top-right — matches the calculator's result panel.</td></tr>
<tr><td>Accent</td><td><code>#CDF74F</code> (lime) — used ONLY on the loss figure and the CTA pill. Never on body text.</td></tr>
<tr><td>Loss figure</td><td>Bold, ~150–190px, lime, centred. The single largest element. Currency symbol small, number huge.</td></tr>
<tr><td>Supporting line</td><td><code>#EFE8D9</code> (vellum), 30–34px. One sentence: "leaking from your freelancing every year."</td></tr>
<tr><td>Context row</td><td><code>#8A8F9E</code> (graphite-2), 22–24px mono. "$1,000/mo · Fiverr · Ghana · 22.5% all-in".</td></tr>
<tr><td>CTA pill</td><td>Lime fill, ink text, 26px bold, pill radius. "Check yours free →" + short link.</td></tr>
<tr><td>Branding</td><td>"THE DOLLAR CLIENT KIT" mono, 18px, letter-spaced, top-left. "fees checked OCT 2026" bottom-right, 16px graphite.</td></tr>
<tr><td>Link</td><td><code>thedollarclientkit.netlify.app/calculator</code> — add <code>?ref=wa</code> to attribute WhatsApp traffic in analytics.</td></tr>
<tr><td>Fallback tier</td><td>If lost &lt; $500/yr → smaller figure, gentler copy ("small leaks add up"). Never shame a low number.</td></tr>
</table>
</div>
<div class="copy">
<h2>Copy tiers (auto-selected by loss amount)</h2>
<table>
<tr><td>High · ≥ $2,000/yr</td><td><b>"$2,700 is leaking from your freelancing every year."</b> — shock value, shareable outrage.</td></tr>
<tr><td>Mid · $500–1,999/yr</td><td><b>"You're losing $1,200/year to payment fees."</b> — matter-of-fact, "fix this" framing.</td></tr>
<tr><td>Low · &lt; $500/yr</td><td><b>"Small leaks: $380/year gone to fees."</b> — never shame; "still worth fixing".</td></tr>
</table>
<p class="hint" style="margin-top:14px">Every image carries the same disclaimer logic as the site: ranges, not quotes, conservative (worst-case) mode. The figure shown is the one the calculator already displayed — the image never invents a number.</p>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════════
// DRAWING — drop drawCard() + generateShareImage() into calculator.html
// ═══════════════════════════════════════════════════════════════════
var INK="#111629", INK2="#1A2036", LIME="#CDF74F", VELLUM="#EFE8D9", G2="#8A8F9E";
function tierFor(loss){ return loss>=2000?"high": loss>=500?"mid":"low"; }
function headline(loss, tier){
if(tier==="high") return "is leaking from your";
if(tier==="mid") return "lost to payment fees";
return "gone to payment fees";
}
function subline(tier){
if(tier==="high") return "every year. And you never saw it.";
if(tier==="mid") return "every year — commission, withdrawal, FX.";
return "every year. Small leaks still leak.";
}
function drawCard(cv, d){
var ctx = cv.getContext("2d");
var W=1080, H=1080;
ctx.clearRect(0,0,W,H);
// background: ink + soft radial glow top-right
ctx.fillStyle=INK; ctx.fillRect(0,0,W,H);
var g=ctx.createRadialGradient(W*0.9,-80,0,W*0.9,-80,760);
g.addColorStop(0,"rgba(26,32,54,1)"); g.addColorStop(1,"rgba(17,22,41,0)");
ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
// faint hex grid (echoes calculator result texture)
ctx.strokeStyle="rgba(255,255,255,0.03)"; ctx.lineWidth=1;
for(var x=-40;x<W+40;x+=48){
for(var y=-40;y<H+40;y+=48){
ctx.beginPath();
ctx.moveTo(x+24,y+1); ctx.lineTo(x+46,y+13); ctx.lineTo(x+46,y+37);
ctx.lineTo(x+24,y+49); ctx.lineTo(x+2,y+37); ctx.lineTo(x+2,y+13);
ctx.closePath(); ctx.stroke();
}
}
var tier = tierFor(d.lost);
var M=80; // safe margin
// brand top-left
ctx.fillStyle=G2; ctx.font="600 20px ui-monospace,Menlo,monospace";
ctx.textBaseline="top"; ctx.textAlign="left";
ctx.fillText("THE DOLLAR CLIENT KIT", M, M);
// "fees checked" pill top-right
var stamp="FEES CHECKED OCT 2026";
ctx.font="600 16px ui-monospace,Menlo,monospace";
var sw=ctx.measureText(stamp).width;
ctx.strokeStyle="rgba(255,255,255,0.16)";
roundRect(ctx,W-M-sw-28,M-6,sw+28,34,17); ctx.stroke();
ctx.fillStyle=G2; ctx.fillText(stamp,M-sw-14,M+2);
// ── the number (hero) ──
var money = d.lost>=1000 ? "$"+Math.round(d.lost).toLocaleString("en-US") : "$"+Math.round(d.lost);
ctx.textAlign="center"; ctx.textBaseline="alphabetic";
var numSize = tier==="high"?196 : tier==="mid"?176 : 156;
ctx.font="700 "+numSize+"px Inter,system-ui,sans-serif";
ctx.fillStyle=LIME;
ctx.fillText(money, W/2, 470);
// supporting headline under the number
ctx.fillStyle=VELLUM;
ctx.font="500 34px Inter,system-ui,sans-serif";
ctx.fillText(headline(d.lost,tier), W/2, 528);
ctx.font="400 30px Inter,system-ui,sans-serif";
ctx.fillStyle=G2;
ctx.fillText(subline(tier), W/2, 574);
// divider
ctx.strokeStyle="rgba(255,255,255,0.08)";
ctx.beginPath(); ctx.moveTo(W/2-180,628); ctx.lineTo(W/2+180,628); ctx.stroke();
// context row: $1,000/mo · Fiverr · Ghana · 22.5% all-in
var ctxline = "$"+d.amount.toLocaleString("en-US")+"/mo · "+d.rail+" · "+d.country+" · "+d.eff+"% all-in";
ctx.fillStyle=G2; ctx.font="500 24px ui-monospace,Menlo,monospace";
ctx.fillText(ctxline, W/2, 686);
// recoverable chip
if(d.gain>0){
var gainTxt = "You could keep +$"+Math.round(d.gain).toLocaleString("en-US")+"/yr";
ctx.font="700 24px Inter,system-ui,sans-serif";
var gw=ctx.measureText(gainTxt).width;
ctx.fillStyle=LIME;
roundRect(ctx,W/2-gw/2-20,726,gw+40,46,23); ctx.fill();
ctx.fillStyle=INK; ctx.fillText(gainTxt, W/2, 758);
}
// CTA pill
var cta="Check yours free →";
ctx.font="700 26px Inter,system-ui,sans-serif";
var cw=ctx.measureText(cta).width;
ctx.fillStyle=LIME;
roundRect(ctx,W/2-cw/2-32,846,cw+64,64,32); ctx.fill();
ctx.fillStyle=INK; ctx.fillText(cta, W/2, 888);
// link
ctx.fillStyle=G2; ctx.font="500 22px ui-monospace,Menlo,monospace";
ctx.fillText("thedollarclientkit.netlify.app/calculator", W/2, 964);
// bottom-right disclaimer
ctx.textAlign="right"; ctx.fillStyle="#5B6072";
ctx.font="500 15px Inter,system-ui,sans-serif";
ctx.fillText("ranges, not quotes · conservative mode", W-M, H-M);
}
function roundRect(ctx,x,y,w,h,r){
ctx.beginPath();
ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function readInputs(){
return {
lost: parseFloat(document.getElementById("in-lost").value)||0,
gain: parseFloat(document.getElementById("in-gain").value)||0,
amount: parseFloat(document.getElementById("in-amount").value)||0,
eff: parseFloat(document.getElementById("in-eff").value)||0,
country:document.getElementById("in-country").value,
rail: document.getElementById("in-rail").value
};
}
function render(){
var d=readInputs();
drawCard(document.getElementById("cv"), d);
var t=tierFor(d.lost);
var msg = t==="high" ? "Tier: HIGH — shock framing (≥ $2,000/yr)."
: t==="mid" ? "Tier: MID — fix-this framing ($500–1,999/yr)."
: "Tier: LOW — gentle framing (< $500/yr). Never shames a small number.";
document.getElementById("tier-note").textContent = msg;
}
function generateShareImage(d){
var cv=document.createElement("canvas"); cv.width=1080; cv.height=1080;
drawCard(cv, d||readInputs());
cv.toBlob(function(b){
var a=document.createElement("a");
a.href=URL.createObjectURL(b);
a.download="dollar-leakage.png";
a.click();
setTimeout(function(){URL.revokeObjectURL(a.href);},2000);
},"image/png");
}
["in-lost","in-gain","in-amount","in-eff","in-country","in-rail"].forEach(function(id){
document.getElementById(id).addEventListener("input",render);
});
document.getElementById("gen").addEventListener("click",function(){ generateShareImage(); });
render();
// ── WIRING NOTE for calculator.html ──────────────────────────────
// 1. Add beside #copyBtn:
// <button class="btn-sm" id="shareBtn" type="button">Share image</button>
// 2. Add this after the existing copyBtn listener:
// $("shareBtn").addEventListener("click",function(){
// generateShareImage({
// lost: LAST.heroLossNumber, // parse from #heroLoss
// gain: LAST.heroGainNumber, // parse from #heroGain
// amount: S.amount,
// eff: LAST.effPctNumber,
// country: cfgCorridorName(S.you),
// rail: LAST.data.inputs.railName
// });
// });
// 3. Append ?ref=wa to the link inside drawCard() when coming from here,
// so Netlify Analytics can attribute WhatsApp traffic.
// ─────────────────────────────────────────────────────────────────
</script>
</body>
</html>