From 14172f9dd64ce91ba5cf51f82c53deb6a81d68a6 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Wed, 3 Jun 2026 17:22:48 -0700 Subject: create daily/unlimited mode, CDN audio file for daily mode --- src/components/Game/index.tsx | 21 +++-- src/components/Player/index.styled.ts | 39 --------- src/components/Player/index.tsx | 137 +++++++++++++++++++++----------- src/components/Result/index.tsx | 42 +++++++--- src/components/YTPlayer/index.styled.ts | 39 +++++++++ src/components/YTPlayer/index.tsx | 100 +++++++++++++++++++++++ src/components/index.ts | 1 + 7 files changed, 275 insertions(+), 104 deletions(-) delete mode 100644 src/components/Player/index.styled.ts create mode 100644 src/components/YTPlayer/index.styled.ts create mode 100644 src/components/YTPlayer/index.tsx (limited to 'src/components') 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>; 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) => ( - + ))} - + {mode === "unlimited" ? ( + + ) : ( + + )} + diff --git a/src/components/Player/index.styled.ts b/src/components/Player/index.styled.ts deleted file mode 100644 index 3c98f1e..0000000 --- a/src/components/Player/index.styled.ts +++ /dev/null @@ -1,39 +0,0 @@ -import styled from "styled-components"; - -export const ProgressBackground = styled.div` - position: relative; - width: 100%; - height: 12px; - background-color: var(--cl-gray-2); - border: 1px solid ${({ theme }) => theme.border}; - margin: 24px 0 4px 0; -`; - -export const Progress = styled.div<{ value: number }>` - width: ${({ value }) => value * 6.25}%; - height: 100%; - background-color: ${({ theme }) => theme.green}; - transition: width 0.5s; -`; - -export const Separator = styled.div` - position: absolute; - top: 0; - width: 1px; - height: 100%; - background-color: ${({ theme }) => theme.border}; -`; - -export const TimeStamps = styled.div` - display: flex; - justify-content: space-between; - width: 100%; - margin-bottom: 12px; -`; - -export const TimeStamp = styled.p` - margin: 0; - font-family: "Roboto Mono", monospace; - font-size: 0.7rem; - color: var(--cl-gray-5); -`; 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(null); +export function Player({ currentTry }: Props) { + const audioRef = React.useRef(null); const currentPlayTime = playTimes[currentTry]; - const [play, setPlay] = React.useState(false); + const [play, setPlay] = React.useState(false); + const [currentTime, setCurrentTime] = React.useState(0); + const [isReady, setIsReady] = React.useState(false); - const [currentTime, setCurrentTime] = React.useState(0); + const CDN_URL = + import.meta.env.VITE_CDN_URL || "https://yena.pinapelz.com/kheardle"; - const [isReady, setIsReady] = React.useState(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 ( <> - {isReady ? ( <> - {currentTime !== 0 && } - {playTimes.map((playTime) => ( + {currentTime !== 0 && ( + + )} + + {playTimes.map((t) => ( ))} + 1s 16s - {!play && ( + + {!play ? ( - )} - {play && ( + ) : ( )} ) : ( -

Loading player...

+

Loading audio...

)} ); 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 ( <> - Today's song is {todaysSolution.artist} - {todaysSolution.name} + {isUnlimited ? "The song was" : "Today's song is"} {todaysSolution.artist} - {todaysSolution.name} {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} /> - - - - Remember to come back in {hoursToNextDay} hours! - + {!isUnlimited && } + + {isUnlimited && onPlayAgain ? ( + + ) : ( + + Remember to come back in {hoursToNextDay} hours! + + )} ); } @@ -132,13 +147,20 @@ export function Result({ todaysSolution={todaysSolution} didGuess={didGuess} currentTry={currentTry} + isUnlimited={isUnlimited} /> - + {!isUnlimited && } - - Try again in {hoursToNextDay} hours. - + {isUnlimited && onPlayAgain ? ( + + ) : ( + + Try again in {hoursToNextDay} hours. + + )} ); } diff --git a/src/components/YTPlayer/index.styled.ts b/src/components/YTPlayer/index.styled.ts new file mode 100644 index 0000000..3c98f1e --- /dev/null +++ b/src/components/YTPlayer/index.styled.ts @@ -0,0 +1,39 @@ +import styled from "styled-components"; + +export const ProgressBackground = styled.div` + position: relative; + width: 100%; + height: 12px; + background-color: var(--cl-gray-2); + border: 1px solid ${({ theme }) => theme.border}; + margin: 24px 0 4px 0; +`; + +export const Progress = styled.div<{ value: number }>` + width: ${({ value }) => value * 6.25}%; + height: 100%; + background-color: ${({ theme }) => theme.green}; + transition: width 0.5s; +`; + +export const Separator = styled.div` + position: absolute; + top: 0; + width: 1px; + height: 100%; + background-color: ${({ theme }) => theme.border}; +`; + +export const TimeStamps = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + margin-bottom: 12px; +`; + +export const TimeStamp = styled.p` + margin: 0; + font-family: "Roboto Mono", monospace; + font-size: 0.7rem; + color: var(--cl-gray-5); +`; 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(null); + + const currentPlayTime = playTimes[currentTry]; + + const [play, setPlay] = React.useState(false); + + const [currentTime, setCurrentTime] = React.useState(0); + + const [isReady, setIsReady] = React.useState(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 ( + <> + + {isReady ? ( + <> + + {currentTime !== 0 && } + {playTimes.map((playTime) => ( + + ))} + + + 1s + 16s + + {!play && ( + + )} + {play && ( + + )} + + ) : ( +

Loading player...

+ )} + + ); +} 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"; -- cgit v1.2.3