diff --git a/frontend/package.json b/frontend/package.json index 94d1832..a7e93ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "dependencies": { "@artsy/fresnel": "^1.5.0", "@hookform/resolvers": "^2.5.1", + "@reduxjs/toolkit": "^1.5.1", "@rehooks/local-storage": "2.4.0", "axios": "^0.21.1", "axios-hooks": "^2.6.3", @@ -33,11 +34,13 @@ "react-intersection-observer": "^8.32.0", "react-linkify": "^1.0.0-alpha", "react-progressive-graceful-image": "^0.6.13", + "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-show-more-text": "^1.4.6", "react-simple-pull-to-refresh": "^1.2.3", "react-toastify": "^7.0.4", "react-virtualized": "9.22.3", + "redux": "^4.1.0", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^2.0.3", "use-query-params": "^1.2.2", diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index be9d477..a90748e 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -4,9 +4,12 @@ import "react-virtualized/styles.css"; import { toast, Zoom } from "react-toastify"; import axios from "axios"; import { configure } from "axios-hooks"; -import { ResponsiveProvider, UserProvider } from "./context-providers"; -import Routes from "./routes"; +import { Provider } from "react-redux"; import styles from "./app.module.scss"; +import { ResponsiveProvider } from "./context-providers"; +import store from "./redux/store"; +import Routes from "./routes"; +import LocalStorageUserManager from "./components/local-storage-user-manager"; toast.configure({ position: "bottom-center", @@ -21,11 +24,12 @@ configure({ axios: axios.create({ baseURL: process.env.API_URL }) }); function App() { return ( - - + + + - - + + ); } diff --git a/frontend/src/components/bottom-bar/bottom-bar.tsx b/frontend/src/components/bottom-bar/bottom-bar.tsx index 51d8302..6d5894d 100644 --- a/frontend/src/components/bottom-bar/bottom-bar.tsx +++ b/frontend/src/components/bottom-bar/bottom-bar.tsx @@ -1,12 +1,17 @@ import { Transition } from "semantic-ui-react"; -import { useContext } from "react"; +import isEqual from "lodash.isequal"; import styles from "./bottom-bar.module.scss"; import EventActionButtons from "../event-action-buttons"; import EventCommentInput from "../event-comment-input"; -import { SingleEventContext } from "../../context-providers"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { setCommenting } from "../../redux/slices/single-event-slice"; function BottomBar() { - const { event, isCommenting, setCommenting } = useContext(SingleEventContext); + const { event, isCommenting } = useAppSelector( + ({ singleEvent: { event, isCommenting } }) => ({ event, isCommenting }), + isEqual, + ); + const dispatch = useAppDispatch(); return (
{isCommenting ? ( - setCommenting(false)} /> + dispatch(setCommenting(false))} + /> ) : ( - setCommenting(true)} /> + dispatch(setCommenting(true))} + /> )}
diff --git a/frontend/src/components/comment/comment.tsx b/frontend/src/components/comment/comment.tsx index 727e1be..783539b 100644 --- a/frontend/src/components/comment/comment.tsx +++ b/frontend/src/components/comment/comment.tsx @@ -1,4 +1,4 @@ -import { memo, useContext, useState } from "react"; +import { memo, useState } from "react"; import classNames from "classnames"; import { useHistory } from "react-router-dom"; import ProgressiveImage from "react-progressive-graceful-image"; @@ -7,10 +7,11 @@ import { displayDateTime } from "../../utils/parser-utils"; import { RELATIVE, USER_ID } from "../../constants"; import { PROFILE_MAIN_PATH } from "../../routes/paths"; import { EventCommentData } from "../../types/events"; -import { SingleEventContext } from "../../context-providers"; import placeholderImage from "../../assets/placeholder-image.gif"; import defaultAvatarImage from "../../assets/avatar.png"; import styles from "./comment.module.scss"; +import { useAppDispatch } from "../../redux/hooks"; +import { setReplyComment } from "../../redux/slices/single-event-slice"; type Props = { comment: EventCommentData; @@ -23,23 +24,14 @@ function Comment({ content, }, }: Props) { - const { setCommenting, setInputComment } = useContext(SingleEventContext); const history = useHistory(); + const dispatch = useAppDispatch(); const [isHoveringReply, setHoveringReply] = useState(false); const onUserClick = () => history.push(PROFILE_MAIN_PATH.replace(`:${USER_ID}`, `${userId}`)); - const onReplyClick = () => { - setCommenting(true); - setInputComment((inputComment) => { - const replyName = `@${name}`; - - return !inputComment || inputComment.slice(-1) === " " - ? `${inputComment}${replyName} ` - : `${inputComment} ${replyName} `; - }); - }; + const onReplyClick = () => dispatch(setReplyComment({ name })); return (
diff --git a/frontend/src/components/event-action-buttons/event-action-buttons.tsx b/frontend/src/components/event-action-buttons/event-action-buttons.tsx index 820d23c..3c3acd1 100644 --- a/frontend/src/components/event-action-buttons/event-action-buttons.tsx +++ b/frontend/src/components/event-action-buttons/event-action-buttons.tsx @@ -1,10 +1,19 @@ -import { useContext, useState } from "react"; +import { useState } from "react"; import classNames from "classnames"; +import isEqual from "lodash.isequal"; import { toast } from "react-toastify"; import { Icon } from "semantic-ui-react"; import IconLoader from "../icon-loader"; -import { SingleEventContext } from "../../context-providers"; import { resolveApiError } from "../../utils/error-utils"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { + useCreateEventSignUp, + useCreateEventLike, + useDeleteEventSignUp, + useDeleteEventLike, + useGetSingleEvent, +} from "../../custom-hooks/api/events-api"; +import { setEvent } from "../../redux/slices/single-event-slice"; import styles from "./event-action-buttons.module.scss"; type Props = { @@ -13,17 +22,37 @@ type Props = { function EventActionButtons({ onClickComment }: Props) { const { - event: { hasLiked, hasSignedUp } = { hasLiked: false, hasSignedUp: false }, - createEventSignUp, - createEventLike, - deleteEventSignUp, - deleteEventLike, - } = useContext(SingleEventContext); + id: eventId, + hasLiked, + hasSignedUp, + } = useAppSelector( + ({ singleEvent: { event: { id, hasSignedUp, hasLiked } = {} } }) => ({ + id, + hasLiked, + hasSignedUp, + }), + isEqual, + ) as { id: number; hasLiked: boolean; hasSignedUp: boolean }; + const dispatch = useAppDispatch(); + + const { getSingleEvent } = useGetSingleEvent(); + const { createEventSignUp } = useCreateEventSignUp(); + const { createEventLike } = useCreateEventLike(); + const { deleteEventSignUp } = useDeleteEventSignUp(); + const { deleteEventLike } = useDeleteEventLike(); const [isSigningUp, setSigningUp] = useState(false); const [isLiking, setLiking] = useState(false); const [isWithdrawing, setWithdrawing] = useState(false); const [isUnliking, setUnliking] = useState(false); + const updateEvent = async ( + updateFunction: (data: { eventId: number }) => Promise, + ) => { + await updateFunction({ eventId }); + const updatedEvent = await getSingleEvent(eventId); + dispatch(setEvent(updatedEvent)); + }; + const onCreateEventSignUp = async () => { if (isSigningUp || isWithdrawing) { return; @@ -32,7 +61,7 @@ function EventActionButtons({ onClickComment }: Props) { try { setSigningUp(true); - await createEventSignUp(); + await updateEvent(createEventSignUp); toast.success("You have joined for the event."); } catch (error) { @@ -50,7 +79,7 @@ function EventActionButtons({ onClickComment }: Props) { try { setLiking(true); - await createEventLike(); + await updateEvent(createEventLike); toast.success("You have liked the event."); } catch (error) { @@ -68,7 +97,7 @@ function EventActionButtons({ onClickComment }: Props) { try { setWithdrawing(true); - await deleteEventSignUp(); + await updateEvent(deleteEventSignUp); toast.success("You have withdrawn from the event."); } catch (error) { @@ -86,7 +115,7 @@ function EventActionButtons({ onClickComment }: Props) { try { setUnliking(true); - await deleteEventLike(); + await updateEvent(deleteEventLike); toast.success("You have unliked the event."); } catch (error) { diff --git a/frontend/src/components/event-body/event-body.tsx b/frontend/src/components/event-body/event-body.tsx index cb9541e..320415a 100644 --- a/frontend/src/components/event-body/event-body.tsx +++ b/frontend/src/components/event-body/event-body.tsx @@ -1,20 +1,27 @@ -import { useContext, useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useParams } from "react-router-dom"; import PlaceholderWrapper from "../placeholder-wrapper"; import NoEventBanner from "../no-event-banner"; -import { SingleEventContext } from "../../context-providers"; import EventInfoView from "../event-info-view"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { useGetSingleEvent } from "../../custom-hooks/api/events-api"; +import { setEvent } from "../../redux/slices/single-event-slice"; function EventBody() { - const { event, getSingleEvent } = useContext(SingleEventContext); - const [isLoading, setLoading] = useState(false); + const event = useAppSelector(({ singleEvent }) => singleEvent.event); + const dispatch = useAppDispatch(); + const { eventId } = useParams<{ eventId: string }>(); + const { isLoading, getSingleEvent } = useGetSingleEvent(); useEffect(() => { (async () => { - setLoading(true); - await getSingleEvent(); - setLoading(false); + dispatch(setEvent(await getSingleEvent(eventId))); })(); - }, [getSingleEvent]); + + return () => { + dispatch(setEvent(undefined)); + }; + }, [eventId, getSingleEvent, dispatch]); return ( void; }; function EventCommentInput({ onClickCancel }: Props) { - const { createEventComment, inputComment, setInputComment } = - useContext(SingleEventContext); + const inputComment = useAppSelector( + ({ singleEvent }) => singleEvent.inputComment, + ); + const eventId = useAppSelector( + ({ singleEvent }) => singleEvent.event?.id, + ) as number; + const dispatch = useAppDispatch(); + + const { getSingleEvent } = useGetSingleEvent(); + const { createEventComment } = useCreateEventComment(); const [isSending, setSending] = useState(false); const onSend = async () => { @@ -21,9 +37,10 @@ function EventCommentInput({ onClickCancel }: Props) { } setSending(true); - await createEventComment({ content: inputComment }); + await createEventComment({ eventId, content: inputComment }); + dispatch(setEvent(await getSingleEvent(eventId))); setSending(false); - setInputComment(""); + dispatch(setInputComment("")); }; return ( @@ -39,7 +56,7 @@ function EventCommentInput({ onClickCancel }: Props) { setInputComment(value)} + onChange={(_, { value }) => dispatch(setInputComment(value))} value={inputComment} />
diff --git a/frontend/src/components/local-storage-user-manager/index.ts b/frontend/src/components/local-storage-user-manager/index.ts new file mode 100644 index 0000000..ed1fd57 --- /dev/null +++ b/frontend/src/components/local-storage-user-manager/index.ts @@ -0,0 +1 @@ +export { default } from "./local-storage-user-manager"; diff --git a/frontend/src/components/local-storage-user-manager/local-storage-user-manager.tsx b/frontend/src/components/local-storage-user-manager/local-storage-user-manager.tsx new file mode 100644 index 0000000..9065373 --- /dev/null +++ b/frontend/src/components/local-storage-user-manager/local-storage-user-manager.tsx @@ -0,0 +1,15 @@ +import { useEffect } from "react"; +import { useAppSelector } from "../../redux/hooks"; +import { saveToLocalStorage } from "../../utils/localStorage-utils"; + +function LocalStorageUserManager() { + const user = useAppSelector(({ user }) => user); + + useEffect(() => { + saveToLocalStorage(user); + }, [user]); + + return null; +} + +export default LocalStorageUserManager; diff --git a/frontend/src/components/pages/events-page/events-page.module.scss b/frontend/src/components/pages/events-page/events-page.module.scss new file mode 100644 index 0000000..ab5b6d1 --- /dev/null +++ b/frontend/src/components/pages/events-page/events-page.module.scss @@ -0,0 +1,5 @@ +.eventsPage { + .pusherContainer.important:after { + opacity: 0 !important; + } +} diff --git a/frontend/src/components/pages/events-page/events-page.tsx b/frontend/src/components/pages/events-page/events-page.tsx index c66add0..c026705 100644 --- a/frontend/src/components/pages/events-page/events-page.tsx +++ b/frontend/src/components/pages/events-page/events-page.tsx @@ -1,25 +1,65 @@ -import { useContext } from "react"; import { Sidebar } from "semantic-ui-react"; -import { SearchContext } from "../../../context-providers"; +import { startOfToday } from "date-fns"; +import classNames from "classnames"; import EventList from "../../event-list"; import TopBar from "../../top-bar"; import PageBody from "../../page-body"; import FullPageContainer from "../../full-page-container"; import SearchSidebar from "../../search-sidebar"; import SearchTab from "../../search-tab"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { + loadCategories, + loadDates, + setLoadingCategories, + setSidebarOpened, +} from "../../../redux/slices/search-slice"; +import { useGetEventCategories } from "../../../custom-hooks/api/events-api"; +import useSearchQueryParams from "../../../custom-hooks/use-search-query-params"; +import { getDatePeriods } from "../../../utils/date-time-utils"; +import styles from "./events-page.module.scss"; function EventsPage() { - const { isSidebarOpened, setSidebarOpened } = useContext(SearchContext); + const { getEventCategories } = useGetEventCategories(); + const isSidebarOpened = useAppSelector( + ({ search }) => search.isSidebarOpened, + ); + const dispatch = useAppDispatch(); + const { searchQuery } = useSearchQueryParams(); + + const onClickSearchTab = async () => { + dispatch(setSidebarOpened(true)); + dispatch( + loadDates({ + datePeriods: getDatePeriods({ + currentDateTime: startOfToday().getTime(), + }), + searchQuery, + }), + ); + dispatch(setLoadingCategories(true)); + + const categories = await getEventCategories(); + + dispatch( + loadCategories({ + categories, + searchQuery, + }), + ); + dispatch(setLoadingCategories(false)); + }; return ( - + - + - setSidebarOpened(true)} />} - /> + } /> ; diff --git a/frontend/src/components/pages/login-page/login-page.tsx b/frontend/src/components/pages/login-page/login-page.tsx index 9a659b9..1bd9e59 100644 --- a/frontend/src/components/pages/login-page/login-page.tsx +++ b/frontend/src/components/pages/login-page/login-page.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { DeepMap, FieldError, FormProvider, useForm } from "react-hook-form"; @@ -14,12 +14,13 @@ import { toast } from "react-toastify"; import FormField from "../../form-field"; import { deepTrim } from "../../../utils/parser-utils"; import { EMAIL, PASSWORD } from "../../../constants"; -import { UserContext } from "../../../context-providers"; import { useCustomAuth } from "../../../custom-hooks/api/auth-api"; import { resolveApiError } from "../../../utils/error-utils"; +import FullPageContainer from "../../full-page-container"; +import { useAppDispatch } from "../../../redux/hooks"; +import { updateUser } from "../../../redux/slices/user-slice"; import catLogo from "../../../assets/logo-cat-green.svg"; import styles from "./login-page.module.scss"; -import FullPageContainer from "../../full-page-container"; const schema = yup.object().shape({ [EMAIL]: yup @@ -46,25 +47,28 @@ function LoginPage() { defaultValues: defaultFormProps, }); const { handleSubmit } = methods; - const { updateUser } = useContext(UserContext); + const dispatch = useAppDispatch(); const { login, isLoading } = useCustomAuth(); useEffect(() => { - updateUser(null); - }, [updateUser]); + dispatch(updateUser(null)); + }, [dispatch]); const onSubmit = useCallback( async (formData: LoginFormProps) => { try { const { id, name, email, access, refresh, profileImageUrl } = await login(deepTrim(formData)); - updateUser({ id, name, email, access, refresh, profileImageUrl }); + + dispatch( + updateUser({ id, name, email, access, refresh, profileImageUrl }), + ); toast.success("Signed in successfully."); } catch (error) { resolveApiError(error); } }, - [login, updateUser], + [login, dispatch], ); const onError = useCallback((error: DeepMap) => { diff --git a/frontend/src/components/search-category-section/search-category-section.tsx b/frontend/src/components/search-category-section/search-category-section.tsx index f9878fc..71fabe4 100644 --- a/frontend/src/components/search-category-section/search-category-section.tsx +++ b/frontend/src/components/search-category-section/search-category-section.tsx @@ -1,31 +1,27 @@ -import { useCallback, useContext, MouseEvent } from "react"; +import { MouseEvent } from "react"; import classNames from "classnames"; +import isEqual from "lodash.isequal"; import { Label, LabelProps } from "semantic-ui-react"; -import { SearchContext } from "../../context-providers"; import PlaceholderWrapper from "../placeholder-wrapper"; import styles from "./search-category-section.module.scss"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { setSelectedCategory } from "../../redux/slices/search-slice"; function SearchCategorySection() { - const { - selectedCategory, - setSelectedCategory, - categories, - isLoadingCategories, - } = useContext(SearchContext); + const { selectedCategory, categories, isLoadingCategories } = useAppSelector( + ({ search: { selectedCategory, categories, isLoadingCategories } }) => ({ + selectedCategory, + categories, + isLoadingCategories, + }), + isEqual, + ); + const dispatch = useAppDispatch(); const onLabelClick: ( event: MouseEvent, data: LabelProps, - ) => void = useCallback( - (_, { value }) => { - setSelectedCategory((selectedCategory) => - selectedCategory === undefined || selectedCategory !== value - ? value - : undefined, - ); - }, - [setSelectedCategory], - ); + ) => void = (_, { value }) => dispatch(setSelectedCategory(value)); return ( - {categories.map((category) => ( + {categories.map((category, index) => (