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"); }) },
|
||||
|
|
|
|||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -13,6 +13,7 @@
|
|||
<meta name="twitter:image" content="https://donothireus.com/pay2play/og-card.png">
|
||||
<meta name="twitter:title" content="Pay2Play! — The Worst Music Player">
|
||||
<meta name="twitter:description" content="Every feature costs money. Even closing the app shows an unskippable ad.">
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<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 rel="stylesheet" href="style.css">
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0d0d1a"/>
|
||||
<stop offset="100%" stop-color="#1a1a3e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#00ee44"/>
|
||||
<stop offset="100%" stop-color="#00aa33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bar1" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#e74c3c88"/>
|
||||
<stop offset="100%" stop-color="#e74c3c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bar2" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#9b59b688"/>
|
||||
<stop offset="100%" stop-color="#9b59b6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bar3" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#3498db88"/>
|
||||
<stop offset="100%" stop-color="#3498db"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bar4" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stop-color="#f39c1288"/>
|
||||
<stop offset="100%" stop-color="#f39c12"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||
|
||||
<!-- Subtle grid pattern -->
|
||||
<g opacity="0.03" stroke="#fff" stroke-width="1">
|
||||
<line x1="0" y1="0" x2="1200" y2="630"/>
|
||||
<line x1="400" y1="0" x2="400" y2="630"/>
|
||||
<line x1="800" y1="0" x2="800" y2="630"/>
|
||||
<line x1="0" y1="210" x2="1200" y2="210"/>
|
||||
<line x1="0" y1="420" x2="1200" y2="420"/>
|
||||
</g>
|
||||
|
||||
<!-- Visualizer bars (left decorative cluster) -->
|
||||
<g transform="translate(80, 130)" opacity="0.7">
|
||||
<rect x="0" y="180" width="18" height="90" rx="4" fill="url(#bar1)"/>
|
||||
<rect x="24" y="120" width="18" height="150" rx="4" fill="url(#bar2)"/>
|
||||
<rect x="48" y="160" width="18" height="110" rx="4" fill="url(#bar3)"/>
|
||||
<rect x="72" y="80" width="18" height="190" rx="4" fill="url(#bar4)"/>
|
||||
<rect x="96" y="140" width="18" height="130" rx="4" fill="url(#bar1)"/>
|
||||
<rect x="120" y="60" width="18" height="210" rx="4" fill="url(#bar2)"/>
|
||||
<rect x="144" y="100" width="18" height="170" rx="4" fill="url(#bar3)"/>
|
||||
<rect x="168" y="150" width="18" height="120" rx="4" fill="url(#bar4)"/>
|
||||
</g>
|
||||
|
||||
<!-- Visualizer bars (right decorative cluster) -->
|
||||
<g transform="translate(930, 130)" opacity="0.7">
|
||||
<rect x="0" y="150" width="18" height="120" rx="4" fill="url(#bar4)"/>
|
||||
<rect x="24" y="100" width="18" height="170" rx="4" fill="url(#bar3)"/>
|
||||
<rect x="48" y="60" width="18" height="210" rx="4" fill="url(#bar2)"/>
|
||||
<rect x="72" y="140" width="18" height="130" rx="4" fill="url(#bar1)"/>
|
||||
<rect x="96" y="80" width="18" height="190" rx="4" fill="url(#bar4)"/>
|
||||
<rect x="120" y="160" width="18" height="110" rx="4" fill="url(#bar3)"/>
|
||||
<rect x="144" y="120" width="18" height="150" rx="4" fill="url(#bar2)"/>
|
||||
<rect x="168" y="180" width="18" height="90" rx="4" fill="url(#bar1)"/>
|
||||
</g>
|
||||
|
||||
<!-- Main title -->
|
||||
<text x="600" y="250" text-anchor="middle" font-family="'Anybody', 'Arial Black', sans-serif" font-weight="900" font-size="96" fill="#fff" letter-spacing="-2">Pay2Play!</text>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<text x="600" y="310" text-anchor="middle" font-family="'IBM Plex Mono', 'Courier New', monospace" font-size="24" fill="#7788aa">The Worst Music Player</text>
|
||||
|
||||
<!-- Feature tags -->
|
||||
<g transform="translate(600, 370)" text-anchor="middle" font-family="'IBM Plex Mono', 'Courier New', monospace" font-size="15" font-weight="600">
|
||||
<g transform="translate(-280, 0)">
|
||||
<rect x="-70" y="-16" width="140" height="32" rx="6" fill="#e74c3c22" stroke="#e74c3c44" stroke-width="1"/>
|
||||
<text fill="#e74c3c">Pause $0.01</text>
|
||||
</g>
|
||||
<g transform="translate(-95, 0)">
|
||||
<rect x="-78" y="-16" width="156" height="32" rx="6" fill="#e67e2222" stroke="#e67e2244" stroke-width="1"/>
|
||||
<text fill="#e67e22">Resume $0.02</text>
|
||||
</g>
|
||||
<g transform="translate(95, 0)">
|
||||
<rect x="-58" y="-16" width="116" height="32" rx="6" fill="#3498db22" stroke="#3498db44" stroke-width="1"/>
|
||||
<text fill="#3498db">Skip $0.05</text>
|
||||
</g>
|
||||
<g transform="translate(280, 0)">
|
||||
<rect x="-78" y="-16" width="156" height="32" rx="6" fill="#f39c1222" stroke="#f39c1244" stroke-width="1"/>
|
||||
<text fill="#f39c12">Volume $0.08</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Tagline -->
|
||||
<text x="600" y="440" text-anchor="middle" font-family="'IBM Plex Mono', 'Courier New', monospace" font-size="18" fill="#556677" font-style="italic">Every feature costs money. Even closing the app.</text>
|
||||
|
||||
<!-- Balance pill -->
|
||||
<g transform="translate(600, 500)">
|
||||
<rect x="-80" y="-18" width="160" height="36" rx="18" fill="#1c1c32"/>
|
||||
<text text-anchor="middle" y="6" font-family="'IBM Plex Mono', 'Courier New', monospace" font-size="16" font-weight="700" fill="url(#accent)">💰 $5.00</text>
|
||||
</g>
|
||||
|
||||
<!-- Bottom branding -->
|
||||
<text x="600" y="580" text-anchor="middle" font-family="'IBM Plex Mono', 'Courier New', monospace" font-size="13" fill="#334455" letter-spacing="3">DONOTHIREUS.COM</text>
|
||||
|
||||
<!-- Corner accent lines -->
|
||||
<line x1="40" y1="40" x2="120" y2="40" stroke="#00ee44" stroke-width="2" opacity="0.4"/>
|
||||
<line x1="40" y1="40" x2="40" y2="120" stroke="#00ee44" stroke-width="2" opacity="0.4"/>
|
||||
<line x1="1160" y1="590" x2="1080" y2="590" stroke="#00ee44" stroke-width="2" opacity="0.4"/>
|
||||
<line x1="1160" y1="590" x2="1160" y2="510" stroke="#00ee44" stroke-width="2" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
|
|
@ -10,6 +10,7 @@ body { background: #0d0d1a; font-family: 'IBM Plex Mono', monospace; }
|
|||
@keyframes balFlash { 0% { color:#00ee44 } 20% { color:#e74c3c } 50% { color:#ff6b6b } 100% { color:#00ee44 } }
|
||||
@keyframes countPulse { 0%,100% { transform:scale(1) } 50% { transform:scale(1.1) } }
|
||||
@keyframes successPop { 0% { transform:scale(0.5);opacity:0 } 50% { transform:scale(1.2) } 100% { transform:scale(1);opacity:1 } }
|
||||
@keyframes fadeOut { from { opacity:1 } to { opacity:0 } }
|
||||
|
||||
/* Range input — larger thumb for touch targets */
|
||||
input[type=range] { -webkit-appearance:none; appearance:none; height:6px; border-radius:3px; outline:none; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue