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",
|
"name": "maisie-heardle",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.30.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
@@ -23,6 +26,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-hero-icons": "^5.2.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
@@ -1101,6 +1105,13 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@sveltejs/acorn-typescript": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||||
@@ -2964,6 +2975,19 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -2977,6 +3001,15 @@
|
|||||||
"node": "*"
|
"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": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@@ -3121,13 +3154,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||||
@@ -3736,19 +3785,6 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"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": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"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"
|
"@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": {
|
"node_modules/vitefu": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
|
||||||
|
@@ -29,9 +29,13 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-hero-icons": "^5.2.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^6.2.5"
|
"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';
|
@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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
<!-- SoundCloud Widget API -->
|
||||||
|
<script src="https://w.soundcloud.com/player/api.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<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">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
</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>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
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 { defineConfig } from 'vite';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit()]
|
plugins: [sveltekit(), tailwindcss()]
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user