Compare commits

..

No commits in common. "6fbfe81918bee023c95ba56e9162375c292adae6" and "d73bd1dd049547c56f5fe0641108580bf240c4ed" have entirely different histories.

18 changed files with 75 additions and 623 deletions

View File

@ -1,19 +0,0 @@
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,32 +13,25 @@ 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: [],
}, },
}; };
@ -53,8 +46,6 @@ 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;
@ -66,12 +57,6 @@ 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;
},
}, },
}); });
@ -108,7 +93,6 @@ 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));
@ -136,22 +120,6 @@ 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");
@ -163,8 +131,7 @@ const addJWT = async (token: string) => {
localStorage.setItem("jwt", token); localStorage.setItem("jwt", token);
}; };
export const { login, logoff, setStatus, setError, setDarkMode, setPrivate } = export const { login, logoff, setStatus, setError } = loginSlice.actions;
loginSlice.actions;
export default loginSlice.reducer; export default loginSlice.reducer;
@ -176,6 +143,3 @@ 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;

View File

@ -7,14 +7,12 @@ interface postSliceInterface {
status: Status; status: Status;
followedPosts: Post[]; followedPosts: Post[];
globalPosts: Post[]; globalPosts: Post[];
aPost: Post | null;
} }
const initialState: postSliceInterface = { const initialState: postSliceInterface = {
status: Status.idle, status: Status.idle,
followedPosts: [], followedPosts: [],
globalPosts: [], globalPosts: [],
aPost: null,
}; };
export const postSlice = createSlice({ export const postSlice = createSlice({
@ -30,9 +28,6 @@ export const postSlice = createSlice({
setGlobalPosts: (state, action) => { setGlobalPosts: (state, action) => {
state.globalPosts = action.payload; state.globalPosts = action.payload;
}, },
setAPost: (state, action) => {
state.aPost = action.payload;
},
}, },
}); });
@ -55,99 +50,26 @@ export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => {
}; };
export const likePost = export const likePost =
(postId: number, postType?: "global" | "followed"): AppThunk => (postId: number): AppThunk =>
async (dispatch: any) => { async (dispatch: any) => {
const postApi = createApi(store); const postApi = createApi(store);
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
await postApi.likePost(postId); await postApi.likePost(postId);
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
if (postType === "global") {
dispatch(fetchGlobalPosts());
} else if (postType === "followed") {
dispatch(fetchFollowedPosts());
} else {
dispatch(fetchFollowedPosts());
dispatch(fetchGlobalPosts());
}
}; };
export const unLikePost = export const unLikePost =
(postId: number, postType?: "global" | "followed"): AppThunk => (postId: number): AppThunk =>
async (dispatch: any) => { async (dispatch: any) => {
const postApi = createApi(store); const postApi = createApi(store);
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
await postApi.unlikePost(postId); await postApi.unlikePost(postId);
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
if (postType === "global") {
dispatch(fetchGlobalPosts());
} else if (postType === "followed") {
dispatch(fetchFollowedPosts());
} else {
dispatch(fetchFollowedPosts());
dispatch(fetchGlobalPosts());
}
}; };
export const fetchAPost = export const { setFollowedPosts, setGlobalPosts, setStatus } =
(postId: number): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
const response = await postApi.getPost(postId);
dispatch(setAPost(response.data));
dispatch(setStatus(Status.idle));
};
export const commentAPost =
(postId: number, comment: string): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.commentPost(postId, { content: comment });
dispatch(fetchAPost(postId));
dispatch(setStatus(Status.idle));
};
export const createPost =
(title: string, content: string): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.createPost({ title, content });
dispatch(fetchGlobalPosts());
dispatch(setStatus(Status.idle));
};
export const updatePost =
(postId: number, title: string, content: string): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.updatePost(postId, { title, content });
dispatch(fetchAPost(postId));
dispatch(fetchGlobalPosts());
dispatch(setStatus(Status.idle));
};
export const deletePost =
(postId: number): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.deletePost(postId);
dispatch(setStatus(Status.idle));
};
export const { setFollowedPosts, setGlobalPosts, setStatus, setAPost } =
postSlice.actions; postSlice.actions;
export default postSlice.reducer; export default postSlice.reducer;
@ -155,7 +77,6 @@ export default postSlice.reducer;
export const selectFollowedPosts = (state: any) => state.post.followedPosts; export const selectFollowedPosts = (state: any) => state.post.followedPosts;
export const selectAllPosts = (state: any) => state.post.globalPosts; export const selectAllPosts = (state: any) => state.post.globalPosts;
export const selectStatus = (state: any) => state.post.status; export const selectStatus = (state: any) => state.post.status;
export const selectAPost = (state: any) => state.post.aPost;
function createApi(store: Store) { function createApi(store: Store) {
return new PostsApi( return new PostsApi(

View File

@ -3,7 +3,6 @@ import { User, UserWithRelations } from "../api";
interface AppAvatarProps { interface AppAvatarProps {
user: User | UserWithRelations; user: User | UserWithRelations;
small?: boolean;
} }
export function AppAvatar(props: AppAvatarProps) { export function AppAvatar(props: AppAvatarProps) {
@ -11,10 +10,6 @@ export function AppAvatar(props: AppAvatarProps) {
<Avatar <Avatar
alt={`${props.user.firstName} ${props.user.lastName}`} alt={`${props.user.firstName} ${props.user.lastName}`}
src={`/images/${props.user.profilePictureId}`} src={`/images/${props.user.profilePictureId}`}
sx={{
width: props.small ? "24px" : "42px",
height: props.small ? "24px" : "42px",
}}
/> />
); );
} }

View File

@ -1,19 +1,14 @@
import { BottomNavigation, BottomNavigationAction, Paper } from "@mui/material"; import { BottomNavigation, BottomNavigationAction, Paper } from "@mui/material";
import { useEffect, useState } from "react"; import { useState } from "react";
import FeedIcon from "@mui/icons-material/Feed"; import FeedIcon from "@mui/icons-material/Feed";
import GlobalIcon from "@mui/icons-material/Public"; import GlobalIcon from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { useLocation, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function BottomAppBar() { export default function BottomAppBar() {
const [value, setValue] = useState(""); const [value, setValue] = useState(0);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
setValue(location.pathname.replace("/", ""));
}, [location]);
const handleClick = (to: string) => () => { const handleClick = (to: string) => () => {
navigate(to); navigate(to);
@ -32,25 +27,21 @@ export default function BottomAppBar() {
> >
<BottomNavigationAction <BottomNavigationAction
label="My Feed" label="My Feed"
value="feed"
icon={<FeedIcon />} icon={<FeedIcon />}
onClick={handleClick("feed")} onClick={handleClick("feed")}
/> />
<BottomNavigationAction <BottomNavigationAction
label="Global Feed" label="Global Feed"
value="global"
icon={<GlobalIcon />} icon={<GlobalIcon />}
onClick={handleClick("global")} onClick={handleClick("global")}
/> />
<BottomNavigationAction <BottomNavigationAction
label="New Post" label="New Post"
value="newpost"
icon={<AddIcon />} icon={<AddIcon />}
onClick={handleClick("newpost")} onClick={handleClick("newpost")}
/> />
<BottomNavigationAction <BottomNavigationAction
label="Search" label="Search"
value="search"
icon={<SearchIcon />} icon={<SearchIcon />}
onClick={handleClick("search")} onClick={handleClick("search")}
/> />

View File

@ -1,16 +1,11 @@
import NotificationsIcon from "@mui/icons-material/Notifications"; import NotificationsIcon from "@mui/icons-material/Notifications";
import { Badge, Tooltip } from "@mui/material"; import { Badge, Tooltip } from "@mui/material";
import { useNavigate } from "react-router-dom";
export default function NotificationBell() { export default function NotificationBell() {
const navigate = useNavigate();
const handleClick = () => {
navigate("/notifications");
};
return ( return (
<Badge badgeContent={0} color="secondary"> <Badge badgeContent={4} color="secondary">
<Tooltip title="Notifications"> <Tooltip title="Notifications" >
<NotificationsIcon onClick={handleClick} /> <NotificationsIcon />
</Tooltip> </Tooltip>
</Badge> </Badge>
); );

View File

@ -21,8 +21,7 @@ import { selectUserInfo } from "../app/loginSlice";
interface PostListItemProps { interface PostListItemProps {
post: Post; post: Post;
postType: "feed" | "user"; postType: "all" | "user";
feedType?: "global" | "followed";
} }
export default function PostListItem(props: PostListItemProps) { export default function PostListItem(props: PostListItemProps) {
@ -41,19 +40,19 @@ export default function PostListItem(props: PostListItemProps) {
props.post.likedBy?.some((user) => user.id === userInfo.id) || false props.post.likedBy?.some((user) => user.id === userInfo.id) || false
); );
setNumberOfLikes(props.post.likedBy?.length || 0); setNumberOfLikes(props.post.likedBy?.length || 0);
}, [props.post, userInfo.id]); }, [props.post.likedBy, userInfo.id]);
const handleLike = () => { const handleLike = () => {
if (!liked) { if (!liked) {
setNumberOfLikes(numberOfLikes + 1); setNumberOfLikes(numberOfLikes + 1);
dispatch(likePost(props.post.id as number, props.feedType)); dispatch(likePost(props.post.id as number));
} }
// If the user has already liked the post, unlike it // If the user has already liked the post, unlike it
else { else {
setNumberOfLikes(numberOfLikes - 1); setNumberOfLikes(numberOfLikes - 1);
dispatch(unLikePost(props.post.id as number, props.feedType)); dispatch(unLikePost(props.post.id as number));
} }
setLiked(!liked); setLiked((prevState) => !prevState);
}; };
const handlePostClick = () => { const handlePostClick = () => {

View File

@ -12,7 +12,6 @@ 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;
@ -20,12 +19,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);
}; };
@ -39,19 +38,9 @@ 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="fixed" position="absolute"
sx={{ zIndex: 1600, height: `${props.height}px` }} sx={{ zIndex: 1600, height: `${props.height}px` }}
> >
<Toolbar disableGutters> <Toolbar disableGutters>
@ -105,10 +94,10 @@ function TopAppBar(props: TopAppBarProps) {
open={Boolean(anchorElUser)} open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu} onClose={handleCloseUserMenu}
> >
<MenuItem key={"profile"} onClick={handleProfileClick}> <MenuItem key={"profile"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Profile"}</Typography> <Typography textAlign="center">{"Profile"}</Typography>
</MenuItem> </MenuItem>
<MenuItem key={"settings"} onClick={handleSettingsClick}> <MenuItem key={"settings"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Settings"}</Typography> <Typography textAlign="center">{"Settings"}</Typography>
</MenuItem> </MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}> <MenuItem key={"logout"} onClick={handleLogout}>

View File

@ -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 { CssBaseline } from "@mui/material"; import { createTheme, CssBaseline, ThemeProvider } 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";
@ -14,17 +14,20 @@ import { Provider } from "react-redux";
import { store } from "./app/store"; import { store } from "./app/store";
import PostList from "./routes/postList"; import PostList from "./routes/postList";
import Profile from "./routes/profile"; import Profile from "./routes/profile";
import PostView from "./routes/post"; import Post 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: "/",
@ -33,11 +36,11 @@ const router = createBrowserRouter([
children: [ children: [
{ {
path: "feed", path: "feed",
element: <PostList type="feed" feedType="followed" />, element: <PostList type="user" />,
}, },
{ {
path: "global", path: "global",
element: <PostList type="feed" feedType="global" />, element: <PostList type="all" />,
}, },
{ {
path: "me", path: "me",
@ -49,11 +52,7 @@ const router = createBrowserRouter([
}, },
{ {
path: "post/:id", path: "post/:id",
element: <PostView />, element: <Post />,
},
{
path: "post/:id/edit",
element: <NewPost />,
}, },
{ {
path: "search", path: "search",
@ -63,14 +62,6 @@ const router = createBrowserRouter([
path: "newpost", path: "newpost",
element: <NewPost />, element: <NewPost />,
}, },
{
path: "settings",
element: <Settings />,
},
{
path: "notifications",
element: <ANotification />,
},
], ],
}, },
{ {
@ -93,10 +84,10 @@ const router = createBrowserRouter([
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<AppThemeProvider> <ThemeProvider theme={defaultTheme}>
<CssBaseline /> <CssBaseline />
<RouterProvider router={router} /> <RouterProvider router={router} />
</AppThemeProvider> </ThemeProvider>
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -1,93 +1,3 @@
import { Box, Button, TextField, Typography } from "@mui/material";
import { useAppDispatch } from "../app/store";
import React, { useEffect } from "react";
import {
createPost,
selectAPost,
updatePost,
} from "../app/postSlice";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
export default function NewPost() { export default function NewPost() {
const { id } = useParams(); return <>New Post</>;
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [newPostText, setNewPostText] = React.useState("");
const [newPostTitle, setNewPostTitle] = React.useState("");
const aPost = useSelector(selectAPost);
useEffect(() => {
if (id) {
setNewPostText(aPost.content);
setNewPostTitle(aPost.title);
}
}, [aPost, id]);
const handleCreatePost = () => {
if (newPostText === "" || newPostTitle === "") return;
dispatch(createPost(newPostTitle, newPostText));
setNewPostText("");
setNewPostTitle("");
navigate("/global");
};
const handleUpdatePost = () => {
if (newPostText === "" || newPostTitle === "") return;
if (!id) return;
dispatch(updatePost(parseInt(id), newPostTitle, newPostText));
setNewPostText("");
setNewPostTitle("");
navigate("/global");
};
return (
<Box
sx={{
margin: "auto",
display: "flex",
flexDirection: "column",
}}
>
<Typography variant="h4" sx={{ marginBottom: 2 }}>
Share your thoughts!
</Typography>
<TextField
label="Title"
value={newPostTitle}
multiline
onChange={(e) => setNewPostTitle(e.target.value)}
sx={{ marginBottom: 2 }}
/>
<TextField
label="Post"
value={newPostText}
onChange={(e) => setNewPostText(e.target.value)}
multiline
rows={4}
sx={{ marginBottom: 2 }}
/>
{id ? (
<Button
variant="contained"
color="success"
fullWidth
onClick={handleUpdatePost}
>
Update
</Button>
) : (
<Button
variant="contained"
color="primary"
fullWidth
onClick={handleCreatePost}
>
Create
</Button>
)}
</Box>
);
} }

View File

@ -1,31 +1,3 @@
import { useSelector } from "react-redux"; export default function Notification() {
import { selectUserInfo } from "../app/loginSlice"; return <>Notification</>;
import { Box, List, ListItem, Paper } from "@mui/material";
export default function ANotification() {
const userInfo = useSelector(selectUserInfo);
return (
<Box sx={{ display: "flex", flexDirection: "column" }}>
<List sx={{ width: "100%" }}>
{userInfo.notifications.length === 0 ? (
<ListItem sx={{ width: "100%" }}>No notifications</ListItem>
) : (
userInfo.notifications.map((notification: any) => (
<ListItem key={notification.id} sx={{ width: "100%" }}>
<Paper
sx={{
width: "100%",
margin: "1rem",
padding: "0.2rem 1rem 0.2rem 1rem",
}}
>
{notification.message}
</Paper>
</ListItem>
))
)}
</List>
</Box>
);
} }

View File

@ -1,155 +1,3 @@
import { useSelector } from "react-redux"; export default function Post() {
import { useNavigate, useParams } from "react-router-dom"; return <>Post</>;
import {
commentAPost,
deletePost,
fetchAPost,
selectAPost,
} from "../app/postSlice";
import { useAppDispatch } from "../app/store";
import { useEffect } from "react";
import React from "react";
import { Post } from "../api";
import {
Box,
Button,
Divider,
Paper,
TextField,
Typography,
} from "@mui/material";
import { AppAvatar } from "../components/appAvatar";
import { selectUserInfo } from "../app/loginSlice";
export default function PostView() {
const postId = useParams<{ id: string }>().id;
const storePost = useSelector(selectAPost);
const selfUser = useSelector(selectUserInfo);
const [postToDisplay, setPostToDisplay] = React.useState<Post | null>(null);
const [newComentText, setNewCommentText] = React.useState("");
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
dispatch(fetchAPost(parseInt(postId!, 10)));
}, []);
useEffect(() => {
setPostToDisplay(storePost);
}, [storePost]);
const handleCommentPost = () => {
if (newComentText.length > 0) {
dispatch(commentAPost(postToDisplay!.id!, newComentText));
setNewCommentText("");
}
};
const handleCancelComment = () => {
setNewCommentText("");
};
const handleDeletePost = () => {
dispatch(deletePost(postToDisplay!.id!));
navigate("/global");
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: "60%",
margin: "auto",
minWidth: "330px",
}}
>
<Box sx={{ width: "100%" }}>
<Paper>
<Box sx={{ margin: "1rem" }}>
<Box
sx={{ display: "flex", alignItems: "center", paddingTop: "1rem" }}
>
{postToDisplay?.createdBy && (
<AppAvatar user={postToDisplay?.createdBy!} />
)}
<Typography
variant="subtitle2"
sx={{ mb: "0.2rem", ml: "0.3rem" }}
>
{postToDisplay?.createdBy?.firstName}{" "}
{postToDisplay?.createdBy?.lastName}
</Typography>
</Box>
<Divider sx={{ mb: "0.3rem" }} />
<Typography variant="h4">{postToDisplay?.title}</Typography>
<Typography variant="body1">{postToDisplay?.content}</Typography>
</Box>
</Paper>
</Box>
<Box sx={{ width: "100%" }}>
{postToDisplay?.createdBy?.id === selfUser?.id && (
<Box>
<Button onClick={() => navigate("edit")}>Edit</Button>
<Button color="warning" onClick={handleDeletePost}>
Delete
</Button>
</Box>
)}
<TextField
label="Add a comment..."
fullWidth
multiline
value={newComentText}
onChange={(e) => setNewCommentText(e.target.value)}
InputProps={{
endAdornment: (
<>
<Button
variant="contained"
onClick={handleCommentPost}
sx={{
display: `${newComentText.length > 0 ? "inherit" : "none"}`,
}}
>
Accept
</Button>
<Button
variant="outlined"
onClick={handleCancelComment}
sx={{
display: `${newComentText.length > 0 ? "inherit" : "none"}`,
ml: "0.5rem",
}}
>
Cancel
</Button>
</>
),
}}
sx={{
mb: "1rem",
}}
/>
<Box>
{postToDisplay?.comments?.map((comment) => (
<Paper>
<Box sx={{ margin: "1rem" }}>
<Box sx={{ display: "flex", mb: "0.3rem" }}>
<AppAvatar user={comment.createdBy!} small />
<Typography variant="subtitle2" sx={{ ml: "0.5rem" }}>
{comment.createdBy?.firstName} {comment.createdBy?.lastName}
</Typography>
</Box>
<Divider sx={{ mb: "0.3rem" }} />
<Typography variant="body1">{comment.content}</Typography>
</Box>
</Paper>
))}
</Box>
</Box>
</Box>
);
} }

View File

@ -14,8 +14,7 @@ import PostListItem from "../components/postListItem";
import { Status } from "../util/types"; import { Status } from "../util/types";
interface PostListProps { interface PostListProps {
type: "feed" | "user"; type: "all" | "user";
feedType?: "global" | "followed";
} }
export default function PostList(props: PostListProps) { export default function PostList(props: PostListProps) {
@ -26,33 +25,13 @@ export default function PostList(props: PostListProps) {
const [posts, setPosts] = useState<Post[]>([]); const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => { useEffect(() => {
if (props.feedType === "global") {
dispatch(fetchGlobalPosts()); dispatch(fetchGlobalPosts());
} else {
dispatch(fetchFollowedPosts()); dispatch(fetchFollowedPosts());
} }, []);
}, [props.feedType]);
useEffect(() => { useEffect(() => {
let postsToDisplay: Post[]; setPosts(props.type === "all" ? globalPosts : followedPosts);
if (props.feedType === "global") { }, [followedPosts, globalPosts, props.type]);
postsToDisplay = globalPosts;
} else {
postsToDisplay = followedPosts;
}
let sortedPosts: Post[] = [];
if (postsToDisplay.length > 0) {
sortedPosts = [...postsToDisplay].sort((a: Post, b: Post) => {
return b.createdAt! > a.createdAt!
? 1
: b.createdAt! < a.createdAt!
? -1
: 0;
});
}
setPosts(sortedPosts);
}, [followedPosts, globalPosts, props.feedType]);
return ( return (
<Box <Box
@ -67,27 +46,17 @@ export default function PostList(props: PostListProps) {
<CircularProgress /> <CircularProgress />
) : ( ) : (
<> <>
{props.feedType === "global" && ( {props.type === "all" && (
<List> <List>
{posts.map((post: Post) => ( {posts.map((post: Post) => (
<PostListItem <PostListItem post={post} postType="all" key={post.id} />
post={post}
postType="feed"
feedType="global"
key={post.id}
/>
))} ))}
</List> </List>
)} )}
{props.feedType === "followed" && ( {props.type === "user" && (
<List> <List>
{followedPosts.map((post: Post) => ( {followedPosts.map((post: Post) => (
<PostListItem <PostListItem post={post} postType="user" key={post.id} />
post={post}
postType="feed"
feedType="followed"
key={post.id}
/>
))} ))}
{followedPosts.length === 0 && ( {followedPosts.length === 0 && (
<Box <Box

View File

@ -62,7 +62,7 @@ export default function Search() {
<Paper> <Paper>
<List> <List>
{displayUserList.map((user) => ( {displayUserList.map((user) => (
<Box key={user.id}> <>
<ListItem key={user.id}> <ListItem key={user.id}>
<Button <Button
fullWidth fullWidth
@ -78,7 +78,7 @@ export default function Search() {
</Button> </Button>
</ListItem> </ListItem>
<Divider sx={{ mb: "0.3rem" }} /> <Divider sx={{ mb: "0.3rem" }} />
</Box> </>
))} ))}
</List> </List>
</Paper> </Paper>

View File

@ -1,56 +0,0 @@
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,9 +213,8 @@ export class UserController {
notifications: true, notifications: true,
}, },
}); });
// Strict check, as we are dealing with a boolean value in this field
user.isPrivate = user.isPrivate = req.body.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);

View File

@ -53,16 +53,18 @@ export class PostController {
const filteredPosts = posts.filter((post) => { const filteredPosts = posts.filter((post) => {
let followStatus = false; let followStatus = false;
// Get if the req user is in the follower list if(post.createdBy.followers){
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
); );
} }
return !(post.createdBy.isPrivate && !followStatus); if (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);
@ -139,32 +141,20 @@ export class PostController {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { id: req.user.id }, where: { id: req.user.id },
relations: { relations: {
followed: { posts: { likedBy: true, comments: true } }, followed: true,
posts: { posts: { likedBy: true, comments: true },
likedBy: true,
comments: true,
createdBy: true,
},
}, },
}); });
// Get all posts from the followed users
const followedPosts = user.followed const followedPosts = user.followed
.map((followedUser) => { .map((followedUser) => followedUser.posts)
followedUser.deleteSensitiveFields();
if (followedUser.posts) {
return followedUser.posts.map((post) => {
if (post.likedBy.length > 0) {
post.likedBy.forEach((user) => user.deleteSensitiveFields());
}
return {
...post,
createdBy: followedUser,
};
});
}
})
.flat(); .flat();
// Remove sensitive fields
followedPosts.forEach((post) => {
post.deleteSensitiveFields();
});
res.send(followedPosts); res.send(followedPosts);
}); });

View File

@ -28,17 +28,11 @@ export class Post {
public deleteSensitiveFields(){ public deleteSensitiveFields(){
this.createdBy.deleteSensitiveFields() this.createdBy.deleteSensitiveFields()
if (this.likedBy) { if(this.likedBy.length > 0){
if (this.likedBy.length > 0) { this.likedBy.forEach(user => user.deleteSensitiveFields())
this.likedBy.forEach((user) => user.deleteSensitiveFields());
}
}
if (this.comments) {
if (this.comments.length > 0) {
this.comments.forEach((comment) =>
comment.createdBy.deleteSensitiveFields()
);
} }
if(this.comments.length > 0){
this.comments.forEach(comment => comment.createdBy.deleteSensitiveFields())
} }
} }