Add info abt myself, fix skip/play functionality, version bump

This commit is contained in:
Josh Patra
2025-04-20 20:02:47 -04:00
parent 29682adca2
commit fcb484204d

View File

@@ -1,3 +1,4 @@
<!-- src/routes/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import moment from 'moment'; import moment from 'moment';
@@ -138,10 +139,10 @@
let currentTrack = tracks[Math.floor(seededRandom() * tracks.length)]; let currentTrack = tracks[Math.floor(seededRandom() * tracks.length)];
// ─── SEGMENTS ─────────────────────────────────────────────────────────────── // ─── SEGMENTS ───────────────────────────────────────────────────────────────
const SEGMENT_INCREMENTS = [2, 1, 2, 3, 4, 5]; const SEGMENT_INCREMENTS = [2, 1, 2, 3, 4, 5]; // seconds
const segmentDurations = SEGMENT_INCREMENTS.reduce<number[]>((a, inc) => { const segmentDurations = SEGMENT_INCREMENTS.reduce<number[]>((acc, inc) => {
a.push((a.at(-1) ?? 0) + inc * 1000); acc.push((acc.at(-1) ?? 0) + inc * 1000);
return a; return acc;
}, []); }, []);
const TOTAL_MS = segmentDurations.at(-1)!; const TOTAL_MS = segmentDurations.at(-1)!;
const TOTAL_SECONDS = TOTAL_MS / 1000; const TOTAL_SECONDS = TOTAL_MS / 1000;
@@ -158,13 +159,17 @@
let iframeElement: HTMLIFrameElement; let iframeElement: HTMLIFrameElement;
let widget: any; let widget: any;
let widgetReady = false; let widgetReady = false;
let loading = true; // disable play until warmed up let loading = true;
let artworkUrl = ''; let artworkUrl = '';
let isPlaying = false; let isPlaying = false;
let currentPosition = 0; let currentPosition = 0;
let snippetTimeout: ReturnType<typeof setTimeout>;
let progressInterval: ReturnType<typeof setInterval>; let progressInterval: ReturnType<typeof setInterval>;
let fullDuration = 0; let fullDuration = 0;
/* ── NEW: guards the PAUSE handler during skips ── */
let skipInProgress = false;
let showHowTo = false; let showHowTo = false;
let showInfo = false; let showInfo = false;
let darkMode = let darkMode =
@@ -203,14 +208,13 @@
function formatTime(ms: number) { function formatTime(ms: number) {
const s = Math.floor(ms / 1000); const s = Math.floor(ms / 1000);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '00')}`; return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
} }
function startPolling() { function startPolling() {
isPlaying = true; isPlaying = true;
skipInProgress = false; // clear guard once new snippet starts
clearInterval(progressInterval); clearInterval(progressInterval);
// progress updater—fires reliably on mobile
progressInterval = setInterval(() => { progressInterval = setInterval(() => {
widget.getPosition((pos: number) => { widget.getPosition((pos: number) => {
currentPosition = pos; currentPosition = pos;
@@ -221,9 +225,12 @@
function stopAllTimers() { function stopAllTimers() {
isPlaying = false; isPlaying = false;
clearInterval(progressInterval); clearInterval(progressInterval);
clearTimeout(snippetTimeout);
} }
// ─── WIDGET SETUP ───────────────────────────────────────────────────────────
onMount(async () => { onMount(async () => {
// load SC API if missing
if (typeof window.SC === 'undefined') { if (typeof window.SC === 'undefined') {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const tag = document.createElement('script'); const tag = document.createElement('script');
@@ -243,12 +250,13 @@
countdownInterval = setInterval(updateTime, 1000); countdownInterval = setInterval(updateTime, 1000);
widget = SC.Widget(iframeElement); widget = SC.Widget(iframeElement);
// READY
widget.bind(SC.Widget.Events.READY, () => { widget.bind(SC.Widget.Events.READY, () => {
widget.getDuration((d: number) => (fullDuration = d)); widget.getDuration((d: number) => (fullDuration = d));
widget.getCurrentSound((sound: any) => { widget.getCurrentSound((sound: any) => {
artworkUrl = sound.artwork_url || ''; artworkUrl = sound.artwork_url || '';
}); });
// warm up // warm up
setTimeout(() => { setTimeout(() => {
widget.play(); widget.play();
@@ -259,19 +267,32 @@
}, 750); }, 750);
}); });
widget.bind(SC.Widget.Events.PAUSE, stopAllTimers); // PAUSE
widget.bind(SC.Widget.Events.FINISH, () => { widget.bind(SC.Widget.Events.PAUSE, () => {
/* Was this pause triggered by the Skip button? */
if (skipInProgress) {
stopAllTimers(); // clean up polling + timeouts
/* Immediately launch the next snippet */
playSegment(); // startPolling() in there will clear skipInProgress
return; // ← IMPORTANT: dont fall through to default branch
}
/* Normal user pause or endofsnippet pause */
stopAllTimers(); stopAllTimers();
}); });
// FINISH
widget.bind(SC.Widget.Events.FINISH, stopAllTimers);
// PLAY_PROGRESS
widget.bind(SC.Widget.Events.PLAY_PROGRESS, (e: { currentPosition: number }) => { widget.bind(SC.Widget.Events.PLAY_PROGRESS, (e: { currentPosition: number }) => {
if (!isPlaying) return; if (!isPlaying) return;
const limit = gameOver ? fullDuration : segmentDurations[attemptCount]; const limit = gameOver ? fullDuration : segmentDurations[attemptCount];
currentPosition = e.currentPosition; currentPosition = e.currentPosition;
if (e.currentPosition >= limit) { if (e.currentPosition >= limit) {
widget.pause(); widget.pause(); // will call PAUSE -> stopAllTimers
currentPosition = limit; currentPosition = limit;
stopAllTimers();
} }
}); });
}); });
@@ -279,14 +300,14 @@
onDestroy(() => { onDestroy(() => {
stopAllTimers(); stopAllTimers();
clearInterval(countdownInterval); clearInterval(countdownInterval);
if (widget?.unbind) { if (widget?.unbind) Object.values(SC.Widget.Events).forEach((ev) => widget.unbind(ev));
Object.values(SC.Widget.Events).forEach((ev) => widget.unbind(ev));
}
}); });
// ─── GAME ACTIONS ───────────────────────────────────────────────────────────
function playSegment() { function playSegment() {
if (!widgetReady || loading) return; if (!widgetReady || loading) return;
stopAllTimers(); stopAllTimers();
currentPosition = 0;
widget.seekTo(0); widget.seekTo(0);
widget.play(); widget.play();
startPolling(); startPolling();
@@ -303,15 +324,22 @@
function skipIntro() { function skipIntro() {
if (!widgetReady || gameOver) return; if (!widgetReady || gameOver) return;
skipInProgress = true; // guard the PAUSE handler
widget.pause(); // PAUSE event will handle restart
clearTimeout(snippetTimeout);
currentPosition = 0;
// record the skip
attemptInfos = [...attemptInfos, { status: 'skip' }]; attemptInfos = [...attemptInfos, { status: 'skip' }];
attemptCount++; attemptCount++;
userInput = ''; userInput = '';
selectedTrack = null; selectedTrack = null;
if (attemptCount >= maxAttempts) { if (attemptCount >= maxAttempts) {
revealAnswer(); revealAnswer();
} else {
playSegment();
} }
// no playSegment() call here PAUSE handler restarts next snippet
} }
function submitGuess() { function submitGuess() {
@@ -328,7 +356,7 @@
if (selectedTrack.title.toLowerCase() === ans) { if (selectedTrack.title.toLowerCase() === ans) {
attemptInfos = [...attemptInfos, { status: 'correct', title: currentTrack.title }]; attemptInfos = [...attemptInfos, { status: 'correct', title: currentTrack.title }];
gameOver = true; gameOver = true;
message = `✅ Correct! It was ${currentTrack.title}. You got it ${ message = `✅ Correct! It was ${currentTrack.title}. You got it ${
attemptCount === maxAttempts attemptCount === maxAttempts
? 'on the last try! Close one!' ? 'on the last try! Close one!'
: `in ${attemptCount} ${attemptCount === 1 ? 'try' : 'tries'}.` : `in ${attemptCount} ${attemptCount === 1 ? 'try' : 'tries'}.`
@@ -374,11 +402,12 @@
<div class="absolute inset-0 bg-black/40"></div> <div class="absolute inset-0 bg-black/40"></div>
<div <div
class="relative w-4/5 max-w-md rounded-lg p-8 text-center" class="relative w-4/5 max-w-md rounded-lg p-8 text-center"
style="background:{darkMode ? COLORS.text : COLORS.background};color:{darkMode style="
? COLORS.background background: {darkMode ? COLORS.text : COLORS.background};
: COLORS.text}" color: {darkMode ? COLORS.background : COLORS.text}
"
> >
<h2 class="mb-4 text-2xl font-bold uppercase" style="color:{COLORS.primary}">How to Play</h2> <h2 class="mb-4 text-2xl font-bold uppercase" style="color: {COLORS.primary}">How to Play</h2>
<ul class="space-y-4 text-base"> <ul class="space-y-4 text-base">
<li>🎵 Play the snippet.</li> <li>🎵 Play the snippet.</li>
<li>🔊 Skips & wrongs unlock more.</li> <li>🔊 Skips & wrongs unlock more.</li>
@@ -386,7 +415,10 @@
</ul> </ul>
<button <button
class="mt-6 rounded px-6 py-2 font-semibold" class="mt-6 rounded px-6 py-2 font-semibold"
style="background:{COLORS.primary};color:{darkMode ? COLORS.text : COLORS.background}" style="
background: {COLORS.primary};
color: {darkMode ? COLORS.text : COLORS.background}
"
on:click={() => (showHowTo = false)} on:click={() => (showHowTo = false)}
> >
Close Close
@@ -401,23 +433,48 @@
<div class="absolute inset-0 bg-black/40"></div> <div class="absolute inset-0 bg-black/40"></div>
<div <div
class="relative w-4/5 max-w-md space-y-4 rounded-lg p-8" class="relative w-4/5 max-w-md space-y-4 rounded-lg p-8"
style="background:{darkMode ? COLORS.text : COLORS.background};color:{darkMode style="background:{darkMode ? COLORS.text : COLORS.background}; color:{darkMode
? COLORS.background ? COLORS.background
: COLORS.text}" : COLORS.text}"
> >
<p class="font-semibold">{ARTIST_NAME} Test your knowledge!</p> <p class="font-semibold">{ARTIST_NAME} Test your knowledge!</p>
<p>All songs used are copyrighted and belong to {ARTIST_NAME}.</p> <p>All songs used are copyrighted and belong to {ARTIST_NAME}.</p>
<p>I do not, and never will, collect any of your personal data.</p> <p>I do not, and never will, collect any of your personal data.</p>
<hr class="my-4" style="border-color:{darkMode ? COLORS.background : COLORS.text}" /> <hr class="my-4" style="border-color:{darkMode ? COLORS.background : COLORS.text}" />
<p class="text-xs" style="color:{COLORS.accent}"> <p class="text-xs" style="color:{COLORS.accent}">
Prepared with SoundCloud, Svelte, Tailwind CSS, Inter font, svelte-hero-icons, and moment.js<br Prepared with SoundCloud, Svelte, Tailwind CSS, Inter font, svelte-hero-icons, and moment.js<br
/><br /> />
Game version: 3.0.0 Game version: 3.1.0
</p> </p>
<!-- Added links -->
<p class="text-xs">
<a
href="https://github.com/SoPat712/maisie-heardle"
target="_blank"
rel="noopener noreferrer"
class="underline hover:text-{COLORS.primary}"
>
View Source on GitHub
</a>
</p>
<p class="text-xs">
<a
href="https://joshpatra.me/"
target="_blank"
rel="noopener noreferrer"
class="underline hover:text-{COLORS.primary}"
>
My Portfolio
</a>
</p>
<p class="text-sm italic">New track in <strong>{timeLeft}</strong></p> <p class="text-sm italic">New track in <strong>{timeLeft}</strong></p>
<button <button
class="mt-4 rounded px-6 py-2 font-semibold" class="mt-4 rounded px-6 py-2 font-semibold"
style="background:{COLORS.primary};color:{darkMode ? COLORS.text : COLORS.background}" style="background:{COLORS.primary}; color:{darkMode ? COLORS.text : COLORS.background}"
on:click={() => (showInfo = false)} on:click={() => (showInfo = false)}
> >
Close Close
@@ -429,29 +486,30 @@
<!-- Main UI --> <!-- Main UI -->
<div <div
class="fixed inset-0 flex flex-col overflow-hidden" class="fixed inset-0 flex flex-col overflow-hidden"
style="background:{darkMode ? COLORS.text : COLORS.background};color:{darkMode style="
? COLORS.background background: {darkMode ? COLORS.text : COLORS.background};
: COLORS.text}" color: {darkMode ? COLORS.background : COLORS.text}
"
> >
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between px-4 pt-4"> <div class="flex items-center justify-between px-4 pt-4">
<div class="flex space-x-2"> <div class="flex space-x-2">
<button on:click={() => (showInfo = true)}> <button on:click={() => (showInfo = true)}>
<Icon src={InformationCircle} class="h-6 w-6" style="color:{COLORS.primary}" /> <Icon src={InformationCircle} class="h-6 w-6" style="color: {COLORS.primary}" />
</button> </button>
<button on:click={toggleDark}> <button on:click={toggleDark}>
<Icon src={darkMode ? Sun : Moon} class="h-6 w-6" style="color:{COLORS.primary}" /> <Icon src={darkMode ? Sun : Moon} class="h-6 w-6" style="color: {COLORS.primary}" />
</button> </button>
</div> </div>
<h1 class="flex-1 text-center font-serif text-lg font-bold whitespace-nowrap sm:text-3xl"> <h1 class="flex-1 text-center font-serif text-lg font-bold whitespace-nowrap sm:text-3xl">
Heardle {ARTIST_NAME} Heardle {ARTIST_NAME}
</h1> </h1>
<button on:click={() => (showHowTo = true)}> <button on:click={() => (showHowTo = true)}>
<Icon src={QuestionMarkCircle} class="h-6 w-6" style="color:{COLORS.secondary}" /> <Icon src={QuestionMarkCircle} class="h-6 w-6" style="color: {COLORS.secondary}" />
</button> </button>
</div> </div>
<hr class="mx-4 my-3" style="border-color:{darkMode ? COLORS.background : COLORS.text}" /> <hr class="mx-4 my-3" style="border-color: {darkMode ? COLORS.background : COLORS.text}" />
<!-- Attempts --> <!-- Attempts -->
{#if !gameOver} {#if !gameOver}
@@ -459,23 +517,28 @@
{#each attemptInfos as info} {#each attemptInfos as info}
<div <div
class="flex h-12 items-center rounded border px-3 font-semibold" class="flex h-12 items-center rounded border px-3 font-semibold"
style="border-color:{info.status === 'skip' style="
border-color: {info.status === 'skip'
? COLORS.primary ? COLORS.primary
: info.status === 'wrong' : info.status === 'wrong'
? COLORS.accent ? COLORS.accent
: COLORS.secondary};color:{info.status === 'skip' : COLORS.secondary};
color: {info.status === 'skip'
? COLORS.primary ? COLORS.primary
: info.status === 'wrong' : info.status === 'wrong'
? COLORS.accent ? COLORS.accent
: COLORS.secondary}" : COLORS.secondary}
"
> >
{#if info.status === 'skip'} Skipped{:else if info.status === 'wrong'}☒ {info.title}{:else}✓ {info.title}{/if} {#if info.status === 'skip'} Skipped
{:else if info.status === 'wrong'}☒ {info.title}
{:else}✓ {info.title}{/if}
</div> </div>
{/each} {/each}
{#each Array(maxAttempts - attemptInfos.length) as _} {#each Array(maxAttempts - attemptInfos.length) as _}
<div <div
class="h-12 rounded border" class="h-12 rounded border"
style="border-color:{darkMode ? COLORS.background : COLORS.text}" style="border-color: {darkMode ? COLORS.background : COLORS.text}"
></div> ></div>
{/each} {/each}
</div> </div>
@@ -489,7 +552,7 @@
target="_blank" target="_blank"
rel="noopener" rel="noopener"
class={`flex items-center overflow-hidden rounded-lg border-2 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`} class={`flex items-center overflow-hidden rounded-lg border-2 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
style="border-color:{COLORS.primary}" style="border-color: {COLORS.primary}"
> >
{#if artworkUrl} {#if artworkUrl}
<img <img
@@ -499,8 +562,8 @@
/> />
{/if} {/if}
<div class="px-4 py-2"> <div class="px-4 py-2">
<div class="font-semibold" style="color:{COLORS.primary}">{currentTrack.title}</div> <div class="font-semibold" style="color: {COLORS.primary}">{currentTrack.title}</div>
<div class="text-sm" style="color:{COLORS.accent}">{ARTIST_NAME}</div> <div class="text-sm" style="color: {COLORS.accent}">{ARTIST_NAME}</div>
</div> </div>
</a> </a>
<p class="mt-4 text-center font-medium">{message}</p> <p class="mt-4 text-center font-medium">{message}</p>
@@ -521,17 +584,17 @@
<!-- Progress bar --> <!-- Progress bar -->
<div <div
class="relative mb-2 w-full overflow-hidden rounded border" class="relative mb-2 w-full overflow-hidden rounded border"
style="height:1.25rem;border-color:{darkMode ? COLORS.background : COLORS.text}" style="height:1.25rem; border-color: {darkMode ? COLORS.background : COLORS.text}"
> >
<div <div
class="absolute top-0 left-0 h-full transition-[width] duration-100" class="absolute top-0 left-0 h-full transition-[width] duration-100"
style="width:{fillPercent}%;background:{COLORS.accent}" style="width: {fillPercent}%; background: {COLORS.accent}"
></div> ></div>
{#if !gameOver} {#if !gameOver}
{#each boundaries as b} {#each boundaries as b}
<div <div
class="absolute top-0 bottom-0" class="absolute top-0 bottom-0"
style="left:{(b / TOTAL_SECONDS) * 100}%;border-left:1px solid {darkMode style="left: {(b / TOTAL_SECONDS) * 100}%; border-left:1px solid {darkMode
? COLORS.background ? COLORS.background
: COLORS.text}" : COLORS.text}"
></div> ></div>
@@ -548,13 +611,13 @@
<button <button
on:click={togglePlayPause} on:click={togglePlayPause}
class="flex h-16 w-16 items-center justify-center rounded-full border-2 disabled:opacity-50" class="flex h-16 w-16 items-center justify-center rounded-full border-2 disabled:opacity-50"
style="border-color:{loading ? '#888888' : COLORS.accent}" style="border-color: {loading ? '#888888' : COLORS.accent}"
disabled={loading} disabled={loading}
> >
<Icon <Icon
src={isPlaying ? Pause : Play} src={isPlaying ? Pause : Play}
class="h-8 w-8" class="h-8 w-8"
style="color:{loading ? '#888888' : COLORS.accent}" style="color: {loading ? '#888888' : COLORS.accent}"
/> />
</button> </button>
</div> </div>
@@ -570,14 +633,14 @@
on:keydown={onInputKeydown} on:keydown={onInputKeydown}
on:focus={() => (selectedTrack = null)} on:focus={() => (selectedTrack = null)}
class="w-full rounded border px-3 py-2" class="w-full rounded border px-3 py-2"
style="border-color:{COLORS.primary};background:{darkMode style="border-color: {COLORS.primary}; background: {darkMode
? COLORS.text ? COLORS.text
: COLORS.background};color:{darkMode ? COLORS.background : COLORS.text}" : COLORS.background}; color: {darkMode ? COLORS.background : COLORS.text}"
/> />
{#if suggestions.length} {#if suggestions.length}
<ul <ul
class="absolute bottom-full left-0 z-10 mb-1 max-h-36 w-full overflow-y-auto rounded border" class="absolute bottom-full left-0 z-10 mb-1 max-h-36 w-full overflow-y-auto rounded border"
style="border-color:{darkMode ? COLORS.background : COLORS.text};background:{darkMode style="border-color: {darkMode ? COLORS.background : COLORS.text}; background: {darkMode
? COLORS.text ? COLORS.text
: COLORS.background}" : COLORS.background}"
> >
@@ -586,7 +649,7 @@
<button <button
type="button" type="button"
class="w-full px-3 py-2 text-left" class="w-full px-3 py-2 text-left"
style="color:{darkMode ? COLORS.background : COLORS.text}" style="color: {darkMode ? COLORS.background : COLORS.text}"
on:click={() => selectSuggestion(s)} on:click={() => selectSuggestion(s)}
> >
{s.title}  <span style="opacity:0.7">{s.artist}</span> {s.title}  <span style="opacity:0.7">{s.artist}</span>
@@ -600,17 +663,14 @@
<button <button
on:click={skipIntro} on:click={skipIntro}
class="rounded px-4 py-2 font-semibold" class="rounded px-4 py-2 font-semibold"
style="background:{COLORS.primary};color:{COLORS.background}" style="background: {COLORS.primary}; color: {COLORS.background}"
> >
{#if nextIncrementSec > 0}Skip (+{nextIncrementSec}s){:else}I don't know it{/if} {#if nextIncrementSec > 0}Skip (+{nextIncrementSec}s){:else}I don't know it{/if}
</button> </button>
<button <button
on:click={() => { on:click={submitGuess}
submitGuess();
// togglePlayPause();
}}
class="rounded px-4 py-2 font-semibold" class="rounded px-4 py-2 font-semibold"
style="background:{COLORS.secondary};color:{COLORS.background}" style="background: {COLORS.secondary}; color: {COLORS.background}"
disabled={!userInput} disabled={!userInput}
> >
Submit Submit