mirror of
https://github.com/UiO-Gaming/name-100-uio-gamers.git
synced 2026-11-05 13:56:53 +01:00
add guess goal selector
This commit is contained in:
8 files changed
+124
-14
No files matched your search
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
export const GOAL_AMOUNT = 100;
|
|
||||||
@@ -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:",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user