Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…course-assessment-g17 into edit-user-profile

* 'master' of https://github.com/CS3219-AY2324S1/ay2324s1-course-assessment-g17:
  fix: fix edit question again
  fix: handle initial data in multiselect
  refactor: change category multiselect to react-select
  refactor: convert complexity dropdown to chakra-react-select
  fix: complexity intersection
  fix: change timeout to 30s
  feat: add matching logic
  refactor: remove user context
  • Loading branch information
carriezhengjr committed Oct 1, 2023
2 parents a3f7b7d + 99bd1dd commit 8197314
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 107 deletions.
3 changes: 2 additions & 1 deletion backend/matching-service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Server, Socket } from "socket.io";
import registerMatchingHandlers from "./socket/matchingHandler";
import cors from "cors";
import mongoose from "mongoose";
import matching from "./models/matching";

dotenv.config();
const mongoString = process.env.MONGO_CONNECTION_STRING as string;
Expand All @@ -13,7 +14,7 @@ const PORT = process.env.PORT as string;

const app = express();
app.use(
cors({ origin: FRONTEND_URL, optionsSuccessStatus: 200, credentials: true }),
cors({ origin: FRONTEND_URL, optionsSuccessStatus: 200, credentials: true })
);

const httpServer = createServer();
Expand Down
85 changes: 85 additions & 0 deletions backend/matching-service/src/controllers/matchingController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FilterQuery } from "mongoose";
import Matching, {
MatchStatusEnum,
QuestionComplexityEnum,
} from "../models/matching";
import matching from "../models/matching";

interface MatchingInfo {
user_id: number;
Expand All @@ -26,3 +28,86 @@ export async function insertMatching(matchingInfo: MatchingInfo) {
throw error;
}
}

function findIntersection(arr1: string[], arr2: string[]) {
// If one user has no preference, take the preferences of the other user.
if (!arr1) return arr2;
if (!arr2) return arr1;

// If they both have preferences, take only what is common between them.
return arr1.filter((item) => arr2.includes(item));
}

export async function findMatch(matchingInfo: MatchingInfo) {
const difficulty_level_query: FilterQuery<typeof Matching>[] =
matchingInfo.difficulty_level
? [
{
$or: [
{
difficulty_levels: {
$elemMatch: { $in: matchingInfo.difficulty_level },
},
},
{
difficulty_levels: {
$size: 0,
},
},
],
},
]
: [];

const topics_query: FilterQuery<typeof Matching>[] = matchingInfo.topics
? [
{
$or: [
{
categories: {
$elemMatch: { $in: matchingInfo.topics },
},
},
{
categories: {
$size: 0,
},
},
],
},
]
: [];

const potentialMatch = await Matching.findOne({
status: MatchStatusEnum.PENDING,
$and: difficulty_level_query.concat(topics_query),
}).exec();

if (!potentialMatch) return null;

const processedMatch = {
...potentialMatch.toObject(),
categories: findIntersection(
potentialMatch.categories,
matchingInfo.topics
),
difficulty_levels: findIntersection(
potentialMatch.difficulty_levels,
matchingInfo.difficulty_level
),
};

await potentialMatch.updateOne({ status: MatchStatusEnum.MATCHED });
return processedMatch;
}

export async function markAsTimeout(matchingInfo: MatchingInfo) {
await matching.findOneAndUpdate(
{
user_id: matchingInfo.user_id,
socket_id: matchingInfo.socket_id,
status: MatchStatusEnum.PENDING,
},
{ status: MatchStatusEnum.TIMEOUT }
);
}
45 changes: 39 additions & 6 deletions backend/matching-service/src/socket/matchingHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import http from "http";
import dotenv from "dotenv";
import { Server, Socket } from "socket.io";
import { insertMatching } from "../controllers/matchingController";
import {
findMatch,
insertMatching,
markAsTimeout,
} from "../controllers/matchingController";
import { store } from "../utils/store";
import matching from "../models/matching";

export enum QuestionComplexityEnum {
EASY = "Easy",
Expand Down Expand Up @@ -30,12 +36,39 @@ const registerMatchingHandlers = (io: Server, socket: Socket) => {
status: MatchStatusEnum.PENDING,
};

insertMatching(matchingInfo);
const result = await findMatch(matchingInfo);
if (!result) {
// If no match found, insert pending match to DB and start timeout.
insertMatching(matchingInfo);
store[matchingInfo.user_id] = setTimeout(() => {
// TODO: update match status
markAsTimeout(matchingInfo);
socket.emit("timeout");
}, 30000);
} else {
// If match found:
// 1. Insert matched match to DB
// 2. Clear first request's timeout
// 3. TODO: do something with match results
// 4. Notify both users.
insertMatching({ ...matchingInfo, status: MatchStatusEnum.MATCHED });

// TODO: track socket id somewhere
// TODO: check db for potential match
// TODO: save timeout somewhere to revoke on match
setTimeout(() => socket.emit("timeout"), 5000);
const matchResult = {
userOne: matchingInfo.user_id,
userTwo: result.user_id,
categories: result.categories,
difficulty_level: result.difficulty_levels,
};
clearTimeout(store[matchResult.userTwo]);

// TODO: do something with matchResult
console.log("Found a match!");
console.log(matchResult);

// Notify both users of the match result.
socket.emit("matchFound");
socket.to(result.socket_id).emit("matchFound");
}
});
socket.on("disconnect", () => {
// TODO: drop user from matching
Expand Down
1 change: 1 addition & 0 deletions backend/matching-service/src/utils/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const store: { [key: string]: NodeJS.Timeout } = {};
6 changes: 4 additions & 2 deletions backend/question-service/src/controllers/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export const getQuestionCategories: RequestHandler = (req, res) => {
export const updateQuestion: RequestHandler[] = [
param("questionId")
.notEmpty()
.withMessage("questionId field cannot be empty."),
.withMessage("questionId field cannot be empty.")
.toInt(),
param("questionId").isNumeric().withMessage("questionId should be a number."),
body("title").notEmpty().trim().withMessage("title cannot be empty."),
body("categories").isArray().withMessage("categories should be an array."),
Expand Down Expand Up @@ -161,6 +162,7 @@ export const updateQuestion: RequestHandler[] = [

const sameQuestionExists = await QuestionModel.exists({
title: formData.title,
questionID: { $ne: questionId },
});

if (sameQuestionExists) {
Expand All @@ -174,7 +176,7 @@ export const updateQuestion: RequestHandler[] = [
const finalQuestion = await QuestionModel.findByIdAndUpdate(
existingQuestion._id,
formData,
{ new: true },
{ new: true }
);

res.status(200).json({ data: finalQuestion, status: "success" });
Expand Down
78 changes: 78 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/react-dom": "^18.2.7",
"allotment": "^1.19.3",
"axios": "^1.5.0",
"chakra-react-select": "^4.7.2",
"css-loader": "^6.8.1",
"dompurify": "^3.0.5",
"eslint-config-react-app": "^7.0.1",
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/form/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import { type OptionBase, Select } from 'chakra-react-select';

interface MultiSelectProps<T> {
options: Array<Option<T>>;
onChange: (selected: T[]) => void;
initialOptions?: Array<Option<T>>;
}

interface Option<T> extends OptionBase {
label: string;
value: T;
}

const MultiSelect = <T,>({ options, onChange, initialOptions }: MultiSelectProps<T>): JSX.Element => {
const [selectedOptions, setSelectedOptions] = useState<ReadonlyArray<Option<T>>>([]);
const handleChange = (selectedOptions: ReadonlyArray<Option<T>>): void => {
onChange(selectedOptions.map((option) => option.value));
setSelectedOptions(selectedOptions);
};

useEffect(() => {
if (initialOptions !== undefined) setSelectedOptions(initialOptions);
}, [initialOptions]);

return (
<Select
isMulti
name="complexities"
options={options}
placeholder="Select difficulty..."
closeMenuOnSelect={false}
value={selectedOptions}
onChange={handleChange}
/>
);
};

export default MultiSelect;
Loading

0 comments on commit 8197314

Please sign in to comment.