mirror of
https://github.com/SoPat712/maisie-heardle.git
synced 2025-08-21 10:18:45 -04:00
Base app in working condition
This commit is contained in:
83
package-lock.json
generated
83
package-lock.json
generated
@@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "maisie-heardle",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
@@ -23,6 +26,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-hero-icons": "^5.2.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
@@ -1101,6 +1105,13 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@steeze-ui/heroicons": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@steeze-ui/heroicons/-/heroicons-2.4.2.tgz",
|
||||
"integrity": "sha512-66luL+uaxyC6mcZigewH4phfDxNWj4sH+n6qK2VnY3zcgpMmNAgVQbMGfZYfKhLqrUo13BlqpmhWuHqAUpehlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
@@ -2964,6 +2975,19 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -2977,6 +3001,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -3121,13 +3154,13 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
@@ -3702,6 +3735,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-hero-icons": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hero-icons/-/svelte-hero-icons-5.2.0.tgz",
|
||||
"integrity": "sha512-KpdMTL0bOnkxciEmDXvyVF/R5nrZ1x1uHCSt9gMrrbEd3g5HSIaaDChOutTOfeI+cZ3EJbb+OcBH/lBzJr1aEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@steeze-ui/heroicons": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||
@@ -3736,19 +3785,6 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -4393,19 +4429,6 @@
|
||||
"@esbuild/win32-x64": "0.25.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
|
||||
|
@@ -29,9 +29,13 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-hero-icons": "^5.2.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.30.1"
|
||||
}
|
||||
}
|
||||
|
10
src/app.css
10
src/app.css
@@ -1 +1,11 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
|
||||
@import 'tailwindcss';
|
||||
/* load any custom fonts here, e.g.: */
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
@@ -2,9 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<!-- SoundCloud Widget API -->
|
||||
<script src="https://w.soundcloud.com/player/api.js"></script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
499
src/lib/HeardleGame.svelte
Normal file
499
src/lib/HeardleGame.svelte
Normal file
@@ -0,0 +1,499 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import moment from 'moment';
|
||||
import { Icon, Play, Pause, InformationCircle, QuestionMarkCircle } from 'svelte-hero-icons';
|
||||
declare const SC: any;
|
||||
|
||||
// ─── CONFIG & THEME ───────────────────────────────────────────────────────────
|
||||
const ARTIST_NAME = 'Maisie Peters';
|
||||
const SC_USER = 'maisie-peters';
|
||||
|
||||
const COLORS = {
|
||||
background: '#ffffff',
|
||||
text: '#050315',
|
||||
primary: '#83D4FD',
|
||||
secondary: '#F484C1',
|
||||
accent: '#F0440E'
|
||||
};
|
||||
|
||||
// ─── TRACK LIST ───────────────────────────────────────────────────────────────
|
||||
type TrackData = { title: string; slug: string };
|
||||
const TRACKS_DATA: TrackData[] = [
|
||||
{ title: 'Holy Revival', slug: 'holy-revival' },
|
||||
{ title: 'The Song', slug: 'the-song' },
|
||||
{ title: 'Guy On A Horse', slug: 'guy-on-a-horse' },
|
||||
{ title: 'The Last One', slug: 'the-last-one' },
|
||||
{ title: 'Yoko', slug: 'yoko' },
|
||||
{ title: 'Truth Is', slug: 'truth-is' },
|
||||
// { title: "There It Goes (Acoustic)", slug: "there-it-goes-acoustic" },
|
||||
{ title: 'The Band And I', slug: 'the-band-and-i' },
|
||||
{ title: 'There It Goes', slug: 'there-it-goes' },
|
||||
{ title: 'Watch', slug: 'watch' },
|
||||
{ title: 'History Of Man', slug: 'history-of-man' },
|
||||
{ title: 'You’re Just A Boy (And I’m Kinda The Man)', slug: 'youre-just-a-boy-and-im-kinda' },
|
||||
{ title: 'The Good Witch', slug: 'the-good-witch' },
|
||||
{ title: 'Want You Back', slug: 'want-you-back' },
|
||||
{ title: 'BSC', slug: 'bsc' },
|
||||
{ title: 'Run', slug: 'run' },
|
||||
{ title: 'Wendy', slug: 'wendy' },
|
||||
{ title: 'Coming Of Age', slug: 'coming-of-age' },
|
||||
{ title: 'Therapy', slug: 'therapy' },
|
||||
// { title: "Lost The Breakup (Acoustic)", slug: "lost-the-breakup-acoustic" },
|
||||
// { title: "Lost The Breakup (The Wild Remix)", slug: "lost-the-breakup-the-wild" },
|
||||
{ title: 'Two Weeks Ago', slug: 'two-weeks-ago' },
|
||||
{ title: 'Lost The Breakup', slug: 'lost-the-breakup' },
|
||||
{ title: 'Lost The Breakup', slug: 'lost-the-breakup-1' },
|
||||
// { title: "Body Better (Acapella)", slug: "body-better-acapella-acapella" },
|
||||
// { title: "Body Better (Acoustic)", slug: "body-better-acoustic-acoustic" },
|
||||
{ title: 'Body Better', slug: 'body-better' },
|
||||
{ title: 'Together This Christmas', slug: 'together-this-christmas' },
|
||||
{ title: 'Not Another Rockstar', slug: 'not-another-rockstar' },
|
||||
{ title: 'Good Enough', slug: 'good-enough' },
|
||||
{ title: 'Blonde', slug: 'blonde' },
|
||||
// { title: "Cate’s Brother (BRELAND's Version)", slug: "cates-brother-brelands-version" },
|
||||
// { title: "Cate’s Brother (Matt's Version)", slug: "cates-brother-matts-version" },
|
||||
{ title: 'Cate’s Brother', slug: 'cates-brother' },
|
||||
{ title: 'I’m Trying (Not Friends)', slug: 'im-trying-not-friends' },
|
||||
{ title: 'Villain', slug: 'villain' },
|
||||
{ title: 'Elvis Song', slug: 'elvis-song' },
|
||||
{ title: 'Talking To Strangers', slug: 'talking-to-strangers' },
|
||||
{ title: 'Love Him I Don’t', slug: 'love-him-i-dont' },
|
||||
{ title: 'Outdoor Pool', slug: 'outdoor-pool' },
|
||||
{ title: 'Boy', slug: 'boy' },
|
||||
{ title: 'Hollow', slug: 'hollow' },
|
||||
{ title: 'Tough Act', slug: 'tough-act' },
|
||||
{ title: 'Volcano', slug: 'volcano' },
|
||||
{ title: 'Brooklyn', slug: 'brooklyn' },
|
||||
{ title: 'You Signed Up For This', slug: 'you-signed-up-for-this' },
|
||||
// { title: "Psycho (Danny L Harle Remix)", slug: "psycho-danny-l-harle-remix" },
|
||||
// { title: "Psycho (Acoustic)", slug: "psycho-acoustic" },
|
||||
// { title: "Psycho (Joel Corry Remix)", slug: "psycho-joel-corry-remix" },
|
||||
{ title: 'Psycho', slug: 'psycho' },
|
||||
{ title: 'The Party', slug: 'the-party' },
|
||||
{ title: 'Lunar Years', slug: 'lunar-years' },
|
||||
{ title: 'Happy Hunting Ground (feat. Griff)', slug: 'happy-hunting-ground-feat' },
|
||||
{ title: 'Helicopter', slug: 'helicopter' },
|
||||
{ title: 'Milhouse', slug: 'milhouse' },
|
||||
{ title: 'Glowing Review', slug: 'glowing-review' },
|
||||
// { title: "I Want You To Change (Because You Want To Change) [feat. Bear's Den]", slug: "i-want-you-to-change-because" },
|
||||
{ title: 'Neck Of The Woods', slug: 'neck-of-the-woods' },
|
||||
{ title: 'Funeral (feat. James Bay)', slug: 'funeral-feat-james-bay' },
|
||||
// { title: "John Hughes Movie (Acoustic)", slug: "john-hughes-movie-acoustic" },
|
||||
// { title: "John Hughes Movie (Oliver Nelson Remix)", slug: "john-hughes-movie-oliver" },
|
||||
{ title: 'John Hughes Movie', slug: 'john-hughes' },
|
||||
// { title: "Maybe Don't (feat. JP Saxe) [Acoustic]", slug: "maybe-dont-feat-jp-saxe-1" },
|
||||
// { title: "Maybe Don't (feat. JP Saxe) [MOTi Remix]", slug: "maybe-dont-feat-jp-saxe-moti" },
|
||||
// { title: "Maybe Don't (feat. JP Saxe) [HONNE Remix]", slug: "maybe-dont-feat-jp-saxe-honne" },
|
||||
{ title: "Maybe Don't (feat. JP Saxe)", slug: 'maybe-dont-feat-jp-saxe' },
|
||||
// { title: "Sad Girl Summer (Cavetown Rework)", slug: "sad-girl-summer-cavetown" },
|
||||
// { title: "Sad Girl Summer (emo version)", slug: "sad-girl-summer-emo-version" },
|
||||
{ title: 'Sad Girl Summer', slug: 'sad-girl-summer' },
|
||||
{ title: 'The List', slug: 'the-list' },
|
||||
{ title: 'Daydreams', slug: 'daydreams' },
|
||||
// { title: "Take Care of Yourself (Live Acoustic)", slug: "take-care-of-yourself-acoustic" },
|
||||
// { title: "Adore You (Breydon Beggs Remix)", slug: "adore-you-breydon-beggs-remix" },
|
||||
// { title: "Adore You (Acoustic)", slug: "adore-you-acoustic" },
|
||||
// { title: "This Is On You (Acoustic)", slug: "this-is-on-you-acoustic" },
|
||||
{ title: 'Look At Me Now', slug: 'look-at-me-now-1' },
|
||||
{ title: 'Take Care Of Yourself', slug: 'take-care-of-yourself-1' },
|
||||
{ title: 'Personal Best', slug: 'personal-best-1' },
|
||||
{ title: 'April Showers', slug: 'april-showers' },
|
||||
{ title: 'Adore You', slug: 'adore-you' },
|
||||
{ title: 'This Is On You', slug: 'this-is-on-you-1' },
|
||||
{ title: 'This Is On You', slug: 'this-is-on-you' },
|
||||
{ title: 'Favourite Ex', slug: 'favourite-ex' },
|
||||
// { title: "Stay Young (Acoustic)", slug: "stay-young-acoustic" },
|
||||
{ title: 'Stay Young', slug: 'stay-young' },
|
||||
{ title: 'Enough For You', slug: 'enough-for-you' },
|
||||
{ title: 'You To You', slug: 'you-to-you' },
|
||||
{ title: 'Architecture', slug: 'architecture-1' },
|
||||
{ title: 'Feels Like This', slug: 'feels-like-this' },
|
||||
{ title: 'Details', slug: 'details' },
|
||||
{ title: 'In My Head', slug: 'in-my-head' },
|
||||
{ title: "Best I'll Ever Sing", slug: 'best-ill-ever-sing-1' },
|
||||
{ title: 'Worst of You', slug: 'worst-of-you' }
|
||||
];
|
||||
|
||||
type Track = { title: string; artist: string; url: string };
|
||||
const tracks: Track[] = TRACKS_DATA.map(({ title, slug }) => ({
|
||||
title,
|
||||
artist: ARTIST_NAME,
|
||||
url: `https://soundcloud.com/${SC_USER}/${slug}`
|
||||
}));
|
||||
|
||||
// ─── DAILY‐SEEDED RANDOM TRACK ───────────────────────────────────────────────
|
||||
let seed = parseInt(moment().format('YYYYMMDD'), 10);
|
||||
function seededRandom() {
|
||||
seed = (seed * 9301 + 49297) % 233280;
|
||||
return seed / 233280;
|
||||
}
|
||||
let currentTrack: Track = tracks[Math.floor(seededRandom() * tracks.length)];
|
||||
|
||||
// ─── SEGMENTS ───────────────────────────────────────────────────────────────
|
||||
const SEGMENT_INCREMENTS = [2, 1, 2, 3, 4, 5];
|
||||
const segmentDurations = SEGMENT_INCREMENTS.reduce<number[]>((acc, inc) => {
|
||||
acc.push((acc.at(-1) ?? 0) + inc * 1000);
|
||||
return acc;
|
||||
}, []);
|
||||
const TOTAL_MS = segmentDurations.at(-1)!;
|
||||
const TOTAL_SECONDS = TOTAL_MS / 1000;
|
||||
const maxAttempts = SEGMENT_INCREMENTS.length;
|
||||
$: boundaries = segmentDurations.map((ms) => ms / 1000).slice(0, -1);
|
||||
|
||||
// ─── GAME STATE ─────────────────────────────────────────────────────────────
|
||||
type Info = { status: 'skip' | 'wrong' | 'correct'; title?: string };
|
||||
let attemptInfos: Info[] = [];
|
||||
let attemptCount = 0;
|
||||
let gameOver = false;
|
||||
let message = '';
|
||||
|
||||
let iframeElement: HTMLIFrameElement;
|
||||
let widget: any;
|
||||
let widgetReady = false;
|
||||
let isPlaying = false;
|
||||
let currentPosition = 0;
|
||||
|
||||
let showHowTo = false;
|
||||
let showInfo = false;
|
||||
let userInput = '';
|
||||
let suggestions: Track[] = [];
|
||||
let selectedTrack: Track | null = null;
|
||||
let inputEl: HTMLInputElement;
|
||||
|
||||
$: suggestions = userInput
|
||||
? tracks.filter((t) => t.title.toLowerCase().includes(userInput.toLowerCase())).slice(0, 5)
|
||||
: [];
|
||||
|
||||
$: fillPercent = (currentPosition / TOTAL_MS) * 100;
|
||||
$: nextIncrementSec =
|
||||
attemptCount < SEGMENT_INCREMENTS.length - 1 ? SEGMENT_INCREMENTS[attemptCount + 1] : 0;
|
||||
|
||||
function formatTime(ms: number) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
widget = SC.Widget(iframeElement);
|
||||
widget.bind(SC.Widget.Events.READY, () => (widgetReady = true));
|
||||
|
||||
// precise snippet cutoff in PLAY_PROGRESS:
|
||||
widget.bind(SC.Widget.Events.PLAY_PROGRESS, (e: { currentPosition: number }) => {
|
||||
const limit = segmentDurations[attemptCount];
|
||||
if (e.currentPosition >= limit) {
|
||||
currentPosition = limit;
|
||||
widget.pause();
|
||||
isPlaying = false;
|
||||
} else {
|
||||
currentPosition = e.currentPosition;
|
||||
}
|
||||
});
|
||||
|
||||
widget.bind(SC.Widget.Events.PLAY, () => (isPlaying = true));
|
||||
widget.bind(SC.Widget.Events.PAUSE, () => (isPlaying = false));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
widget?.unbind && Object.values(SC.Widget.Events).forEach((ev: string) => widget.unbind(ev));
|
||||
});
|
||||
|
||||
function playSegment() {
|
||||
if (!widgetReady || gameOver) return;
|
||||
const limit = segmentDurations[attemptCount];
|
||||
currentPosition = 0;
|
||||
widget.seekTo(0);
|
||||
widget.play();
|
||||
}
|
||||
|
||||
function togglePlayPause() {
|
||||
if (!widgetReady || gameOver) return;
|
||||
isPlaying ? widget.pause() : playSegment();
|
||||
}
|
||||
|
||||
function skipIntro() {
|
||||
if (!widgetReady || gameOver) return;
|
||||
attemptInfos = [...attemptInfos, { status: 'skip' }];
|
||||
attemptCount++;
|
||||
userInput = '';
|
||||
selectedTrack = null;
|
||||
if (attemptCount >= maxAttempts) revealAnswer();
|
||||
else playSegment();
|
||||
}
|
||||
|
||||
function submitGuess() {
|
||||
if (!widgetReady || !selectedTrack || gameOver) return;
|
||||
attemptCount++;
|
||||
const ans = currentTrack.title.toLowerCase();
|
||||
if (selectedTrack.title.toLowerCase() === ans) {
|
||||
attemptInfos = [...attemptInfos, { status: 'correct', title: currentTrack.title }];
|
||||
gameOver = true;
|
||||
message = `✅ Correct! It was “${currentTrack.title}.”`;
|
||||
widget.pause();
|
||||
} else {
|
||||
attemptInfos = [...attemptInfos, { status: 'wrong', title: selectedTrack.title }];
|
||||
userInput = '';
|
||||
selectedTrack = null;
|
||||
if (attemptCount >= maxAttempts) revealAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
function revealAnswer() {
|
||||
gameOver = true;
|
||||
message = `❌ Out of tries! It was “${currentTrack.title}.”`;
|
||||
widget.pause();
|
||||
}
|
||||
|
||||
function onInputKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !gameOver) {
|
||||
e.preventDefault();
|
||||
if (!selectedTrack && suggestions.length) {
|
||||
selectedTrack =
|
||||
suggestions.find((t) => t.title.toLowerCase() === userInput.toLowerCase()) ||
|
||||
suggestions[0];
|
||||
}
|
||||
if (selectedTrack) submitGuess();
|
||||
}
|
||||
}
|
||||
|
||||
function selectSuggestion(s: Track) {
|
||||
selectedTrack = s;
|
||||
userInput = s.title;
|
||||
suggestions = [];
|
||||
inputEl.blur();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- How to Play Modal -->
|
||||
{#if showHowTo}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0" style="background: rgba(0,0,0,0.4)"></div>
|
||||
<div
|
||||
class="relative w-4/5 max-w-md rounded-lg p-8 text-center"
|
||||
style="background: {COLORS.background}"
|
||||
>
|
||||
<h2
|
||||
class="mb-4 text-2xl font-bold uppercase"
|
||||
style="color: {COLORS.primary}; font-family: 'Inter', sans-serif"
|
||||
>
|
||||
How to Play
|
||||
</h2>
|
||||
<ul class="space-y-4 text-base" style="color: {COLORS.text}">
|
||||
<li>🎵 Play the snippet.</li>
|
||||
<li>🔊 Skips & wrongs unlock more.</li>
|
||||
<li>👍 Guess in as few tries as possible!</li>
|
||||
</ul>
|
||||
<div class="mt-6 flex justify-center">
|
||||
<button
|
||||
class="rounded px-6 py-2 font-semibold"
|
||||
style="background: {COLORS.primary}; color: {COLORS.background}"
|
||||
on:click={() => (showHowTo = false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Info Modal -->
|
||||
{#if showInfo}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0" style="background: rgba(0,0,0,0.4)"></div>
|
||||
<div
|
||||
class="relative w-4/5 max-w-md space-y-4 rounded-lg p-8"
|
||||
style="background: {COLORS.background}; color: {COLORS.text}"
|
||||
>
|
||||
<p class="text-base font-semibold">
|
||||
{ARTIST_NAME} – Test your {ARTIST_NAME} knowledge!
|
||||
</p>
|
||||
<p class="text-base">
|
||||
All songs used are copyrighted and belong to {ARTIST_NAME}.
|
||||
</p>
|
||||
<hr style="border-color: {COLORS.text}" class="my-4" />
|
||||
<p class="text-xs" style="color: {COLORS.accent}">
|
||||
Prepared with SoundCloud, Svelte, Tailwind, Inter font,<br />
|
||||
svelte‑hero‑icons
|
||||
</p>
|
||||
<p class="text-xs" style="color: {COLORS.accent}">Game version: 1.0.0</p>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<button
|
||||
class="rounded px-6 py-2 font-semibold"
|
||||
style="background: {COLORS.primary}; color: {COLORS.background}"
|
||||
on:click={() => (showInfo = false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Main UI anchored to viewport -->
|
||||
<div
|
||||
class="fixed inset-0 flex flex-col overflow-hidden"
|
||||
style="
|
||||
background: {COLORS.background};
|
||||
color: {COLORS.text};
|
||||
font-family: 'Inter', sans-serif;
|
||||
"
|
||||
>
|
||||
<!-- header -->
|
||||
<div class="flex items-center justify-between px-4 pt-4">
|
||||
<button on:click={() => (showInfo = true)}>
|
||||
<Icon src={InformationCircle} class="h-6 w-6" style="color: {COLORS.primary}" />
|
||||
</button>
|
||||
<h1 class="flex-grow text-center font-serif text-3xl font-bold">
|
||||
Heardle – {ARTIST_NAME}
|
||||
</h1>
|
||||
<button on:click={() => (showHowTo = true)}>
|
||||
<Icon src={QuestionMarkCircle} class="h-6 w-6" style="color: {COLORS.accent}" />
|
||||
</button>
|
||||
</div>
|
||||
<hr class="mx-4 my-3" style="border-color: {COLORS.text}" />
|
||||
|
||||
<!-- attempts -->
|
||||
<div class="mb-6 flex-shrink-0 space-y-2 px-4">
|
||||
{#each attemptInfos as info}
|
||||
<div
|
||||
class="flex h-12 items-center rounded border px-3 font-semibold"
|
||||
style="
|
||||
border-color:
|
||||
{info.status === 'skip'
|
||||
? COLORS.primary
|
||||
: info.status === 'wrong'
|
||||
? COLORS.accent
|
||||
: COLORS.secondary};
|
||||
color:
|
||||
{info.status === 'skip'
|
||||
? COLORS.primary
|
||||
: info.status === 'wrong'
|
||||
? COLORS.accent
|
||||
: COLORS.secondary};
|
||||
"
|
||||
>
|
||||
{#if info.status === 'skip'}▢ Skipped
|
||||
{:else if info.status === 'wrong'}☒ {info.title}
|
||||
{:else}✓ {info.title}{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#each Array(maxAttempts - attemptInfos.length) as _}
|
||||
<div class="h-12 rounded border" style="border-color: {COLORS.text}"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
bind:this={iframeElement}
|
||||
class="hidden"
|
||||
src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(currentTrack.url)}`}
|
||||
allow="autoplay"
|
||||
title="preview player"
|
||||
></iframe>
|
||||
|
||||
<!-- hollow progress bar -->
|
||||
<div class="mb-4 flex-shrink-0 px-4">
|
||||
<div
|
||||
class="relative w-full overflow-hidden rounded border"
|
||||
style="height: 1.25rem; border-color: {COLORS.text}"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 left-0 h-full"
|
||||
style="width: {fillPercent}%; background: {COLORS.primary}; transition: width 0.1s;"
|
||||
></div>
|
||||
{#each boundaries as b}
|
||||
<div
|
||||
class="absolute top-0 bottom-0"
|
||||
style="
|
||||
left: {(b / TOTAL_SECONDS) * 100}%;
|
||||
border-left: 1px solid {COLORS.text};
|
||||
"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-xs">
|
||||
<span>{formatTime(currentPosition)}</span>
|
||||
<span>{formatTime(TOTAL_MS)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- play/pause -->
|
||||
<div class="mb-4 flex justify-center">
|
||||
<button
|
||||
on:click={togglePlayPause}
|
||||
class="flex h-16 w-16 items-center justify-center rounded-full border-2 disabled:opacity-50"
|
||||
style="border-color: {COLORS.primary}"
|
||||
>
|
||||
<Icon src={isPlaying ? Pause : Play} class="h-8 w-8" style="color: {COLORS.primary}" solid />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- guess & skip/submit or final -->
|
||||
{#if !gameOver}
|
||||
<div class="mb-4 flex-shrink-0 px-4">
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="text"
|
||||
placeholder="Type song title…"
|
||||
bind:value={userInput}
|
||||
on:keydown={onInputKeydown}
|
||||
class="w-full rounded border px-3 py-2"
|
||||
style="
|
||||
border-color: {COLORS.primary};
|
||||
background: {COLORS.background};
|
||||
color: {COLORS.text};
|
||||
"
|
||||
/>
|
||||
{#if suggestions.length}
|
||||
<ul
|
||||
class="mt-1 max-h-36 overflow-y-auto rounded border"
|
||||
style="
|
||||
border-color: {COLORS.text};
|
||||
background: {COLORS.background};
|
||||
"
|
||||
>
|
||||
{#each suggestions as s}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left"
|
||||
style="color: {COLORS.text}"
|
||||
on:click={() => selectSuggestion(s)}
|
||||
>
|
||||
{s.title} – <span style="opacity: 0.7">{s.artist}</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-6 flex justify-between px-4">
|
||||
<button
|
||||
on:click={skipIntro}
|
||||
class="rounded px-4 py-2 font-semibold"
|
||||
style="background: {COLORS.primary}; color: {COLORS.background}"
|
||||
>
|
||||
{#if nextIncrementSec > 0}
|
||||
Skip (+{nextIncrementSec}s)
|
||||
{:else}
|
||||
I don't know it
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
on:click={submitGuess}
|
||||
class="rounded px-4 py-2 font-semibold"
|
||||
style="background: {COLORS.accent}; color: {COLORS.background}"
|
||||
disabled={!selectedTrack}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-6 text-center text-lg" style="color: {COLORS.text}">
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Tailwind in app.css handles all spacing/layout */
|
||||
</style>
|
@@ -1 +1,2 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
// src/lib/index.ts
|
||||
export { default as HeardleGame } from './HeardleGame.svelte';
|
||||
|
@@ -1,7 +1,13 @@
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
<svelte:head>
|
||||
<title>Heardle – Maisie Peters Edition</title>
|
||||
</svelte:head>
|
||||
|
||||
<!-- Remove bg-black/text-white here so your game can supply its own background -->
|
||||
<div class="min-h-screen bg-transparent antialiased">
|
||||
<slot />
|
||||
</div>
|
||||
|
@@ -1,2 +1,5 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script lang="ts">
|
||||
import HeardleGame from '$lib/HeardleGame.svelte';
|
||||
</script>
|
||||
|
||||
<HeardleGame />
|
||||
|
BIN
static/favicon-dark.ico
Normal file
BIN
static/favicon-dark.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
static/favicon-dark.png
Normal file
BIN
static/favicon-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 34 KiB |
10
tailwind.config.cjs
Normal file
10
tailwind.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,svelte,ts,js}'],
|
||||
theme: {
|
||||
extend: {
|
||||
// put your custom colors, spacing, etc. here
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
@@ -1,7 +1,7 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()]
|
||||
plugins: [sveltekit(), tailwindcss()]
|
||||
});
|
||||
|
Reference in New Issue
Block a user