diff --git a/backend-services/feed-service/Dockerfile b/backend-services/feed-service/Dockerfile index 930e71877ecb703a10b5862333181d8fe7a0aa9a..9ce26ec214215609d4d3f84a8abd0ad28d17f335 100644 --- a/backend-services/feed-service/Dockerfile +++ b/backend-services/feed-service/Dockerfile @@ -16,6 +16,9 @@ RUN unzip -d svc target/universal/feed-service-1.0.0.zip \ && rm svc/bin/*.bat \ && mv svc/bin/* svc/bin/start +# Delete the RUNNING_PID fle that incorrectly tells Play there is already an instance running +RUN rm -f svc/RUNNING_PID + EXPOSE 9000 CMD svc/bin/start diff --git a/backend-services/feed-service/app/models/User.scala b/backend-services/feed-service/app/models/User.scala index dec8ed66f42838638d224616cd640897e12571fe..9e2e40d15cf97d788a00d04225f1cdd40c24a30f 100644 --- a/backend-services/feed-service/app/models/User.scala +++ b/backend-services/feed-service/app/models/User.scala @@ -15,11 +15,13 @@ object User { def getUserFriends(userId: ObjectId, jwt: String): Future[Seq[ObjectId]] = { val friendServiceUri: String = ConfigFactory.load().getString("friend.service.uri") - val url: String = s"${friendServiceUri}test/getFriends" + val url: String = s"${friendServiceUri}friends" val queryStringParameters: Seq[(String, String)] = Seq(("userId", userId.toString())) HttpCall.get(url, queryStringParameters, jwt).map[Seq[ObjectId]]((json: JsValue) => { - val sequence: Seq[String] = json.as[Seq[String]] + val result = (json \ "result").as[JsValue] + val sequence: Seq[String] = result.as[Seq[String]] + println(sequence) sequence.map[ObjectId](new ObjectId(_)) }) } diff --git a/backend-services/friend-service/tsconfig.json b/backend-services/friend-service/tsconfig.json index 572ba0f250d7bc9466bb147e8e2c15085bde8e6c..670fa307c8cc3bbcc631568535f81ef00054fc70 100644 --- a/backend-services/friend-service/tsconfig.json +++ b/backend-services/friend-service/tsconfig.json @@ -101,5 +101,4 @@ "exclude":[ "./node_modules" ] - } diff --git a/backend-services/user-service/controllers/appController.js b/backend-services/user-service/controllers/appController.js index 7ac9c54112e0d0d8ba70e486431a0432a72e3ed4..622dc28eaeeb64f956f03fcc45df76783ada0f81 100644 --- a/backend-services/user-service/controllers/appController.js +++ b/backend-services/user-service/controllers/appController.js @@ -1,24 +1,20 @@ -import UserModel from '../model/User.model.js'; -import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import ENV from '../config.js' +import UserModel from "../model/User.model.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import ENV from "../config.js"; // Middleware -export async function verifyUser(req,res, next){ - - try{ - - const { username } = req.method == 'GET' ? req.query : req.body; - - // Check if user exists - let exist = await UserModel.findOne( {username } ); - if(!exist) return res.status(404).send({ error: "Can't find User"}); - next(); +export async function verifyUser(req, res, next) { + try { + const { username } = req.method == "GET" ? req.query : req.body; - } catch (error) { - - return res.status(404).send({ error: "Authentication Error"}) - } + // Check if user exists + let exist = await UserModel.findOne({ username }); + if (!exist) return res.status(404).send({ error: "Can't find User" }); + next(); + } catch (error) { + return res.status(404).send({ error: "Authentication Error" }); + } } /** POST: http://localhost:8080/api/register @@ -29,71 +25,80 @@ export async function verifyUser(req,res, next){ "profile": "" } */ -export async function register(req,res){ - +export async function register(req, res) { try { - const { username, password, profile, email } = req.body; + const { username, password, profile, email } = req.body; - // Checking existing Username - const existUsername = new Promise((resolve, reject) => { - UserModel.findOne({ username }, function(err, user){ - if(err) reject(new Error(err)) - if(user) reject({ error : "Please use unique username"}); + // Checking existing Username + const existUsername = new Promise((resolve, reject) => { + UserModel.findOne({ username }, function (err, user) { + if (err) reject(new Error(err)); + if (user) reject({ error: "Please use unique username" }); - resolve(); - }) + resolve(); }); + }); - // Checking existing Email - const existEmail = new Promise((resolve, reject) => { - UserModel.findOne({ email }, function(err, email){ - if(err) reject(new Error(err)) - if(email) reject({ error : "Please use unique Email"}); + // Checking existing Email + const existEmail = new Promise((resolve, reject) => { + UserModel.findOne({ email }, function (err, email) { + if (err) reject(new Error(err)); + if (email) reject({ error: "Please use unique Email" }); - resolve(); - }) + resolve(); + }); + }); + + Promise.all([existUsername, existEmail]) + .then(() => { + if (password) { + bcrypt + .hash(password, 10) + .then((hashedPassword) => { + const user = new UserModel({ + username, + password: hashedPassword, + profile: profile || "", + email, + }); + + // return save result as a response + user + .save() + .then(async (result) => { + const user = await UserModel.findOne({ username: username }); + + // create jwt token + const token = jwt.sign( + { + userId: user._id, + username: user.username, + admin: false, + }, + ENV.JWT_SECRET, + { expiresIn: "24h" } + ); + + res.status(201).send({ + msg: "User Registered Successfully", + username: user.username, + token, + }); + }) + .catch((error) => res.status(500).send({ error })); + }) + .catch((error) => { + return res.status(500).send({ + error: "Enable to hash password", + }); + }); + } + }) + .catch((error) => { + return res.status(500).send({ error }); }); - - - Promise.all([existUsername, existEmail]) - .then(() => { - if(password){ - bcrypt.hash(password, 10) - .then( hashedPassword => { - - const user = new UserModel({ - username, - password: hashedPassword, - profile: profile || '', - email - }); - - // return save result as a response - user.save() - .then(async (result) => { - const user = await UserModel.findOne({username: username}) - - // create jwt token - const token = jwt.sign({ - userId: user._id, - username : user.username - }, ENV.JWT_SECRET , { expiresIn : "24h"}); - - res.status(201).send({ msg: "User Registered Successfully", username: user.username, token }) - }) - .catch(error => res.status(500).send({error})) - - }).catch(error => { - return res.status(500).send({ - error : "Enable to hash password" - }) - }) - } - }).catch(error => { - return res.status(500).send({ error }) - }) } catch (error) { - return res.status(500).send(error); + return res.status(500).send(error); } } @@ -103,68 +108,65 @@ export async function register(req,res){ "password" : "admin123" } */ -export async function login(req,res){ - - const { username, password } = req.body; - - try { - - UserModel.findOne({ username }) - .then(user => { - bcrypt.compare(password, user.password) - .then(passwordCheck => { - - if(!passwordCheck) return res.status(400).send({ error: "Don't have Password"}); - - // create jwt token - const token = jwt.sign({ - userId: user._id, - username : user.username, - admin: user.admin - }, ENV.JWT_SECRET , { expiresIn : "24h"}); +export async function login(req, res) { + const { username, password } = req.body; - - return res.status(200).send({ - msg: "Login Successful...!", - username: user.username, - token - }); - - }) - .catch(error =>{ - return res.status(400).send({ error: "Password does not Match"}) - }) - }) - .catch( error => { - return res.status(404).send({ error : "Username not Found"}); - }) - - } catch (error) { - return res.status(500).send({ error}); - } + try { + UserModel.findOne({ username }) + .then((user) => { + bcrypt + .compare(password, user.password) + .then((passwordCheck) => { + if (!passwordCheck) + return res.status(400).send({ error: "Don't have Password" }); + + // create jwt token + const token = jwt.sign( + { + userId: user._id, + username: user.username, + admin: user.admin, + }, + ENV.JWT_SECRET, + { expiresIn: "24h" } + ); + + return res.status(200).send({ + msg: "Login Successful...!", + username: user.username, + token, + }); + }) + .catch((error) => { + return res.status(400).send({ error: "Password does not Match" }); + }); + }) + .catch((error) => { + return res.status(404).send({ error: "Username not Found" }); + }); + } catch (error) { + return res.status(500).send({ error }); + } } +export async function getUser(req, res) { + const { username } = req.params; -export async function getUser(req,res){ - - const { username } = req.params; - - try { - - if(!username) return res.status(401).send({error: "Invalid Username"}); + try { + if (!username) return res.status(401).send({ error: "Invalid Username" }); - UserModel.findOne({username}, function(err, user){ - if(err) return res.status(500).send({err}); - if(!user) return res.status(501).send({ error: "Couldn't find the User"}); - - const { password, ...rest } = Object.assign({}, user.toJSON()); + UserModel.findOne({ username }, function (err, user) { + if (err) return res.status(500).send({ err }); + if (!user) + return res.status(501).send({ error: "Couldn't find the User" }); - return res.status(201).send(rest) - }) + const { password, ...rest } = Object.assign({}, user.toJSON()); - }catch(err) { - return res.status(404).send({ error: "Cannot find User Data"}); - } + return res.status(201).send(rest); + }); + } catch (err) { + return res.status(404).send({ error: "Cannot find User Data" }); + } } /** PUT: http://localhost:8080/api/updateuser @@ -178,79 +180,93 @@ body: { profile : '' } */ -export async function updateUser(req,res){ - try { - - const {userId} = req.user; - - if(userId){ - const body = req.body; - - // update the data - UserModel.updateOne({ _id : userId }, body, function(err, data){ - try{ - if(err) throw err; - return res.status(201).send({ msg : "Record Updated...!"}); - } catch (error) { - return res.status(401).send({ error }); - } - - }) - - }else{ - return res.status(401).send({ error : "User Not Found...!"}); +export async function updateUser(req, res) { + try { + const { userId } = req.user; + + if (userId) { + const body = req.body; + + // update the data + UserModel.updateOne({ _id: userId }, body, function (err, data) { + try { + if (err) throw err; + return res.status(201).send({ msg: "Record Updated...!" }); + } catch (error) { + return res.status(401).send({ error }); } - - } catch (error) { - return res.status(401).send({ error }); + }); + } else { + return res.status(401).send({ error: "User Not Found...!" }); } + } catch (error) { + return res.status(401).send({ error }); + } } /** * POST /userlist * This DOES NOT return emails and passwords - * @param {*} req - * @param {*} res - * @returns - * + * @param {*} req + * @param {*} res + * @returns + * * body: {userIdList: []} */ -export const GetUserList = async (req,res) => { +export const GetUserList = async (req, res) => { try { - const {userIdList} = req.body; - const users = await UserModel.find({ '_id': { $in: userIdList } }, {password: 0, email: 0}); - const userHash = users.map(obj => [obj._id.toString(), obj]); - - return res.status(201).send({ userList: Array.from(userHash.entries())}); - } catch(error){ + const { userIdList } = req.body; + const users = await UserModel.find( + { _id: { $in: userIdList } }, + { password: 0, email: 0 } + ); + const userHash = users.map((obj) => [obj._id.toString(), obj]); + + return res.status(201).send({ userList: Array.from(userHash.entries()) }); + } catch (error) { return res.status(401).send({ error }); } -} +}; /** * GET /search * This endpoint OMITS the password and email fields - * @param {*} req - * @param {*} res - * @returns - * + * @param {*} req + * @param {*} res + * @returns + * * query: {query: string} */ - export const Search = async (req,res) => { +export const Search = async (req, res) => { try { - const {searchQuery} = req.query; + const { searchQuery } = req.query; - if(searchQuery === undefined || searchQuery.length === 0){ - throw new Error("Please provide a valid query!") + if (searchQuery === undefined || searchQuery.length === 0) { + throw new Error("Please provide a valid query!"); } - const usersWithMatchingUsername = await UserModel.find({"username": {"$regex": `^${searchQuery}`}}, {password: 0, email: 0}) - const usersWithMatchingName = await UserModel.find({$or: [{"firstName": {"$regex": `^${searchQuery}`,"$options": 'i'}}, {"lastName": {"$regex": `^${searchQuery}`,"$options": 'i'}}]}, {password: 0, email: 0}) - const resultHash = new Map([...usersWithMatchingUsername, ...usersWithMatchingName].map(obj => [obj._id.toString(), obj])); - - return res.status(201).send({result: Array.from(resultHash.entries())}); - } catch(error){ + const usersWithMatchingUsername = await UserModel.find( + { username: { $regex: `^${searchQuery}` } }, + { password: 0, email: 0 } + ); + const usersWithMatchingName = await UserModel.find( + { + $or: [ + { firstName: { $regex: `^${searchQuery}`, $options: "i" } }, + { lastName: { $regex: `^${searchQuery}`, $options: "i" } }, + ], + }, + { password: 0, email: 0 } + ); + const resultHash = new Map( + [...usersWithMatchingUsername, ...usersWithMatchingName].map((obj) => [ + obj._id.toString(), + obj, + ]) + ); + + return res.status(201).send({ result: Array.from(resultHash.entries()) }); + } catch (error) { return res.status(401).send({ error: error.message }); } -} - +}; diff --git a/daily-thought-frontend/.prettierrc b/daily-thought-frontend/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..ab57af61080a2c2528b246271cf7b801b59b4c45 --- /dev/null +++ b/daily-thought-frontend/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/daily-thought-frontend/Dockerfile b/daily-thought-frontend/Dockerfile index 4eda2af323a55c926f186b7e8362695356afd581..daedffb8c81c111d70f4280e75717f96afc2151c 100644 --- a/daily-thought-frontend/Dockerfile +++ b/daily-thought-frontend/Dockerfile @@ -1,14 +1,15 @@ -FROM node:latest as base +# base image +FROM node:latest -# Create app directory +# create & set working directory +RUN mkdir -p /frontend-service/app WORKDIR /frontend-service/app -COPY package*.json ./ -RUN npm install - -# Bundle app source -COPY . . +# copy source files +COPY . /frontend-service/app +# start app +RUN npm install +RUN npm run build EXPOSE 9000 - -CMD [ "npm", "run", "dev" ] \ No newline at end of file +CMD npm run dev \ No newline at end of file diff --git a/daily-thought-frontend/src/components/comments/Comment.tsx b/daily-thought-frontend/src/components/comments/Comment.tsx index 7d83228aca16507bdc7a5949255dcf94b91fee3a..91dea39b427feef2669d7fd99b6d9b8efa26e439 100644 --- a/daily-thought-frontend/src/components/comments/Comment.tsx +++ b/daily-thought-frontend/src/components/comments/Comment.tsx @@ -1,16 +1,12 @@ -import { Comment } from "@/types/comment" -import { HeartIcon } from '@heroicons/react/24/outline' -import { FC, PropsWithChildren } from "react" +import { Comment } from '@/types/comment'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { FC, PropsWithChildren } from 'react'; type CommentProps = { - Comment: Comment, - -} - -const Comment: FC<PropsWithChildren<CommentProps>> = ({ - Comment -}) => { + Comment: Comment; +}; +const Comment: FC<PropsWithChildren<CommentProps>> = ({ Comment }) => { return ( <div className="w-full flex items-center justify-between p-2"> <div className="flex flex-1"> @@ -18,15 +14,15 @@ const Comment: FC<PropsWithChildren<CommentProps>> = ({ <img className="h-8 w-8 rounded-full" src={Comment.User.profile} alt="" /> </div> <div> - <p className="text-xs font-bold">{Comment.User.name}</p> + <p className="text-xs font-bold">{Comment.User.username}</p> <p className="text-sm">This is my comment</p> <p className="text-xs text-gray-500">1 like</p> </div> </div> - + <HeartIcon className="block h-3 w-3" aria-hidden="true" /> </div> - ) -} + ); +}; -export default Comment; \ No newline at end of file +export default Comment; diff --git a/daily-thought-frontend/src/components/form/DailyCompletion.tsx b/daily-thought-frontend/src/components/form/DailyCompletion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..269b783c7cf9754870a795c38b2ae86886269ced --- /dev/null +++ b/daily-thought-frontend/src/components/form/DailyCompletion.tsx @@ -0,0 +1,76 @@ +import { Dialog, Transition } from '@headlessui/react'; +import { CheckCircleIcon } from '@heroicons/react/24/outline'; +import { FC, Fragment, PropsWithChildren, useEffect, useState } from 'react'; + +type DailyCompletionProps = { + show: boolean; +}; + +const DailyPostCompletion: FC<PropsWithChildren<DailyCompletionProps>> = ({ show }) => { + const [isOpen, setIsOpen] = useState(false); + const [done, setDone] = useState(false); + + useEffect(() => { + if (!isOpen && show && !done) { + setIsOpen(true); + setDone(true); + + setTimeout(() => { + setIsOpen(false); + }, 3000); + } + }); + + return ( + <> + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-25" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"> + <Dialog.Title + as="h3" + className="text-lg font-medium leading-6 text-gray-900 text-center" + > + <div className="w-full flex justify-center mb-2"> + <CheckCircleIcon className="h-12 w-12 rounded-full text-c-pink flex items-center justify-center" /> + </div> + Your Daily has been posted! + </Dialog.Title> + <div className="mt-2"> + <p className="text-sm text-gray-500 text-center"> + Now have a look at what your friends have said :) + </p> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +}; + +export default DailyPostCompletion; diff --git a/daily-thought-frontend/src/components/form/FriendRequests.tsx b/daily-thought-frontend/src/components/form/FriendRequests.tsx index 88da9f8b0494a97094aac3dab7b86e163706342c..09bf28d7ef5e0b0e93318208ebb82ffdeaacaf80 100644 --- a/daily-thought-frontend/src/components/form/FriendRequests.tsx +++ b/daily-thought-frontend/src/components/form/FriendRequests.tsx @@ -1,65 +1,63 @@ -import { useRequests } from "@/hooks/useRequests"; -import { CheckIcon, XCircleIcon } from "@heroicons/react/24/outline"; -import { FC, PropsWithChildren, useEffect, useState } from "react" -import UserAvatar from "../user/UserAvatar"; +import { useRequests } from '@/hooks/useRequests'; +import { CheckIcon, XCircleIcon } from '@heroicons/react/24/outline'; +import { FC, PropsWithChildren, useEffect, useState } from 'react'; +import UserAvatar from '../user/UserAvatar'; - -const FriendRequestMenu:FC<PropsWithChildren> = ({ -}) => { - const [userRequestList, setUserRequestList] = useState<Map<string, any>>(new Map()) - const {requests, rehydrateRequests, setRehydrateRequests} = useRequests(); +const FriendRequestMenu: FC<PropsWithChildren> = ({}) => { + const [userRequestList, setUserRequestList] = useState<Map<string, any>>(new Map()); + const { requests, rehydrateRequests, setRehydrateRequests } = useRequests(); const fetchUserDetails = async () => { - const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/userlist` - if(requests){ - const JsonData = JSON.stringify({userIdList: Array.from(requests?.keys())}) + const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/userlist`; + if (requests) { + const JsonData = JSON.stringify({ userIdList: Array.from(requests?.keys()) }); const options = { method: 'POST', - headers: {'Content-Type': 'application/json',}, - body: JsonData, - } - - const response = await fetch(endpoint, options) - const data = await response.json() + headers: { 'Content-Type': 'application/json' }, + body: JsonData + }; - console.log(data) - setUserRequestList(new Map(data.userList)) + const response = await fetch(endpoint, options); + const data = await response.json(); + setUserRequestList(new Map(data.userList)); } - } + }; const handleRequest = async (userId: string, accept: boolean) => { - const endpoint = `${process.env.NEXT_PUBLIC_FRIEND_SERVICE_URL}friends/requests/${accept ? "accept" : "reject"}` - const JsonData = JSON.stringify({request_id: requests?.get(userId)._id}) + const endpoint = `${process.env.NEXT_PUBLIC_FRIEND_SERVICE_URL}friends/requests/${ + accept ? 'accept' : 'reject' + }`; + const JsonData = JSON.stringify({ request_id: requests?.get(userId)._id }); const options = { method: 'PUT', - headers: {'Content-Type': 'application/json','Authorization': `Bearer ${sessionStorage.getItem("token")}`}, - body: JsonData, - } + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sessionStorage.getItem('token')}` + }, + body: JsonData + }; - const response = await fetch(endpoint, options) - if(!response.ok){ - const data = await response.json() - console.log(data) + const response = await fetch(endpoint, options); + if (!response.ok) { + const data = await response.json(); } else { - setRehydrateRequests(true) + setRehydrateRequests(true); } - } + }; useEffect(() => { - fetchUserDetails() - },[rehydrateRequests]) + fetchUserDetails(); + }, [rehydrateRequests]); return ( <div className="p-1"> - <div className="text-xs text-gray-400 border-b w-full pb-1"> - Friend requests - </div> - + <div className="text-xs text-gray-400 border-b w-full pb-1">Friend requests</div> + <div> {Array.from(userRequestList?.values()).map((req, index) => { - const {profile, username, firstName, lastName} = req[1] + const { profile, username, firstName, lastName } = req[1]; return ( <div key={req[0]}> <div className="flex justify-between items-center"> @@ -82,14 +80,18 @@ const FriendRequestMenu:FC<PropsWithChildren> = ({ </div> </div> </div> - ) + ); })} </div> - {userRequestList.size === 0 && <div className="text-xs text-gray-400 flex justify-center mt-2"> No pending friend requests</div>} - + {userRequestList.size === 0 && ( + <div className="text-xs text-gray-400 flex justify-center mt-2"> + {' '} + No pending friend requests + </div> + )} </div> - ) -} + ); +}; -export default FriendRequestMenu \ No newline at end of file +export default FriendRequestMenu; diff --git a/daily-thought-frontend/src/components/form/NewComment.tsx b/daily-thought-frontend/src/components/form/NewComment.tsx new file mode 100644 index 0000000000000000000000000000000000000000..09d9578a72b59d805868925bf5867cf74f6f03a0 --- /dev/null +++ b/daily-thought-frontend/src/components/form/NewComment.tsx @@ -0,0 +1,115 @@ +import { Transition, Dialog } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { FC, FormEvent, Fragment, PropsWithChildren, useState } from 'react'; + +type NewComment = { + postContent: string; + question: string; + show: boolean; + name: string; + onClose: () => void; +}; + +const NewComment: FC<PropsWithChildren<NewComment>> = ({ + postContent, + show, + name, + onClose, + question +}) => { + const [error, setError] = useState<string>(''); + const [commentValue, setCommentValue] = useState<string>(''); + + return ( + <> + <Transition appear show={show} as={Fragment}> + <Dialog as="div" className="relative z-10" onClose={() => onClose()}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-25" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"> + <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> + <div className="flex w-full justify-between"> + <div className="mb-3 flex flex-col justify-center"> + <h1 className="text-2xl font-bold tracking-tight text-gray-900">Daily</h1> + <h1 className="text-sm text-gray-400">{`${question}`}</h1> + </div> + + <button + type="button" + className="ml-auto flex items-start flex-shrink-1 rounded-full text-gray-400 p-1 hover:text-c-pink hover:bg-white focus:outline-none" + onClick={() => onClose()} + > + <XMarkIcon className="h-6 w-6" aria-hidden="true" /> + </button> + </div> + + <p className="text-lg text-c-pink">{`${name} answered`}</p> + + <div className="text-2xl text-center p-2 text-gray-900 font-light">{`"${postContent}"`}</div> + </Dialog.Title> + + <div className="mt-2"> + <form + className="px-2" + // onSubmit={(e: FormEvent<HTMLFormElement>) => handleSubmit(e)} + > + <div className="mb-4"> + <label + htmlFor="about" + className="block text-sm font-medium leading-6 text-sm text-gray-900" + > + {`Comment on ${name}'s post`} + </label> + <div className="mt-2"> + <textarea + id="about" + name="about" + rows={2} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={commentValue} + onChange={(e) => setCommentValue(e.target.value)} + /> + </div> + <p className="text-xs text-c-pink m-1">{error}</p> + </div> + + <button + type="submit" + className="rounded-md bg-c-pink px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-c-green focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + Submit + </button> + </form> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +}; + +export default NewComment; diff --git a/daily-thought-frontend/src/components/form/QuestionSubmit.tsx b/daily-thought-frontend/src/components/form/QuestionSubmit.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5085cc83e4fd7d1d5ab4b9b3a1e6d22a067db93b --- /dev/null +++ b/daily-thought-frontend/src/components/form/QuestionSubmit.tsx @@ -0,0 +1,152 @@ +import { User } from '@/types/user'; +import { Dialog, Menu, Transition } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { FC, FormEvent, Fragment, PropsWithChildren, useState } from 'react'; +import UserAvatar from '../user/UserAvatar'; +import DailyPostCompletion from './DailyCompletion'; + +type QuestionSubmitProps = { + questionData: { question: string; questionId: string }; + user: User; + open: boolean; + onClose: () => void; + onSubmit: () => void; +}; + +const QuestionSubmit: FC<PropsWithChildren<QuestionSubmitProps>> = ({ + questionData, + user, + open, + onClose, + onSubmit +}) => { + const [dailyResponse, setDailyResponse] = useState<string>(''); + const [error, setError] = useState<string>(''); + + const handleChange = (value: string) => { + setDailyResponse(value); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!dailyResponse) { + setError("Can't post an empty Daily!"); + } else { + const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}daily/create`; + const JsonData = JSON.stringify({ + content: dailyResponse, + questionId: questionData.questionId, + userId: user.id + }); + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sessionStorage.getItem('token')}` + }, + body: JsonData + }; + const response = await fetch(endpoint, options); + if (!response.ok) { + const data = await response.statusText; + console.log(data); + } else { + onSubmit(); + onClose(); + } + } + }; + + return ( + <> + <Transition appear show={open} as={Fragment}> + <Dialog as="div" className="relative z-10" onClose={onClose}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-25" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"> + <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> + <div className="flex w-full justify-between"> + <div className="mb-3 flex flex-col justify-center"> + <h1 className="text-2xl font-bold tracking-tight text-gray-900">Daily</h1> + <p className="text-sm font-normal text-gray-400">Today's question</p> + </div> + + <button + type="button" + className="ml-auto flex items-start flex-shrink-1 rounded-full text-gray-400 p-1 hover:text-c-pink hover:bg-white focus:outline-none" + onClick={() => onClose()} + > + <XMarkIcon className="h-6 w-6" aria-hidden="true" /> + </button> + </div> + + <div className="text-2xl font-bold tracking-tight text-center p-4 text-c-pink"> + {questionData.question} + </div> + </Dialog.Title> + + <div className="mt-2"> + <form + className="px-2" + onSubmit={(e: FormEvent<HTMLFormElement>) => handleSubmit(e)} + > + <div className="mb-4"> + <label + htmlFor="about" + className="block text-sm font-medium leading-6 text-sm text-gray-900" + > + Your response + </label> + <div className="mt-2"> + <textarea + id="about" + name="about" + rows={2} + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={dailyResponse} + onChange={(e) => handleChange(e.target.value)} + /> + </div> + <p className="text-xs text-c-pink m-1">{error}</p> + </div> + + <button + type="submit" + className="rounded-md bg-c-pink px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-c-green focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + Submit + </button> + </form> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +}; + +export default QuestionSubmit; diff --git a/daily-thought-frontend/src/components/form/UserSearch.tsx b/daily-thought-frontend/src/components/form/UserSearch.tsx index 0f6b44cadc5b21038a4899b7694ca3539908b72c..9eb8a5cbab442def17d9d2174ab99a4c73faae99 100644 --- a/daily-thought-frontend/src/components/form/UserSearch.tsx +++ b/daily-thought-frontend/src/components/form/UserSearch.tsx @@ -1,139 +1,152 @@ -import useDebounce from "@/hooks/useDebounce"; -import { useFriends } from "@/hooks/useFriends"; -import { useSentRequests } from "@/hooks/useSentRequests"; -import { useUser } from "@/hooks/useUser"; -import { UserPlusIcon } from "@heroicons/react/24/outline"; -import { FC, PropsWithChildren, useEffect, useState } from "react"; -import UserAvatar from "../user/UserAvatar"; -import BasicField from "./BasicField"; +import useDebounce from '@/hooks/useDebounce'; +import { useFriends } from '@/hooks/useFriends'; +import { useSentRequests } from '@/hooks/useSentRequests'; +import { useUser } from '@/hooks/useUser'; +import { UserPlusIcon } from '@heroicons/react/24/outline'; +import { FC, PropsWithChildren, useEffect, useState } from 'react'; +import UserAvatar from '../user/UserAvatar'; +import BasicField from './BasicField'; type UserSearchProps = { limit?: number; -} +}; -const UserSearch:FC<PropsWithChildren<UserSearchProps>> = ({ - limit = 6 -}) => { - const [searchQuery, setSearchQuery] = useState<string>(""); - const debouncedSearchQuery = useDebounce<string>(searchQuery, 500) +const UserSearch: FC<PropsWithChildren<UserSearchProps>> = ({ limit = 6 }) => { + const [searchQuery, setSearchQuery] = useState<string>(''); + const debouncedSearchQuery = useDebounce<string>(searchQuery, 500); const [searchResults, setSearchResults] = useState<Map<string, any>>(new Map([])); const [loading, setLoading] = useState<boolean>(false); - const {sentRequests, rehydrateSentRequests, setRehydrateSentRequests} = useSentRequests(); - const {friends, rehydrateFriends, setRehydrateFriends} = useFriends(); - const [sentRequestsIds, setSentRequestIds] = useState<Set<string>>(new Set([])) - const {user, setRehydrateUser} = useUser() - + const { sentRequests, rehydrateSentRequests, setRehydrateSentRequests } = useSentRequests(); + const { friends, rehydrateFriends, setRehydrateFriends } = useFriends(); + const [sentRequestsIds, setSentRequestIds] = useState<Set<string>>(new Set([])); + const { user, setRehydrateUser } = useUser(); useEffect(() => { - if(sentRequests !== undefined){ - setSentRequestIds(new Set(Array.from(sentRequests?.keys()))) + if (sentRequests !== undefined) { + setSentRequestIds(new Set(Array.from(sentRequests?.keys()))); } - }, [rehydrateSentRequests]) + }, [rehydrateSentRequests]); useEffect(() => { - setLoading(true) - if(searchQuery.length === 0){ - setSearchResults(new Map([])) + setLoading(true); + if (searchQuery.length === 0) { + setSearchResults(new Map([])); } - }, [searchQuery, setSearchResults]) + }, [searchQuery, setSearchResults]); useEffect(() => { - if(debouncedSearchQuery.length > 0){ + if (debouncedSearchQuery.length > 0) { const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/search?searchQuery=${debouncedSearchQuery}`; fetch(endpoint).then(async (result) => { const data = await result.json(); - setLoading(false) - const newResults = new Map<string, any>(data.result) - if(user) - newResults.delete(user?.id) - setSearchResults(newResults) + setLoading(false); + const newResults = new Map<string, any>(data.result); + if (user) newResults.delete(user?.id); + setSearchResults(newResults); }); } - }, [debouncedSearchQuery]) + }, [debouncedSearchQuery]); const sendRequest = async (receiverId: string) => { - const endpoint = `${process.env.NEXT_PUBLIC_FRIEND_SERVICE_URL}friends/requests` - const JSONdata = JSON.stringify({receiver_id: receiverId}) - const headers = {'Authorization': `Bearer ${sessionStorage.getItem("token")}`, 'Content-Type': 'application/json'} - const response = await fetch(endpoint, {method: "POST",headers, body: JSONdata}) - - if(!response.ok){ - const data = await response.json() - console.log(data.error) + const endpoint = `${process.env.NEXT_PUBLIC_FRIEND_SERVICE_URL}friends/requests`; + const JSONdata = JSON.stringify({ receiver_id: receiverId }); + const headers = { + Authorization: `Bearer ${sessionStorage.getItem('token')}`, + 'Content-Type': 'application/json' + }; + const response = await fetch(endpoint, { method: 'POST', headers, body: JSONdata }); + + if (!response.ok) { + const data = await response.json(); } else { - setRehydrateSentRequests(true) + setRehydrateSentRequests(true); } - } + }; return ( <div> - <BasicField name="search" placeholder="Search users" onChange={setSearchQuery} autocomplete={false}/> - <div className="border-t pt-2 flex item-center flex-col"> - {!searchQuery && - <div className='flex items-center justify-center mb-2'> - <p className='text-xs text-gray-500'> - Enter a search query to find friends - </p> + <BasicField + name="search" + placeholder="Search users" + onChange={setSearchQuery} + autocomplete={false} + /> + <div className="border-t pt-2 flex item-center flex-col"> + {!searchQuery && ( + <div className="flex items-center justify-center mb-2"> + <p className="text-xs text-gray-500">Enter a search query to find friends</p> </div> - } + )} <div className="border-b"> - {searchQuery && !loading && Array.from(searchResults.values()).map((result, index) => { - if(index < limit && !friends?.has(result._id) && result._id !== user?.id){ - return ( - <div className="flex justify-between items-center hover:bg-gray-100 rounded-md mb-2" key={index}> - <UserAvatar key={result._id} username={result.username} firstName={result.firstName} lastName={result.lastName} /> - - {!sentRequestsIds.has(result._id) && - <button - type="button" - className="ml-auto mr-1 flex-shrink-1 rounded-full text-c-pink p-1 hover:text-gray-200 focus:outline-none " - onClick={() => sendRequest(result._id)} + {searchQuery && + !loading && + Array.from(searchResults.values()).map((result, index) => { + if (index < limit && !friends?.has(result._id) && result._id !== user?.id) { + return ( + <div + className="flex justify-between items-center hover:bg-gray-100 rounded-md mb-2" + key={index} > - <UserPlusIcon className="h-6 w-6" aria-hidden="true" /> - </button> - } - - {sentRequestsIds.has(result._id) && - <p className="text-xs text-gray-400 mr-3">Pending</p> - } - - </div> - ) + <UserAvatar + key={result._id} + username={result.username} + firstName={result.firstName} + lastName={result.lastName} + /> + + {!sentRequestsIds.has(result._id) && ( + <button + type="button" + className="ml-auto mr-1 flex-shrink-1 rounded-full text-c-pink p-1 hover:text-gray-200 focus:outline-none " + onClick={() => sendRequest(result._id)} + > + <UserPlusIcon className="h-6 w-6" aria-hidden="true" /> + </button> + )} + + {sentRequestsIds.has(result._id) && ( + <p className="text-xs text-gray-400 mr-3">Pending</p> + )} + </div> + ); } - })} + })} - {searchQuery && loading && + {searchQuery && loading && ( <div className="flex justify-center mb-2"> <div className="h-6 w-6 animate-spin text-gray-300 rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]" - role="status"> - </div> - </div>} + role="status" + ></div> + </div> + )} - {searchQuery && !loading && searchResults.size === 0 && + {searchQuery && !loading && searchResults.size === 0 && ( <div> - <p className='text-xs text-gray-500 flex justify-center mb-2'> - No results found - </p> + <p className="text-xs text-gray-500 flex justify-center mb-2">No results found</p> </div> - } + )} </div> - - <div className="w-full pt-2 text-xs mx-auto flex text-c-pink justify-center"> - {friends && friends.size > 0? "Friends" : "No friends added"} + {friends && friends.size > 0 ? 'Friends' : 'No friends added'} </div> - {friends && Array.from(friends.values()).map((result, index) => { - return ( - <div className="flex justify-between hover:bg-gray-100 rounded-md" key={index}> - <UserAvatar key={index} username={result[1].username} firstName={result[1].firstName} lastName={result[1].lastName} /> - </div> - ) + {friends && + Array.from(friends.values()).map((result, index) => { + return ( + <div className="flex justify-between hover:bg-gray-100 rounded-md" key={index}> + <UserAvatar + key={index} + username={result[1].username} + firstName={result[1].firstName} + lastName={result[1].lastName} + /> + </div> + ); })} - </div> </div> - ) -} + </div> + ); +}; -export default UserSearch; \ No newline at end of file +export default UserSearch; diff --git a/daily-thought-frontend/src/components/post/AnswerCard.tsx b/daily-thought-frontend/src/components/post/AnswerCard.tsx index 8a18c81f8f33cb5d6295e18d99d5d234139a090b..fa25d768a285e61b4c4889d7cf1236fd6d8b3483 100644 --- a/daily-thought-frontend/src/components/post/AnswerCard.tsx +++ b/daily-thought-frontend/src/components/post/AnswerCard.tsx @@ -1,72 +1,109 @@ -import { Post } from "@/types/post" -import { FC, PropsWithChildren } from "react" -import { EyeSlashIcon, HeartIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline' -import LikeStack from "./LikeStack" -import Comment from "../comments/Comment" -import styles from '../../styles/AnswerCard.module.css' +import { FC, PropsWithChildren, useState } from 'react'; +import { EyeSlashIcon, HeartIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline'; +import { HeartIcon as Heart } from '@heroicons/react/24/solid'; +import LikeStack from './LikeStack'; +import Comment from '../comments/Comment'; +import styles from '../../styles/AnswerCard.module.css'; +import { User } from '@/types/user'; +import NewComment from '../form/NewComment'; +import { FeedPost } from '@/hooks/useFeed'; type AnswerCardProps = { - post: Post; + post: FeedPost; + question: string; + user: User; + loggedInUser: User; + userList: Map<string, User>; + comments: any[]; commentLimit: number; -} + rehydrateFeed: () => void; +}; const AnswerCard: FC<PropsWithChildren<AnswerCardProps>> = ({ post, - commentLimit + user, + question, + userList, + loggedInUser, + commentLimit, + comments, + rehydrateFeed }) => { + const name = user.firstName ? user.firstName : '@' + user.username; + const [showCommentForm, setShowCommentForm] = useState<boolean>(false); + const isLiked = post.usersLiked.includes(loggedInUser.id); + + const likePost = async () => { + const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}daily/${ + isLiked ? 'unlike' : 'like' + }`; + const JSONdata = JSON.stringify({ dailyId: post.id, likerId: loggedInUser.id }); + const headers = { + Authorization: `Bearer ${sessionStorage.getItem('token')}`, + 'Content-Type': 'application/json' + }; + const response = await fetch(endpoint, { method: 'PUT', headers, body: JSONdata }); + + if (!response.ok) { + const data = await response.json(); + console.log(data); + } else { + rehydrateFeed(); + } + }; return ( - <div className="flex-1 rounded-3xl bg-white text-lg shadow-lg overflow-hidden relative"> - <div className="p-4"> - <p className="text-sm text-gray-600">{`${post.User.name}'s answer`}</p> - <div> - This is my super duper answer. My name is Timmy C and I enjoy smooth jazz. - </div> - <div className="mt-2 flex "> - <div className="flex-1"> + <div className="flex-1 rounded-3xl bg-white text-lg shadow-lg overflow-hidden static"> + <div className="px-6 py-6 pb-3"> + <p className="text-sm text-gray-500 mb-1">{`${name}'s answer`}</p> + <div className="text-gray-900">{post.content}</div> + <div className="flex "> + <div className="flex-1 mt-2"> <button type="button" - className="ml-auto mr-2 flex-shrink-0 rounded-full bg-white p-1 text-c-pink hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-c-pink" + className=" mr-4 p-2 rounded-full bg-white text-c-pink hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 " + onClick={() => likePost()} > - <span className="sr-only">View notifications</span> - <HeartIcon className="h-6 w-6" aria-hidden="true"/> + {isLiked ? ( + <Heart className="h-6 w-6 text-c-pink" aria-hidden="true" /> + ) : ( + <HeartIcon className="h-6 w-6" aria-hidden="true" /> + )} </button> <button type="button" - className="ml-auto mr-2 flex-shrink-0 rounded-full bg-white p-1 text-c-pink hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-c-pink" + className=" p-2 rounded-full bg-white text-c-pink hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2" + onClick={() => setShowCommentForm(true)} > - <span className="sr-only">View notifications</span> - <ChatBubbleLeftIcon className="h-6 w-6" aria-hidden="true"/> + <ChatBubbleLeftIcon className="h-6 w-6" aria-hidden="true" /> </button> </div> - <LikeStack Users={[post.User, post.User, post.User, post.User, post.User, post.User]} /> + <LikeStack userList={userList} postLikes={post.usersLiked} /> </div> </div> - - - - <div className="border-t bg-gray-50 p-2"> - <p className="text-xs text-gray-500 hover:text-c-pink">View all 3 comments</p> - <Comment Comment={{Data: "asdfg", User: post.User}} /> - </div> - - {/* <div className={styles.hidden}> - <div className="flex justify-center flex-col items-center"> - <EyeSlashIcon className="h-10 w-10 text-c-pink"/> - <p className="text-sm text-c-pink">This post is hidden</p> - - <button - className="rounded-md bg-white px-3.5 py-2 mt-2 text-xs font-semibold text-c-pink shadow-sm hover:bg-c-pink hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-400" - > - Post your daily - </button> + <div className="border-t bg-gray-50 p-2 hover:bg-gray-100"> + {comments.length > 0 && ( + <div> + <p className="text-xs text-gray-500 hover:text-c-pink">View all 3 comments</p> + <Comment Comment={{ Data: 'asdfg', User: user, Likes: [] }} /> + </div> + )} + {comments.length === 0 && ( + <div className="w-full flex justify-between"> + <p className="p-2 text-xs text-gray-500">No comments yet!</p> + </div> + )} </div> - </div> */} - - - </div> - ) -} + <NewComment + name={name} + show={showCommentForm} + onClose={() => setShowCommentForm(false)} + question={question} + postContent={post.content} + /> + </div> + ); +}; -export default AnswerCard \ No newline at end of file +export default AnswerCard; diff --git a/daily-thought-frontend/src/components/post/LikeStack.tsx b/daily-thought-frontend/src/components/post/LikeStack.tsx index 5ab25e4e0d32f68710c6c9eaf3576993e9e7893b..2efa417d71e2600bc9762cc3624baae1cac8f77d 100644 --- a/daily-thought-frontend/src/components/post/LikeStack.tsx +++ b/daily-thought-frontend/src/components/post/LikeStack.tsx @@ -1,42 +1,50 @@ -import { User } from "@/types/user" -import { FC, PropsWithChildren } from "react" +import { User } from '@/types/user'; +import { UserCircleIcon } from '@heroicons/react/24/solid'; +import { FC, PropsWithChildren } from 'react'; +import UserList from '../user/UserList'; type LikeStackProps = { - Users: User[] -} - -const LikeStack:FC<PropsWithChildren<LikeStackProps>> = ({ - Users -}) => { + userList: Map<string, User>; + postLikes: string[]; +}; +const LikeStack: FC<PropsWithChildren<LikeStackProps>> = ({ userList, postLikes }) => { return ( - <> - <div className="flex -space-x-1 overflow-hidden items-center"> - {Users.map((user, index) => { - if(index < 3){ - return ( - <div> - <img - className="inline-block h-6 w-6 rounded-full ring-2 ring-white" - src={user.Avatar} - alt="" - /> - </div> - ) - } else if (index === 3) { - return ( - <div className="inline-block h-6 w-6 rounded-full ring-2 ring-white bg-gray-100 flex items-center justify-center"> - <p className="text-c-pink text-xs">{`+${Users.length - 4}`}</p> - </div> - ) - } else { - return (null) - } - })} - - </div> - </> - ) -} + <div className="flex items-end bg-gray-50 rounded-full items-center px-2 flex-shrink-1"> + {postLikes.length > 0 && ( + <div className="flex -space-x-1 overflow-hidden items-center"> + <div className="mx-2 text-xs text-c-pink ">{`${postLikes.length} Like${ + postLikes.length === 1 ? '' : 's' + }`}</div> + <UserList Users={userList !== undefined ? postLikes.map((id) => userList.get(id)) : []}> + {postLikes.map((user, index) => { + if (index < 3) { + return ( + <div> + <div className="rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-c-pink"> + <UserCircleIcon className="h-6 w-6 rounded-full text-gray-300 bg-white flex items-center justify-center" /> + </div> + </div> + ); + } else if (index === 3) { + return ( + <div className="inline-block h-6 w-6 rounded-full ring-2 ring-white bg-gray-100 flex items-center justify-center"> + <p className="text-c-pink text-xs">{`+${userList.size - 4}`}</p> + </div> + ); + } else { + return null; + } + })} + </UserList> + </div> + )} + + {postLikes.length === 0 && ( + <div className="text-xs text-gray-400 mx-2">Be the first to like!</div> + )} + </div> + ); +}; -export default LikeStack; \ No newline at end of file +export default LikeStack; diff --git a/daily-thought-frontend/src/components/post/Post.tsx b/daily-thought-frontend/src/components/post/Post.tsx index b7f9d2074af13474def045e721d0145f130aa92a..4c3d445ffd0835d68f708c22e6cd17b944ab4399 100644 --- a/daily-thought-frontend/src/components/post/Post.tsx +++ b/daily-thought-frontend/src/components/post/Post.tsx @@ -1,44 +1,57 @@ -import { Post } from "@/types/post" +import { FeedPost } from '@/hooks/useFeed'; +import { User } from '@/types/user'; -import { FC, PropsWithChildren } from "react"; -import AnswerCard from "./AnswerCard"; -import LikeStack from "./LikeStack"; +import { FC, PropsWithChildren } from 'react'; +import UserAvatar from '../user/UserAvatar'; +import AnswerCard from './AnswerCard'; +import LikeStack from './LikeStack'; type PostProps = { - post: Post; -} - -const Post:FC<PropsWithChildren<PostProps>> = ({ - post + post: FeedPost; + question: string; + user: User; + loggedInUser: User; + lightText?: boolean; + userList: Map<string, User>; + rehydrateFeed: () => void; +}; + +const Post: FC<PropsWithChildren<PostProps>> = ({ + post, + user, + lightText = false, + question, + userList, + loggedInUser, + rehydrateFeed }) => { - return ( <> - <div className="flex w-full py-4 flex-col"> - - <div className="flex m-2 items-center"> - <div className="flex h-12 w-12 max-w-xs items-center justify-center rounded-full bg-c-pink text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-pink-400 mr-2"> - <img className="h-12 w-12 rounded-full" src={post.User.profile} alt="" /> - </div> - <div className="flex-1 flex-col"> - <p className="">{post.User.name}</p> - <p className="text-sm text-gray-500">{`@${post.User.username}`}</p> + <div className="flex w-full py-4 flex-col"> + <div className="flex items-center"> + <UserAvatar + useLightText={lightText} + username={user.username} + firstName={user.firstName} + lastName={user.lastName} + /> </div> - <div className="flex flex-col justify-end"> - <p className="text-xs text-gray-500">Just now</p> + + <div className="flex-1 rounded-3xl font-white md:ml-10 m-2"> + <AnswerCard + user={user} + loggedInUser={loggedInUser} + post={post} + comments={[]} + question={question} + rehydrateFeed={() => rehydrateFeed()} + userList={userList} + commentLimit={1} + /> </div> </div> - - - <div className="flex-1 rounded-3xl font-white md:ml-16"> - <AnswerCard post={post}/> - </div> - - - </div> - </> - ) -} + ); +}; -export default Post \ No newline at end of file +export default Post; diff --git a/daily-thought-frontend/src/components/user/QuestionCard.tsx b/daily-thought-frontend/src/components/user/QuestionCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e80f4c557fdbb4291bff662ddba20ebde295cbee --- /dev/null +++ b/daily-thought-frontend/src/components/user/QuestionCard.tsx @@ -0,0 +1,60 @@ +import { User } from '@/types/user'; +import { FC, PropsWithChildren } from 'react'; + +type QuestionCardProps = { + questionData: any; + posted: boolean; + user: User; + action: () => void; +}; + +const QuestionCard: FC<PropsWithChildren<QuestionCardProps>> = ({ + questionData, + posted, + user, + children, + action +}) => { + return ( + <div className="flex-1 max-w-xl flex flex-col justify-center"> + <div className="flex rounded-full my-4 m-2"> + <div className="flex w-full flex-col"> + <div className="flex w-full justify-between"> + <div className="mb-3 flex flex-col justify-center"> + <h1 className="text-2xl font-bold tracking-tight text-gray-900 text-white">Daily</h1> + <p className="text-sm font-normal text-white">Today's question</p> + </div> + </div> + + <div className="sm:mx-6 flex-1 rounded-3xl font-white "> + <div className=" transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all m-2"> + <div className="text-lg font-medium leading-6 text-gray-900"> + <div className="flex justify-center items-center flex-col"> + <div className="text-2xl font-bold tracking-tight text-center text-c-pink"> + {questionData.question} + </div> + </div> + </div> + </div> + </div> + {!posted && ( + <div className="flex justify-center w-full py-2"> + <button + type="button" + className="rounded-md bg-white px-3 py-2 text-sm font-semibold text-c-pink shadow-sm hover:bg-c-green focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + onClick={() => action()} + > + Answer + </button> + </div> + )} + </div> + + <div></div> + </div> + {posted && <div className="p-2 pt-0">{children}</div>} + </div> + ); +}; + +export default QuestionCard; diff --git a/daily-thought-frontend/src/components/user/UserAvatar.tsx b/daily-thought-frontend/src/components/user/UserAvatar.tsx index 5abc13866fcd714f49fe67a6352daf5bc4f53ec7..e11a8d1c55cc384dd78e2345f5c44c2b2de5d449 100644 --- a/daily-thought-frontend/src/components/user/UserAvatar.tsx +++ b/daily-thought-frontend/src/components/user/UserAvatar.tsx @@ -1,30 +1,39 @@ -import { UserCircleIcon, UserPlusIcon } from "@heroicons/react/24/outline"; -import { FC, PropsWithChildren } from "react"; +import { UserCircleIcon, UserPlusIcon } from '@heroicons/react/24/solid'; +import { FC, PropsWithChildren } from 'react'; type UserAvatarProps = { - firstName?: string, - lastName?: string, - profile?:string, - username: string -} + useLightText?: boolean; + firstName?: string; + lastName?: string; + profile?: string; + username: string; +}; -const UserAvatar:FC<PropsWithChildren<UserAvatarProps>> = ({ +const UserAvatar: FC<PropsWithChildren<UserAvatarProps>> = ({ + useLightText = false, username, firstName, - lastName, - + lastName }) => { + const userNameClasses = firstName ? 'text-sm' : 'ml-1 text-md'; + return ( - <div className='flex items-center p-1'> - <div className=""> - <UserCircleIcon className='h-12 w-12 rounded-full text-gray-300 flex items-center justify-center' /> + <div className="flex items-center p-1"> + <div className="rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-c-pink"> + <UserCircleIcon className="h-12 w-12 rounded-full text-gray-300 bg-white flex items-center justify-center" /> </div> - <div className='text-sm ml-1 text-gray-900 whitespace-nowrap'> + <div + className={`text-sm ml-1 ${ + useLightText ? 'text-white' : 'text-gray-900' + } whitespace-nowrap`} + > {firstName && <div>{`${firstName} ${lastName || null}`}</div>} - <p className='text-sm text-gray-500 font-normal'>@{username}</p> - </div> + <p className={`${userNameClasses} ${useLightText ? 'text-gray-100' : 'text-gray-500'}`}> + @{username} + </p> </div> - ) -} + </div> + ); +}; -export default UserAvatar; \ No newline at end of file +export default UserAvatar; diff --git a/daily-thought-frontend/src/components/user/UserList.tsx b/daily-thought-frontend/src/components/user/UserList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7512ef270c0d9bc8c2d3aa1eb89d03ebbd50932d --- /dev/null +++ b/daily-thought-frontend/src/components/user/UserList.tsx @@ -0,0 +1,58 @@ +import { User } from '@/types/user'; +import { Popover, Transition } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import { FC, Fragment, PropsWithChildren } from 'react'; +import UserAvatar from './UserAvatar'; + +type UserListProps = { + Users: (User | undefined)[]; +}; + +const UserList: FC<PropsWithChildren<UserListProps>> = ({ Users, children }) => { + return ( + <div className="m-3"> + <Popover className="flex"> + <Popover.Button> + <div className="flex -space-x-1 overflow-hidden relative items-center">{children}</div> + </Popover.Button> + + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="opacity-0 translate-y-1" + enterTo="opacity-100 translate-y-0" + leave="transition ease-in duration-150" + leaveFrom="opacity-100 translate-y-0" + leaveTo="opacity-0 translate-y-1" + > + <Popover.Panel className="max-w-xs absolute z-50 left-2/3 mt-8 w-screen w-60 -translate-x-1/2 transform px-4 sm:px-0"> + <div className=" relative overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"> + <div className="bg-white p-3"> + <span className="flex items-center border-b"> + <span className="text-xs font-medium text-gray-400 mb-1">{`${Users.length} Like${ + Users.length === 1 ? '' : 's' + }`}</span> + </span> + <div> + {Users.map((user) => { + if (user !== undefined) { + return ( + <UserAvatar + username={user.username} + firstName={user.firstName} + lastName={user.lastName} + /> + ); + } + })} + </div> + </div> + </div> + </Popover.Panel> + </Transition> + </Popover> + </div> + ); +}; + +export default UserList; diff --git a/daily-thought-frontend/src/hooks/useFeed.ts b/daily-thought-frontend/src/hooks/useFeed.ts new file mode 100644 index 0000000000000000000000000000000000000000..e51054925daef37211b759856c8f138d1435091e --- /dev/null +++ b/daily-thought-frontend/src/hooks/useFeed.ts @@ -0,0 +1,101 @@ +import { User } from '@/types/user'; +import { useState, useEffect } from 'react'; + +type UseFeedProps = { + user: User | undefined; + questionData: any | undefined; +}; + +type FeedResponse = { + userDaily: FeedPost; + feed: []; +}; + +export type FeedPost = { + id: string; + userId: string; + createdAt: string; + content: string; + questionId: string; + usersLiked: string[]; +}; + +export const useFeed = ({ user, questionData }: UseFeedProps) => { + const [rehydrateFeed, setRehydrateFeed] = useState(false); + const [feed, setFeed] = useState<FeedResponse | undefined>(undefined); + const [feedUsers, setFeedUsers] = useState<Map<string, User> | undefined>(undefined); + const [rehydrateFeedUsers, setRehydrateFeedUsers] = useState(false); + + useEffect(() => { + if (feed !== undefined) return; + setRehydrateFeed(true); + }, [feed]); + + const fetchFeed = async () => { + if (user !== undefined && questionData !== undefined) { + const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}feed?userId=${user.id}&questionId=${questionData.questionId}`; + const headers = { Authorization: `Bearer ${sessionStorage.getItem('token')}` }; + const response = await fetch(endpoint, { headers }); + + if (!response.ok) { + } else { + return await response.json(); + } + } + }; + + const fetchUserList = async (userIdList: string[]) => { + const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/userlist`; + const JSONdata = JSON.stringify({ userIdList }); + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSONdata + }; + + const response = await fetch(endpoint, options); + return await response.json(); + }; + + useEffect(() => { + if (!rehydrateFeedUsers) return; + + const userSet = new Set<string>([]); + if (user) { + userSet.add(user.id); + } + + feed?.feed.forEach((post: FeedPost) => { + userSet.add(post.userId); + post.usersLiked.forEach((userLikeId) => { + userSet.add(userLikeId); + }); + }); + + if (userSet.size > 0) { + fetchUserList(Array.from(userSet)).then((userListRes) => { + const newFeedUsers = new Map<string, any>(); + userListRes.userList.forEach((item: any) => { + newFeedUsers.set(item[1][0], item[1][1]); + }); + + if (newFeedUsers.size === userSet.size) { + setFeedUsers(newFeedUsers); + } + }); + } + setRehydrateFeedUsers(false); + }); + + useEffect(() => { + if (!rehydrateFeed) return; + + fetchFeed().then((res: FeedResponse) => { + setFeed(res); + + setRehydrateFeed(false); + }); + }, [rehydrateFeed]); + + return { feed, rehydrateFeed, feedUsers, setRehydrateFeed, setRehydrateFeedUsers }; +}; diff --git a/daily-thought-frontend/src/hooks/usePost.ts b/daily-thought-frontend/src/hooks/usePost.ts new file mode 100644 index 0000000000000000000000000000000000000000..31ff3a6a7ed5f607e461d0a8c2422ba02ea62aff --- /dev/null +++ b/daily-thought-frontend/src/hooks/usePost.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; + +export const usePost = () => { + const [rehydratePost, setRehydratePost] = useState(false); + const [post, setPost] = useState<Map<string, any> | undefined>(undefined); + + useEffect(() => { + if (post !== undefined) return; + setRehydratePost(true); + }, [post]); + + const fetchPost = async () => { + const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}friends/requests/sent`; + const headers = { Authorization: `Bearer ${sessionStorage.getItem('token')}` }; + const response = await fetch(endpoint, { headers }); + return await response.json(); + }; + + useEffect(() => { + if (!rehydratePost) return; + + fetchPost().then((res) => { + const requestsHash = new Map<string, any>([]); + res.requests.forEach((request: any) => { + requestsHash.set(request.TargetUser, request); + }); + setPost(requestsHash); + setRehydratePost(false); + }); + }, [rehydratePost]); + return { + post, + rehydratePost, + setRehydratePost + }; +}; diff --git a/daily-thought-frontend/src/hooks/useQuestion.ts b/daily-thought-frontend/src/hooks/useQuestion.ts index c066baeaef0506c8f6a1cef446911f9739b83baf..fe8d49c51406f6a39fdee3069faa59b9c8aba9cb 100644 --- a/daily-thought-frontend/src/hooks/useQuestion.ts +++ b/daily-thought-frontend/src/hooks/useQuestion.ts @@ -1,5 +1,5 @@ -import Router from "next/router" -import { useState, useEffect } from "react" +import Router from 'next/router'; +import { useState, useEffect } from 'react'; export const useQuestion = () => { const [rehydrateQuestion, setRehydrateQuestion] = useState(false); @@ -11,22 +11,22 @@ export const useQuestion = () => { }, [question]); const fetchQuestion = async () => { - const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}question` - const response = await fetch(endpoint, {method: 'get'}) - if(response.ok){ - const data = await response.json() - return data + const endpoint = `${process.env.NEXT_PUBLIC_FEED_SERVICE_URL}question`; + + const response = await fetch(endpoint, { method: 'get' }); + if (response.ok) { + const data = await response.json(); + return data; } - } + }; useEffect(() => { if (!rehydrateQuestion) return; - fetchQuestion().then(res => { - console.log(res) - setQuestion(res) + fetchQuestion().then((res) => { + setQuestion(res); setRehydrateQuestion(false); - }) - }, [rehydrateQuestion]) - return {question, rehydrateQuestion, setRehydrateQuestion} -} \ No newline at end of file + }); + }, [rehydrateQuestion]); + return { question, rehydrateQuestion, setRehydrateQuestion }; +}; diff --git a/daily-thought-frontend/src/hooks/useUser.ts b/daily-thought-frontend/src/hooks/useUser.ts index c50326b2f22bb0e7b29fa0dfbdaa906b45bedd7b..78d3cf7ab47dcb936eabbd3ccb3d1cae202b9f5b 100644 --- a/daily-thought-frontend/src/hooks/useUser.ts +++ b/daily-thought-frontend/src/hooks/useUser.ts @@ -1,6 +1,6 @@ -import { User } from "@/types/user" -import Router from "next/router" -import { useState, useEffect } from "react" +import { User } from '@/types/user'; +import Router from 'next/router'; +import { useState, useEffect } from 'react'; export const useUser = () => { const [rehydrateUser, setRehydrateUser] = useState(false); @@ -12,25 +12,27 @@ export const useUser = () => { }, [user]); const fetchUser = async () => { - const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/user/${sessionStorage.getItem('username')}` - const response = await fetch(endpoint) - if(!response.ok){ - sessionStorage.clear() - Router.push("/") + const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/user/${sessionStorage.getItem( + 'username' + )}`; + const response = await fetch(endpoint); + if (!response.ok) { + sessionStorage.clear(); + Router.push('/'); } else { - const data = await response.json() - return data + const data = await response.json(); + return data; } - } + }; useEffect(() => { if (!rehydrateUser) return; - fetchUser().then(res => { - const {_id, username, email, profile, firstName, lastName } = res - setUser({id: _id, email, username, profile, firstName, lastName}) + fetchUser().then((res) => { + const { _id, username, email, profile, firstName, lastName } = res; + setUser({ id: _id, email, username, profile, firstName, lastName }); setRehydrateUser(false); - }) - }, [rehydrateUser]) - return {user, setRehydrateUser} -} \ No newline at end of file + }); + }, [rehydrateUser]); + return { user, setRehydrateUser }; +}; diff --git a/daily-thought-frontend/src/hooks/useUserDaily.ts b/daily-thought-frontend/src/hooks/useUserDaily.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb165d422f08cedd3801c4527076ebc281da780d --- /dev/null +++ b/daily-thought-frontend/src/hooks/useUserDaily.ts @@ -0,0 +1,3 @@ +export const useUserDaily = () => { + +} diff --git a/daily-thought-frontend/src/pages/feed.tsx b/daily-thought-frontend/src/pages/feed.tsx index bad582f42676f46dee3953b6cb524e7973a90c14..5307b2d4287a2f779d5b3b709ccd14efe05a6c21 100644 --- a/daily-thought-frontend/src/pages/feed.tsx +++ b/daily-thought-frontend/src/pages/feed.tsx @@ -1,49 +1,129 @@ -import NavBar from "@/components/navigation/NavBar"; -import Post from "@/components/post/Post"; -import { useUser } from "@/hooks/useUser"; -import { useQuestion } from "@/hooks/useQuestion" +import NavBar from '@/components/navigation/NavBar'; +import Post from '@/components/post/Post'; +import { useUser } from '@/hooks/useUser'; +import { useQuestion } from '@/hooks/useQuestion'; +import QuestionSubmit from '@/components/form/QuestionSubmit'; +import { useEffect, useState } from 'react'; +import MenuTransition from '@/components/navigation/MenuTransition'; +import DailyPostCompletion from '@/components/form/DailyCompletion'; +import { FeedPost, useFeed } from '@/hooks/useFeed'; +import QuestionCard from '@/components/user/QuestionCard'; +const Feed = () => { + const { user, setRehydrateUser } = useUser(); + const { question, rehydrateQuestion, setRehydrateQuestion } = useQuestion(); + const { feed, rehydrateFeed, feedUsers, setRehydrateFeed, setRehydrateFeedUsers } = useFeed({ + user, + questionData: question + }); + const [showQuestionForm, setShowQuestionForm] = useState<boolean>(false); + const [postComplete, setPostComplete] = useState<boolean>(false); -const userx = { - id: "asdf", - name: 'Tom Cook', - email: 'tom@example.com', - username: 'TomCook', - profile: - 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', -} + console.log(feed); + console.log(feedUsers); -const Feed = () => { - const {user, setRehydrateUser} = useUser(); - const {question, rehydrateQuestion, setRehydrateQuestion} = useQuestion(); + useEffect(() => { + if (question === undefined && user === undefined) { + } else { + setRehydrateFeed(true); + } + }, [question, user]); - console.log(question) + useEffect(() => { + if (feedUsers === undefined && feed !== undefined) { + setRehydrateFeedUsers(true); + } + }, [feedUsers, feed]); - return( - <> - <div className="w-full"> - <NavBar user={user} /> - </div> - <div className="flex w-full bg-c-pink flex-col items-center py-3 pt-10"> - <div className="flex-1 max-w-4xl flex items-center flex-col justify-center"> - <div className={`p-8 bg-c-pink`}> - <h1 className="text-4xl font-bold tracking-tight text-c-green text-center">This is my question of the day</h1> + const onQuestionSubmit = () => { + setPostComplete(true); + setRehydrateFeed(true); + }; + + return ( + <div className="w-full h-screen flex flex-col"> + <div className=""> + <NavBar user={user} /> + </div> + <div className="flex w-full bg-c-pink flex-col items-center py-3 pt-20 justify-center items-center shadow-lg"> + <div className="p-4 flex-1 max-w-4xl flex items-center flex-col justify-center w-full "> + {question !== undefined && user !== undefined && feed !== undefined && ( + <div className="max-w-4xl w-full justify-center flex"> + <QuestionCard + questionData={question} + user={user} + posted={feed.userDaily !== null} + action={() => setShowQuestionForm(true)} + > + {feedUsers !== undefined && ( + <Post + loggedInUser={user} + post={feed.userDaily} + user={user} + lightText + question={question.question} + rehydrateFeed={() => setRehydrateFeed(true)} + userList={feedUsers} + /> + )} + </QuestionCard> </div> - </div> + )} + </div> + </div> + + {user && question && ( + <div> + <QuestionSubmit + questionData={question} + user={user} + onClose={() => setShowQuestionForm(false)} + open={showQuestionForm} + onSubmit={() => onQuestionSubmit()} + /> + <DailyPostCompletion show={postComplete} /> </div> - - <div className="flex-1 max-w-4xl flex items-center flex-col justify-center mx-auto"> - <div className="rounded-full my-4 w-xl"> - <div className="flex w-full justify-center flex-col items-center mx-auto p-3 sm:px-6 lg:px-8 max-w-4xl"> - <Post post={{User: userx, Content: "This is my post"}}/> - <Post post={{User: userx, Content: "This is my post"}}/> - <Post post={{User: userx, Content: "This is my post"}}/> - <Post post={{User: userx, Content: "This is my post"}}/> + )} + + <div className="flex-1 flex items-center flex-col justify-center mx-auto w-full h-full bg-gray-50"> + {(feed === undefined || feed?.feed.length === 0) && ( + <div className="bg-gray-50 flex flex-1 justify-center items-center w-full flex-col"> + <p className="text-sm text-gray-400">It's pretty quiet down here...</p> + <a className="text-sm text-c-pink hover:text-c-green" href="/search"> + Find friends + </a> + </div> + )} + + {feed !== undefined && feed?.feed.length > 0 && feedUsers !== undefined && ( + <div className="p-4 flex-1 max-w-2xl flex items-center flex-col w-full"> + <p className="text-sm text-c-pink">Your feed</p> + <div className="max-w-4xl w-full justify-center flex"> + <div className="flex w-full justify-center flex-col items-center mx-auto p-3 sm:px-6 lg:px-8 max-w-4xl overflow-scroll"> + {feed.feed.map((feedPost: FeedPost) => { + const feedUser = feedUsers.get(feedPost.userId); + + if (feedUser !== undefined && user !== undefined) { + return ( + <Post + post={feedPost} + user={feedUser} + question={question.question} + rehydrateFeed={() => setRehydrateFeed(true)} + loggedInUser={user} + userList={feedUsers} + /> + ); + } + })} + </div> </div> + <p className="text-sm text-gray-300">This is the end of your feed</p> </div> - </div> - </> - ) -} + )} + </div> + </div> + ); +}; -export default Feed; \ No newline at end of file +export default Feed; diff --git a/daily-thought-frontend/src/pages/post.tsx b/daily-thought-frontend/src/pages/post.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2dc0ad30d42482658730305a57c67bdb8b5f4ff3 --- /dev/null +++ b/daily-thought-frontend/src/pages/post.tsx @@ -0,0 +1,16 @@ +import NavBar from '@/components/navigation/NavBar'; +import { useUser } from '@/hooks/useUser'; + +const Post = () => { + const { user, setRehydrateUser } = useUser(); + + return ( + <div className="w-full h-screen flex flex-col"> + <div className=""> + <NavBar user={user} /> + </div> + </div> + ); +}; + +export default Post; diff --git a/daily-thought-frontend/src/pages/profile.tsx b/daily-thought-frontend/src/pages/profile.tsx index a65b8d1a36e88e06b3acbe98c48879fa3c0a4c7b..5bbf95db078c06f6c1c4d18bf52cc556210c34fc 100644 --- a/daily-thought-frontend/src/pages/profile.tsx +++ b/daily-thought-frontend/src/pages/profile.tsx @@ -1,34 +1,35 @@ -import NavBar from "@/components/navigation/NavBar"; -import { User } from "@/types/user"; -import { PhotoIcon, UserCircleIcon } from "@heroicons/react/24/outline"; -import Router from "next/router"; -import { FormEvent, useEffect, useState } from "react"; +import NavBar from '@/components/navigation/NavBar'; +import { User } from '@/types/user'; +import { PhotoIcon, UserCircleIcon } from '@heroicons/react/24/outline'; +import Router from 'next/router'; +import { FormEvent, useEffect, useState } from 'react'; const Profile = () => { - const [user, setUser] = useState<undefined | User>(undefined) + const [user, setUser] = useState<undefined | User>(undefined); const fetchUser = async () => { - const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/user/${sessionStorage.getItem('username')}` - const response = await fetch(endpoint) - return await response.json() - - } + const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/user/${sessionStorage.getItem( + 'username' + )}`; + const response = await fetch(endpoint); + return await response.json(); + }; useEffect(() => { - if(!user){ - fetchUser().then(res => { - const {_id, username, email, profile, firstName, lastName } = res - setUser({id: _id, email, username, profile, firstName, lastName}) - }) + if (!user) { + fetchUser().then((res) => { + const { _id, username, email, profile, firstName, lastName } = res; + setUser({ id: _id, email, username, profile, firstName, lastName }); + }); } - }) + }); const handleItemChange = (item: string, value: string) => { - if(user){ - const updated = {...user, ...{[item]: value}} - setUser(updated) + if (user) { + const updated = { ...user, ...{ [item]: value } }; + setUser(updated); } - } + }; const handleSubmit = async (event: FormEvent) => { const JSONdata = JSON.stringify({ @@ -37,20 +38,19 @@ const Profile = () => { profile: user?.profile, firstName: user?.firstName, lastName: user?.lastName - }) - const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/updateuser` + }); + const endpoint = `${process.env.NEXT_PUBLIC_USER_SERVICE_URL}api/updateuser`; const options = { method: 'PUT', headers: { - 'Authorization': `Bearer ${sessionStorage.getItem("token")}`, - 'Content-Type': 'application/json', + Authorization: `Bearer ${sessionStorage.getItem('token')}`, + 'Content-Type': 'application/json' }, - body: JSONdata, - } - const response = await fetch(endpoint, options) - const result = await response.json() - - } + body: JSONdata + }; + const response = await fetch(endpoint, options); + const result = await response.json(); + }; return ( <div className="w-full h-screen flex flex-col"> @@ -58,133 +58,155 @@ const Profile = () => { <NavBar user={user} /> </div> - <form className="max-w-4xl mx-auto pt-10 p-3" onSubmit={(e) => handleSubmit(e)}> - <div className="h-16"></div> - <div className="space-y-12 w-full"> - <div className="border-b border-gray-900/10 pb-12"> - <h2 className="text-base font-semibold leading-7 text-gray-900">Profile</h2> - <p className="mt-1 text-sm leading-6 text-gray-600"> - This information will be displayed publicly so be careful what you share. - </p> - - <div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div className="sm:col-span-3"> - <label htmlFor="first-name" className="block text-sm font-medium leading-6 text-gray-900"> - First name - </label> - <div className="mt-2"> - <input - type="text" - name="first-name" - id="first-name" - autoComplete="given-name" - className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - value={user?.firstName} - onChange={(e) => handleItemChange("firstName", e.target.value)} - /> + <form className="max-w-4xl mx-auto pt-10 p-3" onSubmit={(e) => handleSubmit(e)}> + <div className="h-16"></div> + <div className="space-y-12 w-full"> + <div className="border-b border-gray-900/10 pb-12"> + <h2 className="text-base font-semibold leading-7 text-gray-900">Profile</h2> + <p className="mt-1 text-sm leading-6 text-gray-600"> + This information will be displayed publicly so be careful what you share. + </p> + + <div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div className="sm:col-span-3"> + <label + htmlFor="first-name" + className="block text-sm font-medium leading-6 text-gray-900" + > + First name + </label> + <div className="mt-2"> + <input + type="text" + name="first-name" + id="first-name" + autoComplete="given-name" + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={user?.firstName} + onChange={(e) => handleItemChange('firstName', e.target.value)} + /> + </div> </div> - </div> - <div className="sm:col-span-3"> - <label htmlFor="last-name" className="block text-sm font-medium leading-6 text-gray-900"> - Last name - </label> - <div className="mt-2"> - <input - type="text" - name="last-name" - id="last-name" - autoComplete="family-name" - className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - value={user?.lastName} - onChange={(e) => handleItemChange("lastName", e.target.value)} - /> - </div> - </div> - - - <div className="sm:col-span-4"> - <label htmlFor="username" className="block text-sm font-medium leading-6 text-gray-900"> - Username - </label> - <div className="mt-2"> - <div className="flex bg-gray-100 rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600 sm:max-w-md"> - <span className="flex select-none items-center pl-3 text-gray-500 sm:text-sm">dialy.com/</span> + <div className="sm:col-span-3"> + <label + htmlFor="last-name" + className="block text-sm font-medium leading-6 text-gray-900" + > + Last name + </label> + <div className="mt-2"> <input type="text" - name="username" - id="username" - autoComplete="username" - className="block flex-1 border-0 bg-transparent py-1.5 pl-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" - placeholder="janesmith" - value={user?.username} - onChange={(e) => handleItemChange("username", e.target.value)} - disabled + name="last-name" + id="last-name" + autoComplete="family-name" + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={user?.lastName} + onChange={(e) => handleItemChange('lastName', e.target.value)} /> </div> </div> - </div> - <div className="col-span-full"> - <label htmlFor="photo" className="block text-sm font-medium leading-6 text-gray-900"> - Photo - </label> - <div className="mt-2 flex items-center gap-x-3"> - <UserCircleIcon className="h-12 w-12 text-gray-300" aria-hidden="true" /> - <button - type="button" - className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + <div className="sm:col-span-4"> + <label + htmlFor="username" + className="block text-sm font-medium leading-6 text-gray-900" > - Change - </button> + Username + </label> + <div className="mt-2"> + <div className="flex bg-gray-100 rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600 sm:max-w-md"> + <span className="flex select-none items-center pl-3 text-gray-500 sm:text-sm"> + dialy.com/ + </span> + <input + type="text" + name="username" + id="username" + autoComplete="username" + className="block flex-1 border-0 bg-transparent py-1.5 pl-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" + placeholder="janesmith" + value={user?.username} + onChange={(e) => handleItemChange('username', e.target.value)} + disabled + /> + </div> + </div> </div> - </div> - + <div className="col-span-full"> + <label + htmlFor="photo" + className="block text-sm font-medium leading-6 text-gray-900" + > + Photo + </label> + <div className="mt-2 flex items-center gap-x-3"> + <UserCircleIcon className="h-12 w-12 text-gray-300" aria-hidden="true" /> + <button + type="button" + className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + > + Change + </button> + </div> + </div> + </div> </div> - </div> - - <div className="border-b border-gray-900/10 pb-12"> - <h2 className="text-base font-semibold leading-7 text-gray-900">Personal Information</h2> - <p className="mt-1 text-sm leading-6 text-gray-600">This won't be shared with anyone.</p> - <div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div className="sm:col-span-4"> - <label htmlFor="email" className="block text-sm font-medium leading-6 text-gray-900"> - Email address - </label> - <div className="mt-2"> - <input - id="email" - name="email" - type="email" - autoComplete="email" - className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - value={user?.email} - onChange={(e) => handleItemChange("email", e.target.value)} - /> + <div className="border-b border-gray-900/10 pb-12"> + <h2 className="text-base font-semibold leading-7 text-gray-900"> + Personal Information + </h2> + <p className="mt-1 text-sm leading-6 text-gray-600"> + This won't be shared with anyone. + </p> + + <div className="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <div className="sm:col-span-4"> + <label + htmlFor="email" + className="block text-sm font-medium leading-6 text-gray-900" + > + Email address + </label> + <div className="mt-2"> + <input + id="email" + name="email" + type="email" + autoComplete="email" + className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={user?.email} + onChange={(e) => handleItemChange('email', e.target.value)} + /> + </div> </div> </div> </div> </div> + <div className="mt-6 flex items-center justify-end gap-x-6"> + <button + type="button" + className="text-sm font-semibold leading-6 text-gray-900" + onClick={() => { + Router.push('/feed'); + }} + > + Cancel + </button> + <button + type="submit" + className="rounded-md bg-c-pink px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-c-green focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + Save + </button> + </div> + </form> + </div> + ); +}; - </div> - - <div className="mt-6 flex items-center justify-end gap-x-6"> - <button type="button" className="text-sm font-semibold leading-6 text-gray-900" onClick={() => {Router.push("/feed")}}> - Cancel - </button> - <button - type="submit" - className="rounded-md bg-c-pink px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-c-green focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - > - Save - </button> - </div> - </form> - </div> - ) -} - -export default Profile; \ No newline at end of file +export default Profile; diff --git a/daily-thought-frontend/src/pages/register.tsx b/daily-thought-frontend/src/pages/register.tsx index 10bbee4961de4fcd1fca11025c3a270c4989e105..0cfc41e3f321d6ca4db72203c1d40b8df7229604 100644 --- a/daily-thought-frontend/src/pages/register.tsx +++ b/daily-thought-frontend/src/pages/register.tsx @@ -1,4 +1,4 @@ -import BasicField from "@/components/form/basicField"; +import BasicField from "../components/form/BasicField"; import { FormEvent, useState } from "react"; import Router from 'next/router' diff --git a/daily-thought-frontend/src/pages/userPost.tsx b/daily-thought-frontend/src/pages/userPost.tsx deleted file mode 100644 index a609924848a84535f0b4931ff128c33f5c2ed696..0000000000000000000000000000000000000000 --- a/daily-thought-frontend/src/pages/userPost.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import NavBar from "@/components/navigation/NavBar"; -import AnswerCard from "@/components/post/AnswerCard"; -import Post from "@/components/post/Post"; - -const user = { - Name: 'Tom Cook', - email: 'tom@example.com', - Username: 'TomCook', - Avatar: - 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80', -} - - -const UserPost = () => { - - return ( - <div className="min-h-screen bg-c-pink"> - <div className="w-full"> - <NavBar user={user} /> - </div> - <div className="flex w-full bg-c-pink flex-col items-center py-3 pt-10"> - <div className="flex-1 max-w-4xl flex items-center flex-col justify-center"> - <div className="p-8"> - <h1 className="text-4xl font-bold tracking-tight text-c-green text-center">This is my question of the day</h1> - </div> - <div className="rounded-full w-xl"> - - <div className="flex w-full justify-center flex-col items-center mx-auto p-3 sm:px-6 lg:px-8 max-w-4xl"> - <AnswerCard post={{User: user, Content: "This is my post"}}/> - </div> - </div> - - - </div> - </div> - </div> - ) -} - -export default UserPost; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 90461df838d3503e0cde8b9364590a96666e51e3..d7d5fd18ae19a98a098116499b0b23647271d586 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,65 +1,71 @@ version: "3.9" -services: +services: feed-service: - build: - context: './backend-services/feed-service' + build: + context: "./backend-services/feed-service" dockerfile: Dockerfile restart: unless-stopped - ports: - - "9001:9000" + ports: + - "9001:9000" environment: - - MONGO_URI=mongodb://feed-mongo:27017/ - - JWT_PRIVATE_KEY=yB/uX5KdyjHN9P34IE49HxAcrlQ4gfvpVJEzGbo5E/I= + - MONGO_URI=mongodb://feed-mongo:27017/ + - JWT_PRIVATE_KEY=yB/uX5KdyjHN9P34IE49HxAcrlQ4gfvpVJEzGbo5E/I= + - FRIEND_SERVICE_URI=http://friend-service:9000/ user-service: build: - context: './backend-services/user-service' + context: "./backend-services/user-service" dockerfile: Dockerfile restart: unless-stopped ports: - - "9002:9000" + - "9002:9000" environment: - - MONGO_URI=mongodb://user-mongo:27017/userdb + - MONGO_URI=mongodb://user-mongo:27017/userdb friend-service: build: - context: './backend-services/friend-service' + context: "./backend-services/friend-service" dockerfile: Dockerfile restart: unless-stopped ports: - - "9003:9000" + - "9003:9000" environment: - - MONGO_HOST=friend-mongo - - MONGO_PORT=27017 - - MONGO_DBNAME=friends + - MONGO_HOST=friend-mongo + - MONGO_PORT=27017 + - MONGO_DBNAME=friends - # frontend-service: - # build: - # context: './daily-thought-frontend' - # dockerfile: Dockerfile - # restart: unless-stopped - # ports: - # - "8000:9000" + frontend-service: + build: + context: "./daily-thought-frontend" + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8000:9000" + volumes: + - ./daily-thought-frontend:/frontend-service/app + - /frontend-service/app/node_modules + - /frontend-service/app/.next + environment: + - NEXT_PUBLIC_FEED_SERVICE_URL=http://localhost:9001/ + - NEXT_PUBLIC_USER_SERVICE_URL=http://localhost:9002/ + - NEXT_PUBLIC_FRIEND_SERVICE_URL=http://localhost:9003/ feed-mongo: - image: mongo - container_name: feed-mongo - ports: - - "27017:27017" + image: mongo + container_name: feed-mongo + ports: + - "27017:27017" user-mongo: image: mongo container_name: user-mongo - volumes: + volumes: - "./mongo/user:/data/user" ports: - "27018:27017" friend-mongo: - image: mongo - container_name: friend-mongo - ports: - - "27019:27017" - - - \ No newline at end of file + image: mongo + container_name: friend-mongo + ports: + - "27019:27017"