aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages
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/pages
parent818db3ef4aadf489dba5ba8ba4f3bb4e150f0b22 (diff)
create daily/unlimited mode, CDN audio file for daily mode
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/DailyPage.tsx111
-rw-r--r--src/pages/LandingPage.tsx92
-rw-r--r--src/pages/UnlimitedPage.tsx78
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>
+ );
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage