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 { Configuration, Post, PostsApi } from "../api";
import { Status } from "../util/types";
import { store } from "./store";
import { AppThunk, store } from "./store";
interface postSlice {
interface postSliceInterface {
status: Status;
followedPosts: Post[];
globalPosts: Post[];
}
const initialState: postSlice = {
const initialState: postSliceInterface = {
status: Status.idle,
followedPosts: [],
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);
dispatch(setStatus(Status.loading));
@ -40,7 +40,7 @@ export const fetchFollowedPosts = () => async (dispatch: any) => {
dispatch(setStatus(Status.idle));
};
export const fetchGlobalPosts = () => async (dispatch: any) => {
export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
@ -49,6 +49,26 @@ export const fetchGlobalPosts = () => async (dispatch: any) => {
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 } =
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 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 {
post: Post;
postType: "all" | "user";
}
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 (
<ListItem>
<ListItemText
primary={props.post.title}
secondary={`${props.post.createdBy?.firstName} ${props.post.createdBy?.lastName}`}
/>
<ListItem
sx={{
display: "flex",
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>
);
}
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"];
function TopAppBar() {
interface TopAppBarProps {
height: number;
}
function TopAppBar(props: TopAppBarProps) {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const userInfo = useSelector(selectUserInfo);
@ -25,7 +29,6 @@ function TopAppBar() {
null
);
console.log(userInfo);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
@ -41,15 +44,14 @@ function TopAppBar() {
};
return (
<AppBar position="static">
<Container maxWidth="xl" sx={{ zIndex: 2500 }}>
<AppBar position="absolute" sx={{zIndex:1600, height: `${props.height}px` }}>
<Toolbar disableGutters>
<Typography
variant="h6"
noWrap
component="a"
href="#app-bar-with-responsive-menu"
sx={{
marginLeft: "1rem",
mr: 2,
display: "flex",
fontFamily: "monospace",
@ -72,7 +74,7 @@ function TopAppBar() {
<NotificationBell />
</Box>
<Box sx={{ flexGrow: 0 }}>
<Box sx={{ flexGrow: 0, mr:"2rem" }}>
<Tooltip title="Open settings" >
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar
@ -109,7 +111,6 @@ function TopAppBar() {
</Menu>
</Box>
</Toolbar>
</Container>
</AppBar>
);
}

View File

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

View File

@ -7,7 +7,7 @@ import {
} from "../app/postSlice";
import { store, useAppDispatch } from "../app/store";
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 PostListItem from "../components/postListItem";
@ -25,17 +25,49 @@ export default function PostList(props: PostListProps) {
if (props.type === "user") dispatch(fetchFollowedPosts());
}, []);
console.log(globalPosts);
return (
<>
<Box
sx={{
width: "100%",
alignContent: "center",
justifyContent: "center",
display: "flex",
}}
>
{props.type === "all" && (
<List>
{globalPosts.map((post: Post) => (
<PostListItem post={post} key={post.id} />
<PostListItem post={post} postType="all" key={post.id} />
))}
</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";
const drawerWidth = 240;
const topAppBarHeight = 64;
export default function Root() {
const navigate = useNavigate();
@ -30,7 +31,7 @@ export default function Root() {
return (
<>
<Box>
<TopAppBar />
<TopAppBar height={topAppBarHeight} />
</Box>
<Box
sx={{
@ -51,6 +52,7 @@ export default function Root() {
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${width > 600 ? drawerWidth : 0}px)` },
marginTop: { xs: `${topAppBarHeight}px`},
}}
>
<Outlet />