mirror of
https://github.com/SoPat712/maisie-heardle.git
synced 2025-08-21 18:28:45 -04:00
Add info abt myself, fix skip/play functionality, version bump
This commit is contained in:
@@ -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 SET‑UP ───────────────────────────────────────────────────────────
|
||||||
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: don’t fall through to default branch
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal user pause or end‑of‑snippet 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,9 +402,10 @@
|
|||||||
<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">
|
||||||
@@ -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
|
||||||
@@ -408,12 +440,37 @@
|
|||||||
<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"
|
||||||
@@ -429,9 +486,10 @@
|
|||||||
<!-- 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">
|
||||||
@@ -459,17 +517,22 @@
|
|||||||
{#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 _}
|
||||||
@@ -605,10 +668,7 @@
|
|||||||
{#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}
|
||||||
|
Reference in New Issue
Block a user