Add new paywalled features, polish, and favicon
New subscriptions: Like ($0.25, does nothing), HD Audio (removes low-pass filter from synth), Recently Played (shows current song), Cancellation Processing ($4.99 to unsubscribe). Polish: welcome banner fades out smoothly, exit ad plays an annoying jingle via Tone.js, 64kbps Suffering Quality badge shown when HD Audio not purchased, favicon added. Moved og-card.svg source to tools/ (not needed in public/). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d08d886956
commit
5ba61db478
6 changed files with 46 additions and 5 deletions
|
|
@ -48,6 +48,10 @@ const SUBS = {
|
|||
exit: { name:"Silent Exit™", price:"$1.99/mo", micro:0.25, desc:"Close the app without an unskippable ad.", color:"#c0392b", dismiss:"I love ads actually" },
|
||||
eq: { name:"EQ Preset Pack™", price:"$0.50/preset",micro:0.50, desc:"Shape your sound. Shape your spending.", color:"#d35400", dismiss:"Flat is a vibe" },
|
||||
bluetooth: { name:"Wireless Freedom Pass™", price:"$2.99/mo", micro:0.15, desc:"Cut the cord. Not the cost.", color:"#2980b9", dismiss:"Wires are retro" },
|
||||
like: { name:"Appreciation License™", price:"$1.99/mo", micro:0.25, desc:"Express feelings. For a fee.", color:"#e74c3c", dismiss:"I feel nothing" },
|
||||
hd: { name:"HD Audio™", price:"$3.99/mo", micro:0.20, desc:"Upgrade from 64kbps Suffering Quality™.", color:"#9b59b6", dismiss:"Lo-fi is a genre" },
|
||||
history: { name:"Recently Played™", price:"$2.49/mo", micro:0.15, desc:"We know what you listened to. Pay to find out too.", color:"#1abc9c", dismiss:"I'll forget" },
|
||||
unsub: { name:"Cancellation Processing™",price:"$4.99/unsub", micro:4.99, desc:"Freedom from commitment. Commitment from your wallet.", color:"#c0392b", dismiss:"I'll keep paying" },
|
||||
};
|
||||
|
||||
const LYRICS_DATA = [
|
||||
|
|
@ -88,13 +92,32 @@ const playChaChing = async () => {
|
|||
} catch(e) {}
|
||||
};
|
||||
|
||||
// Annoying jingle for the exit ad
|
||||
const playAdJingle = async () => {
|
||||
try {
|
||||
await Tone.start();
|
||||
const s = new Tone.Synth({
|
||||
oscillator: { type: "square" },
|
||||
envelope: { attack: 0.01, decay: 0.1, sustain: 0.3, release: 0.1 },
|
||||
volume: -15
|
||||
}).toDestination();
|
||||
const now = Tone.now();
|
||||
["C5","E5","G5","C6","G5","E5","C5"].forEach((n, i) =>
|
||||
s.triggerAttackRelease(n, "16n", now + i * 0.12)
|
||||
);
|
||||
setTimeout(() => s.dispose(), 2000);
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// COMPONENTS
|
||||
// ============================================================
|
||||
|
||||
function WelcomeBanner({ balance, onStart }) {
|
||||
const [fading, setFading] = useState(false);
|
||||
const dismiss = () => { setFading(true); setTimeout(onStart, 400); };
|
||||
return (
|
||||
<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.95)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:3000,backdropFilter:"blur(8px)"}}>
|
||||
<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.95)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:3000,backdropFilter:"blur(8px)",animation:fading?"fadeOut 0.4s ease forwards":"none"}}>
|
||||
<div style={{textAlign:"center",maxWidth:340,padding:"40px 28px"}}>
|
||||
<div style={{fontSize:48,marginBottom:16}}>🎵💸</div>
|
||||
<h1 style={{fontSize:36,fontWeight:900,fontFamily:"'Anybody',system-ui,sans-serif",color:"#fff",marginBottom:12,letterSpacing:-1}}>Pay2Play!</h1>
|
||||
|
|
@ -102,7 +125,7 @@ function WelcomeBanner({ balance, onStart }) {
|
|||
Every feature costs money. Pause, skip, volume — all paywalled.
|
||||
Your complimentary balance: <span style={{color:"#00ee44",fontWeight:700}}>${balance.toFixed(2)}</span>
|
||||
</p>
|
||||
<button onClick={onStart} style={{background:"linear-gradient(135deg,#00ee44,#00aa33)",color:"#000",border:"none",borderRadius:12,padding:"16px 36px",fontSize:16,fontWeight:700,cursor:"pointer",boxShadow:"0 4px 20px rgba(0,238,68,0.3)"}}>
|
||||
<button onClick={dismiss} style={{background:"linear-gradient(135deg,#00ee44,#00aa33)",color:"#000",border:"none",borderRadius:12,padding:"16px 36px",fontSize:16,fontWeight:700,cursor:"pointer",boxShadow:"0 4px 20px rgba(0,238,68,0.3)"}}>
|
||||
Start Spending
|
||||
</button>
|
||||
<div style={{fontSize:10,color:"#556677",marginTop:20}}>Part of donothireus.com</div>
|
||||
|
|
@ -153,6 +176,7 @@ function ExitAd({ onDone }) {
|
|||
const [adIdx, setAdIdx] = useState(() => Math.floor(Math.random() * AD_COPY.length));
|
||||
const onDoneR = useRef(onDone);
|
||||
onDoneR.current = onDone;
|
||||
useEffect(() => { playAdJingle(); }, []);
|
||||
useEffect(() => { if(c<=0){onDoneR.current();return;} const t=setTimeout(()=>setC(x=>x-1),1000); return()=>clearTimeout(t); }, [c]);
|
||||
useEffect(() => { const t=setInterval(()=>setAdIdx(i=>(i+1)%AD_COPY.length),3500); return()=>clearInterval(t); }, []);
|
||||
return (
|
||||
|
|
@ -202,6 +226,8 @@ function PayPlay() {
|
|||
const [welcome, setWelcome] = useState(true);
|
||||
const [bright, setBright] = useState(false);
|
||||
const [balKey, setBalKey] = useState(0);
|
||||
const [likes, setLikes] = useState(0);
|
||||
const [hdAudio, setHdAudio] = useState(false);
|
||||
|
||||
const synthR = useRef(null);
|
||||
const loopR = useRef(null);
|
||||
|
|
@ -209,6 +235,7 @@ function PayPlay() {
|
|||
const visT = useRef(null);
|
||||
const hitLimit = useRef(false);
|
||||
const audioR = useRef(null);
|
||||
const filterR = useRef(null);
|
||||
const milestoneR = useRef(new Set());
|
||||
|
||||
const song = SONGS[ci];
|
||||
|
|
@ -301,6 +328,7 @@ function PayPlay() {
|
|||
if (audioR.current) { audioR.current.pause(); audioR.current.currentTime = 0; audioR.current = null; }
|
||||
if (loopR.current) { loopR.current.stop(); loopR.current.dispose(); loopR.current = null; }
|
||||
if (synthR.current) { synthR.current.dispose(); synthR.current = null; }
|
||||
if (filterR.current) { filterR.current.dispose(); filterR.current = null; }
|
||||
try { Tone.getTransport().stop(); } catch(e) {}
|
||||
clearInterval(progT.current); clearInterval(visT.current);
|
||||
setBars(Array(16).fill(2));
|
||||
|
|
@ -327,9 +355,11 @@ function PayPlay() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Tone.js procedural synth
|
||||
// Tone.js procedural synth (low-pass filtered unless HD Audio purchased)
|
||||
await initTone();
|
||||
const synth = new Tone.Synth({ oscillator: { type: s.wave }, envelope: { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.8 }, volume: -30 + vol * 0.3 }).toDestination();
|
||||
const filter = new Tone.Filter(hdAudio ? 20000 : 1200, "lowpass").toDestination();
|
||||
filterR.current = filter;
|
||||
const synth = new Tone.Synth({ oscillator: { type: s.wave }, envelope: { attack: 0.05, decay: 0.2, sustain: 0.4, release: 0.8 }, volume: -30 + vol * 0.3 }).connect(filter);
|
||||
synthR.current = synth;
|
||||
const loop = new Tone.Loop(time => s.gen(synth, time), 2.5);
|
||||
loopR.current = loop;
|
||||
|
|
@ -338,7 +368,7 @@ function PayPlay() {
|
|||
let p = from;
|
||||
progT.current = setInterval(() => { p += 1; setProg(p); if (p >= s.duration) { clearInterval(progT.current); stopAudio(); setOn(false); setProg(0); } }, 1000);
|
||||
visT.current = setInterval(() => setBars(b => b.map(() => 2 + Math.random() * 28)), 150);
|
||||
}, [initTone, stopAudio, vol]);
|
||||
}, [initTone, stopAudio, vol, hdAudio]);
|
||||
|
||||
useEffect(() => () => stopAudio(), [stopAudio]);
|
||||
|
||||
|
|
@ -525,11 +555,15 @@ function PayPlay() {
|
|||
<span style={{fontSize:11,color:muted,minWidth:32}}>{vol}%</span>
|
||||
{!has("volume") && <span style={{fontSize:9,color:"#aa4444"}}>🔒 23-47%</span>}
|
||||
</div>
|
||||
{!hdAudio && !song.url && <div style={{textAlign:"center",fontSize:9,color:"#aa4444",marginTop:-16,marginBottom:8}}>🔊 64kbps Suffering Quality™ — Upgrade to HD Audio</div>}
|
||||
|
||||
{/* Feature buttons (responsive grid) */}
|
||||
<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(100px,1fr))",gap:8,maxWidth:420,padding:"0 20px",marginBottom:16,width:"100%"}}>
|
||||
{[
|
||||
{ l:"Lyrics", i:"📝", a:() => tryAct("lyrics", () => setLyr(x => !x)) },
|
||||
{ l:"HD Audio", i:"🔊", a:() => tryAct("hd", () => { setHdAudio(true); if(filterR.current) filterR.current.frequency.rampTo(20000, 0.5); flash("HD Audio enabled. Hear the difference? (There isn't one.)", "#9b59b6"); }) },
|
||||
{ l:"Like", i:"❤️", a:() => tryAct("like", () => { setLikes(l => l + 1); flash(`Liked! (${likes + 1} total). This does absolutely nothing.`, "#e74c3c"); }) },
|
||||
{ l:"History", i:"📜", a:() => tryAct("history", () => flash(`Recently played: "${song.title}". That's it. You're still on it.`, "#1abc9c")) },
|
||||
{ l:"EQ", i:"🎛️", a:() => tryAct("eq", () => flash("EQ: Flat ($0.50). Bass Boost: another $0.50. Each.", "#f39c12")) },
|
||||
{ l:"Bluetooth", i:"📡", a:() => tryAct("bluetooth", () => flash("Bluetooth connected... for now.", "#3498db")) },
|
||||
{ l:"Dark Mode", i:"🌙", a:() => tryAct("dark", () => { setBright(b => !b); flash(!bright ? "Welcome to Light Mode™. Suffering." : "Dark Mode restored. You paid for the default.", !bright ? "#f39c12" : "#00ee44"); }) },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue