Compare commits

...

5 Commits

Author SHA1 Message Date
Pau Costa Ferrer be57bd32b8 🎨 Removed unused code and imports
Signed-off-by: Pau Costa <mico@micodev.es>
2024-02-10 14:43:33 +01:00
Pau Costa Ferrer 4fdb17733d Implemented post list view
Signed-off-by: Pau Costa <mico@micodev.es>
2024-02-10 14:43:33 +01:00
Pau Costa Ferrer 0cdcc915f4 🩹 The API was missing some ease of use parameters
Signed-off-by: Pau Costa <mico@micodev.es>
2024-02-10 14:43:33 +01:00
Pau Costa Ferrer e27423a2e5 🩹 The API was missing some ease of use parameters
Signed-off-by: Pau Costa <mico@micodev.es>
2024-02-09 13:45:20 +01:00
Pau Costa Ferrer 591dae9567 🏗️ Views and routes boilerplate
Signed-off-by: Pau Costa <mico@micodev.es>
2024-02-08 16:56:47 +01:00
25 changed files with 833 additions and 219 deletions

View File

@ -76,6 +76,27 @@
}
}
},
"Notification": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Notification ID"
},
"message": {
"type": "string",
"description": "Notification message"
},
"timeStamp": {
"type": "string",
"description": "Notification creation date"
},
"seen": {
"type": "boolean",
"description": "Notification seen status"
}
}
},
"User": {
"type": "object",
"properties": {
@ -128,6 +149,24 @@
}
}
]
},
"UserWithRelationsAndNotifications": {
"allOf": [
{
"$ref": "#/components/schemas/UserWithRelations"
},
{
"type": "object",
"properties": {
"notifications": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Notification"
}
}
}
}
]
}
}
},
@ -745,7 +784,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserWithRelations"
"$ref": "#/components/schemas/UserWithRelationsAndNotifications"
}
}
}
@ -837,4 +876,4 @@
}
},
"tags": []
}
}

7
client/openapitools.json Normal file
View File

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.3.0"
}
}

View File

@ -1 +1 @@
7.2.0
7.3.0

View File

@ -186,6 +186,37 @@ export interface CreatePostRequest {
*/
'content'?: string;
}
/**
*
* @export
* @interface Notification
*/
export interface Notification {
/**
* Notification ID
* @type {number}
* @memberof Notification
*/
'id'?: number;
/**
* Notification message
* @type {string}
* @memberof Notification
*/
'message'?: string;
/**
* Notification creation date
* @type {string}
* @memberof Notification
*/
'timeStamp'?: string;
/**
* Notification seen status
* @type {boolean}
* @memberof Notification
*/
'seen'?: boolean;
}
/**
*
* @export
@ -309,6 +340,61 @@ export interface UserWithRelations {
*/
'followers'?: Array<User>;
}
/**
*
* @export
* @interface UserWithRelationsAndNotifications
*/
export interface UserWithRelationsAndNotifications {
/**
* User ID
* @type {number}
* @memberof UserWithRelationsAndNotifications
*/
'id'?: number;
/**
* User first name
* @type {string}
* @memberof UserWithRelationsAndNotifications
*/
'firstName'?: string;
/**
* User last name
* @type {string}
* @memberof UserWithRelationsAndNotifications
*/
'lastName'?: string;
/**
*
* @type {Array<Post>}
* @memberof UserWithRelationsAndNotifications
*/
'posts'?: Array<Post>;
/**
*
* @type {Array<Comment>}
* @memberof UserWithRelationsAndNotifications
*/
'comments'?: Array<Comment>;
/**
*
* @type {Array<User>}
* @memberof UserWithRelationsAndNotifications
*/
'followed'?: Array<User>;
/**
*
* @type {Array<User>}
* @memberof UserWithRelationsAndNotifications
*/
'followers'?: Array<User>;
/**
*
* @type {Array<Notification>}
* @memberof UserWithRelationsAndNotifications
*/
'notifications'?: Array<Notification>;
}
/**
* AuthenticationApi - axios parameter creator
@ -441,9 +527,9 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
*/
async authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthLoginPost200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authLoginPost(authLoginPostRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['AuthenticationApi.authLoginPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLoginPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -453,9 +539,9 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
*/
async authLogoutGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthLogoutGet200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authLogoutGet(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['AuthenticationApi.authLogoutGet']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLogoutGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -466,9 +552,9 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
*/
async authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authSignupPost(authSignupPostRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['AuthenticationApi.authSignupPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authSignupPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@ -931,9 +1017,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async commentPost(id: number, commentPostRequest: CommentPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Comment>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.commentPost(id, commentPostRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.commentPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.commentPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -944,9 +1030,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async createPost(createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPost(createPostRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.createPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.createPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -957,9 +1043,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async deletePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deletePost(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.deletePost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.deletePost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -969,9 +1055,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async getAllPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Post>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPosts(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.getAllPosts']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.getAllPosts']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -981,9 +1067,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async getFollowedPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Post>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getFollowedPosts(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.getFollowedPosts']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.getFollowedPosts']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -994,9 +1080,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async getPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getPost(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.getPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.getPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1007,9 +1093,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async likePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.likePost(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.likePost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.likePost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1020,9 +1106,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async unlikePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unlikePost(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.unlikePost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.unlikePost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1034,9 +1120,9 @@ export const PostsApiFp = function(configuration?: Configuration) {
*/
async updatePost(id: number, createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updatePost(id, createPostRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['PostsApi.updatePost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['PostsApi.updatePost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@ -1465,9 +1551,9 @@ export const UsersApiFp = function(configuration?: Configuration) {
*/
async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<User>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.usersGet']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1478,9 +1564,9 @@ export const UsersApiFp = function(configuration?: Configuration) {
*/
async usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowDelete(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1491,9 +1577,9 @@ export const UsersApiFp = function(configuration?: Configuration) {
*/
async usersIdFollowPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowPost(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1504,9 +1590,9 @@ export const UsersApiFp = function(configuration?: Configuration) {
*/
async usersIdGet(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdGet(id, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.usersIdGet']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@ -1514,11 +1600,11 @@ export const UsersApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async usersMeGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelations>> {
async usersMeGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserWithRelationsAndNotifications>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.usersMeGet(options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['UsersApi.usersMeGet']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@ -1575,7 +1661,7 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
usersMeGet(options?: any): AxiosPromise<UserWithRelations> {
usersMeGet(options?: any): AxiosPromise<UserWithRelationsAndNotifications> {
return localVarFp.usersMeGet(options).then((request) => request(axios, basePath));
},
};

View File

@ -5,9 +5,9 @@ import {
AuthSignupPostRequest,
AuthenticationApi,
Configuration,
UsersApi,
} from "../api";
import { AppThunk } from "./store";
import { Axios, AxiosError } from "axios";
interface loginState {
loggedIn: boolean;
@ -15,6 +15,7 @@ interface loginState {
error: string | null;
userInfo: {
firstName: string;
lastName: string;
jwt: string;
};
}
@ -25,6 +26,7 @@ const initialState: loginState = {
error: null,
userInfo: {
firstName: "",
lastName: "",
jwt: "",
},
};
@ -35,7 +37,9 @@ export const loginSlice = createSlice({
reducers: {
login: (state, action) => {
state.loggedIn = true;
state.userInfo.jwt = action.payload;
state.userInfo.jwt = action.payload.jwt;
state.userInfo.firstName = action.payload.firstName;
state.userInfo.lastName = action.payload.lastName;
},
logoff: (state) => {
state.loggedIn = false;
@ -50,7 +54,7 @@ export const loginSlice = createSlice({
},
});
const api = new AuthenticationApi(
const authApi = new AuthenticationApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
})
@ -59,15 +63,30 @@ const api = new AuthenticationApi(
export const postLogin =
(params: AuthLoginPostRequest): AppThunk =>
async (dispatch) => {
let response;
let response, userResponse;
try {
dispatch(setStatus(Status.loading));
response = await api.authLoginPost(params);
response = await authApi.authLoginPost(params);
dispatch(login(response.data.token));
await addJWT(response.data.token || "");
dispatch(setError(""));
// Get user info
const userApi = new UsersApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
accessToken: response.data.token,
})
);
userResponse = await userApi.usersMeGet();
dispatch(
login({
jwt: response.data.token,
firstName: userResponse.data.firstName,
lastName: userResponse.data.lastName,
})
);
dispatch(setStatus(Status.succeeded));
} catch (error) {
dispatch(setStatus(Status.failed));
@ -83,7 +102,7 @@ export const postSignup =
console.log(params);
try {
dispatch(setStatus(Status.loading));
response = await api.authSignupPost(params);
response = await authApi.authSignupPost(params);
dispatch(postLogin({ email: params.email, password: params.password }));
} catch (error) {
@ -97,7 +116,7 @@ export const postLogout = (): AppThunk => async (dispatch) => {
localStorage.removeItem("jwt");
sessionStorage.removeItem("jwt");
dispatch(logoff());
await api.authLogoutGet();
await authApi.authLogoutGet();
};
const addJWT = async (token: string) => {

View File

@ -0,0 +1,88 @@
import { Store, createSlice } from "@reduxjs/toolkit";
import { Configuration, Post, PostsApi } from "../api";
import { Status } from "../util/types";
import { AppThunk, store } from "./store";
interface postSliceInterface {
status: Status;
followedPosts: Post[];
globalPosts: Post[];
}
const initialState: postSliceInterface = {
status: Status.idle,
followedPosts: [],
globalPosts: [],
};
export const postSlice = createSlice({
name: "post",
initialState,
reducers: {
setStatus: (state, action) => {
state.status = action.payload;
},
setFollowedPosts: (state, action) => {
state.followedPosts = action.payload;
},
setGlobalPosts: (state, action) => {
state.globalPosts = action.payload;
},
},
});
export const fetchFollowedPosts = (): AppThunk => async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
const response = await postApi.getFollowedPosts();
dispatch(setFollowedPosts(response.data));
dispatch(setStatus(Status.idle));
};
export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
const response = await postApi.getAllPosts();
dispatch(setGlobalPosts(response.data));
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;
export default postSlice.reducer;
export const selectFollowedPosts = (state: any) => state.post.followedPosts;
export const selectAllPosts = (state: any) => state.post.globalPosts;
export const selectStatus = (state: any) => state.post.status;
function createApi(store: Store) {
return new PostsApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
accessToken: store.getState().login.userInfo.jwt,
})
);
}

View File

@ -1,10 +1,12 @@
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import loginReducer from "./loginSlice";
import postReducer from "./postSlice";
export const store = configureStore({
reducer: {
login: loginReducer,
post: postReducer,
},
});

View File

@ -4,10 +4,15 @@ import FeedIcon from "@mui/icons-material/Feed";
import GlobalIcon from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search";
import { useNavigate } from "react-router-dom";
export default function BottomAppBar() {
const [value, setValue] = useState(0);
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return (
<Paper
sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}
@ -20,10 +25,26 @@ export default function BottomAppBar() {
setValue(newValue);
}}
>
<BottomNavigationAction label="My Feed" icon={<FeedIcon />} />
<BottomNavigationAction label="Global Feed" icon={<GlobalIcon />} />
<BottomNavigationAction label="New Post" icon={<AddIcon />} />
<BottomNavigationAction label="Search" icon={<SearchIcon />} />
<BottomNavigationAction
label="My Feed"
icon={<FeedIcon />}
onClick={handleClick("feed")}
/>
<BottomNavigationAction
label="Global Feed"
icon={<GlobalIcon />}
onClick={handleClick("global")}
/>
<BottomNavigationAction
label="New Post"
icon={<AddIcon />}
onClick={handleClick("newpost")}
/>
<BottomNavigationAction
label="Search"
icon={<SearchIcon />}
onClick={handleClick("search")}
/>
</BottomNavigation>
</Paper>
);

View File

@ -4,7 +4,7 @@ import { Badge, Tooltip } from "@mui/material";
export default function NotificationBell() {
return (
<Badge badgeContent={4} color="secondary">
<Tooltip title="Notifications">
<Tooltip title="Notifications" >
<NotificationsIcon />
</Tooltip>
</Badge>

View File

@ -0,0 +1,135 @@
import {
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) {
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
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

@ -13,6 +13,7 @@ import FeedIcon from "@mui/icons-material/Feed";
import GlobalIcon from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search";
import { Link, useNavigate } from "react-router-dom";
export interface SideAppBarProps {
drawerWidth: number;
@ -20,6 +21,12 @@ export interface SideAppBarProps {
export default function SideAppBar(props: SideAppBarProps) {
const { drawerWidth } = props;
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return (
<Drawer
sx={{
@ -37,7 +44,7 @@ export default function SideAppBar(props: SideAppBarProps) {
<Divider />
<List>
<ListItem key={"myfeed"} disablePadding>
<ListItem key={"myfeed"} disablePadding onClick={handleClick("feed")}>
<ListItemButton>
<ListItemIcon>
<FeedIcon />
@ -46,7 +53,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"globalfeed"} disablePadding>
<ListItem
key={"globalfeed"}
disablePadding
onClick={handleClick("global")}
>
<ListItemButton>
<ListItemIcon>
<GlobalIcon />
@ -55,7 +66,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"newpost"} disablePadding>
<ListItem
key={"newpost"}
disablePadding
onClick={handleClick("newpost")}
>
<ListItemButton>
<ListItemIcon>
<AddIcon />
@ -64,7 +79,7 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"search"} disablePadding>
<ListItem key={"search"} disablePadding onClick={handleClick("search")}>
<ListItemButton>
<ListItemIcon>
<SearchIcon />
@ -72,7 +87,6 @@ export default function SideAppBar(props: SideAppBarProps) {
<ListItemText primary={"Search"} />
</ListItemButton>
</ListItem>
</List>
</Drawer>
);

View File

@ -5,24 +5,25 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu";
import Container from "@mui/material/Container";
import Avatar from "@mui/material/Avatar";
import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import NotificationBell from "./notificationBell";
import {useSelector} from "react-redux";
import {selectUserInfo} from "../app/loginSlice";
import { postLogout, selectUserInfo } from "../app/loginSlice";
import { useAppDispatch } from "../app/store";
const settings = ["Profile", "Account", "Dashboard", "Logout"];
interface TopAppBarProps {
height: number;
}
function TopAppBar() {
const userInfo = useSelector(selectUserInfo);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
function TopAppBar(props: TopAppBarProps) {
const dispatch = useAppDispatch();
const userInfo = useSelector(selectUserInfo);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
null
);
console.log(userInfo)
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
@ -32,16 +33,20 @@ function TopAppBar() {
setAnchorElUser(null);
};
const handleLogout = () => {
dispatch(postLogout());
setAnchorElUser(null);
};
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",
@ -64,10 +69,13 @@ function TopAppBar() {
<NotificationBell />
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<Box sx={{ flexGrow: 0, mr:"2rem" }}>
<Tooltip title="Open settings" >
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt={userInfo.firstName} src="/static/images/avatar/2.jpg" />
<Avatar
alt={`${userInfo.firstName} ${userInfo.lastName}`}
src="/static/images/avatar/2.jpg"
/>
</IconButton>
</Tooltip>
<Menu
@ -86,15 +94,18 @@ function TopAppBar() {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
{settings.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))}
<MenuItem key={"profile"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Profile"}</Typography>
</MenuItem>
<MenuItem key={"settings"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Settings"}</Typography>
</MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}>
<Typography textAlign="center">{"Logout"}</Typography>
</MenuItem>
</Menu>
</Box>
</Toolbar>
</Container>
</AppBar>
);
}

View File

@ -12,6 +12,11 @@ import Register from "./routes/Auth/register";
import AuthRoot from "./routes/Auth/authRoot";
import { Provider } from "react-redux";
import { store } from "./app/store";
import PostList from "./routes/postList";
import Profile from "./routes/profile";
import Post from "./routes/post";
import NewPost from "./routes/newPost";
import Search from "./routes/search";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@ -28,6 +33,36 @@ const router = createBrowserRouter([
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: "feed",
element: <PostList type="user" />,
},
{
path: "global",
element: <PostList type="all" />,
},
{
path: "me",
element: <Profile selfProfile />,
},
{
path: "user/:id",
element: <Profile />,
},
{
path: "post/:id",
element: <Post />,
},
{
path: "search",
element: <Search />,
},
{
path: "newpost",
element: <NewPost />,
},
],
},
{
path: "/auth",

View File

@ -1,5 +1,5 @@
import { Copyright } from "@mui/icons-material";
import { Grid, Paper, Typography } from "@mui/material";
import { Grid, Typography } from "@mui/material";
import { Outlet, useNavigate } from "react-router-dom";
import { StyledDivider } from "../../components/StyledComponents";
import { useSelector } from "react-redux";
@ -12,7 +12,7 @@ export default function AuthRoot() {
useEffect(() => {
if (loggedIn) {
return navigate("/");
return navigate("/feed");
}
});

View File

@ -0,0 +1,3 @@
export default function NewPost() {
return <>New Post</>;
}

View File

@ -0,0 +1,3 @@
export default function Notification() {
return <>Notification</>;
}

View File

@ -0,0 +1,3 @@
export default function Post() {
return <>Post</>;
}

View File

@ -0,0 +1,73 @@
import { useSelector } from "react-redux";
import {
fetchFollowedPosts,
fetchGlobalPosts,
selectAllPosts,
selectFollowedPosts,
} from "../app/postSlice";
import { useAppDispatch } from "../app/store";
import { useEffect } from "react";
import { Box, List, Typography } from "@mui/material";
import { Post } from "../api";
import PostListItem from "../components/postListItem";
interface PostListProps {
type: "all" | "user";
}
export default function PostList(props: PostListProps) {
const dispatch = useAppDispatch();
const followedPosts = useSelector(selectFollowedPosts);
const globalPosts = useSelector(selectAllPosts);
useEffect(() => {
if (props.type === "all") dispatch(fetchGlobalPosts());
if (props.type === "user") dispatch(fetchFollowedPosts());
}, []);
return (
<Box
sx={{
width: "100%",
alignContent: "center",
justifyContent: "center",
display: "flex",
}}
>
{props.type === "all" && (
<List>
{globalPosts.map((post: Post) => (
<PostListItem post={post} postType="all" key={post.id} />
))}
</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

@ -0,0 +1,12 @@
import { useParams } from "react-router-dom";
export interface ProfileProps {
selfProfile?: boolean;
}
export default function Profile(props: ProfileProps) {
const { selfProfile } = props;
const userId = useParams<{ userId: string }>().userId;
return <>Profile</>;
}

View File

@ -1,7 +1,6 @@
import { Outlet, useNavigate } from "react-router-dom";
import { Box } from "@mui/material";
import { postLogout, selectLoggedIn, selectUserInfo } from "../app/loginSlice";
import { useAppDispatch } from "../app/store";
import { selectLoggedIn } from "../app/loginSlice";
import { useSelector } from "react-redux";
import { useEffect } from "react";
import useWindowDimensions from "../app/hooks/useWindowDimensions";
@ -10,10 +9,10 @@ import BottomAppBar from "../components/bottomAppBar";
import TopAppBar from "../components/topAppBar";
const drawerWidth = 240;
const topAppBarHeight = 64;
export default function Root() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const loggedIn = useSelector(selectLoggedIn);
const { width } = useWindowDimensions();
@ -23,14 +22,11 @@ export default function Root() {
}
}, [loggedIn, navigate]);
const handleClick = () => {
dispatch(postLogout());
};
return (
<>
<Box>
<TopAppBar />
<TopAppBar height={topAppBarHeight} />
</Box>
<Box
sx={{
@ -51,6 +47,7 @@ export default function Root() {
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${width > 600 ? drawerWidth : 0}px)` },
marginTop: { xs: `${topAppBarHeight}px`},
}}
>
<Outlet />

View File

@ -0,0 +1,3 @@
export default function Search() {
return <>Search</>;
}

View File

@ -7,10 +7,9 @@ import {Notification} from "../entity/Notification";
import {catchAsync} from "../util/catchAsync";
export class UserController {
private userRepository = AppDataSource.getRepository(User);
private userRepository = AppDataSource.getRepository(User)
private notificationRepository = AppDataSource.getRepository(Notification)
private notificationRepository = AppDataSource.getRepository(Notification);
/**
* @swagger
* /users:
@ -34,13 +33,14 @@ export class UserController {
async (req: Request, res: Response, next: NextFunction) => {
const users = await this.userRepository.find();
// remove sensitive fields
users.forEach(user => {
user.deleteSensitiveFields()
})
// remove sensitive fields
users.forEach((user) => {
user.deleteSensitiveFields();
});
res.status(200).send(users)
})
res.status(200).send(users);
}
);
/**
* @swagger
@ -73,23 +73,28 @@ export class UserController {
// Check if ID is a number
if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
const user = await this.userRepository.findOne({where: {id: parsedId}, relations:{
followed: true,
followers: true,
posts: true,
comments: true,
}})
const user = await this.userRepository.findOne({
where: { id: parsedId },
relations: {
followed: true,
followers: true,
posts: true,
comments: true,
},
});
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
user.deleteSensitiveFields();
user.followed.forEach((followedUser) =>
followedUser.deleteSensitiveFields()
);
user.followers.forEach((follower) => follower.deleteSensitiveFields());
// remove sensitive fields
user.deleteSensitiveFields()
user.followed.forEach(followedUser => followedUser.deleteSensitiveFields())
user.followers.forEach(follower => follower.deleteSensitiveFields())
return res.send(user)
})
return res.send(user);
}
);
/**
* @swagger
@ -106,7 +111,7 @@ export class UserController {
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserWithRelations'
* $ref: '#/components/schemas/UserWithRelationsAndNotifications'
*/
public getMe = catchAsync(
async (req: AppRequest, res: Response, next: NextFunction) => {
@ -117,14 +122,18 @@ export class UserController {
followers: true,
posts: true,
comments: true,
notifications: true,
},
});
user.followed.forEach(followedUser => followedUser.deleteSensitiveFields())
user.followers.forEach(follower => follower.deleteSensitiveFields())
user.followed.forEach((followedUser) =>
followedUser.deleteSensitiveFields()
);
user.followers.forEach((follower) => follower.deleteSensitiveFields());
return res.status(200).send(user)
})
return res.status(200).send(user);
}
);
/**
* @swagger
@ -161,69 +170,74 @@ export class UserController {
// Check if ID is a number
if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
const user = req.user
const userToFollow = await this.userRepository.findOne({
where: {id: parsedId},
relations:{followed: true, followers: true, notifications: true}}
const user = req.user;
const userToFollow = await this.userRepository.findOne({
where: { id: parsedId },
relations: { followed: true, followers: true, notifications: true },
});
if (!userToFollow)
return next(new AppError("No user found with that ID", 404));
// Check if user is already following
if (
user.followed.some(
(followedUser) => followedUser.id === userToFollow.id
)
) {
return next(new AppError("You are already following this user", 400));
}
// Follow the user
user.followed.push(userToFollow);
await this.userRepository.save(user);
// Add the requesting user to the followers of the user being followed
userToFollow.followers.push(user);
// Create a notification for the user being followed
const followNotification = Object.assign(new Notification(), {
seen: false,
message: `${user.firstName} is now following you`,
timeStamp: new Date(),
});
userToFollow.notifications.push(
await this.notificationRepository.save(followNotification)
);
await this.userRepository.save(userToFollow);
if(!userToFollow) return next(new AppError('No user found with that ID', 404))
return res.status(200).send({
status: "success",
message: `You are now following ${userToFollow.firstName}`,
});
}
);
// Check if user is already following
if(user.followed.some(followedUser => followedUser.id === userToFollow.id)){
return next(new AppError('You are already following this user', 400))
}
// Follow the user
user.followed.push(userToFollow)
await this.userRepository.save(user)
// Add the requesting user to the followers of the user being followed
userToFollow.followers.push(user)
// Create a notification for the user being followed
const followNotification = Object.assign(new Notification(),{
seen: false,
message: `${user.firstName} is now following you`,
timeStamp: new Date()
})
userToFollow.notifications.push(
await this.notificationRepository.save(followNotification)
)
await this.userRepository.save(userToFollow)
return res.status(200).send({
status: 'success',
message: `You are now following ${userToFollow.firstName}`
})
})
/**
* @swagger
* /users/{id}/follow:
* delete:
* security:
* - bearerAuth: []
* tags:
* - Users
* summary: Unfollow a user
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: The ID of the user to unfollow
* responses:
* 200:
* description: Successfully unfollowed the user
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserWithRelations'
* 400:
* description: Invalid ID
* 404:
* description: No user found with that ID
*/
/**
* @swagger
* /users/{id}/follow:
* delete:
* security:
* - bearerAuth: []
* tags:
* - Users
* summary: Unfollow a user
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: The ID of the user to unfollow
* responses:
* 200:
* description: Successfully unfollowed the user
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserWithRelations'
* 400:
* description: Invalid ID
* 404:
* description: No user found with that ID
*/
public unfollowUser = catchAsync(
async (req: AppRequest, res: Response, next: NextFunction) => {
const userToUnfollowId = req.params.id;
@ -231,28 +245,37 @@ export class UserController {
// Check if ID is a number
if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
const user = req.user
const userToUnfollow = await this.userRepository.findOne({
where: {id: parsedId},
relations: {followed: true, followers: true}
})
const user = req.user;
const userToUnfollow = await this.userRepository.findOne({
where: { id: parsedId },
relations: { followed: true, followers: true },
});
if(!userToUnfollow) return next(new AppError('No user found with that ID', 404))
// Check if user is following
if(!user.followed.some(followedUser => followedUser.id === userToUnfollow.id)){
return next(new AppError('You are not following this user', 400))
}
// Unfollow the user
user.followed = user.followed.filter(followedUser => followedUser.id !== userToUnfollow.id)
await this.userRepository.save(user)
if (!userToUnfollow)
return next(new AppError("No user found with that ID", 404));
// Check if user is following
if (
!user.followed.some(
(followedUser) => followedUser.id === userToUnfollow.id
)
) {
return next(new AppError("You are not following this user", 400));
}
// Unfollow the user
user.followed = user.followed.filter(
(followedUser) => followedUser.id !== userToUnfollow.id
);
await this.userRepository.save(user);
userToUnfollow.followers = userToUnfollow.followers.filter(follower => follower.id !== user.id)
await this.userRepository.save(userToUnfollow)
return res.status(200).send({
status: 'success',
message: `You are no longer following ${userToUnfollow.firstName}`
})
})
userToUnfollow.followers = userToUnfollow.followers.filter(
(follower) => follower.id !== user.id
);
await this.userRepository.save(userToUnfollow);
return res.status(200).send({
status: "success",
message: `You are no longer following ${userToUnfollow.firstName}`,
});
}
);
}

View File

@ -38,7 +38,7 @@ export class PostController {
*/
public getAllPosts = catchAsync(async (_req, res, _next) => {
const posts = await this.postRepository.find({
relations: { createdBy: true },
relations: { createdBy: true, likedBy: true, comments: {createdBy: true} },
});
// Remove sensitive fields
@ -117,8 +117,12 @@ export class PostController {
public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => {
const user = await this.userRepository.findOne({
where: {id: req.user.id},
relations: {followed: true, posts: true}})
where: { id: req.user.id },
relations: {
followed: true,
posts: { likedBy: true, comments: true },
},
});
const followedPosts = user.followed.map(followedUser => followedUser.posts).flat()

View File

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

View File

@ -83,6 +83,27 @@ const swaggerOptions = {
},
},
},
Notification: {
type: "object",
properties: {
id: {
type: "integer",
description: "Notification ID",
},
message: {
type: "string",
description: "Notification message",
},
timeStamp: {
type: "string",
description: "Notification creation date",
},
seen: {
type: "boolean",
description: "Notification seen status",
},
},
},
User: {
type: "object",
properties: {
@ -136,10 +157,25 @@ const swaggerOptions = {
},
],
},
UserWithRelationsAndNotifications: {
allOf: [
{ $ref: "#/components/schemas/UserWithRelations" },
{
type: "object",
properties: {
notifications: {
type: "array",
items: {
$ref: "#/components/schemas/Notification",
},
},
},
},
],
},
},
},
},
apis: ["**/controller/*.ts"],
};