Add Plausible event tracking and Last-Modified cache busting

Track feature clicks, subscriptions, micro-transactions, top-ups,
share receipt, and close app as Plausible custom events.

Cache-bust style.css and app.js using Last-Modified headers from
HEAD requests — no build step needed, caches break on file change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-18 09:16:20 +01:00
commit 3a457b362b
2 changed files with 28 additions and 5 deletions

View file

@ -75,6 +75,7 @@ const AD_COPY = [
]; ];
const fmt = s => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,"0")}`; const fmt = s => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,"0")}`;
const track = (event, props) => { try { window.plausible(event, { props }); } catch(e) {} };
// Cha-ching sound effect via Tone.js // Cha-ching sound effect via Tone.js
const playChaChing = async () => { const playChaChing = async () => {
@ -279,6 +280,7 @@ function PayPlay() {
}, [flash]); }, [flash]);
const tryAct = (k, fn) => { const tryAct = (k, fn) => {
track("feature_click", { feature: k, subscribed: !!has(k) });
if (has(k)) { fn(); return; } if (has(k)) { fn(); return; }
setModal({ key: k, action: fn }); setModal({ key: k, action: fn });
setModalPhase("choose"); setModalPhase("choose");
@ -291,6 +293,7 @@ function PayPlay() {
if (bal < p) { setModalPhase("broke"); return; } if (bal < p) { setModalPhase("broke"); return; }
charge(p, s.name); charge(p, s.name);
setSubs_(prev => ({ ...prev, [modal.key]: true })); setSubs_(prev => ({ ...prev, [modal.key]: true }));
track("subscribe", { feature: modal.key, price: p });
setSuccessText(`${s.name} is yours. Was it worth it? (It wasn't.)`); setSuccessText(`${s.name} is yours. Was it worth it? (It wasn't.)`);
setModalPhase("success"); setModalPhase("success");
setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1800); setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1800);
@ -304,6 +307,7 @@ function PayPlay() {
const s = SUBS[modal.key]; const s = SUBS[modal.key];
if (bal < s.micro) { setModalPhase("broke"); return; } if (bal < s.micro) { setModalPhase("broke"); return; }
charge(s.micro, `${s.name} (1x)`); charge(s.micro, `${s.name} (1x)`);
track("microtransaction", { feature: modal.key, price: s.micro });
setSuccessText(`One-time access. That's $${s.micro.toFixed(2)} you'll never see again.`); setSuccessText(`One-time access. That's $${s.micro.toFixed(2)} you'll never see again.`);
setModalPhase("success"); setModalPhase("success");
setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1500); setTimeout(() => { modal.action(); setModal(null); setModalPhase("choose"); }, 1500);
@ -317,6 +321,7 @@ function PayPlay() {
setBalKey(k => k + 1); setBalKey(k => k + 1);
flash("+$9.50 ($0.50 processing fee)", "#f39c12"); flash("+$9.50 ($0.50 processing fee)", "#f39c12");
playChaChing(); playChaChing();
track("topup", { source: "modal" });
setModalPhase("choose"); setModalPhase("choose");
}; };
@ -436,7 +441,7 @@ function PayPlay() {
"", `Try it: ${window.location.href}` "", `Try it: ${window.location.href}`
]; ];
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(lines.join("\n")).then(() => flash("Receipt copied! Share your shame.", accent)); navigator.clipboard.writeText(lines.join("\n")).then(() => { flash("Receipt copied! Share your shame.", accent); track("share_receipt", { spent: spent.toFixed(2) }); });
} }
}; };
@ -487,7 +492,7 @@ function PayPlay() {
</div> </div>
<div style={{display:"flex",gap:10,alignItems:"center"}}> <div style={{display:"flex",gap:10,alignItems:"center"}}>
<div key={balKey} style={{background:card,padding:"6px 14px",borderRadius:20,fontSize:12,fontWeight:700,color:accent,animation:balKey>0?"balFlash 0.6s ease":"none",transition:"background 0.3s ease"}}>💰 ${bal.toFixed(2)}</div> <div key={balKey} style={{background:card,padding:"6px 14px",borderRadius:20,fontSize:12,fontWeight:700,color:accent,animation:balKey>0?"balFlash 0.6s ease":"none",transition:"background 0.3s ease"}}>💰 ${bal.toFixed(2)}</div>
<button onClick={() => { setBal(b => +(b + 9.50).toFixed(2)); setSpent(s => +(s + 0.50).toFixed(2)); setActs(x => x + 1); setBalKey(k => k + 1); flash("+$9.50 ($0.50 processing fee)", "#f39c12"); playChaChing(); }} style={{background:accent,color:"#000",border:"none",borderRadius:20,padding:"6px 14px",fontSize:11,fontWeight:700,cursor:"pointer",boxShadow:`0 2px 10px ${accent}33`}}>+$10</button> <button onClick={() => { setBal(b => +(b + 9.50).toFixed(2)); setSpent(s => +(s + 0.50).toFixed(2)); setActs(x => x + 1); setBalKey(k => k + 1); flash("+$9.50 ($0.50 processing fee)", "#f39c12"); playChaChing(); track("topup", { source: "header" }); }} style={{background:accent,color:"#000",border:"none",borderRadius:20,padding:"6px 14px",fontSize:11,fontWeight:700,cursor:"pointer",boxShadow:`0 2px 10px ${accent}33`}}>+$10</button>
</div> </div>
</header> </header>
@ -609,7 +614,7 @@ function PayPlay() {
))} ))}
</section> </section>
<button onClick={() => has("exit") ? setShowExit(true) : setShowAd(true)} style={{background:"transparent",border:"1px solid #e74c3c33",color:"#e74c3c",padding:"10px 28px",borderRadius:8,fontSize:11,cursor:"pointer",marginBottom:16}}> Close App</button> <button onClick={() => { track("close_app", { has_exit: !!has("exit") }); has("exit") ? setShowExit(true) : setShowAd(true); }} style={{background:"transparent",border:"1px solid #e74c3c33",color:"#e74c3c",padding:"10px 28px",borderRadius:8,fontSize:11,cursor:"pointer",marginBottom:16}}> Close App</button>
<footer style={{fontSize:10,color:muted,textAlign:"center",maxWidth:380,padding:"0 20px",lineHeight:1.8,opacity:0.7}}> <footer style={{fontSize:10,color:muted,textAlign:"center",maxWidth:380,padding:"0 20px",lineHeight:1.8,opacity:0.7}}>
Pay2Play! v6.6.6 Music procedurally generated even the songs are cheaply made Pay2Play! v6.6.6 Music procedurally generated even the songs are cheaply made

View file

@ -16,7 +16,7 @@
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Anybody:wght@400;700;900&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Anybody:wght@400;700;900&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" id="app-css" href="style.css">
<!-- Privacy-friendly analytics by Plausible (proxied via /implausibly/) --> <!-- Privacy-friendly analytics by Plausible (proxied via /implausibly/) -->
<script async src="/implausibly/js/pa-PsYdseBlB0-XWi0fXEZ3j.js"></script> <script async src="/implausibly/js/pa-PsYdseBlB0-XWi0fXEZ3j.js"></script>
<script> <script>
@ -30,6 +30,24 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.9/babel.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.9/babel.min.js"></script>
<script type="text/babel" src="app.js" data-type="module"></script> <script>
// Cache-bust local assets using their Last-Modified header
(async () => {
const bust = async (url) => {
try {
const r = await fetch(url, { method: "HEAD" });
const lm = r.headers.get("Last-Modified");
return lm ? url + "?v=" + new Date(lm).getTime() : url;
} catch(e) { return url; }
};
document.getElementById("app-css").href = await bust("style.css");
const s = document.createElement("script");
s.type = "text/babel";
s.src = await bust("app.js");
s.setAttribute("data-type", "module");
document.body.appendChild(s);
Babel.transformScriptTags();
})();
</script>
</body> </body>
</html> </html>