aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-06-03 17:22:48 -0700
committerPinapelz <yukais@pinapelz.com>2026-06-03 17:22:48 -0700
commit14172f9dd64ce91ba5cf51f82c53deb6a81d68a6 (patch)
tree5e12ce4e30ecaed9a2aac48d2959d99a4d8b4ef7 /src/components
parent818db3ef4aadf489dba5ba8ba4f3bb4e150f0b22 (diff)
create daily/unlimited mode, CDN audio file for daily mode
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Game/index.tsx21
-rw-r--r--src/components/Player/index.tsx137
-rw-r--r--src/components/Result/index.tsx40
-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.tsx100
-rw-r--r--src/components/index.ts1
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&apos;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";
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage