User profile view

Signed-off-by: Pau Costa <mico@micodev.es>
main
Pau Costa Ferrer 2024-02-11 13:37:51 +01:00
parent e6f73932fa
commit d73bd1dd04
13 changed files with 1523 additions and 1297 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1406,40 +1406,6 @@ export class PostsApi extends BaseAPI {
*/ */
export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { export const UsersApiAxiosParamCreator = function (configuration?: Configuration) {
return { return {
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
usersGet: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/users`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @summary Unfollow a user * @summary Unfollow a user
@ -1447,10 +1413,10 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
usersIdFollowDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { usersFollowIdDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined // verify required parameter 'id' is not null or undefined
assertParamExists('usersIdFollowDelete', 'id', id) assertParamExists('usersFollowIdDelete', 'id', id)
const localVarPath = `/users/{id}/follow` const localVarPath = `/users/follow/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id))); .replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -1485,10 +1451,10 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
usersIdFollowPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { usersFollowIdPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined // verify required parameter 'id' is not null or undefined
assertParamExists('usersIdFollowPost', 'id', id) assertParamExists('usersFollowIdPost', 'id', id)
const localVarPath = `/users/{id}/follow` const localVarPath = `/users/follow/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id))); .replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -1507,6 +1473,40 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
usersGet: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/users`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -1638,18 +1638,6 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration
export const UsersApiFp = function(configuration?: Configuration) { export const UsersApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration)
return { return {
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<User>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/** /**
* *
* @summary Unfollow a user * @summary Unfollow a user
@ -1657,10 +1645,10 @@ export const UsersApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> { async usersFollowIdDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowDelete(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.usersFollowIdDelete(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[localVarOperationServerIndex]?.url; const localVarOperationServerBasePath = operationServerMap['UsersApi.usersFollowIdDelete']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
}, },
/** /**
@ -1670,10 +1658,22 @@ export const UsersApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async usersIdFollowPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> { async usersFollowIdPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowPost(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.usersFollowIdPost(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[localVarOperationServerIndex]?.url; const localVarOperationServerBasePath = operationServerMap['UsersApi.usersFollowIdPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<User>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
}, },
/** /**
@ -1724,15 +1724,6 @@ export const UsersApiFp = function(configuration?: Configuration) {
export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = UsersApiFp(configuration) const localVarFp = UsersApiFp(configuration)
return { return {
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
usersGet(options?: any): AxiosPromise<Array<User>> {
return localVarFp.usersGet(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary Unfollow a user * @summary Unfollow a user
@ -1740,8 +1731,8 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
usersIdFollowDelete(id: number, options?: any): AxiosPromise<UserWithRelations> { usersFollowIdDelete(id: number, options?: any): AxiosPromise<UserWithRelations> {
return localVarFp.usersIdFollowDelete(id, options).then((request) => request(axios, basePath)); return localVarFp.usersFollowIdDelete(id, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -1750,8 +1741,17 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
usersIdFollowPost(id: number, options?: any): AxiosPromise<UserWithRelations> { usersFollowIdPost(id: number, options?: any): AxiosPromise<UserWithRelations> {
return localVarFp.usersIdFollowPost(id, options).then((request) => request(axios, basePath)); return localVarFp.usersFollowIdPost(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
usersGet(options?: any): AxiosPromise<Array<User>> {
return localVarFp.usersGet(options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -1792,17 +1792,6 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class UsersApi extends BaseAPI { export class UsersApi extends BaseAPI {
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public usersGet(options?: RawAxiosRequestConfig) {
return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary Unfollow a user * @summary Unfollow a user
@ -1811,8 +1800,8 @@ export class UsersApi extends BaseAPI {
* @throws {RequiredError} * @throws {RequiredError}
* @memberof UsersApi * @memberof UsersApi
*/ */
public usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig) { public usersFollowIdDelete(id: number, options?: RawAxiosRequestConfig) {
return UsersApiFp(this.configuration).usersIdFollowDelete(id, options).then((request) => request(this.axios, this.basePath)); return UsersApiFp(this.configuration).usersFollowIdDelete(id, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@ -1823,8 +1812,19 @@ export class UsersApi extends BaseAPI {
* @throws {RequiredError} * @throws {RequiredError}
* @memberof UsersApi * @memberof UsersApi
*/ */
public usersIdFollowPost(id: number, options?: RawAxiosRequestConfig) { public usersFollowIdPost(id: number, options?: RawAxiosRequestConfig) {
return UsersApiFp(this.configuration).usersIdFollowPost(id, options).then((request) => request(this.axios, this.basePath)); return UsersApiFp(this.configuration).usersFollowIdPost(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get all users
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UsersApi
*/
public usersGet(options?: RawAxiosRequestConfig) {
return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath));
} }
/** /**

View File

@ -17,6 +17,8 @@ interface loginState {
firstName: string; firstName: string;
lastName: string; lastName: string;
jwt: string; jwt: string;
id: number;
profilePictureId: string;
}; };
} }
@ -28,6 +30,8 @@ const initialState: loginState = {
firstName: "", firstName: "",
lastName: "", lastName: "",
jwt: "", jwt: "",
id: -1,
profilePictureId: "",
}, },
}; };
@ -37,9 +41,11 @@ export const loginSlice = createSlice({
reducers: { reducers: {
login: (state, action) => { login: (state, action) => {
state.loggedIn = true; state.loggedIn = true;
state.userInfo.id = action.payload.id;
state.userInfo.jwt = action.payload.jwt; state.userInfo.jwt = action.payload.jwt;
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;
}, },
logoff: (state) => { logoff: (state) => {
state.loggedIn = false; state.loggedIn = false;
@ -85,6 +91,8 @@ export const postLogin =
jwt: response.data.token, jwt: response.data.token,
firstName: userResponse.data.firstName, firstName: userResponse.data.firstName,
lastName: userResponse.data.lastName, lastName: userResponse.data.lastName,
id: userResponse.data.id,
profilePictureId: userResponse.data.profilePictureId,
}) })
); );
dispatch(setStatus(Status.succeeded)); dispatch(setStatus(Status.succeeded));

View File

@ -10,6 +10,7 @@ export const store = configureStore({
post: postReducer, post: postReducer,
users: usersReducer, users: usersReducer,
}, },
devTools: true,
}); });
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;

View File

@ -58,7 +58,7 @@ export const getUserWithRelations =
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
try { try {
const response = await api.usersIdGet(userId); const response = await api.usersIdGet(userId);
dispatch(setUserWithRelations(response)); dispatch(setUserWithRelations(response.data));
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
} catch (error) { } catch (error) {
dispatch(setError((error as Error).message)); dispatch(setError((error as Error).message));
@ -73,10 +73,12 @@ export const followUser =
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
try { try {
await api.usersIdFollowPost(userId); console.log("Trying to follow: ", userId);
dispatch(getUsers()); await api.usersFollowIdPost(userId);
} catch (error) { } catch (error) {
dispatch(setError((error as Error).message)); dispatch(setError((error as Error).message));
} finally {
dispatch(getUserWithRelations(userId));
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
} }
}; };
@ -88,10 +90,12 @@ export const unFollowUser =
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
try { try {
await api.usersIdFollowDelete(userId); console.log("Trying to unfollow: ", userId);
dispatch(getUsers()); await api.usersFollowIdDelete(userId);
} catch (error) { } catch (error) {
dispatch(setError((error as Error).message)); dispatch(setError((error as Error).message));
} finally {
dispatch(getUserWithRelations(userId));
dispatch(setStatus(Status.idle)); dispatch(setStatus(Status.idle));
} }
}; };

View File

@ -28,31 +28,31 @@ export default function PostListItem(props: PostListItemProps) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const userInfo = useSelector(selectUserInfo); const userInfo = useSelector(selectUserInfo);
const [liked, setLiked] = useState(false); const [liked, setLiked] = useState(
const [numberOfLikes, setNumberOfLikes] = useState(0); props.post.likedBy?.some((user) => user.id === userInfo.id) || false
);
const [numberOfLikes, setNumberOfLikes] = useState(
props.post.likedBy?.length || 0
);
// On component mount, set the number of likes
// and whether the user has liked the post
useEffect(() => { useEffect(() => {
setLiked(
props.post.likedBy?.some((user) => user.id === userInfo.id) || false
);
setNumberOfLikes(props.post.likedBy?.length || 0); setNumberOfLikes(props.post.likedBy?.length || 0);
setLiked(props.post.likedBy?.includes(userInfo) || false); }, [props.post.likedBy, userInfo.id]);
}, []);
const handleLike = () => { const handleLike = () => {
if (!liked) { if (!liked) {
// Instant feedback to the user
setLiked(true);
setNumberOfLikes(numberOfLikes + 1); setNumberOfLikes(numberOfLikes + 1);
// Dispatch the call to the API
dispatch(likePost(props.post.id as number)); 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 {
setLiked(false);
setNumberOfLikes(numberOfLikes - 1); setNumberOfLikes(numberOfLikes - 1);
dispatch(unLikePost(props.post.id as number)); dispatch(unLikePost(props.post.id as number));
} }
setLiked((prevState) => !prevState);
}; };
const handlePostClick = () => { const handlePostClick = () => {
@ -72,7 +72,13 @@ export default function PostListItem(props: PostListItemProps) {
alignItems: "start", alignItems: "start",
}} }}
> >
<Button onClick={handleUserClick} sx={{ textTransform: "none" }}> <Button
onClick={handleUserClick}
sx={{
textTransform: "none",
display: `${props.postType === "user" ? "none" : "block"}`,
}}
>
<Typography <Typography
gutterBottom gutterBottom
sx={{ textAlign: "left", width: "100%" }} sx={{ textAlign: "left", width: "100%" }}

View File

@ -24,7 +24,7 @@ const root = ReactDOM.createRoot(
const defaultTheme = createTheme({ const defaultTheme = createTheme({
palette: { palette: {
mode: "dark", mode: "light",
}, },
}); });

View File

@ -4,12 +4,14 @@ import {
fetchGlobalPosts, fetchGlobalPosts,
selectAllPosts, selectAllPosts,
selectFollowedPosts, selectFollowedPosts,
selectStatus,
} from "../app/postSlice"; } from "../app/postSlice";
import { useAppDispatch } from "../app/store"; import { store, useAppDispatch } from "../app/store";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { Box, List, Typography } from "@mui/material"; import { Box, CircularProgress, List, Typography } from "@mui/material";
import { Post } from "../api"; import { Post } from "../api";
import PostListItem from "../components/postListItem"; import PostListItem from "../components/postListItem";
import { Status } from "../util/types";
interface PostListProps { interface PostListProps {
type: "all" | "user"; type: "all" | "user";
@ -19,12 +21,18 @@ export default function PostList(props: PostListProps) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const followedPosts = useSelector(selectFollowedPosts); const followedPosts = useSelector(selectFollowedPosts);
const globalPosts = useSelector(selectAllPosts); const globalPosts = useSelector(selectAllPosts);
const status = useSelector(selectStatus);
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => { useEffect(() => {
if (props.type === "all") dispatch(fetchGlobalPosts()); dispatch(fetchGlobalPosts());
if (props.type === "user") dispatch(fetchFollowedPosts()); dispatch(fetchFollowedPosts());
}, []); }, []);
useEffect(() => {
setPosts(props.type === "all" ? globalPosts : followedPosts);
}, [followedPosts, globalPosts, props.type]);
return ( return (
<Box <Box
sx={{ sx={{
@ -34,39 +42,48 @@ export default function PostList(props: PostListProps) {
display: "flex", display: "flex",
}} }}
> >
{props.type === "all" && ( {status === Status.loading ? (
<List> <CircularProgress />
{globalPosts.map((post: Post) => ( ) : (
<PostListItem post={post} postType="all" key={post.id} /> <>
))} {props.type === "all" && (
</List> <List>
)} {posts.map((post: Post) => (
{props.type === "user" && ( <PostListItem post={post} postType="all" key={post.id} />
<List> ))}
{followedPosts.map((post: Post) => ( </List>
<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> {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> </Box>
); );

View File

@ -1,12 +1,155 @@
import {
Box,
Button,
CircularProgress,
List,
Paper,
Typography,
} from "@mui/material";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { AppAvatar } from "../components/appAvatar";
import { useAppDispatch } from "../app/store";
import { useSelector } from "react-redux";
import {
followUser,
getUserWithRelations,
selectAUser,
selectStatus,
unFollowUser,
} from "../app/usersSlice";
import { useEffect } from "react";
import { selectUserInfo } from "../app/loginSlice";
import PostListItem from "../components/postListItem";
import { Post, UserWithRelations } from "../api";
import React from "react";
import { Status } from "../util/types";
export interface ProfileProps { export interface ProfileProps {
selfProfile?: boolean; selfProfile?: boolean;
} }
export default function Profile(props: ProfileProps) { export default function Profile(props: ProfileProps) {
const { selfProfile } = props; const userId = useParams<{ id: string }>().id;
const userId = useParams<{ userId: string }>().userId; const [userToDisplay, setUserToDisplay] =
React.useState<UserWithRelations | null>();
const aUser = useSelector(selectAUser);
const [isFollowed, setIsFollowed] = React.useState(false);
const status = useSelector(selectStatus);
const selfUser = useSelector(selectUserInfo);
const dispatch = useAppDispatch();
return <>Profile</>; useEffect(() => {
if (!props.selfProfile && userId) {
dispatch(getUserWithRelations(parseInt(userId, 10)));
} else {
dispatch(getUserWithRelations(selfUser.id));
}
}, []);
useEffect(() => {
setUserToDisplay(aUser);
if (!props.selfProfile) {
setIsFollowed(
aUser?.followers?.some((user) => user.id === selfUser.id) || false
);
}
}, [aUser, props.selfProfile, selfUser.id]);
const handleFollow = () => {
if (isFollowed) {
dispatch(unFollowUser(userToDisplay!.id!));
} else {
dispatch(followUser(userToDisplay!.id!));
}
setIsFollowed((prevState) => !prevState);
};
return (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
{status === Status.loading && (
<>
<Typography variant="h3">Loading...</Typography>
<CircularProgress />
</>
)}
{userToDisplay && status === Status.idle && (
<>
<Paper
sx={{
width: "100%",
}}
>
<Box>
<Box
sx={{
display: "flex",
padding: "1rem 1rem",
}}
>
<AppAvatar user={userToDisplay!} />
<Typography variant="h4" sx={{ ml: "0.8rem" }}>
{userToDisplay?.firstName} {userToDisplay?.lastName}
</Typography>
</Box>
<Box sx={{ display: "flex", padding: "1rem 1rem" }}>
<Box sx={{ display: "flex" }}>
<Typography
variant="h6"
sx={{ ml: "0.8rem", mr: "0.5rem" }}
>
Following:
</Typography>
<Typography variant="h6">
{userToDisplay?.followed?.length || 0}
</Typography>
</Box>
<Box sx={{ display: "flex" }}>
<Typography
variant="h6"
sx={{ ml: "0.8rem", mr: "0.5rem" }}
>
Followers:{" "}
</Typography>
<Typography variant="h6">
{userToDisplay?.followers?.length || 0}
</Typography>
</Box>
<Box
sx={{
display: `${props.selfProfile ? "none" : "initial"}`,
ml: "0.8rem",
}}
>
<Button
variant={isFollowed ? "outlined" : "contained"}
onClick={handleFollow}
>
{isFollowed ? "Unfollow" : "Follow"}
</Button>
</Box>
</Box>
</Box>
</Paper>
<List sx={{ width: "75%" }}>
{userToDisplay!.posts?.map((post: Post) => (
<PostListItem post={post} postType="user" key={post.id} />
)) || (
<Typography variant="h3">
This user has no posts yet!
</Typography>
)}
</List>
</>
)}
</Box>
</>
);
} }

View File

@ -22,10 +22,17 @@ export default function Search() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
// Dispatch the getUsers actions, and set it as the displayUserList
// once on the first render
useEffect(() => { useEffect(() => {
dispatch(getUsers()); dispatch(getUsers());
}, []); }, []);
useEffect(() => {
setDisplayUserList(userList);
}, [userList]);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = event.currentTarget.value; const searchValue = event.currentTarget.value;
if (searchValue === "") { if (searchValue === "") {
@ -39,6 +46,7 @@ export default function Search() {
const handleClick: MouseEventHandler = (event) => { const handleClick: MouseEventHandler = (event) => {
const userId = event.currentTarget.id; const userId = event.currentTarget.id;
console.log("User Id from the search component: ", userId);
navigate(`/user/${userId}`); navigate(`/user/${userId}`);
}; };

View File

@ -81,7 +81,7 @@ export class UserController {
relations: { relations: {
followed: true, followed: true,
followers: true, followers: true,
posts: true, posts: { likedBy: true },
comments: true, comments: true,
}, },
}); });
@ -89,17 +89,36 @@ export class UserController {
if (!user) return next(new AppError("No user found with that ID", 404)); if (!user) return next(new AppError("No user found with that ID", 404));
// remove sensitive fields // remove sensitive fields
user.deleteSensitiveFields(); user.deletePrivateFields();
user.followed.forEach((followedUser) => if (user.posts) {
followedUser.deleteSensitiveFields() user.posts.forEach((post) => {
); if (post.likedBy) {
user.followers.forEach((follower) => follower.deleteSensitiveFields()); post.likedBy.forEach((user) => user.deleteSensitiveFields());
}
if (post.comments) {
post.comments.forEach((comment) =>
comment.createdBy.deleteSensitiveFields()
);
}
});
}
if (user.followed) {
user.followed.forEach((followedUser) =>
followedUser.deleteSensitiveFields()
);
}
let followStatus = false;
if (user.followers) {
user.followers.forEach((follower) => follower.deleteSensitiveFields());
// Set the followed status of the user
followStatus = user.followers.some(
(follower) => follower.id === req.user.id
);
}
// If the user is private, only return the user // If the user is private, only return the user
// if the requesting user is following // if the requesting user is following
const followStatus = user.followers.some(
(follower) => follower.id === req.user.id
);
if (user.isPrivate && !followStatus) { if (user.isPrivate && !followStatus) {
user.followed = []; user.followed = [];
user.followers = []; user.followers = [];
@ -206,7 +225,7 @@ export class UserController {
/** /**
* @swagger * @swagger
* /users/{id}/follow: * /users/follow/{id}:
* post: * post:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
@ -281,7 +300,7 @@ export class UserController {
/** /**
* @swagger * @swagger
* /users/{id}/follow: * /users/follow/{id}:
* delete: * delete:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []

View File

@ -8,13 +8,13 @@ import {AppError} from "../util/AppError";
import {AppRequest} from "../util/AppRequest"; import {AppRequest} from "../util/AppRequest";
export class PostController { export class PostController {
private postRepository = AppDataSource.getRepository(Post) private postRepository = AppDataSource.getRepository(Post);
private commentRepository = AppDataSource.getRepository(Comment) private commentRepository = AppDataSource.getRepository(Comment);
private notificationRepository = AppDataSource.getRepository(Notification) private notificationRepository = AppDataSource.getRepository(Notification);
private userRepository = AppDataSource.getRepository(User) private userRepository = AppDataSource.getRepository(User);
/** /**
* @swagger * @swagger
@ -36,477 +36,523 @@ export class PostController {
* items: * items:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
*/ */
public getAllPosts = catchAsync(async (_req, res, _next) => { public getAllPosts = catchAsync(async (req: AppRequest, res, _next) => {
const posts = await this.postRepository.find({ const posts = await this.postRepository.find({
relations: { createdBy: true, likedBy: true, comments: {createdBy: true} }, relations: {
createdBy: true,
likedBy: true,
comments: { createdBy: true },
},
}); });
// Remove sensitive fields // Remove sensitive fields
posts.forEach(post => { posts.forEach((post) => {
post.deleteSensitiveFields() post.deleteSensitiveFields();
}) });
// Remove private, non followed users
const filteredPosts = posts.filter((post) => {
let followStatus = false;
res.status(200).send(posts); 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;
});
res.status(200).send(filteredPosts);
}); });
/** /**
* @swagger * @swagger
* /posts/{id}: * /posts/{id}:
* get: * get:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: getPost * operationId: getPost
* summary: Get a post by ID * summary: Get a post by ID
* parameters: * parameters:
* - in: path * - in: path
* name: id * name: id
* required: true * required: true
* description: ID of the post * description: ID of the post
* schema: * schema:
* type: integer * type: integer
* responses: * responses:
* 200: * 200:
* description: A post * description: A post
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
* 400: * 400:
* description: Invalid ID * description: Invalid ID
* 404: * 404:
* description: No post found with that ID * description: No post found with that ID
*/ */
public getPost = catchAsync(async (req, res, next) => { public getPost = catchAsync(async (req, res, next) => {
const { post, errorMessage } = await this.validateRequestAndGetEntities( const { post, errorMessage } = await this.validateRequestAndGetEntities(
req req
); );
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) if (errorMessage == "Invalid ID")
if(errorMessage == 'No post found with that ID'){ return next(new AppError("Invalid ID", 400));
return next(new AppError('No post found with that ID', 404)) if (errorMessage == "No post found with that ID") {
} return next(new AppError("No post found with that ID", 404));
}
// Remove sensitive fields // Remove sensitive fields
post.deleteSensitiveFields() post.deleteSensitiveFields();
res.send(post); res.send(post);
}); });
/** /**
* @swagger * @swagger
* /posts/followed: * /posts/followed:
* get: * get:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: getFollowedPosts * operationId: getFollowedPosts
* summary: Get posts of followed users * summary: Get posts of followed users
* responses: * responses:
* 200: * 200:
* description: A list of posts * description: A list of posts
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * type: array
* items: * items:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
*/ */
public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => { public getFollowedPosts = catchAsync(async (req: AppRequest, res, _next) => {
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: true, followed: true,
posts: { likedBy: true, comments: true }, posts: { likedBy: true, comments: true },
}, },
}); });
const followedPosts = user.followed.map(followedUser => followedUser.posts).flat() const followedPosts = user.followed
.map((followedUser) => followedUser.posts)
.flat();
// Remove sensitive fields // Remove sensitive fields
followedPosts.forEach(post => { followedPosts.forEach((post) => {
post.deleteSensitiveFields() post.deleteSensitiveFields();
}) });
res.send(followedPosts); res.send(followedPosts);
}); });
/** /**
* @swagger * @swagger
* /posts: * /posts:
* post: * post:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: createPost * operationId: createPost
* summary: Create a post * summary: Create a post
* requestBody: * requestBody:
* required: true * required: true
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* properties: * properties:
* title: * title:
* type: string * type: string
* description: Post title * description: Post title
* content: * content:
* type: string * type: string
* description: Post content * description: Post content
* responses: * responses:
* 201: * 201:
* description: A post * description: A post
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
* 400: * 400:
* description: Title and content are required * description: Title and content are required
*/ */
public createPost = catchAsync(async (req : AppRequest, res, next) => { public createPost = catchAsync(async (req: AppRequest, res, next) => {
const user = await this.userRepository.findOne({where: {id: req.user.id}}) const user = await this.userRepository.findOne({
const {title, content} = req.body where: { id: req.user.id },
});
const { title, content } = req.body;
if(!title || !content) return next(new AppError('Title and content are required', 400)) if (!title || !content)
return next(new AppError("Title and content are required", 400));
const newPost = Object.assign(new Post(), {
title,
content,
createdBy: user,
createdAt: new Date(),
});
const newPost = Object.assign(new Post(), { const post = await this.postRepository.save(newPost);
title,
content,
createdBy: user,
createdAt: new Date()})
const post = await this.postRepository.save(newPost) // Remove sensitive fields
post.deleteSensitiveFields();
// Remove sensitive fields
post.deleteSensitiveFields()
res.status(201).send(post); res.status(201).send(post);
}); });
/** /**
* @swagger * @swagger
* /posts/{id}: * /posts/{id}:
* patch: * patch:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: updatePost * operationId: updatePost
* summary: Update a post by ID * summary: Update a post by ID
* parameters: * parameters:
* - in: path * - in: path
* name: id * name: id
* required: true * required: true
* description: ID of the post * description: ID of the post
* schema: * schema:
* type: integer * type: integer
* requestBody: * requestBody:
* required: true * required: true
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* properties: * properties:
* title: * title:
* type: string * type: string
* description: Post title * description: Post title
* content: * content:
* type: string * type: string
* description: Post content * description: Post content
* responses: * responses:
* 200: * 200:
* description: A post * description: A post
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
* 400: * 400:
* description: Invalid ID or Title and content are required * description: Invalid ID or Title and content are required
* 404: * 404:
* description: No post found with that ID * description: No post found with that ID
*/ */
public updatePost = catchAsync(async (req : AppRequest, res, next) => { public updatePost = catchAsync(async (req: AppRequest, res, next) => {
const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) const { user, post, errorMessage } =
const {title, content} = req.body await this.validateRequestAndGetEntities(req);
const { title, content } = req.body;
if(!title || !content) return next(new AppError('Title and content are required', 400)) if (!title || !content)
return next(new AppError("Title and content are required", 400));
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) if (errorMessage == "Invalid ID")
if(errorMessage == 'No post found with that ID'){ return next(new AppError("Invalid ID", 400));
return next(new AppError('No post found with that ID', 404)) if (errorMessage == "No post found with that ID") {
} return next(new AppError("No post found with that ID", 404));
}
if(post.createdBy.id !== user.id){ if (post.createdBy.id !== user.id) {
return next(new AppError('You are not authorized to update this post', 403)) return next(
} new AppError("You are not authorized to update this post", 403)
);
}
post.title = title post.title = title;
post.content = content post.content = content;
await this.postRepository.save(post) await this.postRepository.save(post);
// Remove sensitive fields // Remove sensitive fields
post.deleteSensitiveFields() post.deleteSensitiveFields();
res.status(200).send(post); res.status(200).send(post);
}); });
/** /**
* @swagger * @swagger
* /posts/{id}: * /posts/{id}:
* delete: * delete:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: deletePost * operationId: deletePost
* summary: Delete a post by ID * summary: Delete a post by ID
* parameters: * parameters:
* - in: path * - in: path
* name: id * name: id
* required: true * required: true
* description: ID of the post * description: ID of the post
* schema: * schema:
* type: integer * type: integer
* responses: * responses:
* 204: * 204:
* description: No content * description: No content
* 400: * 400:
* description: Invalid ID * description: Invalid ID
* 404: * 404:
* description: No post found with that ID * description: No post found with that ID
*/ */
public deletePost = catchAsync(async (req : AppRequest, res, next) => { public deletePost = catchAsync(async (req: AppRequest, res, next) => {
const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) const { user, post, errorMessage } =
await this.validateRequestAndGetEntities(req);
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) if (errorMessage == "Invalid ID")
if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) return next(new AppError("Invalid ID", 400));
if (errorMessage == "No post found with that ID")
return next(new AppError("No post found with that ID", 404));
if(post.createdBy.id !== user.id){ if (post.createdBy.id !== user.id) {
return next(new AppError('You are not authorized to delete this post', 403)) return next(
} new AppError("You are not authorized to delete this post", 403)
);
}
await this.postRepository.remove(post); await this.postRepository.remove(post);
res.status(204).send(); res.status(204).send();
}); });
/** /**
* @swagger * @swagger
* /posts/{id}/like: * /posts/{id}/like:
* post: * post:
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
* tags: * tags:
* - Posts * - Posts
* operationId: likePost * operationId: likePost
* summary: Like a post by ID * summary: Like a post by ID
* parameters: * parameters:
* - in: path * - in: path
* name: id * name: id
* required: true * required: true
* description: ID of the post * description: ID of the post
* schema: * schema:
* type: integer * type: integer
* responses: * responses:
* 200: * 200:
* description: A post * description: A post
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Post' * $ref: '#/components/schemas/Post'
* 400: * 400:
* description: Invalid ID or You have already liked this post * description: Invalid ID or You have already liked this post
* 404: * 404:
* description: No post found with that ID * description: No post found with that ID
*/ */
public likePost = catchAsync(async (req : AppRequest, res, next) => { public likePost = catchAsync(async (req: AppRequest, res, next) => {
const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) const { user, post, errorMessage } =
await this.validateRequestAndGetEntities(req);
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
if(errorMessage == 'No post found with that ID'){
return next(new AppError('No post found with that ID', 404))
}
// Check if user has already liked the post
if(post.likedBy.some(likedUser => likedUser.id === user.id)){
return next(new AppError('You have already liked this post', 400))
}
post.likedBy.push(user)
await this.postRepository.save(post)
// Send notification to post creator
const newNotification = Object.assign(new Notification(),{
message: `${user.firstName} ${user.lastName} liked your post`,
belongsTo: post.createdBy,
timeStamp: new Date()
})
const notification = await this.notificationRepository.save(newNotification)
post.createdBy.notifications.push(notification)
await this.userRepository.save(post.createdBy)
// Remove sensitive fields
post.deleteSensitiveFields()
res.status(200).send(post);
});
/**
* @swagger
* /posts/{id}/like:
* delete:
* security:
* - bearerAuth: []
* tags:
* - Posts
* operationId: unlikePost
* summary: Unlike a post by ID
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the post
* schema:
* type: integer
* responses:
* 200:
* description: A post
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Post'
* 400:
* description: Invalid ID or You have not liked this post
* 404:
* description: No post found with that ID
*/
public unlikePost = catchAsync(async (req : AppRequest, res, next) => {
const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404))
// Check if user likes the post
if(!post.likedBy.some(likedUser => likedUser.id === user.id)){
return next(new AppError('You have not liked this post', 400))
}
post.likedBy = post.likedBy.filter(likedUser => likedUser.id !== user.id)
await this.postRepository.save(post)
// Remove sensitive fields
post.deleteSensitiveFields()
res.status(200).send(post);
});
/**
* @swagger
* /posts/{id}/comment:
* post:
* security:
* - bearerAuth: []
* tags:
* - Posts
* operationId: commentPost
* summary: Comment on a post by ID
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the post
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* content:
* type: string
* description: Comment content
* responses:
* 201:
* description: A comment
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Comment'
* 400:
* description: Invalid Request
* 404:
* description: No post found with that ID
*/
public commentPost = catchAsync(async (req : AppRequest, res, next) => {
const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
if(errorMessage == 'No post found with that ID') {
return next(new AppError('No post found with that ID', 404))
}
const {content} = req.body
const newComment = Object.assign(new Comment(), {
content,
createdBy: user,
post,
createdAt: new Date()
})
const comment = await this.commentRepository.save(newComment)
// Send notification to post creator
const newNotification = Object.assign(new Notification(),{
message: `${user.firstName} ${user.lastName} commented on your post`,
belongsTo: post.createdBy,
timeStamp: new Date()
})
await this.notificationRepository.save(newNotification)
// Remove sensitive fields
comment.createdBy.deleteSensitiveFields()
post.deleteSensitiveFields()
res.status(201).send(comment)
})
private validateRequestAndGetEntities = async (req : AppRequest,) : Promise<validatedEntities> => {
const postId = req.params.id
const parsedId = parseInt(postId)
let errorMessage: 'Invalid ID' | 'No post found with that ID'
// Check if ID is a number
if(isNaN(parsedId)) errorMessage = 'Invalid ID'
const user = req.user
const post = await this.postRepository.findOne({
where: {id: parsedId},
relations: {
likedBy: true,
comments: { createdBy: true },
createdBy: {notifications: true}
}
})
// Check if post exists
if(!post) errorMessage = 'No post found with that ID'
return {user, post, errorMessage}
if (errorMessage == "Invalid ID")
return next(new AppError("Invalid ID", 400));
if (errorMessage == "No post found with that ID") {
return next(new AppError("No post found with that ID", 404));
} }
// Check if user has already liked the post
if (post.likedBy.some((likedUser) => likedUser.id === user.id)) {
return next(new AppError("You have already liked this post", 400));
}
post.likedBy.push(user);
await this.postRepository.save(post);
// Send notification to post creator
const newNotification = Object.assign(new Notification(), {
message: `${user.firstName} ${user.lastName} liked your post`,
belongsTo: post.createdBy,
timeStamp: new Date(),
});
const notification = await this.notificationRepository.save(
newNotification
);
post.createdBy.notifications.push(notification);
await this.userRepository.save(post.createdBy);
// Remove sensitive fields
post.deleteSensitiveFields();
res.status(200).send(post);
});
/**
* @swagger
* /posts/{id}/like:
* delete:
* security:
* - bearerAuth: []
* tags:
* - Posts
* operationId: unlikePost
* summary: Unlike a post by ID
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the post
* schema:
* type: integer
* responses:
* 200:
* description: A post
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Post'
* 400:
* description: Invalid ID or You have not liked this post
* 404:
* description: No post found with that ID
*/
public unlikePost = catchAsync(async (req: AppRequest, res, next) => {
const { user, post, errorMessage } =
await this.validateRequestAndGetEntities(req);
if (errorMessage == "Invalid ID")
return next(new AppError("Invalid ID", 400));
if (errorMessage == "No post found with that ID")
return next(new AppError("No post found with that ID", 404));
// Check if user likes the post
if (!post.likedBy.some((likedUser) => likedUser.id === user.id)) {
return next(new AppError("You have not liked this post", 400));
}
post.likedBy = post.likedBy.filter((likedUser) => likedUser.id !== user.id);
await this.postRepository.save(post);
// Remove sensitive fields
post.deleteSensitiveFields();
res.status(200).send(post);
});
/**
* @swagger
* /posts/{id}/comment:
* post:
* security:
* - bearerAuth: []
* tags:
* - Posts
* operationId: commentPost
* summary: Comment on a post by ID
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the post
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* content:
* type: string
* description: Comment content
* responses:
* 201:
* description: A comment
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Comment'
* 400:
* description: Invalid Request
* 404:
* description: No post found with that ID
*/
public commentPost = catchAsync(async (req: AppRequest, res, next) => {
const { user, post, errorMessage } =
await this.validateRequestAndGetEntities(req);
if (errorMessage == "Invalid ID")
return next(new AppError("Invalid ID", 400));
if (errorMessage == "No post found with that ID") {
return next(new AppError("No post found with that ID", 404));
}
const { content } = req.body;
const newComment = Object.assign(new Comment(), {
content,
createdBy: user,
post,
createdAt: new Date(),
});
const comment = await this.commentRepository.save(newComment);
// Send notification to post creator
const newNotification = Object.assign(new Notification(), {
message: `${user.firstName} ${user.lastName} commented on your post`,
belongsTo: post.createdBy,
timeStamp: new Date(),
});
await this.notificationRepository.save(newNotification);
// Remove sensitive fields
comment.createdBy.deleteSensitiveFields();
post.deleteSensitiveFields();
res.status(201).send(comment);
});
private validateRequestAndGetEntities = async (
req: AppRequest
): Promise<validatedEntities> => {
const postId = req.params.id;
const parsedId = parseInt(postId);
let errorMessage: "Invalid ID" | "No post found with that ID";
// Check if ID is a number
if (isNaN(parsedId)) errorMessage = "Invalid ID";
const user = req.user;
const post = await this.postRepository.findOne({
where: { id: parsedId },
relations: {
likedBy: true,
comments: { createdBy: true },
createdBy: { notifications: true },
},
});
// Check if post exists
if (!post) errorMessage = "No post found with that ID";
return { user, post, errorMessage };
};
} }
interface validatedEntities { interface validatedEntities {
user: User user: User

View File

@ -53,10 +53,16 @@ export class User {
} }
public deleteSensitiveFields() { public deleteSensitiveFields() {
delete this.password; this.deleteExtraFields();
delete this.email; this.deletePrivateFields();
delete this.notifications; }
public deleteExtraFields() {
delete this.followed; delete this.followed;
delete this.followers; delete this.followers;
} }
public deletePrivateFields(){
delete this.email;
delete this.password;
delete this.notifications;
}
} }