add guess goal selector

This commit is contained in:
leander committed 2025-06-22 13:30:03 +02:00
1 parent d096be3357
commit 6840c6340a
8 files changed
+124 -14

No files matched your search

+9
View File
@@ -81,6 +81,15 @@ function memberMatchesInput(member: DiscordGuildMember, normalizedInput: string)
return username === normalizedInput || globalName === normalizedInput || nickname === normalizedInput; return username === normalizedInput || globalName === normalizedInput || nickname === normalizedInput;
} }
export async function getPayingMemberCount(): Promise<number> {
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<Match> { export async function findDiscordMember(input: string, guessedIds: string[] = []): Promise<Match> {
const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID; const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID;
+22
View File
@@ -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 });
}
}
+47
View File
@@ -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<GoalSelectorProps> = ({ 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 (
<div className="flex items-center gap-2">
<label htmlFor="goal-select" className="font-semibold">
{t.goalSelectorLabel}
</label>
<select
id="goal-select"
value={goalAmount}
onChange={(e) => setGoalAmount(Number(e.target.value))}
disabled={disabled}
className="rounded border border-gray-300 bg-white px-2 py-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
{options.map((option) => (
<option key={option} value={option}>
{option === totalMembers ? `${t.subject} (${option})` : option}
</option>
))}
</select>
</div>
);
};
export default GoalSelector;
+6 -3
View File
@@ -1,14 +1,17 @@
import { GOAL_AMOUNT } from "@/consts";
import { useLanguage } from "@/LanguageContext"; import { useLanguage } from "@/LanguageContext";
import { translations } from "@/translations"; import { translations } from "@/translations";
const Header = () => { interface HeaderProps {
goalAmount?: number;
}
const Header: React.FC<HeaderProps> = ({ goalAmount = 100 }) => {
const { language } = useLanguage(); const { language } = useLanguage();
const t = translations[language]; const t = translations[language];
return ( return (
<header> <header>
<h1 className="mt-24 mb-12 text-center text-4xl font-bold"> <h1 className="mt-24 mb-12 text-center text-4xl font-bold">
{t.appTitle.split(" ")[0]} {GOAL_AMOUNT} <span className="border-accent-primary border-b-2">{t.subject}</span> {t.appTitle.split(" ")[0]} {goalAmount} <span className="border-accent-primary border-b-2">{t.subject}</span>
</h1> </h1>
</header> </header>
); );
+35 -7
View File
@@ -1,13 +1,13 @@
"use client"; "use client";
import { GOAL_AMOUNT } from "@/consts";
import { useLanguage } from "@/LanguageContext"; import { useLanguage } from "@/LanguageContext";
import { translations } from "@/translations"; import { translations } from "@/translations";
import type { Match } from "@/types"; import type { Match } from "@/types";
import type { Session } from "next-auth"; import type { Session } from "next-auth";
import Image from "next/image"; 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 Footer from "./Footer";
import GoalSelector from "./GoalSelector";
import GuessesList from "./GuessesList"; import GuessesList from "./GuessesList";
import GuessForm from "./GuessForm"; import GuessForm from "./GuessForm";
import Header from "./Header"; import Header from "./Header";
@@ -30,6 +30,23 @@ export default function HomeClient({ session }: { session: Session }) {
const [repeatMessage, setRepeatMessage] = useState(""); const [repeatMessage, setRepeatMessage] = useState("");
const { language } = useLanguage(); const { language } = useLanguage();
const t = translations[language]; 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]); 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 // Stop timer when 100 correct guesses are made
React.useEffect(() => { React.useEffect(() => {
if (correctMatchesCount === GOAL_AMOUNT) { if (correctMatchesCount === goalAmount) {
stopTimer(); stopTimer();
} }
}, [correctMatchesCount]); }, [correctMatchesCount, goalAmount]);
const handleRestart = () => { const handleRestart = () => {
stopTimer(); stopTimer();
@@ -135,8 +152,19 @@ export default function HomeClient({ session }: { session: Session }) {
<HowToPlayButton onClick={() => setHowToPlayOpen(true)} ariaLabel={t.howToPlayTitle} /> <HowToPlayButton onClick={() => setHowToPlayOpen(true)} ariaLabel={t.howToPlayTitle} />
</div> </div>
</div> </div>
<Header /> <Header goalAmount={goalAmount} />
<main className="row-start-2 flex w-full flex-1 flex-col items-center gap-6 px-4 pt-28 pb-12 sm:pt-12"> <main className="row-start-2 flex w-full flex-1 flex-col items-center gap-6 px-4 pt-28 pb-12 sm:pt-12">
{!timerStarted && (
<GoalSelector
goalAmount={goalAmount}
setGoalAmount={(amount) => {
setGoalAmount(amount);
handleRestart();
}}
totalMembers={totalMembers}
disabled={timerStarted}
/>
)}
{timerStarted && ( {timerStarted && (
<div className="flex gap-4"> <div className="flex gap-4">
<RestartButton onClick={handleRestart} /> <RestartButton onClick={handleRestart} />
@@ -150,10 +178,10 @@ export default function HomeClient({ session }: { session: Session }) {
setRepeatMessage(""); setRepeatMessage("");
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
disabled={correctMatchesCount === GOAL_AMOUNT} disabled={correctMatchesCount === goalAmount}
loading={loading} loading={loading}
/> />
<Score matchesCount={correctMatchesCount} /> <Score matchesCount={correctMatchesCount} goalAmount={goalAmount} />
{repeatMessage && <div className="mb-2 font-semibold text-red-500">{repeatMessage}</div>} {repeatMessage && <div className="mb-2 font-semibold text-red-500">{repeatMessage}</div>}
{matches.length > 0 && ( {matches.length > 0 && (
<> <>
+3 -3
View File
@@ -1,14 +1,14 @@
import { GOAL_AMOUNT } from "@/consts";
import React from "react"; import React from "react";
interface ScoreProps { interface ScoreProps {
matchesCount: number; matchesCount: number;
goalAmount: number;
} }
const Score: React.FC<ScoreProps> = ({ matchesCount }) => { const Score: React.FC<ScoreProps> = ({ matchesCount, goalAmount }) => {
return ( return (
<p className="text-xl"> <p className="text-xl">
<span className="font-bold">{matchesCount}</span> / {GOAL_AMOUNT} <span className="font-bold">{matchesCount}</span> / {goalAmount}
</p> </p>
); );
}; };
-1
View File
@@ -1 +0,0 @@
export const GOAL_AMOUNT = 100;
+2
View File
@@ -21,6 +21,7 @@ export const translations = {
accessDeniedMessage: "You don't have the paying member role in the UiO Gaming Discord server.", accessDeniedMessage: "You don't have the paying member role in the UiO Gaming Discord server.",
tryAgain: "Try logging in again", tryAgain: "Try logging in again",
notPayingMember: "not a paying member", notPayingMember: "not a paying member",
goalSelectorLabel: "Number of gamers to name:",
}, },
no: { no: {
submittedAnswers: "Dine svar", submittedAnswers: "Dine svar",
@@ -43,5 +44,6 @@ export const translations = {
accessDeniedMessage: 'Du har ikke rollen "Betalende medlem" i UiO Gaming sin Discord-server.', accessDeniedMessage: 'Du har ikke rollen "Betalende medlem" i UiO Gaming sin Discord-server.',
tryAgain: "Logg på igjen", tryAgain: "Logg på igjen",
notPayingMember: "ikke betalende medlem", notPayingMember: "ikke betalende medlem",
goalSelectorLabel: "Velg antall som må navngis:",
}, },
}; };