Publish BOFU batch: 7 canonical pages, trimmed titles/metas, rails-compar redirect fix (46 pages, 41 redirects)
029c12c Download share-image.html from thedollartoolkit/thedollarclientkit: direct link, hf CLI and curl.
- Browser
- Download file 16.3 kB
-
https://huggingface.co/spaces/thedollartoolkit/thedollarclientkit/resolve/main/share-image.html
- Command line
-
hf download hf://spaces/thedollartoolkit/thedollarclientkit/share-image.html
-
curl -L -o share-image.html https://huggingface.co/spaces/thedollartoolkit/thedollarclientkit/resolve/main/share-image.html
16.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WhatsApp Share Image Generator & 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 & 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 < $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 · < $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> | |