diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-06-04 18:34:18 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-06-04 18:34:18 -0700 |
| commit | 95602704d53bb72d96d4947869bce63830888d52 (patch) | |
| tree | 89f158d98beb5667ececf371258545aef5c64732 | |
| parent | 66a7890647d225957d81c8b361a46d0f797025da (diff) | |
overhaul how daily is updated from server to client
| -rw-r--r-- | server/index.ts | 6 | ||||
| -rw-r--r-- | src/components/Game/index.tsx | 25 | ||||
| -rw-r--r-- | src/components/Player/index.tsx | 14 | ||||
| -rw-r--r-- | src/helpers/fetchSolution.ts | 21 | ||||
| -rw-r--r-- | src/pages/DailyPage.tsx | 10 | ||||
| -rw-r--r-- | src/react-app-env.d.ts | 9 |
6 files changed, 54 insertions, 31 deletions
diff --git a/server/index.ts b/server/index.ts index 0bcfed6..c9cc6b4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -19,8 +19,7 @@ const DAILY_SONGS_FILE = path.resolve( type Song = (typeof songs)[number]; type DailySongs = Record<string, Song>; -function getObfuscationKey(): Buffer { - const date = new Date().toISOString().split('T')[0]; +function getObfuscationKey(date = getUtcDate()): Buffer { return Buffer.from(SALT + date); } @@ -75,10 +74,11 @@ function getDailySong(date: string): Song { app.get('/today', (_req, res) => { const date = getUtcDate(); const song = getDailySong(date); - const obfuscationKey = getObfuscationKey(); + const obfuscationKey = getObfuscationKey(date); const songJson = JSON.stringify(song); const obfuscatedData = xorBuffer(Buffer.from(songJson, 'utf8'), obfuscationKey); res.json({ + date, data: obfuscatedData.toString('hex'), }); }); diff --git a/src/components/Game/index.tsx b/src/components/Game/index.tsx index cde682d..59f9289 100644 --- a/src/components/Game/index.tsx +++ b/src/components/Game/index.tsx @@ -11,6 +11,7 @@ import * as Styled from "./index.styled"; interface Props { guesses: GuessType[]; todaysSolution: Song; + dailyDate?: string; currentTry: number; didGuess: boolean; setSelectedSong: React.Dispatch<React.SetStateAction<Song | undefined>>; @@ -24,17 +25,10 @@ function getUtcDate() { return new Date().toISOString().split("T")[0]; } -function checkDailyIsGenerated(): boolean { - const CDN_URL = import.meta.env.VITE_CDN_URL; - if (!CDN_URL) return false; - - const date = getUtcDate(); - return !!localStorage.getItem(`${CDN_URL}/${date}.mp3`); -} - export function Game({ guesses, todaysSolution, + dailyDate, currentTry, didGuess, setSelectedSong, @@ -43,22 +37,23 @@ export function Game({ mode = "daily", onPlayAgain, }: Props) { - const [sessionDate] = React.useState(() => getUtcDate()); const recentFinishedPlay = localStorage.getItem("recentFinishedPlay"); const hasFinishedCurrentRound = didGuess || currentTry >= guesses.length; - const isGameOver = hasFinishedCurrentRound; + const hasFinishedResponseDaily = + mode === "daily" && !!dailyDate && recentFinishedPlay === dailyDate; + const isGameOver = hasFinishedCurrentRound || hasFinishedResponseDaily; const isBlocked = mode === "daily" && - !!recentFinishedPlay && - new Date(sessionDate) > new Date(recentFinishedPlay) && - !checkDailyIsGenerated(); + !!dailyDate && + !hasFinishedResponseDaily && + new Date(getUtcDate()) > new Date(dailyDate); React.useEffect(() => { if (mode !== "daily") return; if (!hasFinishedCurrentRound) return; - localStorage.setItem("recentFinishedPlay", sessionDate); - }, [mode, hasFinishedCurrentRound, sessionDate]); + localStorage.setItem("recentFinishedPlay", dailyDate ?? getUtcDate()); + }, [mode, hasFinishedCurrentRound, dailyDate]); if (isBlocked) { return <h1>Daily MIXX is not available yet. Check back soon!</h1>; diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx index 841d256..bbf35b0 100644 --- a/src/components/Player/index.tsx +++ b/src/components/Player/index.tsx @@ -33,6 +33,7 @@ export function Player({ currentTry }: Props) { const [play, setPlay] = React.useState(false); const [currentTime, setCurrentTime] = React.useState(0); const [isReady, setIsReady] = React.useState(false); + const [isUnavailable, setIsUnavailable] = React.useState(false); const [volume, setVolume] = React.useState(loadVolume); const CDN_URL = @@ -65,12 +66,21 @@ export function Player({ currentTry }: Props) { ); React.useEffect(() => { + setIsReady(false); + setIsUnavailable(false); + const audio = new Audio(`${CDN_URL}/${dateString}.mp3`); audio.volume = loadVolume(); audioRef.current = audio; audio.addEventListener("loadeddata", () => { setIsReady(true); + setIsUnavailable(false); + }); + + audio.addEventListener("error", () => { + setIsReady(false); + setIsUnavailable(true); }); audio.addEventListener("timeupdate", () => { @@ -138,7 +148,9 @@ export function Player({ currentTry }: Props) { return ( <> - {isReady ? ( + {isUnavailable ? ( + <p>We are still generating the audio! Come back later.</p> + ) : isReady ? ( <> <Styled.ProgressBackground> {currentTime !== 0 && ( diff --git a/src/helpers/fetchSolution.ts b/src/helpers/fetchSolution.ts index 10c4fa1..e3010fe 100644 --- a/src/helpers/fetchSolution.ts +++ b/src/helpers/fetchSolution.ts @@ -20,25 +20,32 @@ function xor(data: Uint8Array, key: Uint8Array): Uint8Array { return output; } -function getObfuscationKey(): Uint8Array { - const date = new Date().toISOString().split('T')[0]; +function getObfuscationKey(date = new Date().toISOString().split('T')[0]): Uint8Array { return new TextEncoder().encode(SALT + date); } -function decryptResponse(data: string): Song { - const obfuscationKey = getObfuscationKey(); +function decryptResponse(data: string, date?: string): Song { + const obfuscationKey = getObfuscationKey(date); const obfuscatedBytes = hexToBytes(data); const decrypted = xor(obfuscatedBytes, obfuscationKey); return JSON.parse(new TextDecoder().decode(decrypted)) as Song; } -export async function getDailySolution(): Promise<Song> { +export interface DailySolution { + date: string; + song: Song; +} + +export async function getDailySolution(): Promise<DailySolution> { const solutionData = await fetch(`${API_URL}/today`); if (!solutionData.ok) { throw new Error(`Failed to fetch solution: ${solutionData.statusText}`); } - const { data } = await solutionData.json(); - return decryptResponse(data); + const { data, date } = await solutionData.json(); + return { + date, + song: decryptResponse(data, date), + }; } export async function getSelectSolution(): Promise<Song> { diff --git a/src/pages/DailyPage.tsx b/src/pages/DailyPage.tsx index 5033366..24ac20c 100644 --- a/src/pages/DailyPage.tsx +++ b/src/pages/DailyPage.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { Song } from "../types/song"; import { GuessType } from "../types/guess"; -import { getDailySolution } from "../helpers/fetchSolution"; +import { DailySolution, getDailySolution } from "../helpers/fetchSolution"; import { useGameState } from "../hooks/useGameState"; import { Header, InfoPopUp, Game, Footer } from "../components"; @@ -10,7 +9,7 @@ import { Header, InfoPopUp, Game, Footer } from "../components"; import * as Styled from "../app.styled"; export function DailyPage() { - const [todaysSolution, setTodaysSolution] = React.useState<Song | null>(null); + const [todaysSolution, setTodaysSolution] = React.useState<DailySolution | null>(null); const firstRun = localStorage.getItem("firstRun") === null; @@ -70,7 +69,7 @@ export function DailyPage() { didGuess, skip, guess, - } = useGameState({ solution: todaysSolution, persist: true }); + } = useGameState({ solution: todaysSolution?.song ?? null, persist: true }); const [isInfoPopUpOpen, setIsInfoPopUpOpen] = React.useState<boolean>(firstRun); @@ -98,7 +97,8 @@ export function DailyPage() { <Game guesses={guesses} didGuess={didGuess} - todaysSolution={todaysSolution} + todaysSolution={todaysSolution.song} + dailyDate={todaysSolution.date} currentTry={currentTry} setSelectedSong={setSelectedSong} skip={skip} diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 6431bc5..f6733c9 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -1 +1,10 @@ /// <reference types="react-scripts" /> +declare global { + interface ImportMetaEnv { + readonly VITE_CDN_URL?: string; + } + + interface ImportMeta { + readonly env: ImportMetaEnv; + } +} |
