diff --git a/components/wrapped/WrappedContainer.tsx b/components/wrapped/WrappedContainer.tsx new file mode 100644 index 0000000..516d15f --- /dev/null +++ b/components/wrapped/WrappedContainer.tsx @@ -0,0 +1,22 @@ +import React, { ReactNode } from "react"; + +type WrappedContainerProps = { + children: ReactNode; + bg?: string; + text?: string; +}; + +export const WrappedContainer = ({ + children, + bg = "bg-card", + text = "text-lightGray", +}: WrappedContainerProps) => { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/components/wrapped/WrappedPlayer.ts b/components/wrapped/WrappedPlayer.ts new file mode 100644 index 0000000..39de078 --- /dev/null +++ b/components/wrapped/WrappedPlayer.ts @@ -0,0 +1,29 @@ +import EventEmitter from "events"; +import SpotifyFramePlayer from "./spotify/FramePlayer"; +import { SLIDES, Slide } from "./slides"; + +export default class WrappedPlayer extends EventEmitter { + public currentSlide: Slide | null = null; + + constructor(public spotifyPlayer: SpotifyFramePlayer | null = null) { + super(); + } + + public async play(): Promise { + for (let i = 0; i < SLIDES.length; i++) { + const slide = SLIDES[i]; + this.currentSlide = slide; + + if (this.currentSlide.spotify && this.spotifyPlayer) { + await this.spotifyPlayer.playSong(this.currentSlide.spotify); + } + + this.emit("update"); + await this.wait(slide.duration); + } + } + + private wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/components/wrapped/WrappedPlayerComponent.tsx b/components/wrapped/WrappedPlayerComponent.tsx new file mode 100644 index 0000000..7889102 --- /dev/null +++ b/components/wrapped/WrappedPlayerComponent.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from "react"; +import { CSSTransition, TransitionGroup } from "react-transition-group"; +import { Loader2 } from "lucide-react"; +import WrappedPlayer from "./WrappedPlayer"; +import { WrappedContainer } from "./WrappedContainer"; +import SpotifyFramePlayer from "./spotify/FramePlayer"; + +type WrappedPlayerComponentProps = { + spotify: SpotifyFramePlayer | null; + team: any; +}; + +const LoadingPlayerComponent: React.FC = () => ( + + + +); + +const WrappedPlayerComponent: React.FC = ({ + spotify, + ...props +}) => { + const [player] = useState(() => new WrappedPlayer(spotify)); + const [, setForceUpdate] = useState(0); + + useEffect(() => { + const forceUpdateHandler = () => setForceUpdate((prev) => prev + 1); + + player.on("update", forceUpdateHandler); + player.play(); + + return () => { + player.off("update", forceUpdateHandler); + }; + }, [player]); + + useEffect(() => { + player.spotifyPlayer = spotify; + }, [player, spotify]); + + const Component = player.currentSlide?.component || LoadingPlayerComponent; + + return ( + + + + + + ); +}; + +export default WrappedPlayerComponent; diff --git a/components/wrapped/slides/FirstSlide.tsx b/components/wrapped/slides/FirstSlide.tsx new file mode 100644 index 0000000..9e72bdd --- /dev/null +++ b/components/wrapped/slides/FirstSlide.tsx @@ -0,0 +1,9 @@ +import { WrappedContainer } from "../WrappedContainer"; + +export const FirstSlide = () => { + return ( + +

This is the First Slide

+
+ ); +}; diff --git a/components/wrapped/slides/IntroSlide.tsx b/components/wrapped/slides/IntroSlide.tsx new file mode 100644 index 0000000..f17d634 --- /dev/null +++ b/components/wrapped/slides/IntroSlide.tsx @@ -0,0 +1,21 @@ +import { WrappedContainer } from "../WrappedContainer"; + +export const IntroSlide = (props: any) => { + return ( + +

+ Did someone say {props.team} Wrap? +

+

+ Get ready to see season highlights, match videos, & so much more! +

+ + +
+ ); +}; diff --git a/components/wrapped/slides/SecondSlide.tsx b/components/wrapped/slides/SecondSlide.tsx new file mode 100644 index 0000000..47b1e9f --- /dev/null +++ b/components/wrapped/slides/SecondSlide.tsx @@ -0,0 +1,9 @@ +import { WrappedContainer } from "../WrappedContainer"; + +export const SecondSlide = () => { + return ( + +

This is the Second Slide

+
+ ); +}; diff --git a/components/wrapped/slides/ThirdSlide.tsx b/components/wrapped/slides/ThirdSlide.tsx new file mode 100644 index 0000000..d5a9064 --- /dev/null +++ b/components/wrapped/slides/ThirdSlide.tsx @@ -0,0 +1,9 @@ +import { WrappedContainer } from "../WrappedContainer"; + +export const ThirdSlide = () => { + return ( + +

This is the Third Slide

+
+ ); +}; diff --git a/components/wrapped/slides/index.tsx b/components/wrapped/slides/index.tsx new file mode 100644 index 0000000..0a23685 --- /dev/null +++ b/components/wrapped/slides/index.tsx @@ -0,0 +1,31 @@ +import { FirstSlide } from "./FirstSlide"; +import { SecondSlide } from "./SecondSlide"; +import { ThirdSlide } from "./ThirdSlide"; + +export interface Slide { + name: string; + component: React.FC; + duration: number; + spotify?: string; +} + +export const SLIDES: Slide[] = [ + { + name: "First Slide", + component: FirstSlide, + duration: 6000, + spotify: "spotify:track:0RiRZpuVRbi7oqRdSMwhQY", + }, + { + name: "Second Slide", + component: SecondSlide, + duration: 6000, + spotify: "spotify:track:47Mf1u67oqXtWBAaUHUSi7", + }, + { + name: "Third Slide", + component: ThirdSlide, + duration: 6000, + spotify: "spotify:track:14zDy9P7qf0oBDOtHMNoKV", + }, +]; diff --git a/components/wrapped/spotify/FramePlayer.ts b/components/wrapped/spotify/FramePlayer.ts new file mode 100644 index 0000000..ba5ef36 --- /dev/null +++ b/components/wrapped/spotify/FramePlayer.ts @@ -0,0 +1,181 @@ +import EventEmitter from "events"; + +export default class SpotifyFramePlayer extends EventEmitter { + public embedController: EmbedController | null = null; + public canPlaySongs = false; + + private currentIFrame: HTMLIFrameElement | null = null; + private previousIFrame: HTMLIFrameElement | null = null; + + public loadLibrary(): Promise { + return new Promise((resolve) => { + const script = document.createElement("script"); + script.src = "https://open.spotify.com/embed-podcast/iframe-api/v1"; + script.async = true; + document.body.appendChild(script); + + const timeout = setTimeout(() => { + resolve(); + }, 7000); + + window.onSpotifyIframeApiReady = (IFrameAPI: SpotifyIframeApi) => { + const element = document.getElementById("spotify")!; + const options = { + uri: "spotify:track:0RiRZpuVRbi7oqRdSMwhQY", + width: 300, + height: 80, + theme: "dark", + }; + + IFrameAPI.createController(element, options, (EmbedController) => { + this.embedController = EmbedController; + + const enablePlayback = () => { + if (this.canPlaySongs) return; + + this.canPlaySongs = true; + this.embedController!.removeListener("ready", enablePlayback); + this.emit("ready"); + + clearTimeout(timeout); + resolve(); + }; + + const defaultIframe = document + .getElementById("spotify-wrapper") + ?.querySelector("iframe"); + + if (defaultIframe) { + defaultIframe.style.opacity = "0"; + defaultIframe.style.position = "absolute"; + defaultIframe.style.top = "-1000px"; + } + + this.embedController!.addListener("ready", enablePlayback); + }); + }; + }); + } + + public playSong(uri: string): Promise { + return new Promise(async (resolve) => { + if (!this.canPlaySongs) return resolve(); + if (!this.embedController) return resolve(); + + const playTimeout = setTimeout(() => { + this.canPlaySongs = false; + resolve(); + }, 6000); + + const container = document.getElementById("spotify-wrapper"); + this.previousIFrame = this.currentIFrame; + const frameElement = document.createElement("div"); + + if (container?.firstChild) { + container!.insertBefore(frameElement, container.firstChild); + } else { + container!.appendChild(frameElement); + } + + const oembed = await fetch(`https://open.spotify.com/oembed?url=${uri}`) + .then((response) => response.json()) + .catch(() => { + return { + html: "", + }; + }); + + if (!oembed.html) { + this.destroyPreviousIFrame(); + resolve(); + return; + } + + oembed.html = oembed.html.replace("encrypted-media; ", ""); + frameElement.innerHTML = oembed.html; + + this.setupNewIframe(frameElement); + + await this.waitForIframe(); + + this.embedController.loadUri(uri); + this.embedController.resume(); + + setTimeout(() => this.destroyPreviousIFrame(), 1000); + await this.waitForSpotify(); + + this.currentIFrame!.style.opacity = "1"; + clearTimeout(playTimeout); + resolve(); + }); + } + + private setupNewIframe(frameElement: HTMLDivElement): void { + const iframe = frameElement.querySelector("iframe")!; + this.embedController!.iframeElement = iframe; + this.currentIFrame = iframe; + this.currentIFrame!.style.opacity = "0"; + this.currentIFrame!.setAttribute("width", "300"); + this.currentIFrame!.setAttribute("height", "80"); + } + + private waitForIframe(): Promise { + return new Promise((resolve) => { + const checkIfReady = (e: MessageEvent) => { + if ( + e.source === this.currentIFrame?.contentWindow && + e.data.type === "ready" + ) { + window.removeEventListener("message", checkIfReady); + resolve(); + } + }; + + window.addEventListener("message", checkIfReady); + }); + } + + private destroyPreviousIFrame(): void { + if (this.previousIFrame) { + this.previousIFrame.remove(); + this.previousIFrame = null; + } + } + + private waitForSpotify(): Promise { + return new Promise((resolve) => { + let hasResolved = false; + + const checkIfReady = (state: { + data: { + isBuffering: boolean; + duration: number; + position: number; + }; + }) => { + if (hasResolved) { + return; + } + + if ( + !state.data.isBuffering && + state.data.duration > 0 && + state.data.position > 0 + ) { + this.embedController?.removeListener("playback_update", checkIfReady); + hasResolved = true; + resolve(); + } + }; + + this.embedController?.addListener("playback_update", checkIfReady); + }); + } + + public async pause(): Promise { + if (!this.canPlaySongs) return; + if (!this.embedController) return; + + this.embedController.pause(); + } +} diff --git a/components/wrapped/spotify/SpotifyPlayer.tsx b/components/wrapped/spotify/SpotifyPlayer.tsx new file mode 100644 index 0000000..3d14480 --- /dev/null +++ b/components/wrapped/spotify/SpotifyPlayer.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export const SpotifyPlayer = () => { + return ( +
+
+
+ ); +} \ No newline at end of file diff --git a/components/wrapped/spotify/spotify.d.ts b/components/wrapped/spotify/spotify.d.ts new file mode 100644 index 0000000..7a5d716 --- /dev/null +++ b/components/wrapped/spotify/spotify.d.ts @@ -0,0 +1,28 @@ +interface EmbedController { + loadUri(uri: string): void; + play(): void; + playFromStart(): void; + togglePlay(): void; + pause(): void; + resume(): void; + seek(seconds: number): void; + destroy(): void; + + addListener(event: string, callback: (state: any) => void): void; + once(event: string, callback: () => void): void; + removeListener(event: string, callback: (state: any) => void): void; + + iframeElement: HTMLIFrameElement; +} + +interface SpotifyIframeApi { + createController( + element: HTMLElement, + options: { uri: string; width?: number; height?: number }, + callback: (embedController: EmbedController) => void + ): void; +} + +interface Window { + onSpotifyIframeApiReady: (iframeAPI: SpotifyIframeApi) => void; +} diff --git a/package.json b/package.json index 9a1cb7f..d480c41 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/node": "20.2.5", "@types/react": "18.2.8", "@types/react-dom": "18.2.4", + "@types/react-transition-group": "^4.4.6", "@vercel/analytics": "^1.0.1", "autoprefixer": "10.4.14", "axios": "^1.4.0", @@ -37,6 +38,7 @@ "framer-motion": "^10.12.16", "haversine-distance": "^1.2.1", "lodash": "^4.17.21", + "lucide-react": "^0.236.0", "next": "13.4.4", "next-api-handler": "^0.4.10", "next-auth": "^4.22.1", @@ -51,6 +53,7 @@ "react-google-autocomplete": "^2.7.3", "react-hot-toast": "^2.4.1", "react-icons": "^4.9.0", + "react-transition-group": "^4.4.5", "sharp": "^0.32.1", "tailwindcss": "3.3.2", "typescript": "5.0.4", diff --git a/pages/team/[team]/wrap.tsx b/pages/team/[team]/wrap.tsx index 7fe2cc2..c580fc0 100644 --- a/pages/team/[team]/wrap.tsx +++ b/pages/team/[team]/wrap.tsx @@ -1,9 +1,40 @@ import { GetServerSideProps, GetServerSidePropsContext } from "next"; -import db from "@/lib/db"; -import { Match } from "@prisma/client"; +import WrappedPlayerComponent from "@/components/wrapped/WrappedPlayerComponent"; +// import db from "@/lib/db"; +// import { Match } from "@prisma/client"; +import { useRouter } from "next/router"; +import { SpotifyPlayer } from "@/components/wrapped/spotify/SpotifyPlayer"; +import SpotifyFramePlayer from "@/components/wrapped/spotify/FramePlayer"; +import { useEffect, useState } from "react"; +import { IntroSlide } from "@/components/wrapped/slides/IntroSlide"; -export default function LiveFieldViewPage({ teamMatches }: any) { - return

Testing

; +export default function TeamWrapPage() { + const router = useRouter(); + const { team } = router.query; + const [spotify, setSpotify] = useState(null); + const [page, setPage] = useState("main"); + + useEffect(() => { + const loadSpotify = async () => { + const getSpotify = new SpotifyFramePlayer(); + await getSpotify.loadLibrary(); + setSpotify(getSpotify); + }; + + if (!spotify) loadSpotify(); + }); + + return ( + <> + + + {page === "main" && } + + {page === "ready" && ( + + )} + + ); } export const getServerSideProps: GetServerSideProps = async ( @@ -11,45 +42,45 @@ export const getServerSideProps: GetServerSideProps = async ( ): Promise<{ props: any }> => { const { team }: any = context.params; - const teamMatches = await db.match.findMany({ - where: { - event_key: { - contains: "2023", - }, - OR: [ - { - alliances: { - path: ["red", "team_keys"], - array_contains: `frc${team}`, - }, - }, - { - alliances: { - path: ["blue", "team_keys"], - array_contains: `frc${team}`, - }, - }, - ], - }, - }); + // const teamMatches = await db.match.findMany({ + // where: { + // event_key: { + // contains: "2023", + // }, + // OR: [ + // { + // alliances: { + // path: ["red", "team_keys"], + // array_contains: `frc${team}`, + // }, + // }, + // { + // alliances: { + // path: ["blue", "team_keys"], + // array_contains: `frc${team}`, + // }, + // }, + // ], + // }, + // }); - const highestScoringMatches: Match[] = teamMatches - .sort((a: any, b: any) => { - const scoreA = a.alliances.red.score + a.alliances.blue.score; - const scoreB = b.alliances.red.score + b.alliances.blue.score; - return scoreB - scoreA; - }) - .slice(0, 2); + // const highestScoringMatches: Match[] = teamMatches + // .sort((a: any, b: any) => { + // const scoreA = a.alliances.red.score + a.alliances.blue.score; + // const scoreB = b.alliances.red.score + b.alliances.blue.score; + // return scoreB - scoreA; + // }) + // .slice(0, 2); return { props: { - teamMatches: JSON.parse( - JSON.stringify( - highestScoringMatches, - (key: string, value) => - typeof value === "bigint" ? value.toString() : value // return everything else unchanged - ) - ), + // teamMatches: JSON.parse( + // JSON.stringify( + // highestScoringMatches, + // (key: string, value) => + // typeof value === "bigint" ? value.toString() : value // return everything else unchanged + // ) + // ), }, }; }; diff --git a/styles/globals.css b/styles/globals.css index 212d2be..dfa8a22 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -87,6 +87,28 @@ a:hover { } } +.fade-enter { + transform: translateY(100%); +} + +.fade-enter-active { + transform: translateY(0%); + transition: transform 300ms; +} + +.fade-exit { + position: absolute; + top: 0; + left: 0; + + transform: translateY(0%); +} + +.fade-exit-active { + transform: translateY(-100%); + transition: transform 300ms; +} + ::-webkit-scrollbar { width: 2px; } diff --git a/yarn.lock b/yarn.lock index 80528a1..ca9fa5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1905,7 +1905,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.11.2", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.22.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb" integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ== @@ -2641,6 +2641,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e" + integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@18.2.8": version "18.2.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.8.tgz#a77dcffe4e9af148ca4aa8000c51a1e8ed99e2c8" @@ -3680,6 +3687,14 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -5328,6 +5343,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lucide-react@^0.236.0: + version "0.236.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.236.0.tgz#156270061d68f81b4bda0daa03c3487dbfe403b8" + integrity sha512-himeKF7nVgOQ1BNcyBgk41E4/rcbmI6Zw8Q4o57nlynsFvIBA/DMacFpzKYdcyBReUj8jf08xTnCGyn/niLvwQ== + lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -6126,7 +6146,7 @@ prisma@^4.15.0: dependencies: "@prisma/engines" "4.15.0" -prop-types@^15.5.0, prop-types@^15.8.1: +prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6242,6 +6262,16 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"