From 6840c6340a20262f01f6a8721b276c552a8fde75 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Sun, 22 Jun 2025 13:30:03 +0200 Subject: [PATCH] add guess goal selector --- src/app/api/guess/dataProvider.ts | 9 ++++++ src/app/api/members/count/route.ts | 22 ++++++++++++++ src/app/components/GoalSelector.tsx | 47 +++++++++++++++++++++++++++++ src/app/components/Header.tsx | 9 ++++-- src/app/components/HomeClient.tsx | 42 +++++++++++++++++++++----- src/app/components/Score.tsx | 6 ++-- src/app/consts.ts | 1 - src/app/translations.ts | 2 ++ 8 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 src/app/api/members/count/route.ts create mode 100644 src/app/components/GoalSelector.tsx delete mode 100644 src/app/consts.ts diff --git a/src/app/api/guess/dataProvider.ts b/src/app/api/guess/dataProvider.ts index 56a1ef4..da979b8 100644 --- a/src/app/api/guess/dataProvider.ts +++ b/src/app/api/guess/dataProvider.ts @@ -81,6 +81,15 @@ function memberMatchesInput(member: DiscordGuildMember, normalizedInput: string) return username === normalizedInput || globalName === normalizedInput || nickname === normalizedInput; } +export async function getPayingMemberCount(): Promise { + const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID; + if (!payingMemberRoleId) { + throw new Error("Missing DISCORD_REQUIRED_ROLE_ID environment variable"); + } + const members = await getDiscordMembers(); + return members.filter((m) => m.roles.includes(payingMemberRoleId)).length; +} + export async function findDiscordMember(input: string, guessedIds: string[] = []): Promise { const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID; diff --git a/src/app/api/members/count/route.ts b/src/app/api/members/count/route.ts new file mode 100644 index 0000000..a826ecd --- /dev/null +++ b/src/app/api/members/count/route.ts @@ -0,0 +1,22 @@ +import { authOptions } from "@/lib/auth"; +import { getServerSession } from "next-auth/next"; +import { NextResponse } from "next/server"; +import { getPayingMemberCount } from "../../guess/dataProvider"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const session = await getServerSession(authOptions); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const count = await getPayingMemberCount(); + return NextResponse.json({ count }); + } catch (error) { + console.error("Error fetching paying member count:", error); + return NextResponse.json({ error: "Failed to fetch paying member count" }, { status: 500 }); + } +} diff --git a/src/app/components/GoalSelector.tsx b/src/app/components/GoalSelector.tsx new file mode 100644 index 0000000..001b9e2 --- /dev/null +++ b/src/app/components/GoalSelector.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useLanguage } from "@/LanguageContext"; +import { translations } from "@/translations"; +import React from "react"; + +interface GoalSelectorProps { + goalAmount: number; + setGoalAmount: (amount: number) => void; + totalMembers: number; + disabled: boolean; +} + +const GoalSelector: React.FC = ({ goalAmount, setGoalAmount, totalMembers, disabled }) => { + const { language } = useLanguage(); + const t = translations[language]; + + const options = [25, 50, 100]; + if (totalMembers > 0 && !options.includes(totalMembers)) { + // Add totalMembers to options and sort + options.push(totalMembers); + options.sort((a, b) => a - b); + } + + return ( +
+ + +
+ ); +}; + +export default GoalSelector; diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index 8865091..06cdfc3 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -1,14 +1,17 @@ -import { GOAL_AMOUNT } from "@/consts"; import { useLanguage } from "@/LanguageContext"; import { translations } from "@/translations"; -const Header = () => { +interface HeaderProps { + goalAmount?: number; +} + +const Header: React.FC = ({ goalAmount = 100 }) => { const { language } = useLanguage(); const t = translations[language]; return (

- {t.appTitle.split(" ")[0]} {GOAL_AMOUNT} {t.subject} + {t.appTitle.split(" ")[0]} {goalAmount} {t.subject}

); diff --git a/src/app/components/HomeClient.tsx b/src/app/components/HomeClient.tsx index 1a04745..8da4589 100644 --- a/src/app/components/HomeClient.tsx +++ b/src/app/components/HomeClient.tsx @@ -1,13 +1,13 @@ "use client"; -import { GOAL_AMOUNT } from "@/consts"; import { useLanguage } from "@/LanguageContext"; import { translations } from "@/translations"; import type { Match } from "@/types"; import type { Session } from "next-auth"; import Image from "next/image"; -import React, { useMemo, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import Footer from "./Footer"; +import GoalSelector from "./GoalSelector"; import GuessesList from "./GuessesList"; import GuessForm from "./GuessForm"; import Header from "./Header"; @@ -30,6 +30,23 @@ export default function HomeClient({ session }: { session: Session }) { const [repeatMessage, setRepeatMessage] = useState(""); const { language } = useLanguage(); const t = translations[language]; + const [goalAmount, setGoalAmount] = useState(100); + const [totalMembers, setTotalMembers] = useState(0); + + useEffect(() => { + const fetchTotalMembers = async () => { + try { + const res = await fetch("/api/members/count"); + if (res.ok) { + const data = await res.json(); + setTotalMembers(data.count); + } + } catch (error) { + console.error("Failed to fetch total members", error); + } + }; + fetchTotalMembers(); + }, []); const correctMatchesCount = useMemo(() => matches.filter((m) => m.correct).length, [matches]); @@ -55,10 +72,10 @@ export default function HomeClient({ session }: { session: Session }) { // Stop timer when 100 correct guesses are made React.useEffect(() => { - if (correctMatchesCount === GOAL_AMOUNT) { + if (correctMatchesCount === goalAmount) { stopTimer(); } - }, [correctMatchesCount]); + }, [correctMatchesCount, goalAmount]); const handleRestart = () => { stopTimer(); @@ -135,8 +152,19 @@ export default function HomeClient({ session }: { session: Session }) { setHowToPlayOpen(true)} ariaLabel={t.howToPlayTitle} /> -
+
+ {!timerStarted && ( + { + setGoalAmount(amount); + handleRestart(); + }} + totalMembers={totalMembers} + disabled={timerStarted} + /> + )} {timerStarted && (
@@ -150,10 +178,10 @@ export default function HomeClient({ session }: { session: Session }) { setRepeatMessage(""); }} onSubmit={handleSubmit} - disabled={correctMatchesCount === GOAL_AMOUNT} + disabled={correctMatchesCount === goalAmount} loading={loading} /> - + {repeatMessage &&
{repeatMessage}
} {matches.length > 0 && ( <> diff --git a/src/app/components/Score.tsx b/src/app/components/Score.tsx index 7bd1230..1afaa16 100644 --- a/src/app/components/Score.tsx +++ b/src/app/components/Score.tsx @@ -1,14 +1,14 @@ -import { GOAL_AMOUNT } from "@/consts"; import React from "react"; interface ScoreProps { matchesCount: number; + goalAmount: number; } -const Score: React.FC = ({ matchesCount }) => { +const Score: React.FC = ({ matchesCount, goalAmount }) => { return (

- {matchesCount} / {GOAL_AMOUNT} + {matchesCount} / {goalAmount}

); }; diff --git a/src/app/consts.ts b/src/app/consts.ts deleted file mode 100644 index 3d8420f..0000000 --- a/src/app/consts.ts +++ /dev/null @@ -1 +0,0 @@ -export const GOAL_AMOUNT = 100; diff --git a/src/app/translations.ts b/src/app/translations.ts index 1246408..162d037 100644 --- a/src/app/translations.ts +++ b/src/app/translations.ts @@ -21,6 +21,7 @@ export const translations = { accessDeniedMessage: "You don't have the paying member role in the UiO Gaming Discord server.", tryAgain: "Try logging in again", notPayingMember: "not a paying member", + goalSelectorLabel: "Number of gamers to name:", }, no: { submittedAnswers: "Dine svar", @@ -43,5 +44,6 @@ export const translations = { accessDeniedMessage: 'Du har ikke rollen "Betalende medlem" i UiO Gaming sin Discord-server.', tryAgain: "Logg på igjen", notPayingMember: "ikke betalende medlem", + goalSelectorLabel: "Velg antall som må navngis:", }, };