From 22206ce243e18a13b8965b5a7eca96c0ad4bffd8 Mon Sep 17 00:00:00 2001
From: "github-classroom[bot]"
<66690702+github-classroom[bot]@users.noreply.github.com>
Date: Sat, 1 Mar 2025 03:30:26 +0000
Subject: [PATCH 001/144] Setting up GitHub Classroom Feedback
From 4e0f7a28a07332db7660f5a79579de5c7c77acb0 Mon Sep 17 00:00:00 2001
From: "github-classroom[bot]"
<66690702+github-classroom[bot]@users.noreply.github.com>
Date: Sat, 1 Mar 2025 03:30:29 +0000
Subject: [PATCH 002/144] add deadline
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 62aad77..69871ee 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[](https://classroom.github.com/a/fE-a_qEp)
The individual and team project for this class are designed to mirror the experiences of a software engineer joining a new development team: you will be “onboarded” to our codebase, make several individual contributions, and then form a team to propose, develop and implement new features. The codebase that we’ll be developing on is a Fake Stack Overflow project (let’s call it HuskyFlow). You will get an opportunity to work with the starter code which provides basic skeleton for the app and then additional features will be proposed and implemented by you! All implementation will take place in the TypeScript programming language, using React for the user interface.
## Getting Started
From 40f761ddd25e43bfe88aa94c8010f57b27d150bd Mon Sep 17 00:00:00 2001
From: jessicazha0
Date: Wed, 12 Mar 2025 19:18:39 -0400
Subject: [PATCH 003/144] contributions backend and hook
---
.../main/userContributions/index.css | 0
.../main/userContributions/index.tsx | 0
client/src/hooks/useUserContributions.ts | 43 ++++++++
client/src/services/contributionsService.ts | 27 ++++++
server/controllers/contribution.controller.ts | 97 +++++++++++++++++++
5 files changed, 167 insertions(+)
create mode 100644 client/src/components/main/userContributions/index.css
create mode 100644 client/src/components/main/userContributions/index.tsx
create mode 100644 client/src/hooks/useUserContributions.ts
create mode 100644 client/src/services/contributionsService.ts
create mode 100644 server/controllers/contribution.controller.ts
diff --git a/client/src/components/main/userContributions/index.css b/client/src/components/main/userContributions/index.css
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/components/main/userContributions/index.tsx b/client/src/components/main/userContributions/index.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/client/src/hooks/useUserContributions.ts b/client/src/hooks/useUserContributions.ts
new file mode 100644
index 0000000..29053e0
--- /dev/null
+++ b/client/src/hooks/useUserContributions.ts
@@ -0,0 +1,43 @@
+import { useEffect, useState } from 'react';
+import getUserContributions from '../services/contributionsService';
+
+/**
+ * Custom hook to fetch and manage user contributions (questions, answers, comments) over a date range.
+ * Supports fetching contributions for any user by providing a `userID`.
+ *
+ * @param userID - The ID of the user whose contributions are being fetched.
+ * @param startDate - The start date of the range (YYYY-MM-DD format).
+ * @param endDate - The end date of the range (YYYY-MM-DD format).
+ * @returns contributions - The fetched contributions data.
+ * @returns loading - Boolean indicating whether data is loading.
+ * @returns error - Any error message encountered during the fetch.
+ */
+const useUserContributions = (userID: string, startDate: string, endDate: string) => {
+ const [contributions, setContributions] = useState<
+ { date: string; questions: number; answers: number; comments: number }[]
+ >([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!userID || !startDate || !endDate) return;
+
+ const fetchContributions = async () => {
+ setLoading(true);
+ try {
+ const data = await getUserContributions(userID, startDate, endDate);
+ setContributions(data);
+ } catch (err) {
+ setError('Error fetching user contributions');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchContributions();
+ }, [userID, startDate, endDate]);
+
+ return { contributions, loading, error };
+};
+
+export default useUserContributions;
diff --git a/client/src/services/contributionsService.ts b/client/src/services/contributionsService.ts
new file mode 100644
index 0000000..60e4759
--- /dev/null
+++ b/client/src/services/contributionsService.ts
@@ -0,0 +1,27 @@
+import api from './config';
+
+const USER_CONTRIBUTIONS_API_URL = `${process.env.REACT_APP_SERVER_URL}/getUserContributions`;
+
+/**
+ * Function to get user contributions (questions, answers, comments) within a specified date range.
+ *
+ * @param userID - The ID of the user.
+ * @param startDate - The start date of the range (YYYY-MM-DD format).
+ * @param endDate - The end date of the range (YYYY-MM-DD format).
+ * @throws Error if there is an issue fetching the user contributions.
+ */
+const getUserContributions = async (
+ userID: string,
+ startDate: string,
+ endDate: string,
+): Promise<{ date: string; questions: number; answers: number; comments: number }[]> => {
+ const res = await api.get(
+ `${USER_CONTRIBUTIONS_API_URL}?userID=${userID}&startDate=${startDate}&endDate=${endDate}`,
+ );
+ if (res.status !== 200) {
+ throw new Error('Error when fetching user contributions');
+ }
+ return res.data;
+};
+
+export default getUserContributions;
diff --git a/server/controllers/contribution.controller.ts b/server/controllers/contribution.controller.ts
new file mode 100644
index 0000000..f308780
--- /dev/null
+++ b/server/controllers/contribution.controller.ts
@@ -0,0 +1,97 @@
+import express, { Request, Response } from 'express';
+import QuestionModel from '../models/questions.model';
+import AnswerModel from '../models/answers.model';
+import CommentModel from '../models/comments.model';
+
+const userContributionsController = () => {
+ const router = express.Router();
+
+ /**
+ * Retrieves a user's contributions (questions, answers, comments) within a specified date range.
+ * Aggregates contributions per day to allow frontend visualization.
+ *
+ * @param req The request object containing userID, startDate, and endDate as query parameters.
+ * @param res The HTTP response object used to send back the found contributions or an error message.
+ *
+ * @returns A Promise that resolves to void.
+ */
+ const getUserContributions = async (req: Request, res: Response): Promise => {
+ const { userID, startDate, endDate } = req.query;
+
+ if (!userID || !startDate || !endDate) {
+ res.status(400).send('Missing required query parameters: userID, startDate, endDate');
+ return;
+ }
+
+ try {
+ const start = new Date(startDate as string);
+ const end = new Date(endDate as string);
+
+ const questions = await QuestionModel.aggregate([
+ { $match: { askedBy: userID, askDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$askDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const answers = await AnswerModel.aggregate([
+ { $match: { ansBy: userID, ansDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$ansDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const comments = await CommentModel.aggregate([
+ { $match: { commentBy: userID, commentDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$commentDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const contributionsMap: Record<
+ string,
+ { questions: number; answers: number; comments: number }
+ > = {};
+
+ const processContributions = (
+ data: { _id: string; count: number }[],
+ type: 'questions' | 'answers' | 'comments',
+ ) => {
+ data.forEach(({ _id, count }) => {
+ if (!contributionsMap[_id]) {
+ contributionsMap[_id] = { questions: 0, answers: 0, comments: 0 };
+ }
+ contributionsMap[_id][type] += count;
+ });
+ };
+
+ processContributions(questions, 'questions');
+ processContributions(answers, 'answers');
+ processContributions(comments, 'comments');
+
+ const contributionsArray = Object.entries(contributionsMap).map(([date, contributions]) => ({
+ date,
+ ...contributions,
+ }));
+
+ res.json(contributionsArray);
+ } catch (err: unknown) {
+ res.status(500).send(`Error when retrieving user contributions: ${(err as Error).message}`);
+ }
+ };
+
+ router.get('/getUserContributions', getUserContributions);
+
+ return router;
+};
+
+export default userContributionsController;
From 7427d65787e6ba3504e6d73153d7d0a0ae69d1f3 Mon Sep 17 00:00:00 2001
From: Khushi Khan <76977629+khushikhan0@users.noreply.github.com>
Date: Fri, 14 Mar 2025 01:15:51 -0400
Subject: [PATCH 004/144] contributions visual
---
.../main/baseComponents/dropdown/index.css | 42 ++++++++++++++++
.../main/baseComponents/dropdown/index.tsx | 48 +++++++++++++++++++
.../main/userContributions/index.css | 0
.../main/userContributions/index.tsx | 0
.../src/components/profileSettings/index.tsx | 3 ++
.../userContributionsComponent/index.css | 20 ++++++++
.../userContributionsComponent/index.tsx | 48 +++++++++++++++++++
7 files changed, 161 insertions(+)
create mode 100644 client/src/components/main/baseComponents/dropdown/index.css
create mode 100644 client/src/components/main/baseComponents/dropdown/index.tsx
delete mode 100644 client/src/components/main/userContributions/index.css
delete mode 100644 client/src/components/main/userContributions/index.tsx
create mode 100644 client/src/components/profileSettings/userContributionsComponent/index.css
create mode 100644 client/src/components/profileSettings/userContributionsComponent/index.tsx
diff --git a/client/src/components/main/baseComponents/dropdown/index.css b/client/src/components/main/baseComponents/dropdown/index.css
new file mode 100644
index 0000000..351e6ae
--- /dev/null
+++ b/client/src/components/main/baseComponents/dropdown/index.css
@@ -0,0 +1,42 @@
+.dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.dropdown-button {
+ background-color: darkgray;
+ color: black;
+ padding: 10px 15px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+}
+
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: white;
+ /* min-width: 150px;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
+ border-radius: 5px;
+ overflow: hidden;
+ z-index: 10; */
+}
+
+.dropdown-content a {
+ color: black;
+ padding: 10px 15px;
+ /* text-decoration: none;
+ display: block;
+ font-size: 14px; */
+}
+
+.dropdown-content a:hover {
+ background-color: lightgray;
+}
+
+.dropdown-content.show {
+ display: block;
+ outline: black;
+}
diff --git a/client/src/components/main/baseComponents/dropdown/index.tsx b/client/src/components/main/baseComponents/dropdown/index.tsx
new file mode 100644
index 0000000..468e5b6
--- /dev/null
+++ b/client/src/components/main/baseComponents/dropdown/index.tsx
@@ -0,0 +1,48 @@
+import React, { ReactNode, useState } from 'react';
+import './index.css';
+
+const Dropdown: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedMonth, setSelectedMonth] = useState('January');
+
+ const toggleDropdown = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const months: string[] = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+
+ const handleMonthClick = (month: string) => {
+ setSelectedMonth(month);
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+ {months.map((month, index) => (
+
+ ))}
+
+
+ );
+};
+
+export default Dropdown;
diff --git a/client/src/components/main/userContributions/index.css b/client/src/components/main/userContributions/index.css
deleted file mode 100644
index e69de29..0000000
diff --git a/client/src/components/main/userContributions/index.tsx b/client/src/components/main/userContributions/index.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/client/src/components/profileSettings/index.tsx b/client/src/components/profileSettings/index.tsx
index 1eae4ed..79bb4ba 100644
--- a/client/src/components/profileSettings/index.tsx
+++ b/client/src/components/profileSettings/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import './index.css';
import useProfileSettings from '../../hooks/useProfileSettings';
+import UserContributionsComponent from './userContributionsComponent';
const ProfileSettings: React.FC = () => {
const {
@@ -125,6 +126,8 @@ const ProfileSettings: React.FC = () => {
>
)}
+
+
{/* ---- Danger Zone (Delete User) ---- */}
{canEditProfile && (
<>
diff --git a/client/src/components/profileSettings/userContributionsComponent/index.css b/client/src/components/profileSettings/userContributionsComponent/index.css
new file mode 100644
index 0000000..40e16c2
--- /dev/null
+++ b/client/src/components/profileSettings/userContributionsComponent/index.css
@@ -0,0 +1,20 @@
+h3.headertext {
+ text-align: center;
+}
+
+.grid-layout {
+ display: grid;
+ grid-template-columns: repeat(7, 30px);
+ grid-template-rows: 30px;
+ gap: 5px;
+ justify-content: center;
+}
+
+.grid-item {
+ width: 30px;
+ height: 30px;
+ border-radius: 10px;
+ background-color: #D9D9D9;
+ box-sizing: border-box;
+ display: flex;
+}
\ No newline at end of file
diff --git a/client/src/components/profileSettings/userContributionsComponent/index.tsx b/client/src/components/profileSettings/userContributionsComponent/index.tsx
new file mode 100644
index 0000000..2423f1d
--- /dev/null
+++ b/client/src/components/profileSettings/userContributionsComponent/index.tsx
@@ -0,0 +1,48 @@
+import getUserContributions from '../../../services/contributionsService';
+import './index.css';
+import useUserContributions from '../../../hooks/useUserContributions';
+import Dropdown from '../../main/baseComponents/dropdown';
+
+interface UserContributionProps {
+ userID: string;
+ startDate: string;
+ endDate: string;
+ err?: string;
+}
+
+const UserContributionsComponent = ({ userID, startDate, endDate, err }: UserContributionProps) => {
+ const { contributions, loading, error } = useUserContributions(userID, startDate, endDate);
+
+ const handleUpdateUserContributions = async (type: string) => {
+ try {
+ await getUserContributions(userID, startDate, endDate);
+ } catch (e) {
+ // Handle error
+ }
+ };
+
+ const daysOfTheWeek = ['Mon', '', 'Wed', '', 'Fri', '', ''];
+
+ return (
+
+
+ {contributions.length} Contributions in
+
+
+ {daysOfTheWeek.map((day, index) => (
+
+ {day === 'Mon' || day === 'Wed' || day === 'Fri' ? day : ''}
+
+ ))}
+
+
+
+ {Array.from({ length: 31 }).map((_, i) => (
+
+ ))}
+
+
+ );
+};
+
+export default UserContributionsComponent;
From 98ec69720c2843487e800a1219bd7c211dd0fa97 Mon Sep 17 00:00:00 2001
From: jessicazha0
Date: Fri, 14 Mar 2025 10:27:35 -0400
Subject: [PATCH 005/144] tests and connect backend
---
server/app.ts | 2 +
server/controllers/contribution.controller.ts | 79 ++----------------
server/services/contribution.service.ts | 83 +++++++++++++++++++
.../contribution.contoller.spec.ts | 67 +++++++++++++++
.../services/contribution.service.spec.ts | 54 ++++++++++++
5 files changed, 213 insertions(+), 72 deletions(-)
create mode 100644 server/services/contribution.service.ts
create mode 100644 server/tests/controllers/contribution.contoller.spec.ts
create mode 100644 server/tests/services/contribution.service.spec.ts
diff --git a/server/app.ts b/server/app.ts
index 573efba..0416f81 100644
--- a/server/app.ts
+++ b/server/app.ts
@@ -18,6 +18,7 @@ import userController from './controllers/user.controller';
import messageController from './controllers/message.controller';
import chatController from './controllers/chat.controller';
import gameController from './controllers/game.controller';
+import userContributionsController from './controllers/contribution.controller';
dotenv.config();
@@ -82,6 +83,7 @@ app.use('/messaging', messageController(socket));
app.use('/user', userController(socket));
app.use('/chat', chatController(socket));
app.use('/games', gameController(socket));
+app.use('/contributions', userContributionsController());
// Export the app instance
export { app, server, startServer };
diff --git a/server/controllers/contribution.controller.ts b/server/controllers/contribution.controller.ts
index f308780..8f9e8eb 100644
--- a/server/controllers/contribution.controller.ts
+++ b/server/controllers/contribution.controller.ts
@@ -1,20 +1,9 @@
import express, { Request, Response } from 'express';
-import QuestionModel from '../models/questions.model';
-import AnswerModel from '../models/answers.model';
-import CommentModel from '../models/comments.model';
+import getUserContributionsByDateRange from '../services/contribution.service';
const userContributionsController = () => {
const router = express.Router();
- /**
- * Retrieves a user's contributions (questions, answers, comments) within a specified date range.
- * Aggregates contributions per day to allow frontend visualization.
- *
- * @param req The request object containing userID, startDate, and endDate as query parameters.
- * @param res The HTTP response object used to send back the found contributions or an error message.
- *
- * @returns A Promise that resolves to void.
- */
const getUserContributions = async (req: Request, res: Response): Promise => {
const { userID, startDate, endDate } = req.query;
@@ -24,66 +13,12 @@ const userContributionsController = () => {
}
try {
- const start = new Date(startDate as string);
- const end = new Date(endDate as string);
-
- const questions = await QuestionModel.aggregate([
- { $match: { askedBy: userID, askDateTime: { $gte: start, $lte: end } } },
- {
- $group: {
- _id: { $dateToString: { format: '%Y-%m-%d', date: '$askDateTime' } },
- count: { $sum: 1 },
- },
- },
- ]);
-
- const answers = await AnswerModel.aggregate([
- { $match: { ansBy: userID, ansDateTime: { $gte: start, $lte: end } } },
- {
- $group: {
- _id: { $dateToString: { format: '%Y-%m-%d', date: '$ansDateTime' } },
- count: { $sum: 1 },
- },
- },
- ]);
-
- const comments = await CommentModel.aggregate([
- { $match: { commentBy: userID, commentDateTime: { $gte: start, $lte: end } } },
- {
- $group: {
- _id: { $dateToString: { format: '%Y-%m-%d', date: '$commentDateTime' } },
- count: { $sum: 1 },
- },
- },
- ]);
-
- const contributionsMap: Record<
- string,
- { questions: number; answers: number; comments: number }
- > = {};
-
- const processContributions = (
- data: { _id: string; count: number }[],
- type: 'questions' | 'answers' | 'comments',
- ) => {
- data.forEach(({ _id, count }) => {
- if (!contributionsMap[_id]) {
- contributionsMap[_id] = { questions: 0, answers: 0, comments: 0 };
- }
- contributionsMap[_id][type] += count;
- });
- };
-
- processContributions(questions, 'questions');
- processContributions(answers, 'answers');
- processContributions(comments, 'comments');
-
- const contributionsArray = Object.entries(contributionsMap).map(([date, contributions]) => ({
- date,
- ...contributions,
- }));
-
- res.json(contributionsArray);
+ const contributions = await getUserContributionsByDateRange(
+ userID as string,
+ startDate as string,
+ endDate as string,
+ );
+ res.json(contributions);
} catch (err: unknown) {
res.status(500).send(`Error when retrieving user contributions: ${(err as Error).message}`);
}
diff --git a/server/services/contribution.service.ts b/server/services/contribution.service.ts
new file mode 100644
index 0000000..f52044e
--- /dev/null
+++ b/server/services/contribution.service.ts
@@ -0,0 +1,83 @@
+import QuestionModel from '../models/questions.model';
+import AnswerModel from '../models/answers.model';
+import CommentModel from '../models/comments.model';
+
+/**
+ * Retrieves a user's contributions (questions, answers, comments) within a specified date range.
+ * Aggregates contributions per day for visualization purposes.
+ *
+ * @param {string} userID - The unique ID of the user.
+ * @param {string} startDate - The start date for filtering contributions.
+ * @param {string} endDate - The end date for filtering contributions.
+ * @returns {Promise<{ date: string; questions: number; answers: number; comments: number }[]>} - A list of contributions grouped by date.
+ */
+const getUserContributionsByDateRange = async (
+ userID: string,
+ startDate: string,
+ endDate: string,
+): Promise<{ date: string; questions: number; answers: number; comments: number }[]> => {
+ try {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+
+ const questions = await QuestionModel.aggregate([
+ { $match: { askedBy: userID, askDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$askDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const answers = await AnswerModel.aggregate([
+ { $match: { ansBy: userID, ansDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$ansDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const comments = await CommentModel.aggregate([
+ { $match: { commentBy: userID, commentDateTime: { $gte: start, $lte: end } } },
+ {
+ $group: {
+ _id: { $dateToString: { format: '%Y-%m-%d', date: '$commentDateTime' } },
+ count: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const contributionsMap: Record<
+ string,
+ { questions: number; answers: number; comments: number }
+ > = {};
+
+ const processContributions = (
+ data: { _id: string; count: number }[],
+ type: 'questions' | 'answers' | 'comments',
+ ) => {
+ data.forEach(({ _id, count }) => {
+ if (!contributionsMap[_id]) {
+ contributionsMap[_id] = { questions: 0, answers: 0, comments: 0 };
+ }
+ contributionsMap[_id][type] += count;
+ });
+ };
+
+ processContributions(questions, 'questions');
+ processContributions(answers, 'answers');
+ processContributions(comments, 'comments');
+
+ return Object.entries(contributionsMap).map(([date, contributions]) => ({
+ date,
+ ...contributions,
+ }));
+ } catch (error) {
+ throw new Error('Error fetching user contributions');
+ }
+};
+
+export default getUserContributionsByDateRange;
diff --git a/server/tests/controllers/contribution.contoller.spec.ts b/server/tests/controllers/contribution.contoller.spec.ts
new file mode 100644
index 0000000..35025e6
--- /dev/null
+++ b/server/tests/controllers/contribution.contoller.spec.ts
@@ -0,0 +1,67 @@
+import mongoose from 'mongoose';
+import supertest from 'supertest';
+import { app } from '../../app';
+import * as userContributionsService from '../../services/contribution.service';
+
+const getUserContributionsSpy = jest.spyOn(userContributionsService, 'default');
+
+describe('GET /getUserContributions', () => {
+ it('should return user contributions within a given date range', async () => {
+ const mockUserID = new mongoose.Types.ObjectId().toString();
+ const mockStartDate = '2024-01-01';
+ const mockEndDate = '2024-12-31';
+
+ const mockContributions = [
+ { date: '2024-03-10', questions: 2, answers: 1, comments: 3 },
+ { date: '2024-03-11', questions: 1, answers: 2, comments: 0 },
+ ];
+
+ getUserContributionsSpy.mockResolvedValueOnce(mockContributions);
+
+ const response = await supertest(app).get(
+ `/contributions/getUserContributions?userID=${mockUserID}&startDate=${mockStartDate}&endDate=${mockEndDate}`,
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual(mockContributions);
+ });
+
+ it('should return 400 if userID is missing', async () => {
+ const response = await supertest(app).get(
+ '/contributions/getUserContributions?startDate=2024-01-01&endDate=2024-12-31',
+ );
+ expect(response.status).toBe(400);
+ expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
+ });
+
+ it('should return 400 if startDate is missing', async () => {
+ const response = await supertest(app).get(
+ '/contributions/getUserContributions?userID=dummyUserId&endDate=2024-12-31',
+ );
+ expect(response.status).toBe(400);
+ expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
+ });
+
+ it('should return 400 if endDate is missing', async () => {
+ const response = await supertest(app).get(
+ '/contributions/getUserContributions?userID=dummyUserId&startDate=2024-01-01',
+ );
+ expect(response.status).toBe(400);
+ expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
+ });
+
+ it('should return 500 if service method throws an error', async () => {
+ const mockUserID = new mongoose.Types.ObjectId().toString();
+ const mockStartDate = '2024-01-01';
+ const mockEndDate = '2024-12-31';
+
+ getUserContributionsSpy.mockRejectedValueOnce(new Error('Database error'));
+
+ const response = await supertest(app).get(
+ `/contributions/getUserContributions?userID=${mockUserID}&startDate=${mockStartDate}&endDate=${mockEndDate}`,
+ );
+
+ expect(response.status).toBe(500);
+ expect(response.text).toContain('Error when retrieving user contributions');
+ });
+});
diff --git a/server/tests/services/contribution.service.spec.ts b/server/tests/services/contribution.service.spec.ts
new file mode 100644
index 0000000..7023358
--- /dev/null
+++ b/server/tests/services/contribution.service.spec.ts
@@ -0,0 +1,54 @@
+import mongoose from 'mongoose';
+import getUserContributionsByDateRange from '../../services/contribution.service';
+import QuestionModel from '../../models/questions.model';
+import AnswerModel from '../../models/answers.model';
+import CommentModel from '../../models/comments.model';
+
+jest.mock('../../models/questions.model', () => ({
+ aggregate: jest.fn(),
+}));
+
+jest.mock('../../models/answers.model', () => ({
+ aggregate: jest.fn(),
+}));
+
+jest.mock('../../models/comments.model', () => ({
+ aggregate: jest.fn(),
+}));
+
+describe('getUserContributionsByDateRange', () => {
+ const mockUserID = new mongoose.Types.ObjectId().toString();
+ const mockStartDate = '2024-01-01';
+ const mockEndDate = '2024-12-31';
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return aggregated contributions data correctly', async () => {
+ (QuestionModel.aggregate as jest.Mock).mockResolvedValueOnce([{ _id: '2024-03-10', count: 2 }]);
+ (AnswerModel.aggregate as jest.Mock).mockResolvedValueOnce([{ _id: '2024-03-10', count: 1 }]);
+ (CommentModel.aggregate as jest.Mock).mockResolvedValueOnce([{ _id: '2024-03-10', count: 3 }]);
+
+ const result = await getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate);
+
+ expect(result).toEqual([{ date: '2024-03-10', questions: 2, answers: 1, comments: 3 }]);
+ });
+
+ it('should return an empty array if no contributions exist', async () => {
+ (QuestionModel.aggregate as jest.Mock).mockResolvedValueOnce([]);
+ (AnswerModel.aggregate as jest.Mock).mockResolvedValueOnce([]);
+ (CommentModel.aggregate as jest.Mock).mockResolvedValueOnce([]);
+
+ const result = await getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate);
+ expect(result).toEqual([]);
+ });
+
+ it('should throw an error if database query fails', async () => {
+ (QuestionModel.aggregate as jest.Mock).mockRejectedValueOnce(new Error('Database error'));
+
+ await expect(
+ getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate),
+ ).rejects.toThrow('Error fetching user contributions');
+ });
+});
From 4dbfcdc893f2927b7e13587f0922707a57924603 Mon Sep 17 00:00:00 2001
From: jessicazha0
Date: Fri, 14 Mar 2025 13:18:18 -0400
Subject: [PATCH 006/144] removed timeframe logic and linked backend
---
.../src/components/profileSettings/index.tsx | 4 +--
.../userContributionsComponent/index.tsx | 23 ++++++-------
client/src/hooks/useUserContributions.ts | 14 ++++----
client/src/services/contributionsService.ts | 17 ++++------
server/app.ts | 4 +--
server/controllers/contribution.controller.ts | 22 ++++++-------
server/services/contribution.service.ts | 20 +++++-------
.../contribution.contoller.spec.ts | 32 +++----------------
.../services/contribution.service.spec.ts | 16 ++++------
9 files changed, 57 insertions(+), 95 deletions(-)
diff --git a/client/src/components/profileSettings/index.tsx b/client/src/components/profileSettings/index.tsx
index 79bb4ba..0ad8df9 100644
--- a/client/src/components/profileSettings/index.tsx
+++ b/client/src/components/profileSettings/index.tsx
@@ -99,6 +99,8 @@ const ProfileSettings: React.FC = () => {
{userData.dateJoined ? new Date(userData.dateJoined).toLocaleDateString() : 'N/A'}
+
+
{/* ---- Reset Password Section ---- */}
{canEditProfile && (
<>
@@ -126,8 +128,6 @@ const ProfileSettings: React.FC = () => {
>
)}
-
-
{/* ---- Danger Zone (Delete User) ---- */}
{canEditProfile && (
<>
diff --git a/client/src/components/profileSettings/userContributionsComponent/index.tsx b/client/src/components/profileSettings/userContributionsComponent/index.tsx
index 2423f1d..100e6d8 100644
--- a/client/src/components/profileSettings/userContributionsComponent/index.tsx
+++ b/client/src/components/profileSettings/userContributionsComponent/index.tsx
@@ -1,28 +1,25 @@
-import getUserContributions from '../../../services/contributionsService';
import './index.css';
import useUserContributions from '../../../hooks/useUserContributions';
import Dropdown from '../../main/baseComponents/dropdown';
interface UserContributionProps {
userID: string;
- startDate: string;
- endDate: string;
err?: string;
}
-const UserContributionsComponent = ({ userID, startDate, endDate, err }: UserContributionProps) => {
- const { contributions, loading, error } = useUserContributions(userID, startDate, endDate);
-
- const handleUpdateUserContributions = async (type: string) => {
- try {
- await getUserContributions(userID, startDate, endDate);
- } catch (e) {
- // Handle error
- }
- };
+const UserContributionsComponent = ({ userID, err }: UserContributionProps) => {
+ const { contributions, loading, error } = useUserContributions(userID);
const daysOfTheWeek = ['Mon', '', 'Wed', '', 'Fri', '', ''];
+ if (loading) {
+ return Loading contributions...
;
+ }
+
+ if (error) {
+ return Error loading contributions: {error}
;
+ }
+
return (
diff --git a/client/src/hooks/useUserContributions.ts b/client/src/hooks/useUserContributions.ts
index 29053e0..8561d11 100644
--- a/client/src/hooks/useUserContributions.ts
+++ b/client/src/hooks/useUserContributions.ts
@@ -6,13 +6,11 @@ import getUserContributions from '../services/contributionsService';
* Supports fetching contributions for any user by providing a `userID`.
*
* @param userID - The ID of the user whose contributions are being fetched.
- * @param startDate - The start date of the range (YYYY-MM-DD format).
- * @param endDate - The end date of the range (YYYY-MM-DD format).
* @returns contributions - The fetched contributions data.
* @returns loading - Boolean indicating whether data is loading.
* @returns error - Any error message encountered during the fetch.
*/
-const useUserContributions = (userID: string, startDate: string, endDate: string) => {
+const useUserContributions = (userID: string) => {
const [contributions, setContributions] = useState<
{ date: string; questions: number; answers: number; comments: number }[]
>([]);
@@ -20,12 +18,16 @@ const useUserContributions = (userID: string, startDate: string, endDate: string
const [error, setError] = useState(null);
useEffect(() => {
- if (!userID || !startDate || !endDate) return;
+ if (!userID) {
+ setContributions([]);
+ return;
+ }
const fetchContributions = async () => {
setLoading(true);
+ setError(null);
try {
- const data = await getUserContributions(userID, startDate, endDate);
+ const data = await getUserContributions(userID);
setContributions(data);
} catch (err) {
setError('Error fetching user contributions');
@@ -35,7 +37,7 @@ const useUserContributions = (userID: string, startDate: string, endDate: string
};
fetchContributions();
- }, [userID, startDate, endDate]);
+ }, [userID]);
return { contributions, loading, error };
};
diff --git a/client/src/services/contributionsService.ts b/client/src/services/contributionsService.ts
index 60e4759..809567f 100644
--- a/client/src/services/contributionsService.ts
+++ b/client/src/services/contributionsService.ts
@@ -1,27 +1,22 @@
import api from './config';
-const USER_CONTRIBUTIONS_API_URL = `${process.env.REACT_APP_SERVER_URL}/getUserContributions`;
+const USER_CONTRIBUTIONS_API_URL = `${process.env.REACT_APP_SERVER_URL}/contributions/getContributions`;
/**
* Function to get user contributions (questions, answers, comments) within a specified date range.
*
* @param userID - The ID of the user.
- * @param startDate - The start date of the range (YYYY-MM-DD format).
- * @param endDate - The end date of the range (YYYY-MM-DD format).
* @throws Error if there is an issue fetching the user contributions.
*/
const getUserContributions = async (
userID: string,
- startDate: string,
- endDate: string,
): Promise<{ date: string; questions: number; answers: number; comments: number }[]> => {
- const res = await api.get(
- `${USER_CONTRIBUTIONS_API_URL}?userID=${userID}&startDate=${startDate}&endDate=${endDate}`,
- );
- if (res.status !== 200) {
- throw new Error('Error when fetching user contributions');
+ try {
+ const res = await api.get(`${USER_CONTRIBUTIONS_API_URL}?userID=${userID}`);
+ return res.data;
+ } catch (error) {
+ throw new Error('Failed to fetch user contributions');
}
- return res.data;
};
export default getUserContributions;
diff --git a/server/app.ts b/server/app.ts
index 0416f81..46c8019 100644
--- a/server/app.ts
+++ b/server/app.ts
@@ -18,7 +18,7 @@ import userController from './controllers/user.controller';
import messageController from './controllers/message.controller';
import chatController from './controllers/chat.controller';
import gameController from './controllers/game.controller';
-import userContributionsController from './controllers/contribution.controller';
+import contributionsController from './controllers/contribution.controller';
dotenv.config();
@@ -83,7 +83,7 @@ app.use('/messaging', messageController(socket));
app.use('/user', userController(socket));
app.use('/chat', chatController(socket));
app.use('/games', gameController(socket));
-app.use('/contributions', userContributionsController());
+app.use('/contributions', contributionsController());
// Export the app instance
export { app, server, startServer };
diff --git a/server/controllers/contribution.controller.ts b/server/controllers/contribution.controller.ts
index 8f9e8eb..933ca76 100644
--- a/server/controllers/contribution.controller.ts
+++ b/server/controllers/contribution.controller.ts
@@ -1,32 +1,28 @@
import express, { Request, Response } from 'express';
-import getUserContributionsByDateRange from '../services/contribution.service';
+import getUserContributions from '../services/contribution.service';
-const userContributionsController = () => {
+const contributionsController = () => {
const router = express.Router();
- const getUserContributions = async (req: Request, res: Response): Promise => {
- const { userID, startDate, endDate } = req.query;
+ const getContributions = async (req: Request, res: Response): Promise => {
+ const { userID } = req.query;
- if (!userID || !startDate || !endDate) {
- res.status(400).send('Missing required query parameters: userID, startDate, endDate');
+ if (!userID) {
+ res.status(400).send('Invalid request');
return;
}
try {
- const contributions = await getUserContributionsByDateRange(
- userID as string,
- startDate as string,
- endDate as string,
- );
+ const contributions = await getUserContributions(userID as string);
res.json(contributions);
} catch (err: unknown) {
res.status(500).send(`Error when retrieving user contributions: ${(err as Error).message}`);
}
};
- router.get('/getUserContributions', getUserContributions);
+ router.get('/getContributions', getContributions);
return router;
};
-export default userContributionsController;
+export default contributionsController;
diff --git a/server/services/contribution.service.ts b/server/services/contribution.service.ts
index f52044e..3230c54 100644
--- a/server/services/contribution.service.ts
+++ b/server/services/contribution.service.ts
@@ -7,21 +7,14 @@ import CommentModel from '../models/comments.model';
* Aggregates contributions per day for visualization purposes.
*
* @param {string} userID - The unique ID of the user.
- * @param {string} startDate - The start date for filtering contributions.
- * @param {string} endDate - The end date for filtering contributions.
* @returns {Promise<{ date: string; questions: number; answers: number; comments: number }[]>} - A list of contributions grouped by date.
*/
-const getUserContributionsByDateRange = async (
+const getUserContributions = async (
userID: string,
- startDate: string,
- endDate: string,
): Promise<{ date: string; questions: number; answers: number; comments: number }[]> => {
try {
- const start = new Date(startDate);
- const end = new Date(endDate);
-
const questions = await QuestionModel.aggregate([
- { $match: { askedBy: userID, askDateTime: { $gte: start, $lte: end } } },
+ { $match: { askedBy: userID } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$askDateTime' } },
@@ -30,8 +23,11 @@ const getUserContributionsByDateRange = async (
},
]);
+ // eslint-disable-next-line
+ console.log('Questions found:', questions);
+
const answers = await AnswerModel.aggregate([
- { $match: { ansBy: userID, ansDateTime: { $gte: start, $lte: end } } },
+ { $match: { ansBy: userID } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$ansDateTime' } },
@@ -41,7 +37,7 @@ const getUserContributionsByDateRange = async (
]);
const comments = await CommentModel.aggregate([
- { $match: { commentBy: userID, commentDateTime: { $gte: start, $lte: end } } },
+ { $match: { commentBy: userID } },
{
$group: {
_id: { $dateToString: { format: '%Y-%m-%d', date: '$commentDateTime' } },
@@ -80,4 +76,4 @@ const getUserContributionsByDateRange = async (
}
};
-export default getUserContributionsByDateRange;
+export default getUserContributions;
diff --git a/server/tests/controllers/contribution.contoller.spec.ts b/server/tests/controllers/contribution.contoller.spec.ts
index 35025e6..33b26d7 100644
--- a/server/tests/controllers/contribution.contoller.spec.ts
+++ b/server/tests/controllers/contribution.contoller.spec.ts
@@ -5,11 +5,9 @@ import * as userContributionsService from '../../services/contribution.service';
const getUserContributionsSpy = jest.spyOn(userContributionsService, 'default');
-describe('GET /getUserContributions', () => {
+describe('GET /getContributions', () => {
it('should return user contributions within a given date range', async () => {
const mockUserID = new mongoose.Types.ObjectId().toString();
- const mockStartDate = '2024-01-01';
- const mockEndDate = '2024-12-31';
const mockContributions = [
{ date: '2024-03-10', questions: 2, answers: 1, comments: 3 },
@@ -19,7 +17,7 @@ describe('GET /getUserContributions', () => {
getUserContributionsSpy.mockResolvedValueOnce(mockContributions);
const response = await supertest(app).get(
- `/contributions/getUserContributions?userID=${mockUserID}&startDate=${mockStartDate}&endDate=${mockEndDate}`,
+ `/contributions/getContributions?userID=${mockUserID}`,
);
expect(response.status).toBe(200);
@@ -27,38 +25,18 @@ describe('GET /getUserContributions', () => {
});
it('should return 400 if userID is missing', async () => {
- const response = await supertest(app).get(
- '/contributions/getUserContributions?startDate=2024-01-01&endDate=2024-12-31',
- );
- expect(response.status).toBe(400);
- expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
- });
-
- it('should return 400 if startDate is missing', async () => {
- const response = await supertest(app).get(
- '/contributions/getUserContributions?userID=dummyUserId&endDate=2024-12-31',
- );
- expect(response.status).toBe(400);
- expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
- });
-
- it('should return 400 if endDate is missing', async () => {
- const response = await supertest(app).get(
- '/contributions/getUserContributions?userID=dummyUserId&startDate=2024-01-01',
- );
+ const response = await supertest(app).get('/contributions/getContributions');
expect(response.status).toBe(400);
- expect(response.text).toBe('Missing required query parameters: userID, startDate, endDate');
+ expect(response.text).toBe('Invalid request');
});
it('should return 500 if service method throws an error', async () => {
const mockUserID = new mongoose.Types.ObjectId().toString();
- const mockStartDate = '2024-01-01';
- const mockEndDate = '2024-12-31';
getUserContributionsSpy.mockRejectedValueOnce(new Error('Database error'));
const response = await supertest(app).get(
- `/contributions/getUserContributions?userID=${mockUserID}&startDate=${mockStartDate}&endDate=${mockEndDate}`,
+ `/contributions/getContributions?userID=${mockUserID}`,
);
expect(response.status).toBe(500);
diff --git a/server/tests/services/contribution.service.spec.ts b/server/tests/services/contribution.service.spec.ts
index 7023358..1dff68f 100644
--- a/server/tests/services/contribution.service.spec.ts
+++ b/server/tests/services/contribution.service.spec.ts
@@ -1,5 +1,5 @@
import mongoose from 'mongoose';
-import getUserContributionsByDateRange from '../../services/contribution.service';
+import getUserContributions from '../../services/contribution.service';
import QuestionModel from '../../models/questions.model';
import AnswerModel from '../../models/answers.model';
import CommentModel from '../../models/comments.model';
@@ -16,10 +16,8 @@ jest.mock('../../models/comments.model', () => ({
aggregate: jest.fn(),
}));
-describe('getUserContributionsByDateRange', () => {
+describe('getUserContributions', () => {
const mockUserID = new mongoose.Types.ObjectId().toString();
- const mockStartDate = '2024-01-01';
- const mockEndDate = '2024-12-31';
afterEach(() => {
jest.clearAllMocks();
@@ -30,7 +28,7 @@ describe('getUserContributionsByDateRange', () => {
(AnswerModel.aggregate as jest.Mock).mockResolvedValueOnce([{ _id: '2024-03-10', count: 1 }]);
(CommentModel.aggregate as jest.Mock).mockResolvedValueOnce([{ _id: '2024-03-10', count: 3 }]);
- const result = await getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate);
+ const result = await getUserContributions(mockUserID);
expect(result).toEqual([{ date: '2024-03-10', questions: 2, answers: 1, comments: 3 }]);
});
@@ -40,15 +38,15 @@ describe('getUserContributionsByDateRange', () => {
(AnswerModel.aggregate as jest.Mock).mockResolvedValueOnce([]);
(CommentModel.aggregate as jest.Mock).mockResolvedValueOnce([]);
- const result = await getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate);
+ const result = await getUserContributions(mockUserID);
expect(result).toEqual([]);
});
it('should throw an error if database query fails', async () => {
(QuestionModel.aggregate as jest.Mock).mockRejectedValueOnce(new Error('Database error'));
- await expect(
- getUserContributionsByDateRange(mockUserID, mockStartDate, mockEndDate),
- ).rejects.toThrow('Error fetching user contributions');
+ await expect(getUserContributions(mockUserID)).rejects.toThrow(
+ 'Error fetching user contributions',
+ );
});
});
From 1c8ec227ead23faff13c019852a47d69352fb58b Mon Sep 17 00:00:00 2001
From: jessicazha0
Date: Fri, 14 Mar 2025 18:07:50 -0400
Subject: [PATCH 007/144] accurate months and colored grids by contributions
---
.../main/baseComponents/dropdown/index.css | 31 ++++---
.../main/baseComponents/dropdown/index.tsx | 19 +++--
.../calendarGrid/index.css | 49 +++++++++++
.../calendarGrid/index.tsx | 84 +++++++++++++++++++
.../userContributionsComponent/index.tsx | 54 ++++++++----
.../yearSelector/index.css | 26 ++++++
.../yearSelector/index.tsx | 26 ++++++
7 files changed, 255 insertions(+), 34 deletions(-)
create mode 100644 client/src/components/profileSettings/userContributionsComponent/calendarGrid/index.css
create mode 100644 client/src/components/profileSettings/userContributionsComponent/calendarGrid/index.tsx
create mode 100644 client/src/components/profileSettings/userContributionsComponent/yearSelector/index.css
create mode 100644 client/src/components/profileSettings/userContributionsComponent/yearSelector/index.tsx
diff --git a/client/src/components/main/baseComponents/dropdown/index.css b/client/src/components/main/baseComponents/dropdown/index.css
index 351e6ae..dc31bcf 100644
--- a/client/src/components/main/baseComponents/dropdown/index.css
+++ b/client/src/components/main/baseComponents/dropdown/index.css
@@ -4,39 +4,48 @@
}
.dropdown-button {
- background-color: darkgray;
+ background-color: lightgray;
color: black;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
+ width: 135px;
+ text-align: center;
}
.dropdown-content {
display: none;
position: absolute;
background-color: white;
- /* min-width: 150px;
+ min-width: 100%;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 5px;
overflow: hidden;
- z-index: 10; */
+ z-index: 10;
+ left: 0;
+ padding: 0;
+ margin: 0;
}
-.dropdown-content a {
- color: black;
- padding: 10px 15px;
- /* text-decoration: none;
+.dropdown-item {
display: block;
- font-size: 14px; */
+ width: 100%;
+ padding: 10px 15px;
+ text-align: left;
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-size: 14px;
+ white-space: nowrap;
+ margin: 0;
}
-.dropdown-content a:hover {
+.dropdown-item:hover {
background-color: lightgray;
}
.dropdown-content.show {
display: block;
- outline: black;
-}
+}
\ No newline at end of file
diff --git a/client/src/components/main/baseComponents/dropdown/index.tsx b/client/src/components/main/baseComponents/dropdown/index.tsx
index 468e5b6..99d4b63 100644
--- a/client/src/components/main/baseComponents/dropdown/index.tsx
+++ b/client/src/components/main/baseComponents/dropdown/index.tsx
@@ -1,9 +1,13 @@
-import React, { ReactNode, useState } from 'react';
+import React, { useState } from 'react';
import './index.css';
-const Dropdown: React.FC = () => {
+interface DropdownProps {
+ onChange: (monthIndex: number) => void;
+}
+
+const Dropdown: React.FC = ({ onChange }) => {
const [isOpen, setIsOpen] = useState(false);
- const [selectedMonth, setSelectedMonth] = useState('January');
+ const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
const toggleDropdown = () => {
setIsOpen(!isOpen);
@@ -24,19 +28,20 @@ const Dropdown: React.FC = () => {
'December',
];
- const handleMonthClick = (month: string) => {
- setSelectedMonth(month);
+ const handleMonthClick = (monthIndex: number) => {
+ setSelectedMonth(monthIndex);
setIsOpen(false);
+ onChange(monthIndex);
};
return (
{months.map((month, index) => (
-