diff --git a/extension/src/components/buttons/RoomSettingsButton.tsx b/extension/src/components/buttons/RoomSettingsButton.tsx
index d4d7bdd..bb8367e 100644
--- a/extension/src/components/buttons/RoomSettingsButton.tsx
+++ b/extension/src/components/buttons/RoomSettingsButton.tsx
@@ -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";
@@ -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(" ");
@@ -210,7 +219,7 @@ function SettingsTabs({
roomSettings: RoomSettings;
setRoomSettings: (roomSettings: RoomSettings) => void;
}) {
- let tabs = ["Topics"];
+ let tabs = ["Topics", "Questions"];
return (
@@ -236,6 +245,10 @@ function SettingsTabs({
roomSettings={roomSettings}
setRoomSettings={setRoomSettings}
/>
+
@@ -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(null);
+ const [searchTerm, setSearchTerm] = useState("");
+ const debounceSearch = useDebounce(searchTerm, 500);
+ const [selectedQuestions, setSelectedQuestions] = useState<
+ QuestionInterface[]
+ >(roomSettings?.questionFilter.questions || []);
+
+ let {
+ data: questions,
+ isFetching,
+ refetch,
+ } = useQuery({
+ 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");
+ }
+
+ 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,
+ 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 (
+
+
+ setSearchTerm(e.target.value)}
+ value={searchTerm}
+ />
+
+
+
+
+
+ {isFetching ? (
+ "Loading"
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+
function DurationSelector({
roomSettings,
setRoomSettings,
diff --git a/extension/src/types/RoomSettings.ts b/extension/src/types/RoomSettings.ts
index 3270ec9..77eab55 100644
--- a/extension/src/types/RoomSettings.ts
+++ b/extension/src/types/RoomSettings.ts
@@ -1,3 +1,5 @@
+import { QuestionInterface } from "./Question";
+
export interface RoomSettings {
questionFilter: QuestionFilter;
duration?: number | null;
@@ -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 = [
diff --git a/server/src/api/app.ts b/server/src/api/app.ts
index 60dcd04..f755060 100644
--- a/server/src/api/app.ts
+++ b/server/src/api/app.ts
@@ -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";
@@ -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" {
diff --git a/server/src/api/questions/question.router.ts b/server/src/api/questions/question.router.ts
new file mode 100644
index 0000000..aba8415
--- /dev/null
+++ b/server/src/api/questions/question.router.ts
@@ -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;
diff --git a/server/src/api/questions/questions.handler.ts b/server/src/api/questions/questions.handler.ts
new file mode 100644
index 0000000..54c7b5d
--- /dev/null
+++ b/server/src/api/questions/questions.handler.ts
@@ -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) {
+ const questions = await prisma.$queryRaw`
+ SELECT *
+ FROM Questions
+ ORDER BY random()
+ LIMIT 20;`;
+
+ res.json(questions);
+}
+
+// search a specific question
+export async function getQuestion(
+ req: Request,
+ res: Response,
+ 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);
+ }
+}
diff --git a/server/src/api/rooms/rooms.handler.ts b/server/src/api/rooms/rooms.handler.ts
index 372fad6..54667e1 100644
--- a/server/src/api/rooms/rooms.handler.ts
+++ b/server/src/api/rooms/rooms.handler.ts
@@ -75,114 +75,200 @@ export async function createRoom(
let roomSettings: RoomSettings = req.body;
let { kind: filterKind, selections } = roomSettings.questionFilter;
- if (filterKind !== QuestionFilterKind.Topics) {
- throw new Error(`Invalid question filter kind: ${filterKind}`);
- }
-
- let filteredQuestions: Question[] = await prisma.question.findMany({
- where: {
- tags: {
- hasSome: selections,
- },
- },
- });
-
- let easyQuestions = filteredQuestions.filter(
- (question) => question.difficulty === "Easy"
- );
- let mediumQuestions = filteredQuestions.filter(
- (question) => question.difficulty === "Medium"
- );
- let hardQuestions = filteredQuestions.filter(
- (question) => question.difficulty === "Hard"
- );
-
- let {
- Easy: numberOfEasy,
- Medium: numberOfMedium,
- Hard: numberOfHard,
- } = getNumberOfQuestionsPerDifficulty(
- roomSettings.difficulty,
- easyQuestions,
- mediumQuestions,
- hardQuestions
- );
-
- // Select 4 random questions
- let randomlySelectedEasyQuestions: Question[] = easyQuestions
- .sort(() => Math.random() - 0.5)
- .slice(0, numberOfEasy);
- let randomlySelectedMediumQuestions: Question[] = mediumQuestions
- .sort(() => Math.random() - 0.5)
- .slice(0, numberOfMedium);
- let randomlySelectedHardQuestions: Question[] = hardQuestions
- .sort(() => Math.random() - 0.5)
- .slice(0, numberOfHard);
-
- let randomlySelectedQuestions =
- randomlySelectedEasyQuestions.concat(
- randomlySelectedMediumQuestions,
- randomlySelectedHardQuestions
- );
-
- // Generate a room ID
- let newRoomId = nanoid(ROOM_ID_LENGTH);
-
- // Create a new room in the db
- let newRoom = await prisma.room.create({
- data: {
- id: newRoomId,
- questionFilterKind: filterKind,
- questionFilterSelections: selections,
- duration: roomSettings.duration,
- },
- });
-
- // Get the roomId and questionId so that you can update the RoomQuestion table
- let questionIdsAndRoom: RoomQuestion[] =
- randomlySelectedQuestions.map((question) => {
- return { questionId: question.id, roomId: newRoom.id };
- });
-
- // Add the questions to the room in the join table (RoomQuestion)
- await prisma.roomQuestion.createMany({
- data: questionIdsAndRoom,
- });
-
- // Update the user table with the roomId
- let user = await prisma.user.update({
- data: {
- roomId: newRoomId,
- },
- where: {
- id: req.user.id,
- },
- });
-
- // Update the room user table with the join time
- const { joinedAt } = await prisma.roomUser.create({
- data: {
- userId: user.id,
- roomId: newRoomId,
- },
- });
- // Update the user session
- req.user.updatedAt = user.updatedAt;
-
- // Update the room session
- let roomSession: RoomSession = {
- roomId: newRoomId,
- questions: randomlySelectedQuestions,
- userColor: generateRandomUserColor(),
- createdAt: newRoom.createdAt,
- duration: newRoom.duration,
- joinedAt,
- };
- await setUserRoomSession(req.user.id, roomSession);
- sendJoinRoomMessage(req.user.username, roomSession);
-
- return res.redirect("../sessions");
+ switch (filterKind) {
+ case QuestionFilterKind.Topics: {
+ let filteredQuestions: Question[] =
+ await prisma.question.findMany({
+ where: {
+ tags: {
+ hasSome: selections,
+ },
+ },
+ });
+
+ let easyQuestions = filteredQuestions.filter(
+ (question) => question.difficulty === "Easy"
+ );
+ let mediumQuestions = filteredQuestions.filter(
+ (question) => question.difficulty === "Medium"
+ );
+ let hardQuestions = filteredQuestions.filter(
+ (question) => question.difficulty === "Hard"
+ );
+
+ let {
+ Easy: numberOfEasy,
+ Medium: numberOfMedium,
+ Hard: numberOfHard,
+ } = getNumberOfQuestionsPerDifficulty(
+ roomSettings.difficulty,
+ easyQuestions,
+ mediumQuestions,
+ hardQuestions
+ );
+
+ // Select 4 random questions
+ let randomlySelectedEasyQuestions: Question[] =
+ easyQuestions
+ .sort(() => Math.random() - 0.5)
+ .slice(0, numberOfEasy);
+ let randomlySelectedMediumQuestions: Question[] =
+ mediumQuestions
+ .sort(() => Math.random() - 0.5)
+ .slice(0, numberOfMedium);
+ let randomlySelectedHardQuestions: Question[] =
+ hardQuestions
+ .sort(() => Math.random() - 0.5)
+ .slice(0, numberOfHard);
+
+ let randomlySelectedQuestions =
+ randomlySelectedEasyQuestions.concat(
+ randomlySelectedMediumQuestions,
+ randomlySelectedHardQuestions
+ );
+
+ // Generate a room ID
+ const newRoomId = nanoid(ROOM_ID_LENGTH);
+
+ // Create a new room in the db
+ const newRoom = await prisma.room.create({
+ data: {
+ id: newRoomId,
+ questionFilterKind: filterKind,
+ questionFilterSelections: selections,
+ duration: roomSettings.duration,
+ },
+ });
+
+ // Get the roomId and questionId so that you can update the RoomQuestion table
+ let questionIdsAndRoom: RoomQuestion[] =
+ randomlySelectedQuestions.map((question) => {
+ return {
+ questionId: question.id,
+ roomId: newRoom.id,
+ };
+ });
+
+ // Add the questions to the room in the join table (RoomQuestion)
+ await prisma.roomQuestion.createMany({
+ data: questionIdsAndRoom,
+ });
+
+ // Update the user table with the roomId
+ let user = await prisma.user.update({
+ data: {
+ roomId: newRoomId,
+ },
+ where: {
+ id: req.user.id,
+ },
+ });
+
+ // Update the room user table with the join time
+ const { joinedAt } = await prisma.roomUser.create({
+ data: {
+ userId: user.id,
+ roomId: newRoomId,
+ },
+ });
+
+ // Update the user session
+ req.user.updatedAt = user.updatedAt;
+
+ // Update the room session
+ let roomSession: RoomSession = {
+ roomId: newRoomId,
+ questions: randomlySelectedQuestions,
+ userColor: generateRandomUserColor(),
+ createdAt: newRoom.createdAt,
+ duration: newRoom.duration,
+ joinedAt,
+ };
+ await setUserRoomSession(req.user.id, roomSession);
+ sendJoinRoomMessage(req.user.username, roomSession);
+
+ return res.redirect("../sessions");
+ }
+
+ case QuestionFilterKind.Questions: {
+ const questions = await prisma.question.findMany({
+ where: {
+ titleSlug: {
+ in: selections,
+ },
+ },
+ });
+
+ // Generate a room ID
+ const newRoomId = nanoid(ROOM_ID_LENGTH);
+
+ // Create a new room in the db
+ const newRoom = await prisma.room.create({
+ data: {
+ id: newRoomId,
+ questionFilterKind: filterKind,
+ questionFilterSelections: selections,
+ duration: roomSettings.duration,
+ },
+ });
+
+ // Get the roomId and questionId so that you can update the RoomQuestion table
+ let questionIdsAndRoom: RoomQuestion[] = questions.map(
+ (question) => {
+ return {
+ questionId: question.id,
+ roomId: newRoom.id,
+ };
+ }
+ );
+
+ // Add the questions to the room in the join table (RoomQuestion)
+ await prisma.roomQuestion.createMany({
+ data: questionIdsAndRoom,
+ });
+
+ // Update the user table with the roomId
+ let user = await prisma.user.update({
+ data: {
+ roomId: newRoomId,
+ },
+ where: {
+ id: req.user.id,
+ },
+ });
+
+ // Update the room user table with the join time
+ const { joinedAt } = await prisma.roomUser.create({
+ data: {
+ userId: user.id,
+ roomId: newRoomId,
+ },
+ });
+
+ // Update the user session
+ req.user.updatedAt = user.updatedAt;
+
+ // Update the room session
+ let roomSession: RoomSession = {
+ roomId: newRoomId,
+ questions,
+ userColor: generateRandomUserColor(),
+ createdAt: newRoom.createdAt,
+ duration: newRoom.duration,
+ joinedAt,
+ };
+ await setUserRoomSession(req.user.id, roomSession);
+ sendJoinRoomMessage(req.user.username, roomSession);
+
+ return res.redirect("../sessions");
+ }
+
+ default:
+ throw new Error(
+ `Invalid question filter kind: ${filterKind}`
+ );
+ }
});
} catch (error) {
return next(error);
diff --git a/server/src/types/RoomSettings.ts b/server/src/types/RoomSettings.ts
index 11ec294..31e4520 100644
--- a/server/src/types/RoomSettings.ts
+++ b/server/src/types/RoomSettings.ts
@@ -23,6 +23,7 @@ export interface QuestionFilter {
export enum QuestionFilterKind {
Topics = "topics",
+ Questions = "questions",
}
export const topics = [