diff --git a/client/src/hooks/useCommunityNames.ts b/client/src/hooks/useCommunityNames.ts
new file mode 100644
index 0000000..8d51f54
--- /dev/null
+++ b/client/src/hooks/useCommunityNames.ts
@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+import { getCommunityNames } from '../services/communityService';
+import { Community } from '../types';
+
+/**
+ * Custom hook to handle fetching community details by community name.
+ *
+ * @param t - The tag object to fetch data for
+ *
+ * @returns community - The current community details.
+ * @returns setCommunity - Setter to manually update the community state if needed.
+ */
+const useCommunityNames = () => {
+ const [communityNames, setCommunityNames] = useState([]);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const res = await getCommunityNames();
+ if (Array.isArray(res)) {
+ setCommunityNames(res);
+ } else {
+ setCommunityNames([]);
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(e);
+ }
+ };
+ fetchData();
+ });
+
+ return {
+ communityNames,
+ };
+};
+
+export default useCommunityNames;
diff --git a/client/src/hooks/useCreateUser.ts b/client/src/hooks/useCreateUser.ts
index 88b8f7b..0b7e6f9 100644
--- a/client/src/hooks/useCreateUser.ts
+++ b/client/src/hooks/useCreateUser.ts
@@ -2,8 +2,9 @@
import { ChangeEvent, useState } from 'react';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';
-import { doc, setDoc } from 'firebase/firestore';
-import { auth, db } from '../firebaseConfig';
+import { auth } from '../firebaseConfig';
+import { User } from '../types';
+import { addUser } from '../services/userService';
import useLoginContext from './useLoginContext';
/**
@@ -48,25 +49,18 @@ const useCreateUser = () => {
event.preventDefault();
setIsLoading(true);
try {
- const { user } = await createUserWithEmailAndPassword(auth, email, password);
+ await createUserWithEmailAndPassword(auth, email, password);
setIsLoading(false);
-
- const userRef = doc(db, 'users', user.email ?? '');
- await setDoc(
- userRef,
- {
- username: user.email,
- first_name: firstName,
- last_name: lastName,
- },
- { merge: true },
- );
-
- setUser({
- username: user.email ?? '',
+ const user: User = {
+ username: email,
+ firstName,
+ lastName,
+ tags: [],
+ community: '',
status: 'low',
- });
-
+ };
+ setUser(user);
+ await addUser(user);
navigate('/new/tagselection');
} catch (error) {
setErrorMessage((error as Error).message);
diff --git a/client/src/hooks/useLogin.ts b/client/src/hooks/useLogin.ts
index c906bbe..1a37881 100644
--- a/client/src/hooks/useLogin.ts
+++ b/client/src/hooks/useLogin.ts
@@ -1,9 +1,9 @@
-/* eslint-disable import/no-extraneous-dependencies */
import { useNavigate } from 'react-router-dom';
import { ChangeEvent, useState } from 'react';
import { signInWithEmailAndPassword } from 'firebase/auth';
-import useLoginContext from './useLoginContext';
import { auth } from '../firebaseConfig';
+import useLoginContext from './useLoginContext';
+import { getUser } from '../services/userService';
/**
* Custom hook to handle login input and submission.
@@ -19,7 +19,6 @@ const useLogin = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
-
const { setUser } = useLoginContext();
const navigate = useNavigate();
@@ -44,8 +43,17 @@ const useLogin = () => {
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
- const { user } = await signInWithEmailAndPassword(auth, email, password);
- setUser({ username: user.email ?? '', status: 'low' });
+ await signInWithEmailAndPassword(auth, email, password);
+ const userData = await getUser(email);
+ setUser({
+ username: userData.username,
+ firstName: userData.firstName,
+ lastName: userData.lastName,
+ tags: userData.tags,
+ community: userData.community,
+ status: userData.status,
+ });
+
navigate('/home');
} catch (error) {
setErrorMessage((error as Error).message);
diff --git a/client/src/services/communityService.ts b/client/src/services/communityService.ts
new file mode 100644
index 0000000..a49587f
--- /dev/null
+++ b/client/src/services/communityService.ts
@@ -0,0 +1,19 @@
+/* eslint-disable import/prefer-default-export */
+import api from './config';
+import { Community } from '../types';
+
+const COMMUNITY_API_URL = `${process.env.REACT_APP_SERVER_URL}/community`;
+
+/**
+ * ADD TESTS??????
+ * @returns names of communities
+ */
+const getCommunityNames = async (): Promise => {
+ const res = await api.get(`${COMMUNITY_API_URL}/getCommunityNames`);
+ if (res.status !== 200) {
+ throw new Error(`Error when fetching community names`);
+ }
+ return res.data;
+};
+
+export { getCommunityNames };
diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts
new file mode 100644
index 0000000..9fbfb53
--- /dev/null
+++ b/client/src/services/userService.ts
@@ -0,0 +1,48 @@
+import { Tag, User } from '../types';
+import api from './config';
+
+const USER_API_URL = `${process.env.REACT_APP_SERVER_URL}/user`;
+
+/**
+ * Adds a new user to the database.
+ *
+ * @param user - The user object containing the user's details.
+ * @throws Error Throws an error if the request fails or the response status is not 200.
+ * @returns The created user object from the server response.
+ */
+const addUser = async (user: User): Promise => {
+ try {
+ const res = await api.post(`${USER_API_URL}/add`, user);
+ if (res.status !== 200) {
+ throw new Error('Error while creating a new user');
+ }
+ return res.data;
+ } catch (error) {
+ throw new Error(`Error while adding user: ${(error as Error).message}`);
+ }
+};
+
+const updateUserTags = async (username: string, tags: string[]): Promise => {
+ const res = await api.put(`${USER_API_URL}/updateTags`, { username, tags });
+ if (res.status !== 200) {
+ throw new Error('Failed to update user tags');
+ }
+ return res.data;
+};
+const updateUserCommunity = async (username: string, community: string): Promise => {
+ const res = await api.put(`${USER_API_URL}/updateCommunity`, { username, community });
+ if (res.status !== 200) {
+ throw new Error('Failed to update user community');
+ }
+ return res.data;
+};
+
+const getUser = async (username: string): Promise => {
+ const res = await api.get(`${USER_API_URL}/getUser`, { params: { username } });
+ if (res.status !== 200) {
+ throw new Error('Failed to fetch user data');
+ }
+ return res.data;
+};
+
+export { addUser, updateUserTags, updateUserCommunity, getUser };
diff --git a/client/src/types.ts b/client/src/types.ts
index e86f034..e699339 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -7,9 +7,11 @@ export type FakeSOSocket = Socket;
*/
export interface User {
username: string;
- first_name?: string;
- last_name?: string;
- status: string;
+ firstName: string;
+ lastName: string;
+ tags: string[];
+ community: string;
+ status: 'low' | 'high';
}
/**
@@ -151,3 +153,18 @@ export interface ServerToClientEvents {
voteUpdate: (vote: VoteUpdatePayload) => void;
commentUpdate: (update: CommentUpdatePayload) => void;
}
+
+/**
+ * Interface representing Community.
+ *
+ * - name - The name of the community.
+ * - tags - An array of strings with the tag names.
+ * - users - An array of strings of the user's names.
+ * - questions - An array of references to `Question` documents associated with the community.
+ */
+export interface Community {
+ name: string;
+ tags: string[];
+ users: string[];
+ questions: Question[];
+}
diff --git a/server/app.ts b/server/app.ts
index 77e0f8a..e3d3de1 100644
--- a/server/app.ts
+++ b/server/app.ts
@@ -13,7 +13,9 @@ import answerController from './controller/answer';
import questionController from './controller/question';
import tagController from './controller/tag';
import commentController from './controller/comment';
+import communityController from './controller/community';
import { FakeSOSocket } from './types';
+import userController from './controller/user';
dotenv.config();
@@ -72,6 +74,8 @@ app.use('/question', questionController(socket));
app.use('/tag', tagController());
app.use('/answer', answerController(socket));
app.use('/comment', commentController(socket));
+app.use('/community', communityController());
+app.use('/user', userController());
// Export the app instance
export { app, server, startServer };
diff --git a/server/controller/community.ts b/server/controller/community.ts
new file mode 100644
index 0000000..ddb23e9
--- /dev/null
+++ b/server/controller/community.ts
@@ -0,0 +1,34 @@
+import express, { Request, Response, Router } from 'express';
+import CommunityModel from '../models/communities';
+
+const communityController = () => {
+ const router: Router = express.Router();
+
+ /**
+ * Retrieves a list of tags along with the number of questions associated with each tag.
+ * If there is an error, the HTTP response's status is updated.
+ *
+ * @param _ The HTTP request object (not used in this function).
+ * @param res The HTTP response object used to send back the tag count mapping.
+ *
+ * @returns A Promise that resolves to void.
+ */
+ /**
+ * @param req The Request object containing the tag name in the URL parameters.
+ * @param res The HTTP response object used to send back the result of the operation.
+ */
+ const getCommunityNames = async (req: Request, res: Response): Promise => {
+ try {
+ const communities = await CommunityModel.find({}); // Retrieve only necessary fields
+ res.json(communities);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to retrieve communities' });
+ }
+ };
+
+ router.get('/getCommunityNames', getCommunityNames); // so that we can show all tags in the frontend
+
+ return router;
+};
+
+export default communityController;
diff --git a/server/controller/user.ts b/server/controller/user.ts
new file mode 100644
index 0000000..518a848
--- /dev/null
+++ b/server/controller/user.ts
@@ -0,0 +1,97 @@
+/* eslint-disable no-console */
+import express, { Request, Response, Router } from 'express';
+import UserModel from '../models/users';
+
+const userController = () => {
+ const router: Router = express.Router();
+
+ /**
+ * Adds a new user to the database.
+ */
+ router.post('/add', async (req: Request, res: Response) => {
+ try {
+ const { username, firstName, lastName, tags, community, status } = req.body;
+
+ const newUser = await UserModel.create({
+ username,
+ firstName,
+ lastName,
+ tags: tags || [],
+ community: community || '',
+ status,
+ });
+
+ return res.status(200).json(newUser);
+ } catch (error) {
+ return res.status(500).json({ error: 'Error adding user' });
+ }
+ });
+
+ router.put('/updateTags', async (req: Request, res: Response) => {
+ try {
+ const { username, tags } = req.body;
+
+ if (!username || !tags) {
+ return res.status(400).json({ error: 'Username and tags are required' });
+ }
+
+ const updatedUser = await UserModel.findOneAndUpdate(
+ { username },
+ { $set: { tags } },
+ { new: true },
+ );
+
+ if (!updatedUser) {
+ return res.status(404).json({ error: 'User not found' });
+ }
+
+ return res.status(200).json(updatedUser);
+ } catch (error) {
+ return res.status(500).json({ error: 'Error updating tags' });
+ }
+ });
+
+ router.put('/updateCommunity', async (req: Request, res: Response) => {
+ try {
+ const { username, community } = req.body;
+
+ if (!username || !community) {
+ return res.status(400).json({ message: 'Username and community are required' });
+ }
+
+ const user = await UserModel.findOneAndUpdate({ username }, { community }, { new: true });
+
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ return res.status(200).json(user);
+ } catch (error) {
+ return res.status(500).json({ message: 'Error updating community' });
+ }
+ });
+
+ router.get('/getUser', async (req: Request, res: Response) => {
+ try {
+ const { username } = req.query;
+
+ if (!username) {
+ return res.status(400).json({ message: 'Username is required' });
+ }
+
+ const user = await UserModel.findOne({ username });
+
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ return res.status(200).json(user);
+ } catch (error) {
+ return res.status(500).json({ message: 'Error fetching user data' });
+ }
+ });
+
+ return router;
+};
+
+export default userController;
diff --git a/server/data/posts_strings.ts b/server/data/posts_strings.ts
index 120637b..2d96919 100644
--- a/server/data/posts_strings.ts
+++ b/server/data/posts_strings.ts
@@ -52,6 +52,12 @@ export const T6_NAME = 'website';
export const T6_DESC =
'A website is a collection of interlinked web pages, typically identified with a common domain name, and published on at least one web server. Websites can serve various purposes, such as information sharing, entertainment, commerce, and social networking.';
+export const T7_NAME = 'css';
+export const T7_DESC =
+ 'Cascading Style Sheets (CSS) is a style sheet language used for describing the presentation of a document written in a markup language like HTML. CSS is a cornerstone technology, alongside HTML and JavaScript.';
+export const T8_NAME = 'aws';
+export const T8_DESC =
+ 'Amazon Web Services (AWS) is a comprehensive, evolving cloud computing platform provided by Amazon. It offers a mix of infrastructure as a service (IaaS), platform as a service (PaaS), and packaged software as a service (SaaS) offerings.';
export const C1_TEXT =
'This explanation about React Router is really helpful! I never realized it was just a wrapper around history. Thanks!';
export const C2_TEXT =
@@ -76,3 +82,8 @@ export const C11_TEXT =
'I found the discussion on SharedPreferences vs apply() very useful. Great explanation of the differences!';
export const C12_TEXT =
"I feel like there's so much more to Android Studio that I'm just scratching the surface of. Thanks for sharing your experience!";
+export const FRONT_END_TAGS = [T1_NAME, T6_NAME, T7_NAME];
+// export const BACK_END_TAGS = ['mongodb', 'node.js'];
+// export const ML_TAGS = ['neural-networks', 'deep-learning'];
+// export const AI_TAGS = ['neural-networks', 'reinforcement-learning', 'pytorch', 'nlp'];
+export const CLOUD_TAGS = [T8_NAME, T5_NAME]; // ADD AZURE, GCP
diff --git a/server/models/communities.ts b/server/models/communities.ts
new file mode 100644
index 0000000..86572e2
--- /dev/null
+++ b/server/models/communities.ts
@@ -0,0 +1,17 @@
+// Community Document Schema
+import mongoose, { Model } from 'mongoose';
+import { Community } from '../types';
+import communitySchema from './schema/community';
+
+/**
+ * Mongoose model for the `Community` collection.
+ *
+ * This model is created using the `Community` interface and the `communitySchema`, representing the
+ * `Community` collection in the MongoDB database, and provides an interface for interacting with
+ * the stored questions.
+ *
+ * @type {Model}
+ */
+const CommunityModel: Model = mongoose.model('Community', communitySchema);
+
+export default CommunityModel;
diff --git a/server/models/schema/community.ts b/server/models/schema/community.ts
new file mode 100644
index 0000000..c8b9985
--- /dev/null
+++ b/server/models/schema/community.ts
@@ -0,0 +1,30 @@
+import { Schema } from 'mongoose';
+/**
+ * Mongoose schema for Community.
+ *
+ * This schema defines the structure each community.
+ * Each community includes the following fields:
+ * - `name`: The name of the community.
+ * - `tags`: An array of strings with the tag names.
+ * - `users`: An array of strings of the user's names.
+ * - `questions`: An array of references to `Question` documents associated with the community.
+ */
+const communitySchema: Schema = new Schema(
+ {
+ name: {
+ type: String,
+ },
+ tags: {
+ type: [String],
+ },
+ users: {
+ type: [String],
+ },
+ questions: {
+ type: [{ type: Schema.Types.ObjectId, ref: 'Question' }],
+ },
+ },
+ { collection: 'Community' },
+);
+
+export default communitySchema;
diff --git a/server/models/schema/user.ts b/server/models/schema/user.ts
new file mode 100644
index 0000000..7ca5849
--- /dev/null
+++ b/server/models/schema/user.ts
@@ -0,0 +1,35 @@
+import { Schema } from 'mongoose';
+
+/**
+ * Mongoose schema for the Tag collection.
+ *
+ * This schema defines the structure for storing tags in the database.
+ * Each tag includes the following fields:
+ * - `name`: The name of the tag. This field is required.
+ * - `description`: A brief description of the tag. This field is required.
+ */
+const userSchema: Schema = new Schema(
+ {
+ username: {
+ type: String,
+ },
+ firstName: {
+ type: String,
+ },
+ lastName: {
+ type: String,
+ },
+ tags: {
+ type: [String],
+ },
+ community: {
+ type: String,
+ },
+ status: {
+ type: String,
+ },
+ },
+ { collection: 'User' },
+);
+
+export default userSchema;
diff --git a/server/models/users.ts b/server/models/users.ts
new file mode 100644
index 0000000..17ffc7a
--- /dev/null
+++ b/server/models/users.ts
@@ -0,0 +1,17 @@
+// Community Document Schema
+import mongoose, { Model } from 'mongoose';
+import { User } from '../types';
+import userSchema from './schema/user';
+
+/**
+ * Mongoose model for the `Community` collection.
+ *
+ * This model is created using the `Community` interface and the `communitySchema`, representing the
+ * `Community` collection in the MongoDB database, and provides an interface for interacting with
+ * the stored questions.
+ *
+ * @type {Model}
+ */
+const UserModel: Model = mongoose.model('User', userSchema);
+
+export default UserModel;
diff --git a/server/populate_db.ts b/server/populate_db.ts
index e8b601c..b69f231 100644
--- a/server/populate_db.ts
+++ b/server/populate_db.ts
@@ -2,7 +2,9 @@ import mongoose from 'mongoose';
import AnswerModel from './models/answers';
import QuestionModel from './models/questions';
import TagModel from './models/tags';
-import { Answer, Comment, Question, Tag } from './types';
+import CommunityModel from './models/communities';
+import UserModel from './models/users';
+import { Answer, Comment, Question, Tag, Community, User } from './types';
import {
Q1_DESC,
Q1_TXT,
@@ -44,6 +46,15 @@ import {
C10_TEXT,
C11_TEXT,
C12_TEXT,
+ FRONT_END_TAGS,
+ // BACK_END_TAGS,
+ // ML_TAGS,
+ // AI_TAGS,
+ CLOUD_TAGS,
+ T7_NAME,
+ T7_DESC,
+ T8_NAME,
+ T8_DESC,
} from './data/posts_strings';
import CommentModel from './models/comments';
@@ -173,6 +184,52 @@ async function questionCreate(
return await QuestionModel.create(questionDetail);
}
+/**
+ * Fetches questions from the database based on the tags provided.
+ *
+ * @param tags An array of tags to search for.
+ * @returns A Promise that resolves to an array of Question documents.
+ */
+async function getQuestionsByTags(tags: Tag[]): Promise {
+ return await QuestionModel.find({ tags: { $in: tags } });
+}
+
+/**
+ * Creates a new Community document in the database.
+ * @param name The name of the community.
+ * @param tags The tags associated with the community.
+ * @param users The users who are part of the community.
+ * @param questions The questions that have been asked in the community.
+ * @returns A Promise that resolves to the created Community document.
+ * @throws An error if any of the parameters are invalid.
+ */
+async function communityCreate(name: string, tags: string[], users: string[], questions: Question[]): Promise {
+ if (name === '' || tags.length === 0) throw new Error('Invalid Community Format');
+ const community: Community = {
+ name: name,
+ tags: tags,
+ users: users,
+ questions: questions,
+ };
+ return await CommunityModel.create(community);
+}
+
+/**
+ * FIX THIS COMMENT
+ */
+// async function userCreate(username: string, firstName: string, lastName: string, tags: Tag[], community: Community, status: string): Promise {
+// if (firstName === '' || lastName === '' || tags.length === 0 || status === '') throw new Error('Invalid Community Format');
+// const user: User = {
+// username: username,
+// firstName: firstName,
+// lastName: lastName,
+// tags: tags,
+// community: community,
+// status: status,
+// };
+// return await UserModel.create(user);
+// }
+
/**
* Populates the database with predefined data.
* Logs the status of the operation to the console.
@@ -185,6 +242,9 @@ const populate = async () => {
const t4 = await tagCreate(T4_NAME, T4_DESC);
const t5 = await tagCreate(T5_NAME, T5_DESC);
const t6 = await tagCreate(T6_NAME, T6_DESC);
+ const t7 = await tagCreate(T7_NAME, T7_DESC);
+ const t8 = await tagCreate(T8_NAME, T8_DESC);
+
const c1 = await commentCreate(C1_TEXT, 'sana', new Date('2023-12-12T03:30:00'));
const c2 = await commentCreate(C2_TEXT, 'ihba001', new Date('2023-12-01T15:24:19'));
@@ -249,6 +309,21 @@ const populate = async () => {
[c12],
);
+ const frontEndQuestions = await getQuestionsByTags([t1, t6, t7]);
+ // const backEndQuestions = await getQuestionsByTags(BACK_END_TAGS);
+ // const machineLearningQuestions = await getQuestionsByTags(ML_TAGS);
+ // const aiQuestions = await getQuestionsByTags(AI_TAGS);
+ const cloudQuestions = await getQuestionsByTags([t5, t8]);
+
+ const community1 = await communityCreate('front-end-development', FRONT_END_TAGS, [], frontEndQuestions);
+ // await communityCreate('back-end-development', BACK_END_TAGS, [], backEndQuestions);
+ // await communityCreate('machine learning', ML_TAGS, [], machineLearningQuestions);
+ // await communityCreate('ai', AI_TAGS, [], aiQuestions);
+ const community2 = await communityCreate('cloud computing', CLOUD_TAGS, [], cloudQuestions);
+
+ // await userCreate('user1', 'John', 'Doe', [t1, t2], community1, 'low');
+
+
console.log('Database populated');
} catch (err) {
console.log('ERROR: ' + err);
@@ -256,6 +331,7 @@ const populate = async () => {
if (db) db.close();
console.log('done');
}
+
};
populate();
diff --git a/server/types.ts b/server/types.ts
index 651291d..84edde6 100644
--- a/server/types.ts
+++ b/server/types.ts
@@ -9,6 +9,28 @@ export type FakeSOSocket = Server;
*/
export type OrderType = 'newest' | 'unanswered' | 'active' | 'mostViewed';
+/**
+ * Interface representing Community
+ */
+export interface Community {
+ name: string;
+ tags: string[];
+ users: string[];
+ questions: Question[];
+}
+
+/**
+ * Interface representing User
+ */
+export interface User {
+ username: string;
+ firstName: string;
+ lastName: string;
+ tags: string[];
+ community: string;
+ status: 'low' | 'high';
+}
+
/**
* Interface representing an Answer document, which contains:
* - _id - The unique identifier for the answer. Optional field