diff --git a/client/src/app/AppThemeProvider.tsx b/client/src/app/AppThemeProvider.tsx new file mode 100644 index 0000000..6ab02b3 --- /dev/null +++ b/client/src/app/AppThemeProvider.tsx @@ -0,0 +1,19 @@ +import { ThemeProvider } from "@emotion/react"; +import React from "react"; +import { useSelector } from "react-redux"; +import { selectDarkMode } from "./loginSlice"; +import { createTheme } from "@mui/material"; + +const AppThemeProvider = ({ children }: { children: React.ReactNode }) => { + const darkThemeEnabled = useSelector(selectDarkMode); + + const defaultTheme = createTheme({ + palette: { + mode: darkThemeEnabled ? "dark" : "light", + }, + }); + + return {children}; +}; + +export default AppThemeProvider; diff --git a/client/src/app/loginSlice.ts b/client/src/app/loginSlice.ts index a788094..2a6de36 100644 --- a/client/src/app/loginSlice.ts +++ b/client/src/app/loginSlice.ts @@ -13,25 +13,32 @@ interface loginState { loggedIn: boolean; status: Status; error: string | null; + darkMode: boolean; userInfo: { firstName: string; lastName: string; jwt: string; id: number; profilePictureId: string; + notifications: Notification[]; + isPrivate: boolean; }; } +const storageDarkMode = localStorage.getItem("darkMode") === "true"; const initialState: loginState = { loggedIn: false, status: Status.idle, + darkMode: storageDarkMode || false, error: null, userInfo: { + isPrivate: false, firstName: "", lastName: "", jwt: "", id: -1, profilePictureId: "", + notifications: [], }, }; @@ -46,6 +53,8 @@ export const loginSlice = createSlice({ state.userInfo.firstName = action.payload.firstName; state.userInfo.lastName = action.payload.lastName; state.userInfo.profilePictureId = action.payload.profilePictureId; + state.userInfo.notifications = action.payload.notifications; + state.userInfo.isPrivate = action.payload.isPrivate; }, logoff: (state) => { state.loggedIn = false; @@ -57,6 +66,12 @@ export const loginSlice = createSlice({ setError: (state, action) => { state.error = action.payload; }, + setDarkMode: (state, action) => { + state.darkMode = action.payload; + }, + setPrivate: (state, action) => { + state.userInfo.isPrivate = action.payload; + }, }, }); @@ -93,6 +108,7 @@ export const postLogin = lastName: userResponse.data.lastName, id: userResponse.data.id, profilePictureId: userResponse.data.profilePictureId, + notifications: userResponse.data.notifications, }) ); dispatch(setStatus(Status.succeeded)); @@ -120,6 +136,22 @@ export const postSignup = } }; +export const updateMe = + (isPrivate: boolean): AppThunk => + async (dispatch) => { + const userApi = new UsersApi( + new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + accessToken: localStorage.getItem("jwt") || "", + }) + ); + console.log("From inside updateME dispatch, param is:", isPrivate); + + await userApi.usersMePatch({ isPrivate }); + const userResponse = await userApi.usersMeGet(); + dispatch(setPrivate(userResponse.data.isPrivate)); + }; + export const postLogout = (): AppThunk => async (dispatch) => { localStorage.removeItem("jwt"); sessionStorage.removeItem("jwt"); @@ -131,7 +163,8 @@ const addJWT = async (token: string) => { localStorage.setItem("jwt", token); }; -export const { login, logoff, setStatus, setError } = loginSlice.actions; +export const { login, logoff, setStatus, setError, setDarkMode, setPrivate } = + loginSlice.actions; export default loginSlice.reducer; @@ -143,3 +176,6 @@ export const selectUserInfo = (state: { login: loginState }) => export const selectErrorMessage = (state: { login: loginState }) => state.login.error; + +export const selectDarkMode = (state: { login: loginState }) => + state.login.darkMode; diff --git a/client/src/components/topAppBar.tsx b/client/src/components/topAppBar.tsx index 2f382ed..75dbdfd 100644 --- a/client/src/components/topAppBar.tsx +++ b/client/src/components/topAppBar.tsx @@ -12,6 +12,7 @@ import {useSelector} from "react-redux"; import { postLogout, selectUserInfo } from "../app/loginSlice"; import { useAppDispatch } from "../app/store"; import { AppAvatar } from "./appAvatar"; +import { useNavigate } from "react-router-dom"; interface TopAppBarProps { height: number; @@ -19,12 +20,12 @@ interface TopAppBarProps { function TopAppBar(props: TopAppBarProps) { const dispatch = useAppDispatch(); + const navigate = useNavigate(); const userInfo = useSelector(selectUserInfo); const [anchorElUser, setAnchorElUser] = React.useState( null ); - const handleOpenUserMenu = (event: React.MouseEvent) => { setAnchorElUser(event.currentTarget); }; @@ -38,6 +39,16 @@ function TopAppBar(props: TopAppBarProps) { setAnchorElUser(null); }; + const handleProfileClick = () => { + navigate("/me"); + setAnchorElUser(null); + }; + + const handleSettingsClick = () => { + navigate("/settings"); + setAnchorElUser(null); + }; + return ( - + {"Profile"} - + {"Settings"} diff --git a/client/src/index.tsx b/client/src/index.tsx index d366174..860cb5d 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -6,7 +6,7 @@ import "@fontsource/roboto"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Root from "./routes/root"; import ErrorPage from "./error-page"; -import { createTheme, CssBaseline, ThemeProvider } from "@mui/material"; +import { CssBaseline } from "@mui/material"; import Login from "./routes/Auth/login"; import Register from "./routes/Auth/register"; import AuthRoot from "./routes/Auth/authRoot"; @@ -17,17 +17,14 @@ import Profile from "./routes/profile"; import PostView from "./routes/post"; import NewPost from "./routes/newPost"; import Search from "./routes/search"; +import Settings from "./routes/settings"; +import ANotification from "./routes/notification"; +import AppThemeProvider from "./app/AppThemeProvider"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); -const defaultTheme = createTheme({ - palette: { - mode: "light", - }, -}); - const router = createBrowserRouter([ { path: "/", @@ -36,11 +33,11 @@ const router = createBrowserRouter([ children: [ { path: "feed", - element: , + element: , }, { path: "global", - element: , + element: , }, { path: "me", @@ -54,6 +51,10 @@ const router = createBrowserRouter([ path: "post/:id", element: , }, + { + path: "post/:id/edit", + element: , + }, { path: "search", element: , @@ -62,6 +63,14 @@ const router = createBrowserRouter([ path: "newpost", element: , }, + { + path: "settings", + element: , + }, + { + path: "notifications", + element: , + }, ], }, { @@ -84,10 +93,10 @@ const router = createBrowserRouter([ root.render( - + - + ); diff --git a/client/src/routes/settings.tsx b/client/src/routes/settings.tsx new file mode 100644 index 0000000..ddb1ff7 --- /dev/null +++ b/client/src/routes/settings.tsx @@ -0,0 +1,56 @@ +import { Box, Checkbox, Divider, Typography } from "@mui/material"; +import { useSelector } from "react-redux"; +import { + selectDarkMode, + selectUserInfo, + setDarkMode, + updateMe, +} from "../app/loginSlice"; +import { useAppDispatch } from "../app/store"; +import { useEffect, useState } from "react"; + +export default function Settings() { + const userInfo = useSelector(selectUserInfo); + const darkModeState = useSelector(selectDarkMode); + const dispatch = useAppDispatch(); + const [isPrivate, setIsPrivate] = useState(false); + const [darkModeLocal, setDarkModeLocal] = useState(false); + + useEffect(() => { + setIsPrivate(userInfo.isPrivate); + }, [userInfo]); + + useEffect(() => { + setDarkModeLocal(darkModeState); + }, [darkModeState]); + + const handleDarkChange = () => { + dispatch(setDarkMode(!darkModeLocal)); + localStorage.setItem("darkMode", darkModeLocal ? "false" : "true"); + }; + + const handlePrivateChange = () => { + dispatch(updateMe(!isPrivate)); + }; + + return ( + + + Private + profile + + + + + Dark + mode + + + + + More Settings + Coming soon... + + + ); +} diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index 3d73af4..45ad857 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -213,8 +213,9 @@ export class UserController { notifications: true, }, }); - - user.isPrivate = req.body.isPrivate || user.isPrivate; + // Strict check, as we are dealing with a boolean value in this field + user.isPrivate = + req.body.isPrivate === undefined ? user.isPrivate : req.body.isPrivate; user.profilePictureId = req.body.profilePictureId || user.profilePictureId; await this.userRepository.save(user); diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts index 2e41794..8bd917f 100644 --- a/server/src/controller/postController.ts +++ b/server/src/controller/postController.ts @@ -51,20 +51,18 @@ export class PostController { }); // Remove private, non followed users const filteredPosts = posts.filter((post) => { - let followStatus = false; - - if(post.createdBy.followers){ + let followStatus = false; + + // Get if the req user is in the follower list + if (post.createdBy.followers) { followStatus = post.createdBy.followers.some( - (follower) => follower.id === req.user.id - ); - } - - if (post.createdBy.isPrivate && !followStatus) { - return post.createdBy.followers.some( (follower) => follower.id === req.user.id ); } - return true; + + return !(post.createdBy.isPrivate && !followStatus); + + }); res.status(200).send(filteredPosts);