diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-06-03 17:22:48 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-06-03 17:22:48 -0700 |
| commit | 14172f9dd64ce91ba5cf51f82c53deb6a81d68a6 (patch) | |
| tree | 5e12ce4e30ecaed9a2aac48d2959d99a4d8b4ef7 /src/pages | |
| parent | 818db3ef4aadf489dba5ba8ba4f3bb4e150f0b22 (diff) | |
create daily/unlimited mode, CDN audio file for daily mode
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/DailyPage.tsx | 111 | ||||
| -rw-r--r-- | src/pages/LandingPage.tsx | 92 | ||||
| -rw-r--r-- | src/pages/UnlimitedPage.tsx | 78 |
3 files changed, 281 insertions, 0 deletions
diff --git a/src/pages/DailyPage.tsx b/src/pages/DailyPage.tsx new file mode 100644 index 0000000..5033366 --- /dev/null +++ b/src/pages/DailyPage.tsx @@ -0,0 +1,111 @@ +import React from "react"; + +import { Song } from "../types/song"; +import { GuessType } from "../types/guess"; +import { getDailySolution } from "../helpers/fetchSolution"; +import { useGameState } from "../hooks/useGameState"; + +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 firstRun = localStorage.getItem("firstRun") === null; + + const initialGuess = { + song: undefined, + state: undefined, + } as GuessType; + + // --- Stats import logic --- + function reloadWithoutQueryParameters() { + location.replace(location.pathname); + } + const urlHash = window.location.hash; + const urlQueryParametersStart = urlHash.indexOf("?"); + const statsImportQueryParameter = + new URLSearchParams(urlHash.substring(urlQueryParametersStart)).get( + "statsImport" + ) || ""; + + function importStats() { + if (statsImportQueryParameter) { + const importedStats = JSON.parse(statsImportQueryParameter); + if (Array.isArray(importedStats)) { + importedStats.forEach((day) => { + if (Array.isArray(day.guesses)) { + if (day.guesses.length == 5) { + day.guesses.push(initialGuess); + } + } + }); + } + localStorage.setItem("stats", JSON.stringify(importedStats)); + reloadWithoutQueryParameters(); + } + } + + if (statsImportQueryParameter) { + if ( + confirm( + "Do you want to import your previous stats? This will overwrite any stats on this site." + ) + ) { + importStats(); + } else { + reloadWithoutQueryParameters(); + } + } + + React.useEffect(() => { + getDailySolution().then((solution) => setTodaysSolution(solution)); + }, []); + + const { + guesses, + currentTry, + setSelectedSong, + didGuess, + skip, + guess, + } = useGameState({ solution: todaysSolution, persist: true }); + + const [isInfoPopUpOpen, setIsInfoPopUpOpen] = + React.useState<boolean>(firstRun); + + const openInfoPopUp = React.useCallback(() => { + setIsInfoPopUpOpen(true); + }, []); + + const closeInfoPopUp = React.useCallback(() => { + if (firstRun) { + localStorage.setItem("firstRun", "false"); + } + setIsInfoPopUpOpen(false); + }, [localStorage.getItem("firstRun")]); + + if (todaysSolution === null) { + return null; + } + + return ( + <main> + <Header openInfoPopUp={openInfoPopUp} /> + {isInfoPopUpOpen && <InfoPopUp onClose={closeInfoPopUp} />} + <Styled.Container> + <Game + guesses={guesses} + didGuess={didGuess} + todaysSolution={todaysSolution} + currentTry={currentTry} + setSelectedSong={setSelectedSong} + skip={skip} + guess={guess} + /> + </Styled.Container> + <Footer /> + </main> + ); +} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx new file mode 100644 index 0000000..e1d29cd --- /dev/null +++ b/src/pages/LandingPage.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import styled from "styled-components"; + +import { appName } from "../constants"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + gap: 24px; +`; + +const Title = styled.h1` + font-family: "Roboto Mono", monospace; + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--cl-white); +`; + +const Subtitle = styled.p` + font-family: "Roboto Mono", monospace; + font-size: 0.9rem; + color: var(--cl-gray-6); + margin: 0; +`; + +const ButtonGroup = styled.div` + display: flex; + gap: 16px; + margin-top: 16px; + + @media (max-width: 480px) { + flex-direction: column; + width: 100%; + padding: 0 24px; + } +`; + +const ModeButton = styled.button<{ variant?: "green" | "purple" }>` + font-family: "Roboto Mono", monospace; + font-size: 1rem; + font-weight: 600; + padding: 16px 32px; + border: 2px solid + ${({ variant }) => + variant === "purple" ? "var(--cl-purple, #a855f7)" : "var(--cl-green-6)"}; + background: transparent; + color: ${({ variant }) => + variant === "purple" ? "var(--cl-purple, #a855f7)" : "var(--cl-green-6)"}; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: ${({ variant }) => + variant === "purple" ? "var(--cl-purple, #a855f7)" : "var(--cl-green-6)"}; + color: var(--cl-black, #000); + } +`; + +const ModeDescription = styled.span` + display: block; + font-size: 0.7rem; + font-weight: 400; + color: var(--cl-gray-6); + margin-top: 4px; +`; + +export function LandingPage() { + const navigate = useNavigate(); + + return ( + <Container> + <Title>{appName}</Title> + <Subtitle>Choose a game mode</Subtitle> + <ButtonGroup> + <ModeButton onClick={() => navigate("/daily")}> + Daily + <ModeDescription>One song per day</ModeDescription> + </ModeButton> + <ModeButton variant="purple" onClick={() => navigate("/unlimited")}> + Unlimited + <ModeDescription>Endless songs, no limits</ModeDescription> + </ModeButton> + </ButtonGroup> + </Container> + ); +} diff --git a/src/pages/UnlimitedPage.tsx b/src/pages/UnlimitedPage.tsx new file mode 100644 index 0000000..761d2b9 --- /dev/null +++ b/src/pages/UnlimitedPage.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +import { Song } from "../types/song"; +import { getSelectSolution } from "../helpers/fetchSolution"; +import { useGameState } from "../hooks/useGameState"; + +import { Header, InfoPopUp, Game, Footer } from "../components"; + +import * as Styled from "../app.styled"; + +export function UnlimitedPage() { + const [solution, setSolution] = React.useState<Song | null>(null); + + const firstRun = localStorage.getItem("firstRun") === null; + + function fetchNewSong() { + setSolution(null); + getSelectSolution().then((s) => setSolution(s)); + } + + React.useEffect(() => { + fetchNewSong(); + }, []); + + const { + guesses, + currentTry, + setSelectedSong, + didGuess, + skip, + guess, + reset, + } = useGameState({ solution, persist: false }); + + const playAgain = React.useCallback(() => { + reset(); + fetchNewSong(); + }, [reset]); + + const [isInfoPopUpOpen, setIsInfoPopUpOpen] = + React.useState<boolean>(firstRun); + + const openInfoPopUp = React.useCallback(() => { + setIsInfoPopUpOpen(true); + }, []); + + const closeInfoPopUp = React.useCallback(() => { + if (firstRun) { + localStorage.setItem("firstRun", "false"); + } + setIsInfoPopUpOpen(false); + }, [localStorage.getItem("firstRun")]); + + if (solution === null) { + return null; + } + + return ( + <main> + <Header openInfoPopUp={openInfoPopUp} /> + {isInfoPopUpOpen && <InfoPopUp onClose={closeInfoPopUp} />} + <Styled.Container> + <Game + guesses={guesses} + didGuess={didGuess} + todaysSolution={solution} + currentTry={currentTry} + setSelectedSong={setSelectedSong} + skip={skip} + guess={guess} + mode="unlimited" + onPlayAgain={playAgain} + /> + </Styled.Container> + <Footer /> + </main> + ); +} |
