diff --git a/client/src/components/AppBanner.js b/client/src/components/AppBanner.js index 1fefad0..029d305 100644 --- a/client/src/components/AppBanner.js +++ b/client/src/components/AppBanner.js @@ -1,5 +1,5 @@ import { useContext, useState } from 'react'; -import { Link } from 'react-router-dom' +import { Link } from 'react-router-dom'; import AuthContext from '../auth'; import { GlobalStoreContext } from '../store' import AppBar from '@mui/material/AppBar'; @@ -17,13 +17,8 @@ export default function AppBanner() { const [anchorEl, setAnchorEl] = useState(null); const isMenuOpen = Boolean(anchorEl); - const handleProfileMenuOpen = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setAnchorEl(null); - }; + const handleProfileMenuOpen = (event) => { setAnchorEl(event.currentTarget); }; + const handleMenuClose = () => { setAnchorEl(null); }; const handleLogout = () => { handleMenuClose(); diff --git a/client/src/components/ListCard.js b/client/src/components/ListCard.js index 2f38dab..f223695 100644 --- a/client/src/components/ListCard.js +++ b/client/src/components/ListCard.js @@ -10,7 +10,9 @@ import DeleteIcon from '@mui/icons-material/Delete'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ThumbDownIcon from '@mui/icons-material/ThumbDown'; +import ThumbDownOutlinedIcon from '@mui/icons-material/ThumbDownOutlined'; import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined'; /** This is a card in our list of top 5 lists. It lets select @@ -38,6 +40,10 @@ function ListCard(props) { event.target.value = ""; } } + function handleRate(event, type) { + event.stopPropagation(); + store.rate(listInfo._id, type); + } async function handleDeleteList(event, id) { event.stopPropagation(); @@ -47,28 +53,54 @@ function ListCard(props) { let text = "", likes = "", dislikes = "", views = "", comment = ""; if (listInfo.published) { text = Published: {listInfo.publishedDate} - likes = - - {listInfo.likes.length} - ; - dislikes = + likes = listInfo.likes.includes(auth.user.email) ? + handleRate(event, "like")} + > + {listInfo.likes.length} + + : + handleRate(event, "like")} + > + {listInfo.likes.length} + + dislikes = listInfo.dislikes.includes(auth.user.email) ? handleRate(event, "dislike")} > {listInfo.dislikes.length} - ; + + : + handleRate(event, "dislike")} + > + {listInfo.dislikes.length} + views = Views: {listInfo.views}; comment = { handleDeleteList(event, listInfo._id) }} aria-label= 'delete' - sx={{ "grid-area": "1 / 3 / 2 / 4" }} + sx={{ gridArea: "1 / 3 / 2 / 4" }} > {views} - + @@ -162,7 +194,7 @@ function ListCard(props) { "grid-row-gap": "0px", width: "100%" }}> -
+
{listInfo.name}
By: {listInfo.ownerName}
@@ -170,7 +202,7 @@ function ListCard(props) { color: "#d4af37", backgroundColor: "#2c2f70", width: "97%", - "grid-area": "2 / 1 / 3 / 2", + gridArea: "2 / 1 / 3 / 2", borderRadius: "10px", fontSize: "24pt", fontWeight: "bold" @@ -184,32 +216,32 @@ function ListCard(props) {
{text}
{likes}
{dislikes}
{ handleDeleteList(event, listInfo._id) }} aria-label= 'delete' - sx={{ "grid-area": "1 / 5 / 2 / 6" }} + sx={{ gridArea: "1 / 5 / 2 / 6" }} > - + {views}
+ } diff --git a/client/src/components/Toolbar.js b/client/src/components/Toolbar.js index 562ba3c..8440b13 100644 --- a/client/src/components/Toolbar.js +++ b/client/src/components/Toolbar.js @@ -1,6 +1,7 @@ -import { useContext } from 'react'; +import { useState, useContext } from 'react'; import { GlobalStoreContext } from '../store/index.js'; -import { Box, IconButton } from '@mui/material'; +import AuthContext from '../auth'; +import { Box, IconButton, Menu, MenuItem } from '@mui/material'; import HomeIcon from '@mui/icons-material/Home'; import GroupIcon from '@mui/icons-material/Groups'; import PersonIcon from '@mui/icons-material/Person'; @@ -8,9 +9,42 @@ import FunctionsIcon from '@mui/icons-material/Functions'; import SortIcon from '@mui/icons-material/Sort'; function Toolbar() { + const { auth } = useContext(AuthContext); const { store } = useContext(GlobalStoreContext); + const [anchorEl, setAnchorEl] = useState(null); + const isMenuOpen = Boolean(anchorEl); function handleChangeView(view) { store.setView(view); } + function handleSort(type) { store.sortBy(type); } + function handleSearch(event) { + if (event.code === "Enter") { + if (event.target.value === "") { + store.loadListData(); + } else { + let filter = {}; + switch (store.view) { + case "home": + filter = { ownerEmail: auth.user.email, name: { $regex: '^' + event.target.value, $options: 'i' } } + break; + case "all": + filter = { published: true, name: { $regex: '^' + event.target.value + '$', $options: 'i' } } + break; + case "user": + filter = { published: true, ownerName: { $regex: '^' + event.target.value + '$', $options: 'i' } } + break; + case "community": + filter = { ownerEmail: "community", name: { $regex: '^' + event.target.value + '$', $options: 'i' } } + break; + default: + return; + } + store.loadListData(filter); + } + } + } + + const handleMenuOpen = (event) => { setAnchorEl(event.currentTarget); } + const handleMenuClose = () => { setAnchorEl(null); } return (
@@ -43,7 +77,12 @@ function Toolbar() { > - +

SORT BY

+ + handleSort("new")}>Publish Date (Newest) + handleSort("old")}>Publish Date (Oldest) + handleSort("views")}>Views + handleSort("likes")}>Likes + handleSort("dislikes")}>Dislikes +
) } diff --git a/client/src/components/WorkspaceScreen.js b/client/src/components/WorkspaceScreen.js index 2d28fcf..e93652f 100644 --- a/client/src/components/WorkspaceScreen.js +++ b/client/src/components/WorkspaceScreen.js @@ -1,4 +1,4 @@ -import { useContext, useEffect } from 'react'; +import { useContext } from 'react'; import Top5Item from './Top5Item.js'; import { GlobalStoreContext } from '../store/index.js'; /** @@ -21,7 +21,14 @@ function WorkspaceScreen() { store.closeCurrentList(); } function handlePublish() { - store.publishCurrentList(); + if (new Set(newItems).size !== newItems.length) { return; } + for (const item of newItems) { + if (item === "" || !/[a-z0-9]/i.test(item.charAt(0))) { return; } + } + let id = store.currentList._id; + store.changeListName(id, newName); + store.changeListItems(id, newItems); + store.publishList(id); store.closeCurrentList(); } diff --git a/client/src/store/index.js b/client/src/store/index.js index 3799b00..61bee49 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -21,7 +21,8 @@ export const GlobalStoreActionType = { UNMARK_LIST_FOR_DELETION: "UNMARK_LIST_FOR_DELETION", SET_CURRENT_LIST: "SET_CURRENT_LIST", SET_OPENED_LIST: "SET_OPENED_LIST", - CHANGE_VIEW: "CHANGE_VIEW" + CHANGE_VIEW: "CHANGE_VIEW", + SORT_LIST_DATA: "SORT_LIST_DATA" } // WITH THIS WE'RE MAKING OUR GLOBAL DATA STORE @@ -84,7 +85,7 @@ function GlobalStoreContextProvider(props) { return setStore({ listData: payload, currentList: null, - openedList: null, + openedList: store.openedList, newListCounter: store.newListCounter, listMarkedForDeletion: null, view: store.view @@ -145,6 +146,17 @@ function GlobalStoreContextProvider(props) { view: payload }); } + // SORT BY + case GlobalStoreActionType.SORT_LIST_DATA: { + return setStore({ + listData: store.listData.sort(payload), + currentList: null, + openedList: store.openedList, + newListCounter: store.newListCounter, + listMarkedForDeletion: null, + view: store.view + }) + } default: return store; } @@ -163,19 +175,7 @@ function GlobalStoreContextProvider(props) { top5List.name = newName; async function updateList(top5List) { response = await api.updateTop5ListById(top5List._id, top5List); - if (response.data.success) { - async function getListData() { - response = await api.getTop5Lists({ ownerEmail: auth.user.email }); - if (response.data.success) { - let dataArray = response.data.data; - storeReducer({ - type: GlobalStoreActionType.UPDATE_LIST_DATA, - payload: { listData: dataArray } - }); - } - } - getListData(); - } + if (response.data.success) { store.loadListData(); } } updateList(top5List); } @@ -188,75 +188,137 @@ function GlobalStoreContextProvider(props) { top5List.items = items; async function updateList(top5List) { response = await api.updateTop5ListById(top5List._id, top5List); - if (response.data.success) { - async function getListData() { - response = await api.getTop5Lists(); - if (response.data.success) { - let dataArray = response.data.data; - storeReducer({ - type: GlobalStoreActionType.UPDATE_LIST_DATA, - payload: { listData: dataArray } - }); - } - } - getListData(); - } + if (response.data.success) { store.loadListData(); } } updateList(top5List); } } - store.publishCurrentList = async function () { - let top5List = store.currentList; - if (auth.user.email !== top5List.ownerEmail) { return; } - top5List.published = true; - let today = new Date(); - const monthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"]; - top5List.publishedDate = monthNames[today.getMonth()] + " " + + today.getDate() + ", " + today.getFullYear(); - console.log(top5List.publishedDate); - async function updateList(top5List) { - let response = await api.updateTop5ListById(top5List._id, top5List); - if (response.data.success) { - async function getListData() { - response = await api.getTop5Lists(); - if (response.data.success) { - let dataArray = response.data.data; - storeReducer({ - type: GlobalStoreActionType.UPDATE_LIST_DATA, - payload: { listData: dataArray } - }); - } - } - getListData(); + store.publishList = async function (id) { + let response = await api.getTop5ListById(id); + if (response.data.success) { + let top5List = response.data.top5List; + if (auth.user.email !== top5List.ownerEmail) { return; } + top5List.published = true; + let today = new Date(); + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"]; + top5List.publishedDate = monthNames[today.getMonth()] + " " + + today.getDate() + ", " + today.getFullYear(); + async function updateList(top5List) { + let response = await api.updateTop5ListById(top5List._id, top5List); + if (response.data.success) { store.loadListData(); } } - } - updateList(top5List); + updateList(top5List); + } } store.comment = async function (user, body) { let response = await api.getTop5ListById(store.openedList._id) if (response.data.success) { let top5List = response.data.top5List; - top5List.comments.push({name: user, body: body}); + top5List.comments.unshift({name: user, body: body}); async function updateList(top5List) { - console.log(top5List); response = await api.updateTop5ListById(top5List._id, top5List); - if (response.data.success) { - async function getListData() { - response = await api.getTop5Lists(); - if (response.data.success) { - let dataArray = response.data.data; - storeReducer({ - type: GlobalStoreActionType.UPDATE_LIST_DATA, - payload: { listData: dataArray } - }); - } - } - getListData(); + if (response.data.success) { store.loadListData(); } + } + updateList(top5List); + } + } + store.rate = async function (id, rating) { + let response = await api.getTop5ListById(id); + if (response.data.success) { + let top5List = response.data.top5List; + let likes = top5List.likes; + let dislikes = top5List.dislikes; + let email = auth.user.email; + if (rating === "like") { + if (likes.includes(email)) { + likes.splice(likes.indexOf(email), 1); + } else { + likes.push(email); + if (dislikes.includes(email)) { dislikes.splice(dislikes.indexOf(email), 1); } + } + } else if (rating === "dislike") { + if (dislikes.includes(email)) { + dislikes.splice(dislikes.indexOf(email), 1); + } else { + dislikes.push(email); + if (likes.includes(email)) { likes.splice(likes.indexOf(email), 1); } } + } else { + return; + } + async function updateList(top5List) { + response = await api.updateTop5ListById(top5List._id, top5List); + if (response.data.success) { store.loadListData(); } } updateList(top5List); } } + store.sortBy = function (type) { + let convertDate = function (date) { + date = date.split(" "); + const monthNames = [null, "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"]; + let day = date[1].substring(0, date[1].length - 1); + if (day.length === 1) { day = "0" + day; } + let month = monthNames.indexOf(date[0]); + if (month.length === 1) { month = "0" + month; } + return date[2] + month + day; + } + switch (type) { + case "new": + storeReducer({ + type: GlobalStoreActionType.SORT_LIST_DATA, + payload: (a, b) => { + if (!a.published) { return 1; } + if (!b.published) { return -1; } + return convertDate(b.publishedDate) - convertDate(a.publishedDate); + } + }); + break; + case "old": + storeReducer({ + type: GlobalStoreActionType.SORT_LIST_DATA, + payload: (a, b) => { + if (!a.published) { return 1; } + if (!b.published) { return -1; } + return convertDate(a.publishedDate) - convertDate(b.publishedDate); + } + }); + break; + case "views": + storeReducer({ + type: GlobalStoreActionType.SORT_LIST_DATA, + payload: (a, b) => { + if (!a.published) { return 1; } + if (!b.published) { return -1; } + return b.views - a.views; + } + }); + break; + case "likes": + storeReducer({ + type: GlobalStoreActionType.SORT_LIST_DATA, + payload: (a, b) => { + if (!a.published) { return 1; } + if (!b.published) { return -1; } + return b.likes.length - a.likes.length; + } + }); + break; + case "dislikes": + storeReducer({ + type: GlobalStoreActionType.SORT_LIST_DATA, + payload: (a, b) => { + if (!a.published) { return 1; } + if (!b.published) { return -1; } + return b.dislikes.length - a.dislikes.length; + } + }); + break; + default: + return; + } + } // THIS FUNCTION PROCESSES CLOSING THE CURRENTLY LOADED LIST store.closeCurrentList = function () { @@ -280,7 +342,8 @@ function GlobalStoreContextProvider(props) { likes: [], dislikes: [], views: 0, - comments: [] + comments: [], + communityPoints: [] }; const response = await api.createTop5List(payload); if (response.data.success) { @@ -288,8 +351,7 @@ function GlobalStoreContextProvider(props) { storeReducer({ type: GlobalStoreActionType.CREATE_NEW_LIST, payload: newList - } - ); + }); // IF IT'S A VALID LIST THEN LET'S START EDITING IT history.push("/top5list/" + newList._id); @@ -387,10 +449,51 @@ function GlobalStoreContextProvider(props) { } else { let response = await api.getTop5ListById(id); if (response.data.success) { - storeReducer({ - type: GlobalStoreActionType.SET_OPENED_LIST, - payload: response.data.top5List - }); + let top5List = response.data.top5List; + if (top5List.published) { + top5List.views = top5List.views + 1; + async function updateList(top5List) { + response = await api.updateTop5ListById(id, top5List); + if (response.data.success) { + let filter = {}; + switch (store.view) { + case "home": + filter = { ownerEmail: auth.user.email } + break; + case "all": + filter = { published: true } + break; + case "user": + filter = { published: true } + break; + case "community": + filter = { ownerEmail: "community" } + break; + default: + return; + } + const response = await api.getTop5Lists(filter); + if (response.data.success) { + setStore({ + listData: response.data.data, + currentList: null, + openedList: top5List, + newListCounter: store.newListCounter, + listMarkedForDeletion: null, + view: store.view + }); + } else { + console.log("API FAILED TO GET THE LISTS"); + } + } + } + updateList(top5List); + } else { + storeReducer({ + type: GlobalStoreActionType.SET_OPENED_LIST, + payload: response.data.top5List + }); + } } } } diff --git a/server/controllers/top5list-controller.js b/server/controllers/top5list-controller.js index ae65772..e1f46fc 100644 --- a/server/controllers/top5list-controller.js +++ b/server/controllers/top5list-controller.js @@ -63,6 +63,7 @@ updateTop5List = async (req, res) => { return res.status(200).json({ success: true, id: top5List._id, + top5List: top5List, message: 'Top 5 List updated!', }) }) @@ -99,6 +100,8 @@ getTop5ListById = async (req, res) => { }).catch(err => console.log(err)) } getTop5Lists = async (req, res) => { + if (req.query.name) { req.query.name = JSON.parse(req.query.name); } + if (req.query.ownerName) { req.query.ownerName = JSON.parse(req.query.ownerName); } await Top5List.find(req.query, (err, top5Lists) => { if (err) { return res.status(400).json({ success: false, error: err }) } return res.status(200).json({ success: true, data: top5Lists }) diff --git a/server/models/top5list-model.js b/server/models/top5list-model.js index ff84242..e24710e 100644 --- a/server/models/top5list-model.js +++ b/server/models/top5list-model.js @@ -12,7 +12,8 @@ const Top5ListSchema = new Schema( likes: { type: [String], required: true }, dislikes: { type: [String], required: true }, views: { type: Number, required: true }, - comments: { type: [{ name: String, body: String }], required: true } + comments: { type: [{ name: String, body: String }], required: true }, + communityPoints: { type: [{ item: String, points: Number }], required: true } }, { timestamps: true }, )