:feature: User settings, and private profile
Signed-off-by: Pau Costa <mico@micodev.es>main
parent
6142dd7000
commit
fc79245f9a
|
|
@ -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;
|
||||||
|
|
@ -13,25 +13,32 @@ interface loginState {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
status: Status;
|
status: Status;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
darkMode: boolean;
|
||||||
userInfo: {
|
userInfo: {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
jwt: string;
|
jwt: string;
|
||||||
id: number;
|
id: number;
|
||||||
profilePictureId: string;
|
profilePictureId: string;
|
||||||
|
notifications: Notification[];
|
||||||
|
isPrivate: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const storageDarkMode = localStorage.getItem("darkMode") === "true";
|
||||||
|
|
||||||
const initialState: loginState = {
|
const initialState: loginState = {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
status: Status.idle,
|
status: Status.idle,
|
||||||
|
darkMode: storageDarkMode || false,
|
||||||
error: null,
|
error: null,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
|
isPrivate: false,
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
jwt: "",
|
jwt: "",
|
||||||
id: -1,
|
id: -1,
|
||||||
profilePictureId: "",
|
profilePictureId: "",
|
||||||
|
notifications: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -46,6 +53,8 @@ export const loginSlice = createSlice({
|
||||||
state.userInfo.firstName = action.payload.firstName;
|
state.userInfo.firstName = action.payload.firstName;
|
||||||
state.userInfo.lastName = action.payload.lastName;
|
state.userInfo.lastName = action.payload.lastName;
|
||||||
state.userInfo.profilePictureId = action.payload.profilePictureId;
|
state.userInfo.profilePictureId = action.payload.profilePictureId;
|
||||||
|
state.userInfo.notifications = action.payload.notifications;
|
||||||
|
state.userInfo.isPrivate = action.payload.isPrivate;
|
||||||
},
|
},
|
||||||
logoff: (state) => {
|
logoff: (state) => {
|
||||||
state.loggedIn = false;
|
state.loggedIn = false;
|
||||||
|
|
@ -57,6 +66,12 @@ export const loginSlice = createSlice({
|
||||||
setError: (state, action) => {
|
setError: (state, action) => {
|
||||||
state.error = action.payload;
|
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,
|
lastName: userResponse.data.lastName,
|
||||||
id: userResponse.data.id,
|
id: userResponse.data.id,
|
||||||
profilePictureId: userResponse.data.profilePictureId,
|
profilePictureId: userResponse.data.profilePictureId,
|
||||||
|
notifications: userResponse.data.notifications,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(setStatus(Status.succeeded));
|
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) => {
|
export const postLogout = (): AppThunk => async (dispatch) => {
|
||||||
localStorage.removeItem("jwt");
|
localStorage.removeItem("jwt");
|
||||||
sessionStorage.removeItem("jwt");
|
sessionStorage.removeItem("jwt");
|
||||||
|
|
@ -131,7 +163,8 @@ const addJWT = async (token: string) => {
|
||||||
localStorage.setItem("jwt", token);
|
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;
|
export default loginSlice.reducer;
|
||||||
|
|
||||||
|
|
@ -143,3 +176,6 @@ export const selectUserInfo = (state: { login: loginState }) =>
|
||||||
|
|
||||||
export const selectErrorMessage = (state: { login: loginState }) =>
|
export const selectErrorMessage = (state: { login: loginState }) =>
|
||||||
state.login.error;
|
state.login.error;
|
||||||
|
|
||||||
|
export const selectDarkMode = (state: { login: loginState }) =>
|
||||||
|
state.login.darkMode;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {useSelector} from "react-redux";
|
||||||
import { postLogout, selectUserInfo } from "../app/loginSlice";
|
import { postLogout, selectUserInfo } from "../app/loginSlice";
|
||||||
import { useAppDispatch } from "../app/store";
|
import { useAppDispatch } from "../app/store";
|
||||||
import { AppAvatar } from "./appAvatar";
|
import { AppAvatar } from "./appAvatar";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface TopAppBarProps {
|
interface TopAppBarProps {
|
||||||
height: number;
|
height: number;
|
||||||
|
|
@ -19,12 +20,12 @@ interface TopAppBarProps {
|
||||||
|
|
||||||
function TopAppBar(props: TopAppBarProps) {
|
function TopAppBar(props: TopAppBarProps) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const userInfo = useSelector(selectUserInfo);
|
const userInfo = useSelector(selectUserInfo);
|
||||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorElUser(event.currentTarget);
|
setAnchorElUser(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
@ -38,6 +39,16 @@ function TopAppBar(props: TopAppBarProps) {
|
||||||
setAnchorElUser(null);
|
setAnchorElUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
navigate("/me");
|
||||||
|
setAnchorElUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
navigate("/settings");
|
||||||
|
setAnchorElUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
|
@ -94,10 +105,10 @@ function TopAppBar(props: TopAppBarProps) {
|
||||||
open={Boolean(anchorElUser)}
|
open={Boolean(anchorElUser)}
|
||||||
onClose={handleCloseUserMenu}
|
onClose={handleCloseUserMenu}
|
||||||
>
|
>
|
||||||
<MenuItem key={"profile"} onClick={handleCloseUserMenu}>
|
<MenuItem key={"profile"} onClick={handleProfileClick}>
|
||||||
<Typography textAlign="center">{"Profile"}</Typography>
|
<Typography textAlign="center">{"Profile"}</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key={"settings"} onClick={handleCloseUserMenu}>
|
<MenuItem key={"settings"} onClick={handleSettingsClick}>
|
||||||
<Typography textAlign="center">{"Settings"}</Typography>
|
<Typography textAlign="center">{"Settings"}</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem key={"logout"} onClick={handleLogout}>
|
<MenuItem key={"logout"} onClick={handleLogout}>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import "@fontsource/roboto";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import Root from "./routes/root";
|
import Root from "./routes/root";
|
||||||
import ErrorPage from "./error-page";
|
import ErrorPage from "./error-page";
|
||||||
import { createTheme, CssBaseline, ThemeProvider } from "@mui/material";
|
import { CssBaseline } from "@mui/material";
|
||||||
import Login from "./routes/Auth/login";
|
import Login from "./routes/Auth/login";
|
||||||
import Register from "./routes/Auth/register";
|
import Register from "./routes/Auth/register";
|
||||||
import AuthRoot from "./routes/Auth/authRoot";
|
import AuthRoot from "./routes/Auth/authRoot";
|
||||||
|
|
@ -17,17 +17,14 @@ import Profile from "./routes/profile";
|
||||||
import PostView from "./routes/post";
|
import PostView from "./routes/post";
|
||||||
import NewPost from "./routes/newPost";
|
import NewPost from "./routes/newPost";
|
||||||
import Search from "./routes/search";
|
import Search from "./routes/search";
|
||||||
|
import Settings from "./routes/settings";
|
||||||
|
import ANotification from "./routes/notification";
|
||||||
|
import AppThemeProvider from "./app/AppThemeProvider";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById("root") as HTMLElement
|
document.getElementById("root") as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultTheme = createTheme({
|
|
||||||
palette: {
|
|
||||||
mode: "light",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
|
@ -36,11 +33,11 @@ const router = createBrowserRouter([
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "feed",
|
path: "feed",
|
||||||
element: <PostList type="user" />,
|
element: <PostList type="feed" feedType="followed" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "global",
|
path: "global",
|
||||||
element: <PostList type="all" />,
|
element: <PostList type="feed" feedType="global" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "me",
|
path: "me",
|
||||||
|
|
@ -54,6 +51,10 @@ const router = createBrowserRouter([
|
||||||
path: "post/:id",
|
path: "post/:id",
|
||||||
element: <PostView />,
|
element: <PostView />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "post/:id/edit",
|
||||||
|
element: <NewPost />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "search",
|
path: "search",
|
||||||
element: <Search />,
|
element: <Search />,
|
||||||
|
|
@ -62,6 +63,14 @@ const router = createBrowserRouter([
|
||||||
path: "newpost",
|
path: "newpost",
|
||||||
element: <NewPost />,
|
element: <NewPost />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "settings",
|
||||||
|
element: <Settings />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "notifications",
|
||||||
|
element: <ANotification />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -84,10 +93,10 @@ const router = createBrowserRouter([
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider theme={defaultTheme}>
|
<AppThemeProvider>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</ThemeProvider>
|
</AppThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -213,8 +213,9 @@ export class UserController {
|
||||||
notifications: true,
|
notifications: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Strict check, as we are dealing with a boolean value in this field
|
||||||
user.isPrivate = req.body.isPrivate || user.isPrivate;
|
user.isPrivate =
|
||||||
|
req.body.isPrivate === undefined ? user.isPrivate : req.body.isPrivate;
|
||||||
user.profilePictureId =
|
user.profilePictureId =
|
||||||
req.body.profilePictureId || user.profilePictureId;
|
req.body.profilePictureId || user.profilePictureId;
|
||||||
await this.userRepository.save(user);
|
await this.userRepository.save(user);
|
||||||
|
|
|
||||||
|
|
@ -53,18 +53,16 @@ export class PostController {
|
||||||
const filteredPosts = posts.filter((post) => {
|
const filteredPosts = posts.filter((post) => {
|
||||||
let followStatus = false;
|
let followStatus = false;
|
||||||
|
|
||||||
if(post.createdBy.followers){
|
// Get if the req user is in the follower list
|
||||||
|
if (post.createdBy.followers) {
|
||||||
followStatus = post.createdBy.followers.some(
|
followStatus = post.createdBy.followers.some(
|
||||||
(follower) => follower.id === req.user.id
|
(follower) => follower.id === req.user.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.createdBy.isPrivate && !followStatus) {
|
return !(post.createdBy.isPrivate && !followStatus);
|
||||||
return post.createdBy.followers.some(
|
|
||||||
(follower) => follower.id === req.user.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).send(filteredPosts);
|
res.status(200).send(filteredPosts);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue