Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_heatmaps.html
Diff
/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_heatmaps.html
added on local at 2026-07-01 21:00:04
Added
+112
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to e85aacf6236f
to e85aacf6236f
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | <div class="page-header"> | |
| 2 | <div><h1>Heatmaps</h1><p>$site_name · click density overlay.</p></div> | |
| 3 | </div> | |
| 4 | ||
| 5 | [if:!$has_pages] | |
| 6 | <div class="card"> | |
| 7 | <p class="muted">No heatmap data yet. Once visitors start clicking, the most popular pages will show up here.</p> | |
| 8 | </div> | |
| 9 | [/if] | |
| 10 | [if:$has_pages] | |
| 11 | <div class="row-2"> | |
| 12 | <div class="card"> | |
| 13 | <div class="card-header"><h3 class="card-title">Pages with click data</h3></div> | |
| 14 | <table class="table"> | |
| 15 | <thead><tr><th>Page</th><th class="right">Clicks</th><th></th></tr></thead> | |
| 16 | <tbody> | |
| 17 | [loop:@pages] | |
| 18 | <tr><td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">$page_path</td><td class="num">$clicks</td><td class="right"><a class="btn btn-sm btn-ghost" href="/heatmaps.cgi?site_id=$site_id&p=$page_path">View</a></td></tr> | |
| 19 | [/loop] | |
| 20 | </tbody> | |
| 21 | </table> | |
| 22 | </div> | |
| 23 | ||
| 24 | <div class="card"> | |
| 25 | <div class="card-header"><h3 class="card-title">Heatmap — $page_path</h3><span class="badge">$n_clicks clicks</span></div> | |
| 26 | <p class="muted text-sm">Live page underneath, click density overlaid on top. Hot zones = most clicks.</p> | |
| 27 | <div style="position:relative; background: linear-gradient(135deg, #1f263a 0%, #161c2d 100%); border-radius:8px; overflow:hidden; aspect-ratio: 16/10;"> | |
| 28 | <!-- The customer's actual page rendered underneath. If their site | |
| 29 | sends X-Frame-Options: DENY / SAMEORIGIN, the iframe stays | |
| 30 | blank and only the fallback background + overlay is shown. --> | |
| 31 | <iframe id="hm-page" | |
| 32 | src="https://$domain$page_path" | |
| 33 | style="position:absolute; inset:0; width:100%; height:100%; border:0; background:#0e1420; filter: brightness(0.35) contrast(0.85) saturate(0.7);" | |
| 34 | sandbox="allow-same-origin" | |
| 35 | referrerpolicy="no-referrer" | |
| 36 | loading="lazy"></iframe> | |
| 37 | <div aria-hidden="true" | |
| 38 | style="position:absolute; inset:0; background: rgba(10,14,22,0.35); pointer-events:none;"></div> | |
| 39 | <canvas id="hm-canvas" | |
| 40 | style="position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; mix-blend-mode:screen;"></canvas> | |
| 41 | </div> | |
| 42 | <div id="hm-blocked-hint" class="muted text-sm" style="display:none; margin-top:8px;"> | |
| 43 | Couldn’t embed <code>$domain</code> in a preview frame (the site blocks iframing). The click overlay is still accurate. To see it on top of the real page, install the ABForge browser preview extension. | |
| 44 | </div> | |
| 45 | </div> | |
| 46 | </div> | |
| 47 | ||
| 48 | <script> | |
| 49 | (function() { | |
| 50 | var pts = $points; | |
| 51 | var c = document.getElementById('hm-canvas'); | |
| 52 | if (!c) return; | |
| 53 | var W = c.clientWidth, H = c.clientHeight; | |
| 54 | c.width = W; c.height = H; | |
| 55 | var ctx = c.getContext('2d'); | |
| 56 | // Detect iframe load failure (X-Frame-Options / CSP) — chrome fires | |
| 57 | // 'load' either way, so we probe by reading document.title of the | |
| 58 | // frame; if same-origin blocked or cross-origin, we can't read it. | |
| 59 | var iframe = document.getElementById('hm-page'); | |
| 60 | if (iframe) { | |
| 61 | var hint = document.getElementById('hm-blocked-hint'); | |
| 62 | iframe.addEventListener('error', function(){ if(hint) hint.style.display='block'; }); | |
| 63 | // Give the browser a beat to attempt the load, then check state. | |
| 64 | setTimeout(function(){ | |
| 65 | try { | |
| 66 | // Cross-origin frame throws on doc access — that's actually the | |
| 67 | // NORMAL case for external customer sites. Only show the hint | |
| 68 | // when the frame is visibly empty (contentDocument is same- | |
| 69 | // origin AND has no head/body loaded). | |
| 70 | var doc = iframe.contentDocument; | |
| 71 | if (doc && (!doc.body || !doc.body.childNodes.length)) { | |
| 72 | if(hint) hint.style.display='block'; | |
| 73 | } | |
| 74 | } catch(e) { /* cross-origin: page probably loaded fine */ } | |
| 75 | }, 2500); | |
| 76 | } | |
| 77 | if (!pts.length) { | |
| 78 | ctx.fillStyle = '#7a8499'; ctx.font = '13px Inter'; | |
| 79 | ctx.fillText('No clicks on this page yet.', 24, 24); | |
| 80 | return; | |
| 81 | } | |
| 82 | // Off-screen accumulation buffer | |
| 83 | var buf = document.createElement('canvas'); buf.width = W; buf.height = H; | |
| 84 | var bctx = buf.getContext('2d'); | |
| 85 | for (var i = 0; i < pts.length; i++) { | |
| 86 | var x = pts[i][0] / 100 * W; | |
| 87 | var y = pts[i][1] / 100 * H; | |
| 88 | var g = bctx.createRadialGradient(x, y, 0, x, y, 28); | |
| 89 | g.addColorStop(0, 'rgba(255,255,255,0.25)'); | |
| 90 | g.addColorStop(0.5, 'rgba(255,255,255,0.1)'); | |
| 91 | g.addColorStop(1, 'rgba(255,255,255,0)'); | |
| 92 | bctx.fillStyle = g; | |
| 93 | bctx.beginPath(); bctx.arc(x, y, 28, 0, Math.PI*2); bctx.fill(); | |
| 94 | } | |
| 95 | // Colorize alpha into a hot gradient | |
| 96 | var img = bctx.getImageData(0, 0, W, H); | |
| 97 | var d = img.data; | |
| 98 | for (var p = 0; p < d.length; p += 4) { | |
| 99 | var a = d[p+3]; | |
| 100 | if (a > 0) { | |
| 101 | // sunset gradient | |
| 102 | var t = a / 255; | |
| 103 | d[p] = Math.round(255 * Math.min(1, 0.5 + t)); | |
| 104 | d[p+1] = Math.round(255 * Math.min(1, t * 0.6)); | |
| 105 | d[p+2] = Math.round(80 * (1 - t)); | |
| 106 | d[p+3] = Math.round(200 * t); | |
| 107 | } | |
| 108 | } | |
| 109 | ctx.putImageData(img, 0, 0); | |
| 110 | })(); | |
| 111 | </script> | |
| 112 | [/if] |