diff --git a/deno.lock b/deno.lock index e9add41..6c15234 100644 --- a/deno.lock +++ b/deno.lock @@ -1,6 +1,7 @@ { "version": "5", "specifiers": { + "npm:@tidal-music/api@0.7": "0.7.0", "npm:@tidal-music/auth@^1.4.0": "1.4.0", "npm:@tidal-music/event-producer@^2.4.0": "2.4.0", "npm:@tidal-music/player@~0.11.2": "0.11.2", @@ -550,6 +551,12 @@ "tslib" ] }, + "@tidal-music/api@0.7.0": { + "integrity": "sha512-WaogpHAlGhwPR/ScapgvyiXMwCIakUQPdR78LVus4vEcXr1m0oKyeuNu54Y+jMSFM26caIk2kPx75yKpiegSmA==", + "dependencies": [ + "openapi-fetch" + ] + }, "@tidal-music/auth@1.4.0": { "integrity": "sha512-b/8n6aHYoMuzGbNTT4QOwm9xjTosHCn9F+Vi26Nq1x1OuK+XK9zbuAkSwma/C5eYHy4fPBTcSw82us5K5mUHmA==", "dependencies": [ @@ -1695,6 +1702,15 @@ "regex-recursion" ] }, + "openapi-fetch@0.15.0": { + "integrity": "sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==", + "dependencies": [ + "openapi-typescript-helpers" + ] + }, + "openapi-typescript-helpers@0.0.15": { + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==" + }, "p-limit@6.2.0": { "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", "dependencies": [ @@ -2309,6 +2325,7 @@ "workspace": { "packageJson": { "dependencies": [ + "npm:@tidal-music/api@0.7", "npm:@tidal-music/auth@^1.4.0", "npm:@tidal-music/event-producer@^2.4.0", "npm:@tidal-music/player@~0.11.2", diff --git a/package.json b/package.json index 7be89a9..e6dd571 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "astro": "astro" }, "dependencies": { + "@tidal-music/api": "^0.7.0", "@tidal-music/auth": "^1.4.0", "@tidal-music/event-producer": "^2.4.0", "@tidal-music/player": "^0.11.2", diff --git a/src/pages/index.astro b/src/pages/index.astro index a7f3ecc..75ee9ef 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,10 +5,82 @@ import "../styles/index.css"
+ A bad answer - Someone + Killer Queen - Queen + Bohemian Rhapsody - Queen - - - The Correct Answer - Queen +
+ +
+
- + + diff --git a/src/scripts/auth.ts b/src/scripts/auth.ts new file mode 100644 index 0000000..54ff481 --- /dev/null +++ b/src/scripts/auth.ts @@ -0,0 +1,9 @@ +import * as auth from "@tidal-music/auth" + +await auth.init({ + clientId: "NkcXqihOmrfZdBla", + clientSecret: "e6ldlWH48BEBOUZV1uIyIWJOf1KUGUEeH6qHkFvNjeU=", + credentialsStorageKey: "ZvZbtTWzx5@1zfiWUluxWD9@di17e1da", +}) + +export default auth diff --git a/src/scripts/client.ts b/src/scripts/client.ts new file mode 100644 index 0000000..bb9a1f9 --- /dev/null +++ b/src/scripts/client.ts @@ -0,0 +1,3 @@ +import { createAPIClient } from "@tidal-music/api" +import auth from "./auth" +export const client = createAPIClient(auth.credentialsProvider) diff --git a/src/scripts/debounce.ts b/src/scripts/debounce.ts new file mode 100644 index 0000000..cfa11e2 --- /dev/null +++ b/src/scripts/debounce.ts @@ -0,0 +1,10 @@ +export const debounce = void>( + fn: T +): ((...args: Parameters) => void) => { + let timer: ReturnType | undefined + + return (...args: Parameters) => { + if (timer) clearTimeout(timer) + timer = setTimeout(() => fn(...args), 500) + } +} diff --git a/src/scripts/playSong.ts b/src/scripts/playSong.ts new file mode 100644 index 0000000..8fe1a9a --- /dev/null +++ b/src/scripts/playSong.ts @@ -0,0 +1,47 @@ +import player from "./player" +import { ExtendableTimeout } from "./timeout" + +const play = document.querySelector("#play") +const played = document.querySelector("#played") +const skip = document.querySelector("#skip") +const available = document.querySelector("#available") + +let phase = 1 +let timer: ExtendableTimeout | null + +const getTimeout = (phase: number) => (2 ** phase - 1) * 1000 + +const playSong = async () => { + played?.classList.remove("playing") + await player.load({ + productId: "36737274", + productType: "track", + sourceId: "", + sourceType: "", + }) + await player.play() + played?.classList.add("playing") + + timer?.cancel() + timer = new ExtendableTimeout(() => { + player.pause() + timer = null + played?.classList.remove("playing") + }, getTimeout(phase)) +} + +play?.addEventListener("click", playSong) + +skip?.addEventListener("click", () => { + if (phase >= 4) return alert("FAIL!!!!") // TODO + phase += 1 + + // @ts-expect-error + available.style["grid-column-end"] = phase + 1 + + if (timer == null) { + playSong() + } else { + timer.extend(getTimeout(phase)) + } +}) diff --git a/src/scripts/player.ts b/src/scripts/player.ts new file mode 100644 index 0000000..e5e586d --- /dev/null +++ b/src/scripts/player.ts @@ -0,0 +1,8 @@ +import * as eventProducer from "@tidal-music/event-producer" +import * as player from "@tidal-music/player" +import auth from "./auth" + +player.setCredentialsProvider(auth.credentialsProvider) +player.setEventSender(eventProducer) + +export default player diff --git a/src/scripts/search.ts b/src/scripts/search.ts new file mode 100644 index 0000000..e135b65 --- /dev/null +++ b/src/scripts/search.ts @@ -0,0 +1,89 @@ +import { client } from "./client" +import { debounce } from "./debounce" + +const search = document.querySelector("#search") as HTMLInputElement + +search?.addEventListener( + "input", + debounce(async (event) => { + const target = event.target as HTMLInputElement + const value = target.value + const tracks = await client.GET( + "/searchResults/{id}/relationships/tracks", + { + params: { + path: { id: value }, + query: { countryCode: "US" }, + }, + } + ) + + if (!tracks.response.ok) + alert( + "An error occurred while fetching tracks, please try again later." + ) + + if (tracks.data?.data == null) return + + const withArtists = await client.GET("/tracks", { + params: { + query: { + "filter[id]": tracks.data.data?.map((track) => track.id), + include: ["artists"], + countryCode: "US", + }, + }, + }) + + if (!withArtists.response.ok) + alert( + "An error occurred while fetching track info, please try again later." + ) + + const autocomplete = document.querySelector("#autocomplete") + + autocomplete?.replaceChildren( + ...(tracks.data?.data + ?.map((orderedTrack) => { + const track = withArtists.data?.data.find( + (track) => track.id == orderedTrack.id + ) + if (track == null) return + + if ( + track.attributes == null || + !( + "title" in track.attributes && + "relationships" in track + ) + ) + return + + const span = document.createElement("button") + + span.dataset.title = track.attributes.title + span.dataset.artists = track.relationships?.artists.data + ?.map((artist) => artist.id) + .join() + span.textContent = `${ + track.attributes.title + } - ${track.relationships?.artists.data + ?.map((songArtist) => { + const artist = withArtists.data?.included?.find( + (artist) => artist.id == songArtist.id + ) + if ( + artist?.attributes == null || + !("name" in artist.attributes) + ) + return + return artist?.attributes?.name + }) + .join(", ")}` + + return span + }) + .filter((element) => element != null) as HTMLSpanElement[]) + ) + }) +) diff --git a/src/scripts/timeout.ts b/src/scripts/timeout.ts new file mode 100644 index 0000000..d90d8e6 --- /dev/null +++ b/src/scripts/timeout.ts @@ -0,0 +1,24 @@ +export class ExtendableTimeout { + private timerId: ReturnType + private timeStart: number + private fn: () => void + + constructor(fn: () => void, time: number) { + this.fn = fn + this.timeStart = Date.now() + this.timerId = setTimeout(fn, time) + } + + extend(time: number): void { + clearTimeout(this.timerId) + + const elapsed = Date.now() - this.timeStart + const newTime = Math.max(0, time - elapsed) + + this.timerId = setTimeout(this.fn, newTime) + } + + cancel(): void { + clearTimeout(this.timerId) + } +} diff --git a/src/styles/index.css b/src/styles/index.css index 67af5e6..eb52243 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,5 +1,6 @@ main > * { display: flex; + gap: 1rem; width: 100%; justify-content: center; align-items: center; @@ -10,26 +11,87 @@ main > * { flex-direction: column; gap: 2rem; - & span { + & > span { display: flex; justify-content: center; align-items: center; font-weight: bold; - background-color: #000; + background: rgb(26, 26, 26); height: 2.4rem; width: 100%; border-radius: 2rem; &.correct { - background-image: linear-gradient( + background: linear-gradient( to bottom right, var(--brand-pink) 0%, var(--brand-purple) 100% ); } + + &.partial { + background: linear-gradient( + to bottom right, + rgb(255, 135, 90) 0%, + rgb(255, 196, 69) 100% + ); + } + + &.incorrect { + background: linear-gradient( + to bottom right, + rgb(185, 74, 72) 0%, + rgb(157, 38, 29) 100% + ); + } + } + + & .search-container { + position: relative; + &, + & > * { + width: 100%; + } + + & > input { + border: none; + border-radius: 1rem; + padding: 1rem; + background: rgb(220, 218, 218); + + &:focus { + outline: 0.3rem solid var(--brand-pink); + } + } + + & > section { + border-radius: 1rem; + background-color: rgb(46, 46, 46); + + max-height: 30vh; + overflow: scroll; + + display: flex; + flex-direction: column; + + position: absolute; + top: 3.5rem; + z-index: 3; + + border: 0.2rem solid var(--brand-gray); + + & button { + width: 100%; + padding: 1rem; + + &:not(:first-child) { + border-top: 0.2rem solid var(--brand-gray); + } + } + } } } diff --git a/src/styles/layout.css b/src/styles/layout.css index 526b11e..2d4e5c0 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -1,3 +1,9 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + html, body { display: flex; @@ -10,6 +16,7 @@ body { :root { --brand-purple: #6e06ff; --brand-pink: #cc56d0; + --brand-gray: rgb(98, 98, 98); background: linear-gradient(black, rgb(18, 2, 38)); font-family: sans-serif; @@ -27,7 +34,7 @@ nav { align-items: center; padding: 2rem 3rem; - height: 5rem; + height: 8rem; & span { font-size: 6rem; text-shadow: 2px 0 var(--brand-purple), -2px 0 var(--brand-purple), @@ -44,26 +51,33 @@ main { justify-content: center; align-items: center; - gap: 1rem; + gap: 1.5rem; flex-grow: 1; } button { + background-color: transparent; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + border: none; + color: white; + + &:hover { + filter: brightness(80%); + } +} + +:not(#autocomplete) > button { --border-radius: 2rem; position: relative; background: white; color: black; - border: none; border-radius: var(--border-radius); padding: 0.2rem 1rem; - margin: 1rem; - display: flex; - justify-content: center; - align-items: center; - - cursor: pointer; &:hover { background: rgb(211, 211, 211); } @@ -73,14 +87,15 @@ button { } } -button::before { +:not(#autocomplete) > button::before { --border-size: 5px; + box-sizing: content-box; content: ""; z-index: -1; border-radius: var(--border-radius); width: 100%; height: 100%; - background-image: linear-gradient( + background: linear-gradient( to bottom right, var(--brand-pink) 0%, var(--brand-purple) 100%