diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/Game/index.tsx | 21 | ||||
| -rw-r--r-- | src/components/Player/index.tsx | 137 | ||||
| -rw-r--r-- | src/components/Result/index.tsx | 40 | ||||
| -rw-r--r-- | src/components/YTPlayer/index.styled.ts (renamed from src/components/Player/index.styled.ts) | 0 | ||||
| -rw-r--r-- | src/components/YTPlayer/index.tsx | 100 | ||||
| -rw-r--r-- | src/components/index.ts | 1 |
6 files changed, 235 insertions, 64 deletions
diff --git a/src/components/Game/index.tsx b/src/components/Game/index.tsx index 2f4a2ec..9024b03 100644 --- a/src/components/Game/index.tsx +++ b/src/components/Game/index.tsx @@ -4,7 +4,7 @@ import { GuessType } from "../../types/guess"; import { Song } from "../../types/song"; import { playTimes } from "../../constants"; -import { Button, Guess, Player, Search, Result } from "../"; +import { Button, Guess, YTPlayer, Search, Result, Player } from "../"; import * as Styled from "./index.styled"; @@ -16,6 +16,8 @@ interface Props { setSelectedSong: React.Dispatch<React.SetStateAction<Song | undefined>>; skip: () => void; guess: () => void; + mode?: "daily" | "unlimited"; + onPlayAgain?: () => void; } export function Game({ @@ -26,6 +28,8 @@ export function Game({ setSelectedSong, skip, guess, + mode = "daily", + onPlayAgain, }: Props) { if (didGuess || currentTry === 6) { return ( @@ -34,19 +38,22 @@ export function Game({ currentTry={currentTry} todaysSolution={todaysSolution} guesses={guesses} + mode={mode} + onPlayAgain={onPlayAgain} /> ); } return ( <> {guesses.map((guess: GuessType, index) => ( - <Guess - key={index} - guess={guess} - active={index === currentTry} - /> + <Guess key={index} guess={guess} active={index === currentTry} /> ))} - <Player id={todaysSolution.youtubeId} currentTry={currentTry} /> + {mode === "unlimited" ? ( + <YTPlayer id={todaysSolution.youtubeId} currentTry={currentTry} /> + ) : ( + <Player currentTry={currentTry} /> + )} + <Search currentTry={currentTry} setSelectedSong={setSelectedSong} /> <Styled.Buttons> diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx index fcfce72..e4dfd9e 100644 --- a/src/components/Player/index.tsx +++ b/src/components/Player/index.tsx @@ -1,101 +1,142 @@ import React from "react"; -import YouTube from "react-youtube"; import { IoPlay, IoPause } from "react-icons/io5"; import { playTimes } from "../../constants"; - -import * as Styled from "./index.styled"; +import * as Styled from "../YTPlayer/index.styled"; interface Props { - id: string; currentTry: number; } -export function Player({ id, currentTry }: Props) { - const opts = { - width: "0", - height: "0", - }; +const MAX_TIME = 16; - // react-youtube doesn't export types for this - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const playerRef = React.useRef<any>(null); +export function Player({ currentTry }: Props) { + const audioRef = React.useRef<HTMLAudioElement | null>(null); const currentPlayTime = playTimes[currentTry]; - const [play, setPlay] = React.useState<boolean>(false); + const [play, setPlay] = React.useState(false); + const [currentTime, setCurrentTime] = React.useState(0); + const [isReady, setIsReady] = React.useState(false); - const [currentTime, setCurrentTime] = React.useState<number>(0); + const CDN_URL = + import.meta.env.VITE_CDN_URL || "https://yena.pinapelz.com/kheardle"; - const [isReady, setIsReady] = React.useState<boolean>(false); + const dateString = new Date().toISOString().split("T")[0]; - React.useEffect(() => { - setInterval(() => { - playerRef.current?.internalPlayer - .getCurrentTime() - .then((time: number) => { - setCurrentTime(time); - }); - }, 250); + const startPlayback = React.useCallback(() => { + const audio = audioRef.current; + if (!audio) return; + + audio.play(); + setPlay(true); + }, []); + + const stopPlayback = React.useCallback(() => { + const audio = audioRef.current; + if (!audio) return; + + audio.pause(); + audio.currentTime = 0; + setPlay(false); }, []); React.useEffect(() => { - if (play) { - if (currentTime * 1000 >= currentPlayTime) { - playerRef.current?.internalPlayer.pauseVideo(); - playerRef.current?.internalPlayer.seekTo(0); + const audio = new Audio(`${CDN_URL}/${dateString}.mp3`); + audioRef.current = audio; + + audio.addEventListener("loadeddata", () => { + setIsReady(true); + }); + + audio.addEventListener("timeupdate", () => { + setCurrentTime(audio.currentTime); + }); + + audio.addEventListener("ended", () => { + setPlay(false); + audio.currentTime = 0; + }); + + return () => { + audio.pause(); + audio.src = ""; + }; + }, [dateString]); + + React.useEffect(() => { + if (!play || !audioRef.current) return; + + const interval = setInterval(() => { + const a = audioRef.current!; + const t = a.currentTime * 1000; + + setCurrentTime(a.currentTime); + + if (t >= currentPlayTime || t >= MAX_TIME * 1000) { + a.pause(); + a.currentTime = 0; setPlay(false); } - } - }, [play, currentTime]); + }, 100); - // don't call play video each time currentTime changes - const startPlayback = React.useCallback(() => { - playerRef.current?.internalPlayer.playVideo(); - setPlay(true); - }, []); + return () => clearInterval(interval); + }, [play, currentPlayTime]); + + React.useEffect(() => { + if (!("mediaSession" in navigator)) return; + + navigator.mediaSession.setActionHandler("play", () => undefined); + navigator.mediaSession.setActionHandler("pause", () => undefined); + navigator.mediaSession.setActionHandler("previoustrack", () => undefined); + navigator.mediaSession.setActionHandler("nexttrack", () => undefined); - const setReady = React.useCallback(() => { - setIsReady(true); + return () => { + navigator.mediaSession.setActionHandler("play", null); + navigator.mediaSession.setActionHandler("pause", null); + navigator.mediaSession.setActionHandler("previoustrack", null); + navigator.mediaSession.setActionHandler("nexttrack", null); + }; }, []); return ( <> - <YouTube opts={opts} videoId={id} onReady={setReady} ref={playerRef} /> {isReady ? ( <> <Styled.ProgressBackground> - {currentTime !== 0 && <Styled.Progress value={currentTime} />} - {playTimes.map((playTime) => ( + {currentTime !== 0 && ( + <Styled.Progress value={currentTime} /> + )} + + {playTimes.map((t) => ( <Styled.Separator - style={{ left: `${(playTime / 16000) * 100}%` }} - key={playTime} + key={t} + style={{ left: `${(t / 16000) * 100}%` }} /> ))} </Styled.ProgressBackground> + <Styled.TimeStamps> <Styled.TimeStamp>1s</Styled.TimeStamp> <Styled.TimeStamp>16s</Styled.TimeStamp> </Styled.TimeStamps> - {!play && ( + + {!play ? ( <IoPlay style={{ cursor: "pointer" }} size={36} - color="var(--cl-green-6)" onClick={startPlayback} /> - )} - {play && ( + ) : ( <IoPause style={{ cursor: "pointer" }} size={36} - color="var(--cl-green-6)" - onClick={startPlayback} + onClick={stopPlayback} /> )} </> ) : ( - <p>Loading player...</p> + <p>Loading audio...</p> )} </> ); diff --git a/src/components/Result/index.tsx b/src/components/Result/index.tsx index 19f9386..6b4560a 100644 --- a/src/components/Result/index.tsx +++ b/src/components/Result/index.tsx @@ -14,17 +14,19 @@ interface SolutionProps { didGuess: boolean; currentTry: number; todaysSolution: Song; + isUnlimited?: boolean; } function Solution({ didGuess, todaysSolution, currentTry, + isUnlimited, }: SolutionProps) { return ( <> <Styled.SongTitle> - Today's song is {todaysSolution.artist} - {todaysSolution.name} + {isUnlimited ? "The song was" : "Today's song is"} {todaysSolution.artist} - {todaysSolution.name} </Styled.SongTitle> {didGuess && ( @@ -86,6 +88,8 @@ interface Props { currentTry: number; todaysSolution: Song; guesses: GuessType[]; + mode?: "daily" | "unlimited"; + onPlayAgain?: () => void; } export function Result({ @@ -93,6 +97,8 @@ export function Result({ todaysSolution, guesses, currentTry, + mode = "daily", + onPlayAgain, }: Props) { const hoursToNextDay = Math.floor( (new Date(new Date().setHours(24, 0, 0, 0)).getTime() - @@ -102,6 +108,8 @@ export function Result({ 60 ); + const isUnlimited = mode === "unlimited"; + if (didGuess) { const textForTry = ["Perfect!", "Wow!", "Super!", "Congrats!", "Nice!"]; @@ -113,13 +121,20 @@ export function Result({ todaysSolution={todaysSolution} didGuess={didGuess} currentTry={currentTry} + isUnlimited={isUnlimited} /> - <ShareButton guesses={guesses} variant="green" /> + {!isUnlimited && <ShareButton guesses={guesses} variant="green" />} - <Styled.TimeToNext> - Remember to come back in {hoursToNextDay} hours! - </Styled.TimeToNext> + {isUnlimited && onPlayAgain ? ( + <Button variant="green" onClick={onPlayAgain}> + Play Again + </Button> + ) : ( + <Styled.TimeToNext> + Remember to come back in {hoursToNextDay} hours! + </Styled.TimeToNext> + )} </> ); } @@ -132,13 +147,20 @@ export function Result({ todaysSolution={todaysSolution} didGuess={didGuess} currentTry={currentTry} + isUnlimited={isUnlimited} /> - <ShareButton guesses={guesses} variant="red" /> + {!isUnlimited && <ShareButton guesses={guesses} variant="red" />} - <Styled.TimeToNext> - Try again in {hoursToNextDay} hours. - </Styled.TimeToNext> + {isUnlimited && onPlayAgain ? ( + <Button variant="red" onClick={onPlayAgain}> + Play Again + </Button> + ) : ( + <Styled.TimeToNext> + Try again in {hoursToNextDay} hours. + </Styled.TimeToNext> + )} </> ); } diff --git a/src/components/Player/index.styled.ts b/src/components/YTPlayer/index.styled.ts index 3c98f1e..3c98f1e 100644 --- a/src/components/Player/index.styled.ts +++ b/src/components/YTPlayer/index.styled.ts diff --git a/src/components/YTPlayer/index.tsx b/src/components/YTPlayer/index.tsx new file mode 100644 index 0000000..1aac9ac --- /dev/null +++ b/src/components/YTPlayer/index.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import YouTube from "react-youtube"; +import { IoPlay, IoPause } from "react-icons/io5"; +import { playTimes } from "../../constants"; +import * as Styled from "./index.styled"; + +interface Props { + id: string; + currentTry: number; +} + +export function Player({ id, currentTry }: Props) { + const opts = { + width: "0", + height: "0", + }; + + // react-youtube doesn't export types for this + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const playerRef = React.useRef<any>(null); + + const currentPlayTime = playTimes[currentTry]; + + const [play, setPlay] = React.useState<boolean>(false); + + const [currentTime, setCurrentTime] = React.useState<number>(0); + + const [isReady, setIsReady] = React.useState<boolean>(false); + + React.useEffect(() => { + setInterval(() => { + playerRef.current?.internalPlayer + .getCurrentTime() + .then((time: number) => { + setCurrentTime(time); + }); + }, 250); + }, []); + + React.useEffect(() => { + if (play) { + if (currentTime * 1000 >= currentPlayTime) { + playerRef.current?.internalPlayer.pauseVideo(); + playerRef.current?.internalPlayer.seekTo(0); + setPlay(false); + } + } + }, [play, currentTime]); + + // don't call play video each time currentTime changes + const startPlayback = React.useCallback(() => { + playerRef.current?.internalPlayer.playVideo(); + setPlay(true); + }, []); + + const setReady = React.useCallback(() => { + setIsReady(true); + }, []); + + return ( + <> + <YouTube opts={opts} videoId={id} onReady={setReady} ref={playerRef} /> + {isReady ? ( + <> + <Styled.ProgressBackground> + {currentTime !== 0 && <Styled.Progress value={currentTime} />} + {playTimes.map((playTime) => ( + <Styled.Separator + style={{ left: `${(playTime / 16000) * 100}%` }} + key={playTime} + /> + ))} + </Styled.ProgressBackground> + <Styled.TimeStamps> + <Styled.TimeStamp>1s</Styled.TimeStamp> + <Styled.TimeStamp>16s</Styled.TimeStamp> + </Styled.TimeStamps> + {!play && ( + <IoPlay + style={{ cursor: "pointer" }} + size={36} + color="var(--cl-green-6)" + onClick={startPlayback} + /> + )} + {play && ( + <IoPause + style={{ cursor: "pointer" }} + size={36} + color="var(--cl-green-6)" + onClick={startPlayback} + /> + )} + </> + ) : ( + <p>Loading player...</p> + )} + </> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 0e97ada..4264a1c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,7 @@ export { Guess } from "./Guess"; export { Header } from "./Header"; export { InfoPopUp } from "./InfoPopUp"; export { MiniYouTubePlayer} from "./MiniYouTubePlayer"; +export { Player as YTPlayer } from "./YTPlayer"; export { Player } from "./Player"; export { Result } from "./Result"; export { Search } from "./Search"; |
