diff --git a/src/components/Post.tsx b/src/components/Post.tsx index e3cbf7d43324020e09a17bde25990a5610e8c66e..e32d26c781b16d9941bcf29cfdb0a7c9fd7e3987 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Instalike } from '@jmetterrothan/instalike'; import { Media } from '@jmetterrothan/instalike/dist/types/Instalike'; @@ -7,14 +8,18 @@ import { faLocationDot, faHeart, faCommentDots, faEllipsisVertical } from '@fort // Autres fichiers import useAppDispatch from '../hooks/useAppDispatch'; -import { likepostAsync, unlikePostAsync } from '../redux/feed/thunks'; -import { useState } from 'react'; +import { likepostAsync, unlikePostAsync, followUserFeedAsync, unfollowUserFeedAsync } from '../redux/feed/thunks'; +import { followUserPostAction } from '../redux/post/actions'; +import { addLikePostAsync, deleteLikePostAsync, followUserPostAsync, unfollowUserPostAsync } from '../redux/post/thunks'; + + // COMPOSANTS import Comment from './Comment'; type PostProps = { + post: Instalike.Post; postid: number; username: string; location: string | null; @@ -25,10 +30,11 @@ type PostProps = { likes: number; comments: number; comment_post: Instalike.Comment[]; + inFeed: boolean; }; -const Post = ({ postid, username, location, time_post, img, caption, isLiked, likes, comments, comment_post }: PostProps) => { +const Post = ({ post, postid, username, location, time_post, img, caption, isLiked, likes, comments, comment_post, inFeed }: PostProps) => { const dispatch = useAppDispatch(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -53,7 +59,16 @@ return <> </div> } </div> - <button className="bg-gray-400 text-white font-bold h-10 rounded-md py-2 px-4">follow</button> + {!post.owner.isFollowedByViewer && ( + <button className="bg-gray-400 text-white font-bold h-10 rounded-md py-2 px-4" onClick={() => { + if (inFeed) { + dispatch(followUserFeedAsync(post.id, post.owner.id)); + } else { + dispatch(followUserPostAsync(post.owner.id)); + } + }} + >follow</button> + )} </div> <div className="relative"> {/* ICON */} @@ -62,7 +77,32 @@ return <> </div> {/* DROPDOWN */} <div className={`absolute bg-white rounded border w-32 left-0 mt-2 overflow-hidden ${dropdownOpen ? '' : 'hidden'}`}> - <div className="hover:bg-gray-200 p-2">Ne plus suivre</div> + {post.owner.isFollowedByViewer ? ( + <button className="hover:bg-gray-200 p-2 font-bold text-red-500 w-full text-left" + onClick={() => { + if (inFeed) { + dispatch(unfollowUserFeedAsync(post.id, post.owner.id)); + } else { + dispatch(unfollowUserPostAsync(post.owner.id)); + } + }} + > + Unfollow + </button> + ) : ( + <button className="hover:bg-gray-200 p-2 font-bold text-blue-500 w-full text-left" + onClick={() => { + if (inFeed) { + dispatch(followUserFeedAsync(post.id, post.owner.id)); + } else { + dispatch(followUserPostAsync(post.owner.id)); + } + }} + > + Follow + </button> + )} + <div className="hover:bg-gray-200 p-2">Voir la publication</div> <div className="hover:bg-gray-200 p-2">Copier le lien</div> </div> diff --git a/src/components/Suggestion.tsx b/src/components/Suggestion.tsx index acfdb5f3a8f6218c7ffc7c8fa44a0896f086fb9b..cf05bc7c13893195a83a5419a78b33095083926e 100644 --- a/src/components/Suggestion.tsx +++ b/src/components/Suggestion.tsx @@ -1,12 +1,22 @@ +import { Instalike } from '@jmetterrothan/instalike'; + // ICONS import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faPlus, faCheck } from '@fortawesome/free-solid-svg-icons'; + +// AUTRES FICHIERS +import useAppDispatch from '../hooks/useAppDispatch'; +import { followUserSuggestionAsync, unfollowUserSuggestionAsync } from '../redux/suggestion/thunks'; + + type SuggestionProps = { - firstname: string; + user: Instalike.User; }; -const Suggestion = ({ firstname }: SuggestionProps) => { +const Suggestion = ({ user }: SuggestionProps) => { + const dispatch = useAppDispatch(); + return <> {/* USER / STORY */} @@ -14,14 +24,25 @@ return <> <div className="relative"> <div className="bg-gray-400 flex items-center justify-center rounded-full overflow-hidden w-24 h-24"> {/* <img src="/src/assets/images/pp_user.png" alt="" /> */} - <p className="uppercase text-white text-[38px]">{firstname.charAt(0)}</p> + <p className="uppercase text-white text-[38px]">{user.firstName.charAt(0)}</p> </div> - <button - className="px-2 py-[7px] text-2xl rounded-full bg-gray-300/50 flex items-center justify-center absolute right-0 bottom-0"> + {user.isFollowedByViewer ? ( + <button className="px-2 py-[7px] text-2xl rounded-full bg-blue-200 flex items-center justify-center absolute right-0 bottom-0" onClick={() => { + dispatch(unfollowUserSuggestionAsync(user)); + }} + > + <FontAwesomeIcon className="text-[14px]" icon={faCheck} /> + </button> + ) : ( + <button className="px-2 py-[7px] text-2xl rounded-full bg-gray-300/50 flex items-center justify-center absolute right-0 bottom-0" onClick={() => { + dispatch(followUserSuggestionAsync(user)); + }} + > <FontAwesomeIcon className="text-[14px]" icon={faPlus} /> </button> + )} </div> - <p className="mt-2">{firstname}</p> + <p className="mt-2">{user.firstName}</p> </li> </>; }; diff --git a/src/redux/feed/actions.ts b/src/redux/feed/actions.ts index cf810384542bf40a5271f41a9ec74c086ec57777..51e85b7a9e3ceb64a6f3c23a9ce5d140816d1ddb 100644 --- a/src/redux/feed/actions.ts +++ b/src/redux/feed/actions.ts @@ -6,15 +6,20 @@ export const SET_FEED = 'FEED/SET_USER_FEED'; export const LIKE_POST_FEED = 'FEED/LIKE_POST'; export const UNLIKE_POST_FEED = 'FEED/UNLIKE_POST'; export const DELETE_COMMENT_FEED = 'FEED/DELETE_COMMENT'; +export const UNFOLLOW_USER_FEED = 'FEED/UNFOLLOW_USER_FEED'; +export const FOLLOW_USER_FEED = 'FEED/FOLLOW_USER_FEED'; + export type SetFeedUserAction = AppAction<typeof SET_FEED, Instalike.Post[]>; export type SetLikeFeedAction = AppAction<typeof LIKE_POST_FEED>; export type SetUnlikeFeedAction = AppAction<typeof UNLIKE_POST_FEED>; export type DeleteCommentFeedAction = AppAction<typeof DELETE_COMMENT_FEED>; +export type unfollowUserFeedAction = AppAction<typeof UNFOLLOW_USER_FEED>; +export type followUserFeedAction = AppAction<typeof FOLLOW_USER_FEED>; -export type FeedAction = SetFeedUserAction| SetUnlikeFeedAction | SetLikeFeedAction | SetLikeFeedAction | DeleteCommentFeedAction; +export type FeedAction = SetFeedUserAction| SetUnlikeFeedAction | SetLikeFeedAction | SetLikeFeedAction | DeleteCommentFeedAction | SetLikeFeedAction | unfollowUserFeedAction | followUserFeedAction; @@ -37,3 +42,13 @@ export const deleteCommentFeedAction = (id: number): DeleteCommentFeedAction => type: DELETE_COMMENT_FEED, payload: id, }); + +export const unfollowUserFeedAction = (data: number): unfollowUserFeedAction => ({ + type: UNFOLLOW_USER_FEED, + payload: data, +}); + +export const followUserFeedAction = (data: number): followUserFeedAction => ({ + type: FOLLOW_USER_FEED, + payload: data, +}); diff --git a/src/redux/feed/reducer.ts b/src/redux/feed/reducer.ts index 627f9e1feccb030e4f91fda4f01162956046da3e..9e336df7db53ab5df7efc0330629f72fde36f895 100644 --- a/src/redux/feed/reducer.ts +++ b/src/redux/feed/reducer.ts @@ -1,7 +1,7 @@ import { Instalike } from '@jmetterrothan/instalike'; import { Reducer } from 'redux'; -import { FeedAction, SET_FEED, LIKE_POST_FEED, UNLIKE_POST_FEED, DELETE_COMMENT_FEED } from './actions'; +import { FeedAction, SET_FEED, LIKE_POST_FEED, UNLIKE_POST_FEED, DELETE_COMMENT_FEED, FOLLOW_USER_FEED, UNFOLLOW_USER_FEED } from './actions'; type FeedState = { items: Instalike.Post[]; @@ -40,7 +40,7 @@ const feedReducer: Reducer<FeedState, FeedAction> = (state = initalState, action }), }; - case DELETE_COMMENT_FEED: + case DELETE_COMMENT_FEED: return { ...state, items: state.items.map((post) => { @@ -55,7 +55,27 @@ const feedReducer: Reducer<FeedState, FeedAction> = (state = initalState, action }; }), }; + case UNFOLLOW_USER_FEED: + return { + ...state, + items: state.items.map((post) => { + if (post.id == action.payload) { + return { ...post, owner: { ...post.owner, isFollowedByViewer: false } }; + } + return post; + }), + }; + case FOLLOW_USER_FEED: + return { + ...state, + items: state.items.map((post) => { + if (post.id == action.payload) { + return { ...post, owner: { ...post.owner, isFollowedByViewer: true } }; + } + return post; + }), + }; default: return state; } diff --git a/src/redux/feed/thunks.ts b/src/redux/feed/thunks.ts index 661326fb68c6030391001c1ecf726871ecf8f8a4..c77cf23510b093fb4fcc19108f9f8944204e1382 100644 --- a/src/redux/feed/thunks.ts +++ b/src/redux/feed/thunks.ts @@ -3,7 +3,7 @@ import { data } from 'autoprefixer'; // Autres fichiers import type { AppThunkAction } from '../types'; -import { setUserFeed, likePostFeedAction, unlikePostFeedAction, deleteCommentFeedAction } from './actions'; +import { setUserFeed, likePostFeedAction, unlikePostFeedAction, deleteCommentFeedAction, followUserFeedAction, unfollowUserFeedAction } from './actions'; // Users pour le feed @@ -51,3 +51,19 @@ export const deleteCommentFeedAsync = (postId: number, commentId: number): AppTh } }; }; + +// Follow someone +export const followUserFeedAsync = (postId: number, userId: number): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + await api.users.me.followers.follow(userId); + dispatch(followUserFeedAction(postId)); + }; +}; + +// Unfollow someone +export const unfollowUserFeedAsync = (postId: number, userId: number): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + await api.users.me.followers.unfollow(userId); + dispatch(unfollowUserFeedAction(postId)); + }; +}; \ No newline at end of file diff --git a/src/redux/post/actions.ts b/src/redux/post/actions.ts index 57815b59a151c1b1dcd280f6679ea5d8adff6991..984502d6ac28efcb01696a123e63650084e2b4de 100644 --- a/src/redux/post/actions.ts +++ b/src/redux/post/actions.ts @@ -6,15 +6,20 @@ export const SET_POST = 'POST/SET_POST'; export const REQUEST_POST_START = 'POST/REQUEST_FEED_START'; export const REQUEST_POST_SUCCESS = 'POST/REQUEST_FEED_SUCCESS'; export const REQUEST_POST_FAILURE = 'POST/REQUEST_FEED_FAILURE'; +export const UNFOLLOW_USER_POST = 'POST/UNFOLLOW_USER_POST'; +export const FOLLOW_USER_POST = 'POST/FOLLOW_USER_POST'; + export type setPostAction = AppAction<typeof SET_POST, Instalike.Post>; export type LoadPostStartAction = AppAction<typeof REQUEST_POST_START>; export type LoadPostEndSucessAction = AppAction<typeof REQUEST_POST_SUCCESS>; export type LoadPostEndFailureAction = AppAction<typeof REQUEST_POST_FAILURE>; +export type unfollowUserPostAction = AppAction<typeof UNFOLLOW_USER_POST>; +export type followUserPostAction = AppAction<typeof FOLLOW_USER_POST>; -export type PostAction = setPostAction | LoadPostStartAction | LoadPostEndSucessAction | LoadPostEndFailureAction; +export type PostAction = setPostAction | LoadPostStartAction | LoadPostEndSucessAction | LoadPostEndFailureAction | followUserPostAction | unfollowUserPostAction; export const setPost = (data: Instalike.Post): setPostAction => ({ @@ -35,4 +40,14 @@ export const sucessPostAction = (): LoadPostEndSucessAction => ({ export const failurePostAction = (): LoadPostEndFailureAction => ({ type: REQUEST_POST_FAILURE, payload: undefined, -}); \ No newline at end of file +}); + +export const unfollowUserPostAction = (): unfollowUserPostAction => ({ + type: UNFOLLOW_USER_POST, + payload: undefined, +}); + +export const followUserPostAction = (): followUserPostAction => ({ + type: FOLLOW_USER_POST, + payload: undefined, +}); diff --git a/src/redux/post/reducer.ts b/src/redux/post/reducer.ts index a92bde82f296734838c450b11d3d8baed438f5fa..2705ac965fb6e278f56c849ae3036a2c5205f9e2 100644 --- a/src/redux/post/reducer.ts +++ b/src/redux/post/reducer.ts @@ -2,7 +2,7 @@ import { Instalike } from '@jmetterrothan/instalike'; import { Reducer } from 'redux'; import { PostAction, SET_POST } from './actions'; -import { SetLikeFeedAction, SetUnlikeFeedAction, LIKE_POST_FEED, UNLIKE_POST_FEED } from '../feed/actions' +import { SetLikeFeedAction, SetUnlikeFeedAction, LIKE_POST_FEED, UNLIKE_POST_FEED, UNFOLLOW_USER_FEED, FOLLOW_USER_FEED } from '../feed/actions' type PostState = { data?: Instalike.Post; @@ -12,7 +12,7 @@ const intialState: PostState = { data: undefined, }; -const postReducer: Reducer<PostState, PostAction | SetLikeFeedAction | SetUnlikeFeedAction> = (state = intialState, action) => { +const postReducer: Reducer<PostState, PostAction | SetLikeFeedAction | SetUnlikeFeedAction | unfollowUserFeedAction | followUserFeedAction> = (state = intialState, action) => { switch (action.type) { case SET_POST: return { ...state, data: action.payload }; @@ -26,6 +26,16 @@ const postReducer: Reducer<PostState, PostAction | SetLikeFeedAction | SetUnlike return {...state, data: {...state.data, viewerHasLiked: false, likesCount: state.data.likesCount - 1 } }; } return state; + case UNFOLLOW_USER_FEED: + if(state.data) { + return { ...state, data: { ...state.data, owner: { ...state.data.owner, isFollowedByViewer: false } } }; + } + return state; + case FOLLOW_USER_FEED: + if(state.data) { + return { ...state, data: { ...state.data, owner: { ...state.data.owner, isFollowedByViewer: true } } }; + } + return state; default: return state; } diff --git a/src/redux/post/thunks.ts b/src/redux/post/thunks.ts index 32603febb2971a96ed717cdce8c7e7db166baeaa..74458928729ded9f59d7808374ba9e4da0cef8dc 100644 --- a/src/redux/post/thunks.ts +++ b/src/redux/post/thunks.ts @@ -1,8 +1,10 @@ import { Instalike } from '@jmetterrothan/instalike'; import { data } from 'autoprefixer'; +// AUTRES FICHIERS import { AppThunkAction } from '../types'; -import { failurePostAction, loadPostAction, setPost, sucessPostAction } from './actions'; +import { failurePostAction, loadPostAction, setPost, sucessPostAction, followUserPostAction, unfollowUserPostAction } from './actions'; +import { fetchFeedUserAsync } from '../feed/thunks'; // Calcul temps de publication d'un post / commentaire @@ -61,3 +63,27 @@ export const addPost = (resources: File[], location: string, caption: string): A } }; }; + +// Follow someone +export const followUserPostAsync = (userId: number): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + try { + await api.users.me.followers.follow(userId); + dispatch(followUserPostAction()); + } catch (e) { + throw e; + } + }; +}; + +// Unfollow someone +export const unfollowUserPostAsync = (userId: number): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + try { + await api.users.me.followers.unfollow(userId); + dispatch(unfollowUserPostAction()); + } catch (e) { + throw e; + } + }; +}; diff --git a/src/redux/suggestion/actions.ts b/src/redux/suggestion/actions.ts index b1ea9c2c8a09fef3e57fc95136ba60061353f690..afb8499476e71cf6ea21a4431a8d49bde0b7d529 100644 --- a/src/redux/suggestion/actions.ts +++ b/src/redux/suggestion/actions.ts @@ -3,15 +3,29 @@ import { AppAction } from '../types'; export const SET_SUGGESTION_FEED = 'USERSTORY/SET_FEED'; +export const FOLLOW_USER_SUGGESTION = 'USERSTORY/FOLLOW_USER'; +export const UNFOLLOW_USER_SUGGESTION = 'USERSTORY/UNFOLLOW_USER'; export type getSuggestionFeedAction = AppAction<typeof SET_SUGGESTION_FEED, Instalike.User[]>; +export type followUserSuggestionAction = AppAction<typeof FOLLOW_USER_SUGGESTION, Instalike.User>; +export type unfollowUserSuggestionAction = AppAction<typeof UNFOLLOW_USER_SUGGESTION, Instalike.User>; -export type SuggestionAction = getSuggestionFeedAction; +export type SuggestionAction = getSuggestionFeedAction | unfollowUserSuggestionAction | followUserSuggestionAction; export const setSuggestionAction = (data: Instalike.User[]): getSuggestionFeedAction => ({ type: SET_SUGGESTION_FEED, payload: data, -}); \ No newline at end of file +}); + +export const followUserSuggestionAction = (data: Instalike.User): followUserSuggestionAction => ({ + type: FOLLOW_USER_SUGGESTION, + payload: data, +}); + +export const unfollowUserSuggestionAction = (data: Instalike.User): unfollowUserSuggestionAction => ({ + type: UNFOLLOW_USER_SUGGESTION, + payload: data, +}); diff --git a/src/redux/suggestion/reducer.ts b/src/redux/suggestion/reducer.ts index cf827a49f8ab3196f1b4fa4023bf931a3542b247..7851ed489b1b9616174629a5949e0117ec92a55a 100644 --- a/src/redux/suggestion/reducer.ts +++ b/src/redux/suggestion/reducer.ts @@ -1,7 +1,7 @@ import { Instalike } from '@jmetterrothan/instalike'; import { Reducer } from 'redux'; -import { SET_SUGGESTION_FEED, SuggestionAction } from './actions'; +import { FOLLOW_USER_SUGGESTION, SET_SUGGESTION_FEED, SuggestionAction, UNFOLLOW_USER_SUGGESTION } from './actions'; type SuggestionState = { @@ -16,8 +16,27 @@ const initalState: SuggestionState = { const suggestionReducer: Reducer<SuggestionState, SuggestionAction> = (state = initalState, action) => { switch (action.type) { case SET_SUGGESTION_FEED: - console.log(action.payload); return { ...state, data: action.payload }; + case FOLLOW_USER_SUGGESTION: + return { + ...state, + data: state.data.map((user) => { + if (user === action.payload) { + return { ...user, isFollowedByViewer: true }; + } + return user; + }), + }; + case UNFOLLOW_USER_SUGGESTION: + return { + ...state, + data: state.data.map((user) => { + if (user === action.payload) { + return { ...user, isFollowedByViewer: false }; + } + return user; + }), + }; default: return state; } diff --git a/src/redux/suggestion/thunks.ts b/src/redux/suggestion/thunks.ts index c60d04a6daf614097fb07cf96e7582b7d130bf44..9860ba944a1c16d50fe76d44a813b1ae23d90751 100644 --- a/src/redux/suggestion/thunks.ts +++ b/src/redux/suggestion/thunks.ts @@ -1,14 +1,24 @@ +import { Instalike } from '@jmetterrothan/instalike'; import { AppThunkAction } from '../types'; -import { setSuggestionAction } from './actions'; +import { setSuggestionAction, followUserSuggestionAction, unfollowUserSuggestionAction } from './actions'; export const fetchSuggestionAsync = (): AppThunkAction<Promise<void>> => { return async (dispatch, getState, api) => { - try { - const { data } = await api.users.me.followSuggestions.fetch({ amount: 5 }); - dispatch(setSuggestionAction(data)); - } catch (e) { + const { data } = await api.users.me.followSuggestions.fetch({ amount: 5 }); + dispatch(setSuggestionAction(data)); + }; +}; - throw e; - } +export const followUserSuggestionAsync = (user: Instalike.User): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + await api.users.me.followers.follow(user.id); + dispatch(followUserSuggestionAction(user)); + }; +}; + +export const unfollowUserSuggestionAsync = (user: Instalike.User): AppThunkAction<Promise<void>> => { + return async (dispatch, getState, api) => { + await api.users.me.followers.unfollow(user.id); + dispatch(unfollowUserSuggestionAction(user)); }; }; diff --git a/src/views/FeedView.tsx b/src/views/FeedView.tsx index c6caa841a6131e3d1183bb32147ccce43c03ea60..f34767589c276301278acb9b40be2c009cd9fba2 100644 --- a/src/views/FeedView.tsx +++ b/src/views/FeedView.tsx @@ -47,7 +47,7 @@ return <> return ( <Suggestion key={user.id} - firstname={user.firstName} + user={user} ></Suggestion> ); }) @@ -60,7 +60,7 @@ return <> console.log(post) return ( - <Post key={post.id} + <Post post={post} key={post.id} postid={post.id} username={post.owner.userName} location={post.location} @@ -71,6 +71,7 @@ return <> likes={post.likesCount} comments={post.commentsCount} comment_post={post.previewComments} + inFeed={true} ></Post> ); })} diff --git a/src/views/PostView.tsx b/src/views/PostView.tsx index 770b0a15db2a5ebe956fd8b9ed900cac96b9acf8..fc54f6c6a5280d99ef022c1637c9d09bb19c7c8c 100644 --- a/src/views/PostView.tsx +++ b/src/views/PostView.tsx @@ -35,7 +35,7 @@ const PostView = () => { <div className="max-w-[640px] mx-auto py-16 px-4"> {/* A POST */} {post && ( - <Post key={post.id} + <Post post={post} key={post.id} postid={post.id} username={post.owner.userName} location={post.location} @@ -46,6 +46,7 @@ const PostView = () => { likes={post.likesCount} comments={post.commentsCount} comment_post={post.previewComments} + inFeed={true} ></Post> )} </div>