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:
parent
996b8c9a90
commit
3a457b362b
2 changed files with 28 additions and 5 deletions
|
|
@ -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 •
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue