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;
|
||||
}
|
||||
|
||||
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> {
|
||||
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 { translations } from "@/translations";
|
||||
|
||||
const Header = () => {
|
||||
interface HeaderProps {
|
||||
goalAmount?: number;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ goalAmount = 100 }) => {
|
||||
const { language } = useLanguage();
|
||||
const t = translations[language];
|
||||
return (
|
||||
<header>
|
||||
<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>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -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 }) {
|
||||
<HowToPlayButton onClick={() => setHowToPlayOpen(true)} ariaLabel={t.howToPlayTitle} />
|
||||
</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">
|
||||
{!timerStarted && (
|
||||
<GoalSelector
|
||||
goalAmount={goalAmount}
|
||||
setGoalAmount={(amount) => {
|
||||
setGoalAmount(amount);
|
||||
handleRestart();
|
||||
}}
|
||||
totalMembers={totalMembers}
|
||||
disabled={timerStarted}
|
||||
/>
|
||||
)}
|
||||
{timerStarted && (
|
||||
<div className="flex gap-4">
|
||||
<RestartButton onClick={handleRestart} />
|
||||
@@ -150,10 +178,10 @@ export default function HomeClient({ session }: { session: Session }) {
|
||||
setRepeatMessage("");
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
disabled={correctMatchesCount === GOAL_AMOUNT}
|
||||
disabled={correctMatchesCount === goalAmount}
|
||||
loading={loading}
|
||||
/>
|
||||
<Score matchesCount={correctMatchesCount} />
|
||||
<Score matchesCount={correctMatchesCount} goalAmount={goalAmount} />
|
||||
{repeatMessage && <div className="mb-2 font-semibold text-red-500">{repeatMessage}</div>}
|
||||
{matches.length > 0 && (
|
||||
<>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { GOAL_AMOUNT } from "@/consts";
|
||||
import React from "react";
|
||||
|
||||
interface ScoreProps {
|
||||
matchesCount: number;
|
||||
goalAmount: number;
|
||||
}
|
||||
|
||||
const Score: React.FC<ScoreProps> = ({ matchesCount }) => {
|
||||
const Score: React.FC<ScoreProps> = ({ matchesCount, goalAmount }) => {
|
||||
return (
|
||||
<p className="text-xl">
|
||||
<span className="font-bold">{matchesCount}</span> / {GOAL_AMOUNT}
|
||||
<span className="font-bold">{matchesCount}</span> / {goalAmount}
|
||||
</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.",
|
||||
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:",
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user