Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add question selection #50

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 205 additions & 3 deletions extension/src/components/buttons/RoomSettingsButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Dialog, Transition, Tab } from "@headlessui/react";
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
ChangeEvent,
Fragment,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import Spinner from "../Spinner";
import XIcon from "../../icons/XIcon";
import SettingsIcon from "../../icons/SettingsIcon";
Expand All @@ -11,7 +19,8 @@ import {
topics,
defaultRoomSettings,
} from "../../types/RoomSettings";
import { Difficulty } from "../../types/Question";
import { Difficulty, QuestionInterface } from "../../types/Question";
import { SERVER_URL } from "../../config";

function classNames(...classes: any[]) {
return classes.filter(Boolean).join(" ");
Expand Down Expand Up @@ -210,7 +219,7 @@ function SettingsTabs({
roomSettings: RoomSettings;
setRoomSettings: (roomSettings: RoomSettings) => void;
}) {
let tabs = ["Topics"];
let tabs = ["Topics", "Questions"];
return (
<div className="h-full px-2 py-2">
<Tab.Group>
Expand All @@ -236,6 +245,10 @@ function SettingsTabs({
roomSettings={roomSettings}
setRoomSettings={setRoomSettings}
/>
<QuestionSelector
roomSettings={roomSettings}
setRoomSettings={setRoomSettings}
/>
</Tab.Panels>
</Tab.Group>
</div>
Expand Down Expand Up @@ -387,6 +400,195 @@ function TopicSelector({
);
}

function useDebounce(value: any, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
}

function QuestionSelector({
roomSettings,
setRoomSettings,
}: {
roomSettings: RoomSettings;
setRoomSettings: (roomSettings: RoomSettings) => void;
}) {
let searchQuestionRef = useRef<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState("");
const debounceSearch = useDebounce(searchTerm, 500);
const [selectedQuestions, setSelectedQuestions] = useState<
QuestionInterface[]
>(roomSettings?.questionFilter.questions || []);

let {
data: questions,
isFetching,
refetch,
} = useQuery<any[]>({
queryKey: ["questions"],
queryFn: async ({ signal }) => {
let response = await fetch(
`${SERVER_URL}/questions/search/${searchTerm}`,
{
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal,
}
);
if (!response.ok) {
throw new Error("Failed to fetch current players");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix(jake): need to change this, probably refactor the whole queryFn

}

return response.json();
},
refetchOnWindowFocus: false,
enabled: false,
keepPreviousData: false,
});

useEffect(() => {
if (searchTerm) {
refetch();
}
}, [debounceSearch]);

useEffect(() => {
setRoomSettings({
...roomSettings,
questionFilter: {
kind: QuestionFilterKind.Questions,
selections: selectedQuestions.map((q) => q.titleSlug),
questions: selectedQuestions,
},
});
}, [selectedQuestions]);

function handleSelectQuestion(
event: ChangeEvent<HTMLInputElement>,
question: QuestionInterface
) {
const isChecked = event.target.checked;
if (isChecked) {
setSelectedQuestions([...selectedQuestions, question]);
} else {
const deSelected = selectedQuestions.filter(
(each) => each.id !== question.id
);
setSelectedQuestions(deSelected);
}
}

return (
<Tab.Panel>
<div className="flex flex-row items-center justify-between gap-x-2 rounded-lg border border-transparent bg-lc-fg-light px-3 py-[6px] text-lc-text-light focus-within:border-blue-500 hover:border-blue-500 dark:bg-lc-fg dark:text-white">
<input
className="w-full bg-lc-fg-light outline-none dark:bg-lc-fg"
ref={searchQuestionRef}
type="text"
name="roomNumber"
id="question"
placeholder="Search a question"
spellCheck="false"
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
onChange={(e) => setSearchTerm(e.target.value)}
value={searchTerm}
/>
</div>

<hr />

<div className="flex h-28 flex-col items-start justify-between gap-x-2 overflow-auto rounded-lg p-1 hover:border-blue-500 dark:bg-lc-fg dark:text-white">
{isFetching ? (
"Loading"
) : (
<div
className={classNames(
"h-56 w-full overflow-auto rounded-md bg-lc-fg-modal-light dark:bg-lc-fg-modal dark:text-white"
)}
>
<ul className="flex flex-col text-sm">
{questions?.length && searchTerm ? (
questions?.map((q) => (
<label
key={q.id}
className="flex flex-row items-center gap-3 px-3 py-1 even:bg-white even:bg-opacity-[45%] dark:even:bg-lc-bg dark:even:bg-opacity-[35%]"
>
<input
type="checkbox"
name="topics"
value={q.title}
onChange={(e) =>
handleSelectQuestion(e, q)
}
id={q.id}
checked={
!!selectedQuestions.find(
(each) => each.id === q.id
)
}
disabled={
!!!selectedQuestions.find(
(each) => each.id === q.id
) &&
selectedQuestions.length === 4
}
/>
{q.id}. {q.title}
</label>
))
) : (
<p className="p-1">No result found</p>
)}
</ul>
</div>
)}
</div>

<fieldset className="mt-4 flex flex-col items-start justify-around rounded-lg border-4 border-lc-fg-modal-light p-1 pb-1 text-sm text-lc-text-light dark:border-lc-fg-modal dark:text-white">
<legend className="px-2 dark:text-lc-fg-modal-light">
Selected question ({selectedQuestions.length} / 4)
</legend>

{selectedQuestions.map((q) => {
return (
<label
key={q.id}
className="mb-2 flex flex-row items-center gap-3 rounded-md bg-lc-fg-modal-light px-3 py-1 text-sm text-lc-text-light dark:bg-lc-fg-modal dark:text-white"
>
<input
type="checkbox"
name="select-unselect-all"
value={"Select/Unselect All"}
onChange={(e) => {
handleSelectQuestion(e, q);
}}
checked
id={"select-unselect-all"}
/>
{q.id}. {q.title}
</label>
);
})}
</fieldset>
</Tab.Panel>
);
}

function DurationSelector({
roomSettings,
setRoomSettings,
Expand Down
4 changes: 4 additions & 0 deletions extension/src/types/RoomSettings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { QuestionInterface } from "./Question";

export interface RoomSettings {
questionFilter: QuestionFilter;
duration?: number | null;
Expand All @@ -13,10 +15,12 @@ export interface RoomDifficulty {
export interface QuestionFilter {
kind: QuestionFilterKind;
selections: string[];
questions?: QuestionInterface[];
}

export enum QuestionFilterKind {
Topics = "topics",
Questions = "questions",
}

export const topics = [
Expand Down
2 changes: 2 additions & 0 deletions server/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import http from "http";
import { Server } from "socket.io";
import { errorHandler, ensureAuthenticated } from "./middleware";
import roomsRoute from "../api/rooms/rooms.route";
import questionRoute from "../api/questions/question.router";
import submissionsRoute from "../api/submissions/submissions.route";
import authRoute from "../api/auth/auth.route";
import session, { Session } from "express-session";
Expand Down Expand Up @@ -294,6 +295,7 @@ app.use(httplog);
app.use("/auth", authRoute);
app.use("/submissions", ensureAuthenticated, submissionsRoute);
app.use("/rooms", ensureAuthenticated, roomsRoute);
app.use("/questions", ensureAuthenticated, questionRoute);
app.use(errorHandler);

declare module "http" {
Expand Down
10 changes: 10 additions & 0 deletions server/src/api/questions/question.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from "express";
import { getQuestions, getQuestion } from "./questions.handler";
// import * as RoomsHandler from "./rooms.handler";

const router = Router();

router.get("/", getQuestions);
router.get("/search/:searchTerm", getQuestion);

export default router;
41 changes: 41 additions & 0 deletions server/src/api/questions/questions.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from "express";
import prisma from "../../index";
import { Question } from "@prisma/client";

// get 20 questions randomly to search
export async function getQuestions(_: Request, res: Response<any[]>) {
const questions = await prisma.$queryRaw<Question[]>`
SELECT *
FROM Questions
ORDER BY random()
LIMIT 20;`;

res.json(questions);
}

// search a specific question
export async function getQuestion(
req: Request,
res: Response<any[]>,
next: NextFunction
) {
const searchTerm = req.params.searchTerm ?? "";

if (!searchTerm) return;

try {
const results = await prisma.question.findMany({
where: {
title: {
contains: searchTerm,
mode: "insensitive",
},
},
take: 5,
});

res.json(results);
} catch (error) {
next(error);
}
}
Loading