diff --git a/src/app/api/common/answer/answerAPI.ts b/src/app/api/common/answer/answerAPI.ts index 1f4c00e..9ca35a5 100644 --- a/src/app/api/common/answer/answerAPI.ts +++ b/src/app/api/common/answer/answerAPI.ts @@ -1,14 +1,66 @@ -import { ICheckAnswerResult } from "@/interfaces/interfaces"; +import { ICheckAnswerResult, IGetAnswerResult } from "@/interfaces/interfaces"; import { getDailyKey } from "@/util/time"; import { NextRequest, NextResponse } from "next/server"; import { firestore } from "@/app/api/firebase"; +async function getGameAnswerDataFromDatabase(database_collection_name: string, answerKey: string) { + console.log("Getting answer for ", answerKey); + const answerDocRef = firestore + .collection(database_collection_name) + .doc("answers") + .collection("answers") + .doc(answerKey); + + const answerDocData = (await answerDocRef.get()).data(); + + console.log("Answer doc data:", answerDocData); + return answerDocData +} + +export async function getGameAnswer( + req: NextRequest, + database_collection_name: string +): Promise> { + console.log("Fetching today's answer from the database"); + const now = new Date(); + console.log("Today is ", now); + const answerKey = getDailyKey(now); + const answerDocData = await getGameAnswerDataFromDatabase(database_collection_name, answerKey) + + // if the answer is not in the database, return an error + // runbook: if this happens, the reset did NOT happen correctly + if (!answerDocData || !answerDocData.song || !answerDocData.song.length) { + console.error("Answer not found in the database for date", answerKey); + return NextResponse.json( + { + message: `Answer not found in the database for date ${answerKey}`, + song: "", + correct: false, + startTimeStamp: "", + endTimeStamp: "", + }, + { status: 500 } + ); + } + + return NextResponse.json( + { + message: `Here is today's answer.`, + correct: true, + song: answerDocData.song, + startTimeStamp: answerDocData.startTimeStamp, + endTimeStamp: answerDocData.endTimeStamp, + }, + { status: 200 } + ); +} + /** * Check if the answer is correct. * Checks the answer in the database in the "today" document. * @param req */ -export async function getGameAnswer( +export async function checkGameAnswer( req: NextRequest, database_collection_name: string ): Promise> { @@ -25,20 +77,12 @@ export async function getGameAnswer( { status: 400 } ); } + console.log("Fetching today's answer from the database"); const now = new Date(); console.log("Today is ", now); const answerKey = getDailyKey(now); - console.log("Getting answer for ", answerKey); - const answerDocRef = firestore - .collection(database_collection_name) - .doc("answers") - .collection("answers") - .doc(answerKey); - - const answerDocData = (await answerDocRef.get()).data(); - - console.log("Answer doc data:", answerDocData); + const answerDocData = await getGameAnswerDataFromDatabase(database_collection_name, answerKey) // if the answer is not in the database, return an error // runbook: if this happens, the reset did NOT happen correctly diff --git a/src/app/api/game/answer/route.ts b/src/app/api/game/answer/route.ts index 5533932..feb08f1 100644 --- a/src/app/api/game/answer/route.ts +++ b/src/app/api/game/answer/route.ts @@ -1,10 +1,19 @@ import { NextRequest, NextResponse } from "next/server"; import { FIREBASE_DATABASE_COLLECTION_NAME } from "@/constants"; -import { ICheckAnswerResult } from "@/interfaces/interfaces"; -import { getGameAnswer } from "@/app/api/common/answer/answerAPI"; +import { ICheckAnswerResult, IGetAnswerResult } from "@/interfaces/interfaces"; +import { checkGameAnswer, getGameAnswer } from "@/app/api/common/answer/answerAPI"; export async function PATCH( req: NextRequest ): Promise> { - return await getGameAnswer(req, FIREBASE_DATABASE_COLLECTION_NAME); + return await checkGameAnswer(req, FIREBASE_DATABASE_COLLECTION_NAME); } + +export async function GET( + req: NextRequest +): Promise> { + return await getGameAnswer( + req, + FIREBASE_DATABASE_COLLECTION_NAME + ); +} \ No newline at end of file diff --git a/src/app/api/instrumental/answer/route.ts b/src/app/api/instrumental/answer/route.ts index c1278ef..08dc6b2 100644 --- a/src/app/api/instrumental/answer/route.ts +++ b/src/app/api/instrumental/answer/route.ts @@ -1,13 +1,22 @@ import { NextRequest, NextResponse } from "next/server"; import { FIREBASE_DATABASE_COLLECTION_NAME_INSTRUMENTAL_MODE } from "@/constants"; -import { ICheckAnswerResult } from "@/interfaces/interfaces"; -import { getGameAnswer } from "@/app/api/common/answer/answerAPI"; +import { ICheckAnswerResult, IGetAnswerResult } from "@/interfaces/interfaces"; +import { checkGameAnswer, getGameAnswer} from "@/app/api/common/answer/answerAPI"; export async function PATCH( req: NextRequest ): Promise> { - return await getGameAnswer( + return await checkGameAnswer( req, FIREBASE_DATABASE_COLLECTION_NAME_INSTRUMENTAL_MODE ); } + +export async function GET( + req: NextRequest +): Promise> { + return await getGameAnswer( + req, + FIREBASE_DATABASE_COLLECTION_NAME_INSTRUMENTAL_MODE + ); +} \ No newline at end of file diff --git a/src/app/legend/wip_page.tsx b/src/app/legend/page.tsx similarity index 100% rename from src/app/legend/wip_page.tsx rename to src/app/legend/page.tsx diff --git a/src/app/services/gameService.ts b/src/app/services/gameService.ts index de31407..d67ed3c 100644 --- a/src/app/services/gameService.ts +++ b/src/app/services/gameService.ts @@ -1,7 +1,35 @@ import { ValidAPIBaseEndpoint } from "@/constants"; -import { HttpError, ICheckAnswerResult } from "@/interfaces/interfaces"; +import { HttpError, ICheckAnswerResult, IGetAnswerResult } from "@/interfaces/interfaces"; import { getNextResetTime } from "@/util/time"; +export async function getAnswer( + base_endpoint: ValidAPIBaseEndpoint +): Promise { + const response = await fetch(`${base_endpoint}/answer`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + let errorMessage = await response.text(); + if (response.status === 429) { + errorMessage = "Try again in a minute."; + } + throw new HttpError(errorMessage, response.status); + } + + const data: IGetAnswerResult = await response.json(); + + return { + message: data.message, + song: data.song, + startTimeStamp: data.startTimeStamp, + endTimeStamp: data.endTimeStamp, + }; +} + export async function checkAnswer( guess: string, base_endpoint: ValidAPIBaseEndpoint diff --git a/src/components/Game/Game.tsx b/src/components/Game/Game.tsx index 7d726f2..e4e104c 100644 --- a/src/components/Game/Game.tsx +++ b/src/components/Game/Game.tsx @@ -109,12 +109,6 @@ export default function Game({ useEffect(() => { logEvent("page_view"); - if (getYearMonthDay(new Date()) === "2025-11-14") { - createSystemNotification( - "Something broke earlier today! The song did not update properly. If you are having problems guessing the song, try running the game in Incognito mode!" - ); - } - // if we are in legend mode, add a gradient to the background const root = document.documentElement; const body = document.body; @@ -130,7 +124,7 @@ export default function Game({ // optional cleanup of CSS vars root.style.removeProperty("--overlay-gradient"); // advertise legend mode - // createInformationalNotification("Legend mode is out! Go to the main menu and check it out!", "Great news!") + createInformationalNotification("Legend mode is out! Go to the main menu and check it out!", "Great news!") } // load volume from localStorage diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index e2f896f..2f2d91c 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -40,7 +40,7 @@ export default function Menu() { diff --git a/src/components/modals/DisclaimerModal/DisclaimerModal.tsx b/src/components/modals/DisclaimerModal/DisclaimerModal.tsx index 5e2151c..0ffbbde 100644 --- a/src/components/modals/DisclaimerModal/DisclaimerModal.tsx +++ b/src/components/modals/DisclaimerModal/DisclaimerModal.tsx @@ -14,6 +14,7 @@ import { PRIMARY_COLOR } from "@/config/theme"; import { useButtonSound } from "@/hooks/audio/useButtonSound"; import ModalTitle from "../ModalTitle"; import { SUPPORT_EMAIL } from "@/constants"; +import SongLyrics from "../SongLyrics"; export default function DisclaimerModal({ openState, @@ -41,8 +42,11 @@ export default function DisclaimerModal({ Attribution / Credits Game/Coding Yours Truly: Dylan + + I'm super honored that you're playing and enjoying Epicdle! If you enjoyed the game, the best way to support me is to share it with your friends and let me know how much you enjoy the game! + - Got any problems or questions? Shoot me an email:{" "} + Got any problems, questions, or suggestions? Please shoot me an email:{" "} Music Jorge Rivera-Herrans, Winion Entertainment LLC + + + + YouTube + + + + SIM Instrumentals + + + + YouTube Playlist + + + Album Art Disclaimer - I'm just a humble developer who codes for fun. This is a completely + I'm just a humble developer who codes fun things. I will never paywall any features, add ads, or otherwise seek to actively monetize Epicdle. Epicdle is a completely free, fan-made project created out of love for the musical. - If you enjoy the game and would like to support me to help cover the - game server maintenance and other costs coming out of my own pocket, + Still, if you enjoy the game and would like to support me to help cover the + game server maintenance and other related costs coming out of my own pocket, you can do so{" "} (null); + const [isLoading, setIsLoading] = useState(false); + const [confirmingReveal, setConfirmingReveal] = useState(false); + const confirmTimeoutRef = useRef(null); let isLegendary = false; if (base_endpoint === INSTRUMENTAL_GAME_API_BASE_ENDPOINT) { isLegendary = true; } + const { logEvent } = useFirebaseAnalytics(); return ( You didn't guess today's song... + + + + {answer && ( + + The answer was: {answer} + + )} + | null; color?: string; }; export default function SongLyrics({ children, + mtOverride = "xl", color = PRIMARY_COLOR, }: Readonly) { return ( -
+
{children}
); diff --git a/src/components/modals/TutorialModal/TutorialModal.tsx b/src/components/modals/TutorialModal/TutorialModal.tsx index a6f8634..91ffef7 100644 --- a/src/components/modals/TutorialModal/TutorialModal.tsx +++ b/src/components/modals/TutorialModal/TutorialModal.tsx @@ -1,8 +1,8 @@ "use client"; -import { Button, List, Modal, Stack, Text, ThemeIcon } from "@mantine/core"; +import { Anchor, Button, List, Modal, Stack, Text, ThemeIcon } from "@mantine/core"; import { UseDisclosureHandlers } from "@mantine/hooks"; import styles from "./TutorialModal.module.css"; -import { MAX_GUESSES } from "@/constants"; +import { MAX_GUESSES, SUPPORT_EMAIL } from "@/constants"; import { PRIMARY_COLOR } from "@/config/theme"; import { useButtonSound } from "@/hooks/audio/useButtonSound"; import ModalTitle from "../ModalTitle"; @@ -52,8 +52,8 @@ export default function TutorialModal({ <> Give me rhythms and a drumbeat Give me tempos and a motif - Without the lyrics songs are scary - But I wanna be legendary + Without the lyrics, songs are scary + But I wanna be legendary! ) : ( <> @@ -105,7 +105,7 @@ export default function TutorialModal({ - Repeat until you get it! + Repeat until you get it! Each attempt reveals more of the song. @@ -113,7 +113,7 @@ export default function TutorialModal({ {MAX_GUESSES.toString()} or fewer tries - . Each attempt will reveal more of the song. + . You can always also refresh the page to try again. Good luck! diff --git a/src/constants.ts b/src/constants.ts index 41f49a3..1df07b9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -253,7 +253,7 @@ export const SONG_LIST: Song[] = [ { name: "The Challenge", album: "The Ithaca Saga", - perfect_win_text: "Challenge won", + perfect_win_text: "Bullseye", url: "https://youtube.com/embed/Bb6ssHUxrNk?si=yUxEhoiL5lSDVmg7", }, { diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 007b9b6..fc2f375 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -55,6 +55,13 @@ export interface ICheckAnswerResult { endTimeStamp: string; } +export interface IGetAnswerResult { + message: string; + song: string; + startTimeStamp: string; + endTimeStamp: string; +} + export interface IYouTubeVideo { url: string; startTimeStamp: string;