Initial working draft - no styling

Random album selection works
Drag and Drop works
Solution checking works
Stage and high score tracking works
High score and stage tied to session cookie
This commit is contained in:
Mihkel Martin Kasterpalu 2025-01-19 05:24:56 +02:00
parent 0407df4a2a
commit 19127be9a2
14 changed files with 935 additions and 5 deletions

View file

@ -24,7 +24,9 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@types/better-sqlite3": "^7.6.12",
"@types/spotify-web-api-node": "^5.0.11",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.78",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.2",
"eslint": "^9.18.0",
@ -36,6 +38,7 @@
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-dnd-action": "^0.9.54",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
@ -46,6 +49,9 @@
},
"dependencies": {
"better-sqlite3": "^11.8.0",
"drizzle-orm": "^0.38.4"
"drizzle-orm": "^0.38.4",
"nanoid": "^5.0.9",
"spotify-web-api-node": "^5.0.2",
"svelte-kit-sessions": "^0.4.0"
}
}

421
pnpm-lock.yaml generated
View file

@ -14,6 +14,15 @@ importers:
drizzle-orm:
specifier: ^0.38.4
version: 0.38.4(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)
nanoid:
specifier: ^5.0.9
version: 5.0.9
spotify-web-api-node:
specifier: ^5.0.2
version: 5.0.2
svelte-kit-sessions:
specifier: ^0.4.0
version: 0.4.0(@sveltejs/kit@2.16.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7)))(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7)))(svelte@5.19.0)
devDependencies:
'@eslint/compat':
specifier: ^1.2.5
@ -39,9 +48,15 @@ importers:
'@types/better-sqlite3':
specifier: ^7.6.12
version: 7.6.12
'@types/spotify-web-api-node':
specifier: ^5.0.11
version: 5.0.11
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.5.1)
bits-ui:
specifier: 1.0.0-next.78
version: 1.0.0-next.78(svelte@5.19.0)
clsx:
specifier: ^2.1.1
version: 2.1.1
@ -75,6 +90,9 @@ importers:
svelte-check:
specifier: ^4.0.0
version: 4.1.4(picomatch@4.0.2)(svelte@5.19.0)(typescript@5.7.3)
svelte-dnd-action:
specifier: ^0.9.54
version: 0.9.54(svelte@5.19.0)
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
@ -569,6 +587,15 @@ packages:
resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.6.9':
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
'@floating-ui/dom@1.6.13':
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -589,10 +616,17 @@ packages:
resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
engines: {node: '>=18.18'}
'@internationalized/date@3.7.0':
resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/ttlcache@1.4.1':
resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
@ -790,6 +824,9 @@ packages:
svelte: ^5.0.0-next.96 || ^5.0.0
vite: ^5.0.0
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tailwindcss/container-queries@0.1.1':
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
peerDependencies:
@ -818,6 +855,12 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/spotify-api@0.0.25':
resolution: {integrity: sha512-okhoy0U9fPWtwqCfbDyW8VxamhqvXE0gXIVeMOh5HcvEFQvWW2X0VsvdiX/OyiGQpZbZiOJXIGrbnIPfK0AIpA==}
'@types/spotify-web-api-node@5.0.11':
resolution: {integrity: sha512-RS3IkSqH9geC61e8qd+Oy7giOTtiY7ywm0Z4bu5uYuc7XuOcLfDwKjmle85IbpTEdazeCgmIbo8nMLg7WDVvgw==}
'@typescript-eslint/eslint-plugin@8.20.0':
resolution: {integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -916,6 +959,9 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@ -943,6 +989,12 @@ packages:
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bits-ui@1.0.0-next.78:
resolution: {integrity: sha512-jZjG2ObZ/CNyCNaXecpItC7hRXqJAgEfMhr06/eNrf3wHiiPyhdcy4OkzLcJyxeOrDyj+xma8cZTd3JRWqJdAw==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.11.0
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -967,6 +1019,14 @@ packages:
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
call-bind-apply-helpers@1.0.1:
resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==}
engines: {node: '>= 0.4'}
call-bound@1.0.3:
resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==}
engines: {node: '>= 0.4'}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@ -1004,6 +1064,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@ -1011,6 +1075,9 @@ packages:
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -1018,6 +1085,9 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
cookiejar@2.1.4:
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -1051,6 +1121,10 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
@ -1160,6 +1234,10 @@ packages:
sqlite3:
optional: true
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -1175,6 +1253,18 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
peerDependencies:
@ -1301,6 +1391,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fastq@1.18.0:
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
@ -1338,6 +1431,14 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
form-data@3.0.2:
resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==}
engines: {node: '>= 6'}
formidable@1.2.6:
resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==}
deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau'
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@ -1352,6 +1453,14 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.2.7:
resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-tsconfig@4.8.1:
resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
@ -1378,6 +1487,10 @@ packages:
resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==}
engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@ -1385,6 +1498,10 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@ -1413,6 +1530,9 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@ -1510,14 +1630,35 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
methods@1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@2.6.0:
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
engines: {node: '>=4.0.0'}
hasBin: true
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
@ -1562,6 +1703,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.0.9:
resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==}
engines: {node: ^18 || >=20}
hasBin: true
napi-build-utils@1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
@ -1591,6 +1737,10 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-inspect@1.13.3:
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@ -1794,6 +1944,10 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -1840,6 +1994,16 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
runed@0.20.0:
resolution: {integrity: sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==}
peerDependencies:
svelte: ^5.7.0
runed@0.22.0:
resolution: {integrity: sha512-ZWVXWhOr0P5xdNgtviz6D1ivLUDWKLCbeC5SUEJ3zBkqLReVqWHenFxMNFeFaiC5bfxhFxyxzyzB+98uYFtwdA==}
peerDependencies:
svelte: ^5.7.0
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@ -1863,6 +2027,22 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@ -1888,6 +2068,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
spotify-web-api-node@5.0.2:
resolution: {integrity: sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@ -1915,11 +2098,19 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-to-object@1.0.8:
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
superagent@6.1.0:
resolution: {integrity: sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==}
engines: {node: '>= 7.0.0'}
deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -1936,6 +2127,11 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte-dnd-action@0.9.54:
resolution: {integrity: sha512-Hue0e449cd3WOASOeawFhEDUb6WFz6QyMVzuEd2RCWhzUAZS6PvOCLZeJyDkx4uKgYve9PR08yzTlEV0oKaH8A==}
peerDependencies:
svelte: '>=3.23.0 || ^5.0.0-next.0'
svelte-eslint-parser@0.43.0:
resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -1945,6 +2141,18 @@ packages:
svelte:
optional: true
svelte-kit-sessions@0.4.0:
resolution: {integrity: sha512-cWjHwd+EGIuZ0p8CxSqE5EMOT8EUsoYfAnbE8QB+r6FonroYiMvTLUgv8b9dVLC55Yw3UtTntjUaZ5fKJF3XOA==}
peerDependencies:
'@sveltejs/kit': ^1.0.0 || ^2.0.0
svelte: ^5.1.13
svelte-toolbelt@0.7.0:
resolution: {integrity: sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.0.0
svelte@5.19.0:
resolution: {integrity: sha512-qvd2GvvYnJxS/MteQKFSMyq8cQrAAut28QZ39ySv9k3ggmhw4Au4Rfcsqva74i0xMys//OhbhVCNfXPrDzL/Bg==}
engines: {node: '>=18'}
@ -2002,6 +2210,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@ -2382,6 +2593,17 @@ snapshots:
'@eslint/core': 0.10.0
levn: 0.4.1
'@floating-ui/core@1.6.9':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.6.13':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/utils': 0.2.9
'@floating-ui/utils@0.2.9': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -2395,6 +2617,10 @@ snapshots:
'@humanwhocodes/retry@0.4.1': {}
'@internationalized/date@3.7.0':
dependencies:
'@swc/helpers': 0.5.15
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -2404,6 +2630,8 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/ttlcache@1.4.1': {}
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
@ -2578,6 +2806,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17)':
dependencies:
tailwindcss: 3.4.17
@ -2603,6 +2835,12 @@ snapshots:
'@types/resolve@1.20.2': {}
'@types/spotify-api@0.0.25': {}
'@types/spotify-web-api-node@5.0.11':
dependencies:
'@types/spotify-api': 0.0.25
'@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@ -2720,6 +2958,8 @@ snapshots:
aria-query@5.3.2: {}
asynckit@0.4.0: {}
autoprefixer@10.4.20(postcss@8.5.1):
dependencies:
browserslist: 4.24.4
@ -2747,6 +2987,16 @@ snapshots:
dependencies:
file-uri-to-path: 1.0.0
bits-ui@1.0.0-next.78(svelte@5.19.0):
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/dom': 1.6.13
'@internationalized/date': 3.7.0
esm-env: 1.2.2
runed: 0.22.0(svelte@5.19.0)
svelte: 5.19.0
svelte-toolbelt: 0.7.0(svelte@5.19.0)
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -2780,6 +3030,16 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
call-bind-apply-helpers@1.0.1:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.3:
dependencies:
call-bind-apply-helpers: 1.0.1
get-intrinsic: 1.2.7
callsites@3.1.0: {}
camelcase-css@2.0.1: {}
@ -2817,14 +3077,22 @@ snapshots:
color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
commondir@1.0.1: {}
component-emitter@1.3.1: {}
concat-map@0.0.1: {}
cookie@0.6.0: {}
cookiejar@2.1.4: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -2847,6 +3115,8 @@ snapshots:
deepmerge@4.3.1: {}
delayed-stream@1.0.0: {}
detect-libc@2.0.3: {}
devalue@5.1.1: {}
@ -2869,6 +3139,12 @@ snapshots:
'@types/better-sqlite3': 7.6.12
better-sqlite3: 11.8.1
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.1
es-errors: 1.3.0
gopd: 1.2.0
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.83: {}
@ -2881,6 +3157,14 @@ snapshots:
dependencies:
once: 1.4.0
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
esbuild-register@3.6.0(esbuild@0.19.12):
dependencies:
debug: 4.4.0
@ -3100,6 +3384,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-safe-stringify@2.1.1: {}
fastq@1.18.0:
dependencies:
reusify: 1.0.4
@ -3135,6 +3421,14 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@3.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
formidable@1.2.6: {}
fraction.js@4.3.7: {}
fs-constants@1.0.0: {}
@ -3144,6 +3438,24 @@ snapshots:
function-bind@1.1.2: {}
get-intrinsic@1.2.7:
dependencies:
call-bind-apply-helpers: 1.0.1
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-tsconfig@4.8.1:
dependencies:
resolve-pkg-maps: 1.0.0
@ -3171,10 +3483,14 @@ snapshots:
globals@15.14.0: {}
gopd@1.2.0: {}
graphemer@1.4.0: {}
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@ -3196,6 +3512,8 @@ snapshots:
ini@1.3.8: {}
inline-style-parser@0.2.4: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
@ -3277,13 +3595,25 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
math-intrinsics@1.1.0: {}
merge2@1.4.1: {}
methods@1.1.2: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@2.6.0: {}
mimic-response@3.1.0: {}
mini-svg-data-uri@1.4.4: {}
@ -3316,6 +3646,8 @@ snapshots:
nanoid@3.3.8: {}
nanoid@5.0.9: {}
napi-build-utils@1.0.2: {}
natural-compare@1.4.0: {}
@ -3334,6 +3666,8 @@ snapshots:
object-hash@3.0.0: {}
object-inspect@1.13.3: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@ -3471,6 +3805,10 @@ snapshots:
punycode@2.3.1: {}
qs@6.14.0:
dependencies:
side-channel: 1.1.0
queue-microtask@1.2.3: {}
rc@1.2.8:
@ -3537,6 +3875,16 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
runed@0.20.0(svelte@5.19.0):
dependencies:
esm-env: 1.2.2
svelte: 5.19.0
runed@0.22.0(svelte@5.19.0):
dependencies:
esm-env: 1.2.2
svelte: 5.19.0
sade@1.8.1:
dependencies:
mri: 1.2.0
@ -3553,6 +3901,34 @@ snapshots:
shebang-regex@3.0.0: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.3
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.3
es-errors: 1.3.0
get-intrinsic: 1.2.7
object-inspect: 1.13.3
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.3
es-errors: 1.3.0
get-intrinsic: 1.2.7
object-inspect: 1.13.3
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.3
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
@ -3578,6 +3954,12 @@ snapshots:
source-map@0.6.1: {}
spotify-web-api-node@5.0.2:
dependencies:
superagent: 6.1.0
transitivePeerDependencies:
- supports-color
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@ -3606,6 +3988,10 @@ snapshots:
strip-json-comments@3.1.1: {}
style-to-object@1.0.8:
dependencies:
inline-style-parser: 0.2.4
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@ -3616,6 +4002,22 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
superagent@6.1.0:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.0
fast-safe-stringify: 2.1.1
form-data: 3.0.2
formidable: 1.2.6
methods: 1.1.2
mime: 2.6.0
qs: 6.14.0
readable-stream: 3.6.2
semver: 7.6.3
transitivePeerDependencies:
- supports-color
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -3634,6 +4036,10 @@ snapshots:
transitivePeerDependencies:
- picomatch
svelte-dnd-action@0.9.54(svelte@5.19.0):
dependencies:
svelte: 5.19.0
svelte-eslint-parser@0.43.0(svelte@5.19.0):
dependencies:
eslint-scope: 7.2.2
@ -3644,6 +4050,19 @@ snapshots:
optionalDependencies:
svelte: 5.19.0
svelte-kit-sessions@0.4.0(@sveltejs/kit@2.16.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7)))(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7)))(svelte@5.19.0):
dependencies:
'@isaacs/ttlcache': 1.4.1
'@sveltejs/kit': 2.16.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7)))(svelte@5.19.0)(vite@5.4.11(@types/node@22.10.7))
svelte: 5.19.0
svelte-toolbelt@0.7.0(svelte@5.19.0):
dependencies:
clsx: 2.1.1
runed: 0.20.0(svelte@5.19.0)
style-to-object: 1.0.8
svelte: 5.19.0
svelte@5.19.0:
dependencies:
'@ampproject/remapping': 2.3.0
@ -3736,6 +4155,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.8.1: {}
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1

13
src/hooks.server.ts Normal file
View file

@ -0,0 +1,13 @@
import { SESH_SECRET } from '$env/static/private';
import type { Handle } from '@sveltejs/kit';
import { sveltekitSessionHandle } from 'svelte-kit-sessions';
export const handle: Handle = sveltekitSessionHandle({
secret: SESH_SECRET
});
declare module 'svelte-kit-sessions' {
interface SessionData {
userId: string;
}
}

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { dndzone } from 'svelte-dnd-action';
import { flip } from 'svelte/animate';
let { items = $bindable(), image = false, type = 'default' } = $props();
const flipDurationMs = 300;
function handleDndConsider(e: CustomEvent<any>) {
items = e.detail.items;
}
function handleDndFinalize(e: CustomEvent<any>) {
items = e.detail.items;
}
</script>
<section
use:dndzone={{ items, flipDurationMs, type: type }}
onconsider={handleDndConsider}
onfinalize={handleDndFinalize}
class="grid grid-cols-3"
>
{#if image}
{#each items as item, i (item.id)}
<div animate:flip={{ duration: flipDurationMs }}>
<img class="object-cover" alt="Album Art" src={item.value} />
<input type="hidden" name="{type}_{i}" value={item.value} />
</div>
{/each}
{:else}
{#each items as item, i (item.id)}
<div animate:flip={{ duration: flipDurationMs }}>
<p class="text-center">{item.value}</p>
<input type="hidden" name="{type}_{i}" value={item.value} />
</div>
{/each}
{/if}
</section>

View file

@ -0,0 +1,75 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View file

@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -0,0 +1,47 @@
import type { AlbumSolveState } from '$lib/types';
class AlbumState {
private albums: SpotifyApi.AlbumObjectSimplified[] | undefined = undefined;
setAlbums(data: SpotifyApi.AlbumObjectSimplified[]) {
if (!data) {
return;
}
this.albums = data;
}
checkSolve(data: AlbumSolveState[]) {
if (!data || !this.albums) {
return false;
}
for (const solve of data) {
const search = this.albums.filter((album) => album.name === solve.name);
if (!search) {
return false;
}
const matching = search.at(0);
if (!matching) {
return false;
}
if (matching.images.at(0)?.url !== solve.imageUrl) {
return false;
}
for (const artistName of solve.artists) {
if (!matching.artists.find((artist) => artist.name === artistName)) {
return false;
}
}
}
return true;
}
}
export const albumState = new AlbumState();

View file

@ -0,0 +1,60 @@
import type { Player } from '$lib/types';
import { getContext, setContext } from 'svelte';
export class PlayerState {
players = $state<Player[]>([]);
newPlayer(id: string) {
this.players.push({ id: id, stage: 0, highscore: 0 });
}
getStage(id: string) {
const player = this.players.find((player) => player.id === id);
if (!player) {
return undefined;
}
return player.stage;
}
nextStage(id: string) {
const player = this.players.find((player) => player.id === id);
if (!player) {
return;
}
player.stage += 1;
}
score(id: string, win: boolean) {
const player = this.players.find((player) => player.id === id);
if (!player) {
return;
}
if (win) {
player.stage += 1;
if (player.stage > player.highscore) {
player.highscore = player.stage;
}
} else {
player.stage = 0;
}
}
getHighscore(id: string) {
const player = this.players.find((player) => player.id === id);
if (!player) {
return undefined;
}
return player.highscore;
}
}
export const playerState = new PlayerState();

View file

@ -0,0 +1,62 @@
import SpotifyWebApi from 'spotify-web-api-node';
import { CLIENT_ID, CLIENT_SECRET } from '$env/static/private';
import { getRandomSearch } from '$lib/utils';
class SpotifyAPI {
private api = new SpotifyWebApi({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET
});
private exiresAt: Date = $state(new Date());
async refreshAccessToken() {
// If current token is valid for at least 100ms more
if (this.exiresAt.getTime() - new Date().getTime() > 10) {
return true;
}
return await this.api.clientCredentialsGrant().then(
(data) => {
console.log(data.body);
if (!data.body['expires_in'] || !data.body['access_token']) {
return false;
}
const new_date = new Date();
new_date.setSeconds(new_date.getSeconds() + Number(data.body['expires_in']));
this.exiresAt = new_date;
this.api.setAccessToken(data.body['access_token']);
return true;
},
function (err) {
console.log('Something went wrong when retrieving an access token', err);
return false;
}
);
}
async getRandomAlbum() {
if (!(await this.refreshAccessToken())) {
return undefined;
}
const randomSearch = getRandomSearch();
const randomOffset = Math.floor(Math.random() * 1000);
return await this.api.search(randomSearch, ['album'], { limit: 1, offset: randomOffset }).then(
function (data) {
if (data.body.albums?.items?.at(0)) {
return data.body.albums.items.at(0);
}
},
(err) => {
console.log(err);
return undefined;
}
);
}
}
export const spotifyAPI = new SpotifyAPI();

11
src/lib/types.ts Normal file
View file

@ -0,0 +1,11 @@
export type AlbumSolveState = {
name: string;
artists: string[];
imageUrl: string;
};
export type Player = {
id: string;
stage: number;
highscore: number;
};

View file

@ -1,6 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getRandomSearch() {
// A list of all characters that can be chosen.
const characters = 'abcdefghijklmnopqrstuvwxyz';
// Gets a random character from the characters string.
const randomCharacter = characters.charAt(Math.floor(Math.random() * characters.length));
let randomSearch = '';
// Places the wildcard character at the beginning, or both beginning and end, randomly.
switch (Math.round(Math.random())) {
case 0:
randomSearch = randomCharacter + '%';
break;
case 1:
randomSearch = '%' + randomCharacter + '%';
break;
}
return randomSearch;
}
export function shuffleObjectValues<T extends object>(arr: Array<T>): Array<T> {
// Create a copy of the array
const copy = structuredClone(arr);
// Get all keys from the first object
const keys = Object.keys(copy[0] as object);
keys.forEach((key: string) => {
// Get all values for this key with proper type assertion
const values = copy.map((obj) => (obj as { [key: string]: unknown })[key]);
// Fisher-Yates shuffle algorithm
for (let i = values.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[values[i], values[j]] = [values[j], values[i]];
}
// Reassign shuffled values back to objects
copy.forEach((obj, index) => {
(obj as { [key: string]: unknown })[key] = values[index];
});
});
return copy;
}
export function shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}

View file

@ -0,0 +1,83 @@
import { shuffleArray } from '$lib/utils';
import { nanoid } from 'nanoid';
import type { PageServerLoad } from './$types';
import type { AlbumSolveState } from '$lib/types';
import { albumState } from '$lib/server/AlbumState.svelte';
import { playerState } from '$lib/server/PlayerState.svelte';
import { invalidateAll } from '$app/navigation';
const count = 3;
export const load: PageServerLoad = async ({ fetch, locals }) => {
const { session } = locals;
if (!session?.data?.userId) {
await session.setData({ userId: nanoid() });
await session.save();
}
const user = session.data.userId;
let stage = playerState.getStage(user);
if (!stage) {
playerState.newPlayer(user);
stage = playerState.getStage(user);
}
const highscore = playerState.getHighscore(user);
const albumData = await fetch(`/api/getAlbums/${count}`)
.then((res) => {
return res.json();
})
.then((data) => {
return data;
});
const albumNames = albumData.albums.map((album) => ({ id: nanoid(), value: album.name }));
const albumImages = albumData.albums.map((album) => ({
id: nanoid(),
value: album.images.at(0).url
}));
const albumArtists = albumData.albums.map((album) => ({
id: nanoid(),
value: album.artists.map((artist) => artist.name).join(', ')
}));
return {
names: shuffleArray(albumNames),
images: shuffleArray(albumImages),
artists: shuffleArray(albumArtists),
stage: stage,
highscore: highscore
};
};
export const actions = {
default: async ({ request, locals }) => {
const { session } = locals; // you can access `locals.session`
if (!session?.data?.userId) {
return;
}
const user = session.data.userId;
const data = await request.formData();
const state: AlbumSolveState[] = [];
for (let i = 0; i < count; i++) {
const name = data.get(`names_${i}`);
const image = data.get(`images_${i}`);
const artists = data.get(`artists_${i}`);
const artistList = artists.split(', ');
state.push({ name: name, imageUrl: image, artists: artistList });
}
const solved = albumState.checkSolve(state);
playerState.score(user, solved);
return { loading: false };
}
};

View file

@ -1,2 +1,22 @@
<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 { Button } from '$lib/components/ui/button/index.js';
import DndGroup from '$lib/components/DNDGroup.svelte';
import type { PageData } from './$types';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
let { data }: { data: PageData } = $props();
$inspect(data);
</script>
<form method="POST" use:enhance class="grid gap-8">
<DndGroup items={data.names} type="names"></DndGroup>
<DndGroup items={data.artists} type="artists"></DndGroup>
<DndGroup items={data.images} image type="images"></DndGroup>
<div class="flex justify-evenly">
<p>Stage: {data.stage}</p>
<Button type="submit" variant="outline" onsubmit={() => invalidateAll()}>Submit</Button>
<p>High Score: {data.highscore}</p>
</div>
</form>

View file

@ -0,0 +1,20 @@
import { albumState } from '$lib/server/AlbumState.svelte';
import { spotifyAPI } from '$lib/server/Spotify.svelte';
import { json } from '@sveltejs/kit';
export async function GET({ params }) {
const count = params.count || 1;
const albums: SpotifyApi.AlbumObjectSimplified[] = [];
for (let i = 0; i < count; i++) {
const album = await spotifyAPI.getRandomAlbum();
if (album) {
albums.push(album);
}
}
albumState.setAlbums(albums);
return json({ albums: albums });
}