:feature: User settings, and private profile

Signed-off-by: Pau Costa <mico@micodev.es>
main
Pau Costa Ferrer 2024-02-12 12:02:14 +01:00
parent 6142dd7000
commit fc79245f9a
7 changed files with 157 additions and 27 deletions

View File

@ -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 <ThemeProvider theme={defaultTheme}>{children}</ThemeProvider>;
};
export default AppThemeProvider;

View File

@ -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;

View File

@ -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 | HTMLElement>(
null
);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
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 (
<AppBar
position="absolute"
@ -94,10 +105,10 @@ function TopAppBar(props: TopAppBarProps) {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<MenuItem key={"profile"} onClick={handleCloseUserMenu}>
<MenuItem key={"profile"} onClick={handleProfileClick}>
<Typography textAlign="center">{"Profile"}</Typography>
</MenuItem>
<MenuItem key={"settings"} onClick={handleCloseUserMenu}>
<MenuItem key={"settings"} onClick={handleSettingsClick}>
<Typography textAlign="center">{"Settings"}</Typography>
</MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}>

View File

@ -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: <PostList type="user" />,
element: <PostList type="feed" feedType="followed" />,
},
{
path: "global",
element: <PostList type="all" />,
element: <PostList type="feed" feedType="global" />,
},
{
path: "me",
@ -54,6 +51,10 @@ const router = createBrowserRouter([
path: "post/:id",
element: <PostView />,
},
{
path: "post/:id/edit",
element: <NewPost />,
},
{
path: "search",
element: <Search />,
@ -62,6 +63,14 @@ const router = createBrowserRouter([
path: "newpost",
element: <NewPost />,
},
{
path: "settings",
element: <Settings />,
},
{
path: "notifications",
element: <ANotification />,
},
],
},
{
@ -84,10 +93,10 @@ const router = createBrowserRouter([
root.render(
<React.StrictMode>
<Provider store={store}>
<ThemeProvider theme={defaultTheme}>
<AppThemeProvider>
<CssBaseline />
<RouterProvider router={router} />
</ThemeProvider>
</AppThemeProvider>
</Provider>
</React.StrictMode>
);

View File

@ -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 (
<Box>
<Box>
<Checkbox checked={isPrivate} onChange={handlePrivateChange} /> Private
profile
</Box>
<Divider />
<Box>
<Checkbox checked={darkModeLocal} onChange={handleDarkChange} /> Dark
mode
</Box>
<Divider />
<Box>
<Typography variant="h4">More Settings</Typography>
<Typography variant="body1">Coming soon...</Typography>
</Box>
</Box>
);
}

View File

@ -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);

View File

@ -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);