Implemented post list view

Signed-off-by: Pau Costa <mico@micodev.es>
pull/2/head
Pau Costa Ferrer 2024-02-10 14:39:57 +01:00
parent 0cdcc915f4
commit 4fdb17733d
7 changed files with 201 additions and 29 deletions

View File

@ -1,15 +1,15 @@
import { Store, createSlice } from "@reduxjs/toolkit"; import { Store, createSlice } from "@reduxjs/toolkit";
import { Configuration, Post, PostsApi } from "../api"; import { Configuration, Post, PostsApi } from "../api";
import { Status } from "../util/types"; import { Status } from "../util/types";
import { store } from "./store"; import { AppThunk, store } from "./store";
interface postSlice { interface postSliceInterface {
status: Status; status: Status;
followedPosts: Post[]; followedPosts: Post[];
globalPosts: Post[]; globalPosts: Post[];
} }
const initialState: postSlice = { const initialState: postSliceInterface = {
status: Status.idle, status: Status.idle,
followedPosts: [], followedPosts: [],
globalPosts: [], globalPosts: [],
@ -31,7 +31,7 @@ export const postSlice = createSlice({
}, },
}); });
export const fetchFollowedPosts = () => async (dispatch: any) => { export const fetchFollowedPosts = (): AppThunk => async (dispatch: any) => {
const postApi = createApi(store); const postApi = createApi(store);
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
@ -40,7 +40,7 @@ export const fetchFollowedPosts = () => async (dispatch: any) => {
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
}; };
export const fetchGlobalPosts = () => async (dispatch: any) => { export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => {
const postApi = createApi(store); const postApi = createApi(store);
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
@ -49,6 +49,26 @@ export const fetchGlobalPosts = () => async (dispatch: any) => {
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
}; };
export const likePost =
(postId: number): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.likePost(postId);
dispatch(setStatus(Status.idle));
};
export const unLikePost =
(postId: number): AppThunk =>
async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
await postApi.unlikePost(postId);
dispatch(setStatus(Status.idle));
};
export const { setFollowedPosts, setGlobalPosts, setStatus } = export const { setFollowedPosts, setGlobalPosts, setStatus } =
postSlice.actions; postSlice.actions;

View File

@ -1,19 +1,136 @@
import { ListItem, ListItemText } from "@mui/material"; import {
Box,
Button,
Card,
CardActionArea,
CardContent,
Grid,
ListItem,
styled,
Typography,
} from "@mui/material";
import { Post } from "../api"; import { Post } from "../api";
import MessageIcon from "@mui/icons-material/Message";
import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt";
import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt";
import { useNavigate } from "react-router-dom";
import { likePost, unLikePost } from "../app/postSlice";
import { useAppDispatch } from "../app/store";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { selectUserInfo } from "../app/loginSlice";
interface PostListItemProps { interface PostListItemProps {
post: Post; post: Post;
postType: "all" | "user";
} }
export default function PostListItem(props: PostListItemProps) { export default function PostListItem(props: PostListItemProps) {
console.log(props.post); const dispatch = useAppDispatch();
const navigate = useNavigate();
const userInfo = useSelector(selectUserInfo);
const [liked, setLiked] = useState(false);
const [numberOfLikes, setNumberOfLikes] = useState(0);
// On component mount, set the number of likes
// and whether the user has liked the post
useEffect(() => {
setNumberOfLikes(props.post.likedBy?.length || 0);
setLiked(props.post.likedBy?.includes(userInfo) || false);
}, []);
const handleLike = () => {
if (!liked) {
// Instant feedback to the user
setLiked(true);
setNumberOfLikes(numberOfLikes + 1);
// Dispatch the call to the API
dispatch(likePost(props.post.id as number));
}
// If the user has already liked the post, unlike it
else {
setLiked(false);
setNumberOfLikes(numberOfLikes - 1);
dispatch(unLikePost(props.post.id as number));
}
};
const handlePostClick = () => {
navigate(`/post/${props.post.id}`);
};
const handleUserClick = () => {
navigate(`/user/${props.post.createdBy?.id}`);
};
return ( return (
<ListItem> <ListItem
<ListItemText sx={{
primary={props.post.title} display: "flex",
secondary={`${props.post.createdBy?.firstName} ${props.post.createdBy?.lastName}`} flexDirection: "column",
/> mt: "1rem",
justifyContent: "start",
alignItems: "start",
}}
>
<Button onClick={handleUserClick} sx={{ textTransform: "none" }}>
<Typography
gutterBottom
sx={{ textAlign: "left", width: "100%" }}
variant="body2"
component="div"
color="text.secondary"
>
{props.post.createdBy?.firstName} {props.post.createdBy?.lastName}
</Typography>
</Button>
<Card sx={{ maxWidth: 400, width: "100%" }}>
<CardActionArea sx={{ mb: "0.8rem" }} onClick={handlePostClick}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{props.post.title}
</Typography>
<Typography variant="h6" color="text.secondary">
{props.post.content}
</Typography>
</CardContent>
</CardActionArea>
<Grid
container
spacing={1}
sx={{ display: "flex", justifyContent: "end" }}
>
<StyledGrid item>
<Button onClick={handleLike}>
<StyledTypography variant="body2" color="text.secondary">
{numberOfLikes}
</StyledTypography>
{liked ? <ThumbUpAltIcon /> : <ThumbUpOffAltIcon />}
</Button>
</StyledGrid>
<StyledGrid item>
<Button onClick={handlePostClick}>
<StyledTypography variant="body2" color="text.secondary">
{props.post.comments?.length || 0}
</StyledTypography>
<MessageIcon />
</Button>
</StyledGrid>
</Grid>
</Card>
</ListItem> </ListItem>
); );
} }
const StyledGrid = styled(Grid)({
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "auto",
marginBottom: "0.8rem",
marginRight: "0.8rem",
});
const StyledTypography = styled(Typography)({
padding: "0 0.5rem",
});

View File

@ -17,7 +17,11 @@ import { useNavigate } from "react-router-dom";
const settings = ["Profile", "Account", "Dashboard", "Logout"]; const settings = ["Profile", "Account", "Dashboard", "Logout"];
function TopAppBar() { interface TopAppBarProps {
height: number;
}
function TopAppBar(props: TopAppBarProps) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const userInfo = useSelector(selectUserInfo); const userInfo = useSelector(selectUserInfo);
@ -25,7 +29,6 @@ function TopAppBar() {
null null
); );
console.log(userInfo);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => { const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget); setAnchorElUser(event.currentTarget);
@ -41,15 +44,14 @@ function TopAppBar() {
}; };
return ( return (
<AppBar position="static"> <AppBar position="absolute" sx={{zIndex:1600, height: `${props.height}px` }}>
<Container maxWidth="xl" sx={{ zIndex: 2500 }}>
<Toolbar disableGutters> <Toolbar disableGutters>
<Typography <Typography
variant="h6" variant="h6"
noWrap noWrap
component="a" component="a"
href="#app-bar-with-responsive-menu"
sx={{ sx={{
marginLeft: "1rem",
mr: 2, mr: 2,
display: "flex", display: "flex",
fontFamily: "monospace", fontFamily: "monospace",
@ -72,7 +74,7 @@ function TopAppBar() {
<NotificationBell /> <NotificationBell />
</Box> </Box>
<Box sx={{ flexGrow: 0 }}> <Box sx={{ flexGrow: 0, mr:"2rem" }}>
<Tooltip title="Open settings" > <Tooltip title="Open settings" >
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar <Avatar
@ -109,7 +111,6 @@ function TopAppBar() {
</Menu> </Menu>
</Box> </Box>
</Toolbar> </Toolbar>
</Container>
</AppBar> </AppBar>
); );
} }

View File

@ -12,7 +12,7 @@ export default function AuthRoot() {
useEffect(() => { useEffect(() => {
if (loggedIn) { if (loggedIn) {
return navigate("/"); return navigate("/feed");
} }
}); });

View File

@ -7,7 +7,7 @@ import {
} from "../app/postSlice"; } from "../app/postSlice";
import { store, useAppDispatch } from "../app/store"; import { store, useAppDispatch } from "../app/store";
import { useEffect } from "react"; import { useEffect } from "react";
import { List, ListItem, ListItemText } from "@mui/material"; import { Box, List, ListItem, ListItemText, Typography } from "@mui/material";
import { Post } from "../api"; import { Post } from "../api";
import PostListItem from "../components/postListItem"; import PostListItem from "../components/postListItem";
@ -25,17 +25,49 @@ export default function PostList(props: PostListProps) {
if (props.type === "user") dispatch(fetchFollowedPosts()); if (props.type === "user") dispatch(fetchFollowedPosts());
}, []); }, []);
console.log(globalPosts);
return ( return (
<> <Box
sx={{
width: "100%",
alignContent: "center",
justifyContent: "center",
display: "flex",
}}
>
{props.type === "all" && ( {props.type === "all" && (
<List> <List>
{globalPosts.map((post: Post) => ( {globalPosts.map((post: Post) => (
<PostListItem post={post} key={post.id} /> <PostListItem post={post} postType="all" key={post.id} />
))} ))}
</List> </List>
)} )}
{props.type === "user" && <List></List>} {props.type === "user" && (
</> <List>
{followedPosts.map((post: Post) => (
<PostListItem post={post} postType="user" key={post.id} />
))}
{followedPosts.length === 0 && (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography
variant="h3"
sx={{ color: "text.secondary", mb: "2rem" }}
>
Whops!
</Typography>
<Typography textAlign="center" sx={{ color: "text.secondary" }}>
There is no content here! Try following some users, or check the
global feed
</Typography>
</Box>
)}
</List>
)}
</Box>
); );
} }

View File

@ -10,6 +10,7 @@ import BottomAppBar from "../components/bottomAppBar";
import TopAppBar from "../components/topAppBar"; import TopAppBar from "../components/topAppBar";
const drawerWidth = 240; const drawerWidth = 240;
const topAppBarHeight = 64;
export default function Root() { export default function Root() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -30,7 +31,7 @@ export default function Root() {
return ( return (
<> <>
<Box> <Box>
<TopAppBar /> <TopAppBar height={topAppBarHeight} />
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -51,6 +52,7 @@ export default function Root() {
flexGrow: 1, flexGrow: 1,
p: 3, p: 3,
width: { sm: `calc(100% - ${width > 600 ? drawerWidth : 0}px)` }, width: { sm: `calc(100% - ${width > 600 ? drawerWidth : 0}px)` },
marginTop: { xs: `${topAppBarHeight}px`},
}} }}
> >
<Outlet /> <Outlet />