From 191a8ea8203b277d7ef6b52a5381450063b3dfd0 Mon Sep 17 00:00:00 2001 From: justice chimobi Date: Thu, 17 Oct 2024 11:47:58 +0100 Subject: [PATCH] feat: get followers/followings users, clean up code --- src/Route/index.tsx | 56 +++--- src/api/axiosInstance.ts | 47 ++--- src/api/endpoints/userEndpoints.ts | 6 +- src/components/ArticleCard/Articles.tsx | 2 +- src/components/FollowCard/index.tsx | 145 +++++++------- src/components/FollowSection/index.tsx | 111 +++++++++++ src/components/HeroSection/index.tsx | 8 +- src/components/NavBar/navBarLg.tsx | 1 + src/components/index.tsx | 2 + src/hooks/article/useDeleteSavaArticles.ts | 3 - src/hooks/article/useGetArticles.ts | 4 +- src/hooks/article/useGetPaginatedArticles.ts | 12 +- .../article/useGetRecommentedArticles.ts | 12 +- src/hooks/article/useGetSavedArticles.ts | 29 ++- src/hooks/auth/useSignOut.ts | 2 +- src/hooks/thread/useGetPaginatedThreads.ts | 12 +- src/hooks/thread/useGetThreads.ts | 4 +- src/hooks/user/useCreateFollowUser.ts | 3 + src/hooks/user/useCreateUnFollowUser.ts | 3 + ...llowUsers.ts => useGetAllUsersToFollow.ts} | 4 +- src/hooks/user/useGetFollowUserArticles.ts | 12 +- src/hooks/user/useGetFollowingUsers.ts | 54 +++++ .../components/ArticleActionButtons.tsx | 1 + src/pages/Articles/components/ArticleForm.tsx | 4 +- .../Articles/components/RelatedArticles.tsx | 9 +- src/pages/Articles/index.tsx | 16 +- src/pages/Articles/show/index.tsx | 4 +- src/pages/FollowPeople/index.tsx | 107 ++-------- src/pages/Home/Authenticated/ForYou/index.tsx | 3 +- src/pages/Home/Public/Articles/index.tsx | 3 +- src/pages/Home/Public/Threads/index.tsx | 3 +- src/pages/Threads/components/threadForm.tsx | 4 +- src/pages/Threads/index.tsx | 7 +- src/pages/Threads/show/index.tsx | 9 +- src/pages/Users/FollowingUsers/index.tsx | 134 +++++++++++++ src/pages/Users/Profile/index.tsx | 15 +- src/pages/Users/SavedArticles/index.tsx | 188 ++++++++++-------- src/pages/Users/show/articles.tsx | 3 +- src/pages/Users/show/index.tsx | 36 ++-- src/services/articles/index.ts | 78 ++++---- src/services/auth/index.tsx | 16 +- src/services/threads/index.ts | 48 +++-- src/services/user/index.ts | 69 +++++++ src/services/user/index.tsx | 71 ------- src/types/article/index.ts | 67 ++++++- src/types/common.ts | 12 ++ src/types/index.ts | 1 + src/types/thread/index.ts | 50 ++++- src/types/user/index.ts | 29 +++ src/utils/apiHelpers.ts | 12 ++ tsconfig.app.json | 1 + vite.config.ts | 3 +- 52 files changed, 967 insertions(+), 568 deletions(-) create mode 100644 src/components/FollowSection/index.tsx rename src/hooks/user/{useGetFollowUsers.ts => useGetAllUsersToFollow.ts} (85%) create mode 100644 src/hooks/user/useGetFollowingUsers.ts create mode 100644 src/pages/Users/FollowingUsers/index.tsx create mode 100644 src/services/user/index.ts delete mode 100644 src/services/user/index.tsx create mode 100644 src/types/common.ts create mode 100644 src/utils/apiHelpers.ts diff --git a/src/Route/index.tsx b/src/Route/index.tsx index 3732aff..600ee52 100644 --- a/src/Route/index.tsx +++ b/src/Route/index.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, Suspense, lazy } from 'react' +import { FunctionComponent } from 'react' import { createBrowserRouter, createRoutesFromElements, @@ -7,32 +7,31 @@ import { } from 'react-router-dom' import Layout from '@layout/index' -import { NotFound, Loading} from '@components/index' +import { NotFound } from '@components/index' import AuthRoute from './AuthRoute' import PrivateRoute from './privateRoute' - -const Articles = lazy(() => import('@pages/Articles')); -const CreateArticle = lazy(() => import('@pages/Articles/create')); -const EditArticle = lazy(() => import('@pages/Articles/edit')); -const ShowArticle = lazy(() => import('@pages/Articles/show')); -const SavedArticles = lazy(() => import('@pages/Users/SavedArticles')); -const Home = lazy(() => import('@pages/Home')); -const Threads = lazy(() => import('@pages/Threads')); -const CreateThread = lazy(() => import('@pages/Threads/create')); -const EditThread = lazy(() => import('@pages/Threads/edit')); -const ShowThread = lazy(() => import('@pages/Threads/show')); -const ArthoredArticles = lazy(() => import('@pages/Users/AuthoredViews/Articles')); -const ArthoredThreads = lazy(() => import('@pages/Users/AuthoredViews/Threads')); -const Profile = lazy(() => import('@pages/Users/Profile')); -const ProfileEdit = lazy(() => import('@pages/Users/Settings')); -const ShowUserPublicPosts = lazy(() => import('@pages/Users/show')); -const Search = lazy(() => import('@pages/Search')); - -const FollowPeople = lazy(() => import('@pages/FollowPeople')) - -const Login = lazy(() => import('@pages/Auth/Login')); -const Register = lazy(() => import('@pages/Auth/Register')); +import Articles from '@pages/Articles' +import CreateArticle from '@pages/Articles/create' +import EditArticle from '@pages/Articles/edit' +import ShowArticle from '@pages/Articles/show' +import SavedArticles from '@pages/Users/SavedArticles' +import Home from '@pages/Home' +import Threads from '@pages/Threads' +import CreateThread from '@pages/Threads/create' +import EditThread from '@pages/Threads/edit' +import ShowThread from '@pages/Threads/show' +import ArthoredArticles from '@pages/Users/AuthoredViews/Articles' +import ArthoredThreads from '@pages/Users/AuthoredViews/Threads' +import Profile from '@pages/Users/Profile' +import ProfileEdit from '@pages/Users/Settings' +import FollowingUsers from '@pages/Users/FollowingUsers' +import ShowUserPublicPosts from '@pages/Users/show' +import Search from '@pages/Search' +import FollowPeople from '@pages/FollowPeople' + +import Login from '@pages/Auth/Login' +import Register from '@pages/Auth/Register' const routes = createBrowserRouter( createRoutesFromElements( @@ -48,7 +47,7 @@ const routes = createBrowserRouter( } /> } /> - + } /> {/* private route */} @@ -64,6 +63,7 @@ const routes = createBrowserRouter( } /> } /> } /> + } /> {/* end private route */} @@ -78,11 +78,7 @@ const routes = createBrowserRouter( ) const AppRoutes: FunctionComponent = () => { - return ( - }> - - - ) + return ; } export default AppRoutes; \ No newline at end of file diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 3010d78..eb7b25d 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -1,9 +1,7 @@ import axios from 'axios' - import { API_BASE_URL } from '@api/constant' -axios.defaults.baseURL = API_BASE_URL; - +// Configure the base axios instance export const axiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { @@ -14,30 +12,27 @@ export const axiosInstance = axios.create({ withCredentials: true, }); -axiosInstance.interceptors.request.use( - (config) => { - const token = localStorage.getItem('ucType_'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); +// Request Interceptor to add Authorization header +const addAuthorizationHeader = (config: any) => { + const token = localStorage.getItem('ucType_'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; } -); - -axiosInstance.interceptors.response.use( - (response) => response, - (error) => { - if (axios.isAxiosError(error)) { - // Handle 401 (unauthorized) by removing token from localStorage - if (error.response?.status === 401 && localStorage.getItem('ucType_')) { - localStorage.removeItem('ucType_'); - location.href = '/auth/login' - } - return Promise.reject(error); + return config; +} +// Response Interceptor to handle errors +const handleErrorResponse = (error: any) => { + if (axios.isAxiosError(error)) { + // Handle 401 (unauthorized) by removing token from localStorage + if (error.response?.status === 401 && localStorage.getItem('ucType_')) { + localStorage.removeItem('ucType_'); + window.location.href = '/auth/login' } + return Promise.reject(error); } -); +} + +// Apply interceptors +axiosInstance.interceptors.request.use(addAuthorizationHeader, (error) => Promise.reject(error)); +axiosInstance.interceptors.response.use((response) => response, handleErrorResponse); \ No newline at end of file diff --git a/src/api/endpoints/userEndpoints.ts b/src/api/endpoints/userEndpoints.ts index abbd6f1..20206e7 100644 --- a/src/api/endpoints/userEndpoints.ts +++ b/src/api/endpoints/userEndpoints.ts @@ -14,10 +14,14 @@ export const PUBLIC_USER_ENDPOINT = `${API_BASE_URL}/users`; export const GET_THREE_USERS_ENDPOINT = `${API_BASE_URL}/users/get-three-users`; -export const GET_ALL_USERS_ENDPOINT = `${API_BASE_URL}/users/all-users`; +export const GET_ALL_USERS_TO_FOLLOW_ENDPOINT = `${API_BASE_URL}/users/all-users`; export const Get_FOLLOWING_USERS_ARTICLES_ENDPOINT = `${API_BASE_URL}/users/my-follow-users/articles`; export const FOLLOW_USERS_ENDPOINT = `${API_BASE_URL}/users`; export const ONFOLLOW_USERS_ENDPOINT = `${API_BASE_URL}/users`; + +export const GET_FOLLOWING_USERS_ENDPOINT = `${API_BASE_URL}/users/followings`; + +export const GET_FOLLOWERS_USERS_ENDPOINT = `${API_BASE_URL}/users/followers`; \ No newline at end of file diff --git a/src/components/ArticleCard/Articles.tsx b/src/components/ArticleCard/Articles.tsx index 2abd93c..ec02546 100644 --- a/src/components/ArticleCard/Articles.tsx +++ b/src/components/ArticleCard/Articles.tsx @@ -59,7 +59,7 @@ const ArticlesCard: FunctionComponent = ({ {articleImg} { const { data: people, isLoading } = useGetThreeCardUsers(); const { createFollowUserMutation } = useCreateFollowUser() const { createOnFollowUserMutation } = useCreateOnFollowUser(); + const { isOpen, onOpen, onClose } = useDisclosure(); const handleFollowUnfollow = (userId: string, following: boolean) => { - if (following) { + following ? createOnFollowUserMutation.mutate(userId) - } else { - createFollowUserMutation.mutate(userId) - } + : createFollowUserMutation.mutate(userId) } return ( - - {isLoading && ( - - - - )} + <> + + {isLoading && ( + + + + )} - {people && ( - <> - - People to follow - + {people && ( + <> + + People to follow + - - {people?.map((person: any, index: number) => ( - - - - - - - + + {people?.data?.map((person, index: number) => ( + + - {person?.fullname} + - {truncate(person?.bio, 25)} - - - {user && ( - - )} + + + {person?.fullname} + + {truncate(person?.bio, 25)} + + - {!user && ( - + {user && ( + + )} + + {!user && ( - - )} - - ))} + )} + + ))} + + + + See more suggestions + + + + + )} + - - - See more suggestions - - - - - )} - + + ) } diff --git a/src/components/FollowSection/index.tsx b/src/components/FollowSection/index.tsx new file mode 100644 index 0000000..ab191db --- /dev/null +++ b/src/components/FollowSection/index.tsx @@ -0,0 +1,111 @@ +import { FunctionComponent, Fragment } from 'react' +import { Link } from 'react-router-dom' +import { Avatar, Box, Flex, Heading, Spacer, Text } from '@chakra-ui/react' + +import { Button } from '@components/index' +import truncate from '@helpers/truncate' +import { useCreateFollowUser } from '@hooks/user/useCreateFollowUser' +import { useCreateOnFollowUser } from '@hooks/user/useCreateUnFollowUser' +import { useUser } from '@context/userContext' +import { Person } from 'src/types' + +interface IProps { + people: any; + hasMore: boolean; + fetchNext: () => void; + isFetching: boolean; +} + +const FollowSection: FunctionComponent = ({ + people, + hasMore, + fetchNext, + isFetching +}) => { + const { user } = useUser(); + const { createFollowUserMutation } = useCreateFollowUser() + const { createOnFollowUserMutation } = useCreateOnFollowUser(); + + const handleFollowUnfollow = (userId: string, following: boolean) => { + following + ? createOnFollowUserMutation.mutate(userId) // Unfollow the user + : createFollowUserMutation.mutate(userId); // Follow the user + } + + return ( + <> + {people && people?.map((page: any, index: number) => ( + + {(page?.data?.users || page?.data?.followings || page?.data?.followers)?.map((person: Person, index: number) => ( + + + + + + + + + {person?.fullname} + + + + + + {user && ( + + )} + + {!user && ( + + + + )} + + + + {truncate(person?.bio, 150)} + + + ))} + + ))} + + {hasMore && ( + + + + )} + + ) +} + +export default FollowSection; \ No newline at end of file diff --git a/src/components/HeroSection/index.tsx b/src/components/HeroSection/index.tsx index aef7ad5..6fc048c 100644 --- a/src/components/HeroSection/index.tsx +++ b/src/components/HeroSection/index.tsx @@ -31,27 +31,27 @@ const HeroSection: FunctionComponent = () => { Embark on a Delightful Journey of Growth With Community Experts diff --git a/src/components/NavBar/navBarLg.tsx b/src/components/NavBar/navBarLg.tsx index 8aff830..8ed6f70 100644 --- a/src/components/NavBar/navBarLg.tsx +++ b/src/components/NavBar/navBarLg.tsx @@ -52,6 +52,7 @@ const NavBarLg: FunctionComponent = () => { fontWeight={"bold"} as="h4" color={colors.primary} + size={{ base: "xs", md: "lg", lg: "2xl"}} > Learn Hub diff --git a/src/components/index.tsx b/src/components/index.tsx index 4e2032c..f9bc530 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -21,6 +21,7 @@ import LoginForm from '@components/LoginForm' import RegisterForm from '@components/RegisterForm' import ShowLoginModal from '@components/ShowLoginModal' import TextSpeech from '@components/TextSpeech' +import FollowSection from '@components/FollowSection' export { Alert, @@ -46,4 +47,5 @@ export { RegisterForm, ShowLoginModal, TextSpeech, + FollowSection, } \ No newline at end of file diff --git a/src/hooks/article/useDeleteSavaArticles.ts b/src/hooks/article/useDeleteSavaArticles.ts index 07fe2a2..d7a3840 100644 --- a/src/hooks/article/useDeleteSavaArticles.ts +++ b/src/hooks/article/useDeleteSavaArticles.ts @@ -12,11 +12,8 @@ export const useDeleteSaveArticle = () => { successNotification(data?.message) queryClient.invalidateQueries({ queryKey: ['article'] }); - queryClient.invalidateQueries({ queryKey: ['articles'] }); - queryClient.invalidateQueries({ queryKey: ['saved-articles'] }); - queryClient.invalidateQueries({ queryKey: ['recommented-articles'] }); }, onError: (error: any) => { diff --git a/src/hooks/article/useGetArticles.ts b/src/hooks/article/useGetArticles.ts index 7bec383..fc5d233 100644 --- a/src/hooks/article/useGetArticles.ts +++ b/src/hooks/article/useGetArticles.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query' -import { getAllArticles } from '@services/articles' +import { getArticles } from '@services/articles' export const useGetArticles = (limit: number) => { return useQuery({ queryKey: ['articles', limit], - queryFn: () => getAllArticles(limit), + queryFn: () => getArticles(limit), }) } \ No newline at end of file diff --git a/src/hooks/article/useGetPaginatedArticles.ts b/src/hooks/article/useGetPaginatedArticles.ts index e65c129..8d509ce 100644 --- a/src/hooks/article/useGetPaginatedArticles.ts +++ b/src/hooks/article/useGetPaginatedArticles.ts @@ -1,16 +1,8 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' -import { axiosInstance } from '@api/axiosInstance' -import { GET_PAGINATED_ARTICLES_ENDPOINT } from '@api/index' +import { getAllArticles } from '@services/articles' export const useGetPaginatedArticles = (limit: number = 0) => { - const fetchPaginatedArticles = async ({ pageParam = 0 }) => { - const res = await axiosInstance.get(`${GET_PAGINATED_ARTICLES_ENDPOINT}?limit=${limit}&page=${pageParam}`) - - const dataResponse = await res.data; - return { ...dataResponse, prevOffset: pageParam }; - }; - const { data: articlesResponse, fetchNextPage, @@ -20,7 +12,7 @@ export const useGetPaginatedArticles = (limit: number = 0) => { isSuccess } = useInfiniteQuery({ queryKey: ['articles', limit], - queryFn: fetchPaginatedArticles, + queryFn: ({ pageParam = 0 }) => getAllArticles(limit, pageParam), placeholderData: keepPreviousData, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/src/hooks/article/useGetRecommentedArticles.ts b/src/hooks/article/useGetRecommentedArticles.ts index 8c632dd..4c7ec4a 100644 --- a/src/hooks/article/useGetRecommentedArticles.ts +++ b/src/hooks/article/useGetRecommentedArticles.ts @@ -1,16 +1,8 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' -import { axiosInstance } from '@api/axiosInstance' -import { GET_RECOMMENDED_ARTICLES_ENDPOINT } from '@api/index' +import { getRecommentedArticles } from '@services/articles'; export const useGetRecommentedArticles = (limit: number = 0) => { - const fetchRecommentedArticles = async ({ pageParam = 0 }) => { - const res = await axiosInstance.get(`${GET_RECOMMENDED_ARTICLES_ENDPOINT}?limit=${limit}&page=${pageParam}`) - - const dataResponse = await res.data; - return { ...dataResponse, prevOffset: pageParam }; - }; - const { data: recommentedArticlesResponse, fetchNextPage, @@ -20,7 +12,7 @@ export const useGetRecommentedArticles = (limit: number = 0) => { isSuccess } = useInfiniteQuery({ queryKey: ['recommented-articles', limit], - queryFn: fetchRecommentedArticles, + queryFn: ({ pageParam = 0 }) => getRecommentedArticles(limit, pageParam), placeholderData: keepPreviousData, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/src/hooks/article/useGetSavedArticles.ts b/src/hooks/article/useGetSavedArticles.ts index 3647b48..3f0a545 100644 --- a/src/hooks/article/useGetSavedArticles.ts +++ b/src/hooks/article/useGetSavedArticles.ts @@ -1,10 +1,29 @@ -import { useQuery } from '@tanstack/react-query' +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' import { getSavedArticles } from '@services/articles' -export const useSavedArticles = () => { - return useQuery({ - queryKey: ['saved-articles'], - queryFn: getSavedArticles, +export const useSavedArticles = (limit: number = 0) => { + const { + data: saveArticlesResponse, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isSuccess + } = useInfiniteQuery({ + queryKey: ['saved-articles', limit], + queryFn: ({ pageParam = 0 }) => getSavedArticles(limit, pageParam), + placeholderData: keepPreviousData, + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (!lastPage?.data?.pagination?.next_page_url) { + return undefined; + } + return lastPage?.data?.pagination?.current_page + 1 + } }) + + const articles = saveArticlesResponse?.pages ?? null; + + return { articles, isLoading, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage } } \ No newline at end of file diff --git a/src/hooks/auth/useSignOut.ts b/src/hooks/auth/useSignOut.ts index 99a23f5..771bd03 100644 --- a/src/hooks/auth/useSignOut.ts +++ b/src/hooks/auth/useSignOut.ts @@ -14,7 +14,7 @@ export const useSignOut = () => { localStorage.removeItem('ucType_'); - location.href = "/?auth&session=signout"; + window.location.href = "/?auth&session=signout"; }, onError: (error: any) => { errorNotification(error?.response?.data?.message) diff --git a/src/hooks/thread/useGetPaginatedThreads.ts b/src/hooks/thread/useGetPaginatedThreads.ts index b253358..9bb1025 100644 --- a/src/hooks/thread/useGetPaginatedThreads.ts +++ b/src/hooks/thread/useGetPaginatedThreads.ts @@ -1,16 +1,8 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' -import { axiosInstance } from '@api/axiosInstance' -import { GET_PAGINATED_THREADS_ENDPOINT } from '@api/index' +import { getAllThreads } from '@services/threads' export const useGetPaginatedThreads = (limit: number = 0) => { - const fetchPaginatedThreads = async ({ pageParam = 0 }) => { - const res = await axiosInstance.get(`${GET_PAGINATED_THREADS_ENDPOINT}?limit=${limit}&page=${pageParam}`) - - const dataResponse = await res.data; - return { ...dataResponse, prevOffset: pageParam }; - }; - const { data: threadsResponse, fetchNextPage, @@ -20,7 +12,7 @@ export const useGetPaginatedThreads = (limit: number = 0) => { isSuccess } = useInfiniteQuery({ queryKey: ['threads', limit], - queryFn: fetchPaginatedThreads, + queryFn: ({ pageParam = 0 }) => getAllThreads(limit, pageParam), initialPageParam: 1, placeholderData: keepPreviousData, getNextPageParam: (lastPage) => { diff --git a/src/hooks/thread/useGetThreads.ts b/src/hooks/thread/useGetThreads.ts index 7d5cde9..e475bbf 100644 --- a/src/hooks/thread/useGetThreads.ts +++ b/src/hooks/thread/useGetThreads.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query' -import { getAllThreads } from '@services/threads' +import { getThreads } from '@services/threads' export const useGetThreads = (limit: number) => { return useQuery({ queryKey: ['threads', limit], - queryFn: () => getAllThreads(limit), + queryFn: () => getThreads(limit), }) } \ No newline at end of file diff --git a/src/hooks/user/useCreateFollowUser.ts b/src/hooks/user/useCreateFollowUser.ts index e87389f..ab7f1f9 100644 --- a/src/hooks/user/useCreateFollowUser.ts +++ b/src/hooks/user/useCreateFollowUser.ts @@ -17,7 +17,10 @@ export const useCreateFollowUser = () => { queryClient.invalidateQueries({ queryKey: ['articles'] }); queryClient.invalidateQueries({ queryKey: ['public-author-articles'] }); queryClient.invalidateQueries({ queryKey: ['recommented-articles'] }); + queryClient.invalidateQueries({ queryKey: ['saved-articles'] }); queryClient.invalidateQueries({ queryKey: ['get-followed-users-articles'] }); + queryClient.invalidateQueries({ queryKey: ['user-followers'] }); + queryClient.invalidateQueries({ queryKey: ['user-followings'] }); }, onError: (error: any) => { errorNotification(error?.response?.data?.message) diff --git a/src/hooks/user/useCreateUnFollowUser.ts b/src/hooks/user/useCreateUnFollowUser.ts index c90aa40..2af577b 100644 --- a/src/hooks/user/useCreateUnFollowUser.ts +++ b/src/hooks/user/useCreateUnFollowUser.ts @@ -17,7 +17,10 @@ export const useCreateOnFollowUser = () => { queryClient.invalidateQueries({ queryKey: ['articles'] }); queryClient.invalidateQueries({ queryKey: ['public-author-articles'] }); queryClient.invalidateQueries({ queryKey: ['recommented-articles'] }); + queryClient.invalidateQueries({ queryKey: ['saved-articles'] }); queryClient.invalidateQueries({ queryKey: ['get-followed-users-articles'] }); + queryClient.invalidateQueries({ queryKey: ['user-followers'] }); + queryClient.invalidateQueries({ queryKey: ['user-followings'] }); }, onError: (error: any) => { errorNotification(error?.response?.data?.message) diff --git a/src/hooks/user/useGetFollowUsers.ts b/src/hooks/user/useGetAllUsersToFollow.ts similarity index 85% rename from src/hooks/user/useGetFollowUsers.ts rename to src/hooks/user/useGetAllUsersToFollow.ts index 90ddd45..8533f45 100644 --- a/src/hooks/user/useGetFollowUsers.ts +++ b/src/hooks/user/useGetAllUsersToFollow.ts @@ -1,6 +1,6 @@ import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query' -import { getAllFollowUsers, getThreeUsersOnCard } from '@services/user' +import { getAllUsersToFollow, getThreeUsersOnCard } from '@services/user' export const useGetThreeCardUsers = () => { return useQuery({ @@ -19,7 +19,7 @@ export const useGetAllFollowUsers = (limit: number = 0) => { isSuccess } = useInfiniteQuery({ queryKey: ['follow-users'], - queryFn: ({ pageParam = 0 }) => getAllFollowUsers(limit, pageParam), + queryFn: ({ pageParam = 0 }) => getAllUsersToFollow(limit, pageParam), placeholderData: keepPreviousData, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/src/hooks/user/useGetFollowUserArticles.ts b/src/hooks/user/useGetFollowUserArticles.ts index 5f88b16..a6aa2d5 100644 --- a/src/hooks/user/useGetFollowUserArticles.ts +++ b/src/hooks/user/useGetFollowUserArticles.ts @@ -1,16 +1,8 @@ import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' -import { axiosInstance } from '@api/axiosInstance' -import { Get_FOLLOWING_USERS_ARTICLES_ENDPOINT } from '@api/index' +import { getFollowedUsersArticles } from '@services/user'; export const useGetFollowUsersArticles = (limit: number = 0) => { - const fetchPaginatedArticles = async ({ pageParam = 0 }) => { - const res = await axiosInstance.get(`${Get_FOLLOWING_USERS_ARTICLES_ENDPOINT}?limit=${limit}&page=${pageParam}`) - - const dataResponse = await res.data; - return { ...dataResponse, prevOffset: pageParam }; - }; - const { data: followingUsersArticlesResponse, fetchNextPage, @@ -20,7 +12,7 @@ export const useGetFollowUsersArticles = (limit: number = 0) => { isSuccess } = useInfiniteQuery({ queryKey: ['get-followed-users-articles', limit], - queryFn: fetchPaginatedArticles, + queryFn: ({ pageParam = 0 }) => getFollowedUsersArticles(limit, pageParam), placeholderData: keepPreviousData, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/src/hooks/user/useGetFollowingUsers.ts b/src/hooks/user/useGetFollowingUsers.ts new file mode 100644 index 0000000..aff7b3f --- /dev/null +++ b/src/hooks/user/useGetFollowingUsers.ts @@ -0,0 +1,54 @@ +import { getFollowersUsers, getFollowingUsers } from "@services/user"; +import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query"; + +export const useGetUsersFollowings = (limit: number = 0) => { + const { + data: peopleResponse, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isSuccess + } = useInfiniteQuery({ + queryKey: ['user-followings'], + queryFn: ({ pageParam = 0 }) => getFollowingUsers(limit, pageParam), + placeholderData: keepPreviousData, + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (!lastPage?.data?.pagination?.next_page_url) { + return undefined + } + return lastPage?.data?.pagination?.current_page + 1 + } + }) + + const people = peopleResponse?.pages ?? null; + + return { people, isLoading, isSuccess, fetchNextPage, isFetchingNextPage, hasNextPage } +} + +export const useGetUsersFollowers = (limit: number = 0) => { + const { + data: peopleResponse, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isSuccess + } = useInfiniteQuery({ + queryKey: ['user-followers'], + queryFn: ({ pageParam = 0 }) => getFollowersUsers(limit, pageParam), + placeholderData: keepPreviousData, + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (!lastPage?.data?.pagination?.next_page_url) { + return undefined + } + return lastPage?.data?.pagination?.current_page + 1 + } + }) + + const people = peopleResponse?.pages ?? null; + + return { people, isLoading, isSuccess, fetchNextPage, isFetchingNextPage, hasNextPage } +} \ No newline at end of file diff --git a/src/pages/Articles/components/ArticleActionButtons.tsx b/src/pages/Articles/components/ArticleActionButtons.tsx index 98993c9..9f235dc 100644 --- a/src/pages/Articles/components/ArticleActionButtons.tsx +++ b/src/pages/Articles/components/ArticleActionButtons.tsx @@ -24,6 +24,7 @@ const ArticleActionButtons: FunctionComponent = ({ }) => { const { user } = useUser(); const { onClose, onOpen, isOpen } = useDisclosure(); + // url to share on socials const url = window.location.href; const renderSaveButton = () => ( diff --git a/src/pages/Articles/components/ArticleForm.tsx b/src/pages/Articles/components/ArticleForm.tsx index f8ad577..af425f9 100644 --- a/src/pages/Articles/components/ArticleForm.tsx +++ b/src/pages/Articles/components/ArticleForm.tsx @@ -8,7 +8,7 @@ import { useCreateArticle } from '@hooks/article/useCreateArticle' import { useEditArticle } from '@hooks/article/useEditArticle' import { useImageUpload } from '@hooks/useImageUpload' import { errorNotification } from '@helpers/notification' -import { IArticle, IArticleFormProps } from 'src/types' +import { IArticleRequest, IArticleFormProps } from 'src/types' const ArticleForm: FunctionComponent = ({ titleValue = '', @@ -41,7 +41,7 @@ const ArticleForm: FunctionComponent = ({ return; } - const articleData: IArticle = { + const articleData: IArticleRequest = { thumbnail, title, content diff --git a/src/pages/Articles/components/RelatedArticles.tsx b/src/pages/Articles/components/RelatedArticles.tsx index 5be351b..7431016 100644 --- a/src/pages/Articles/components/RelatedArticles.tsx +++ b/src/pages/Articles/components/RelatedArticles.tsx @@ -4,25 +4,26 @@ import { Box, Grid, GridItem, Heading } from '@chakra-ui/react' import { LatestArticleCard } from '@components/index' import { useGetRelatedArticles } from '@hooks/article/useGetRelatedAuthorArticles' +import { IArticles } from 'src/types' const RelatedArticles: FunctionComponent = () => { const { id } = useParams(); const { data: relatedArticles, isSuccess } = useGetRelatedArticles(id!) return ( - - {relatedArticles && isSuccess && ( + + {relatedArticles && isSuccess && relatedArticles?.data.length > 0 && ( <> More articles from this author - {relatedArticles?.map((article: any, index: any) => ( + {relatedArticles?.data.map((article: IArticles, index: any) => ( { const { user } = useUser(); @@ -43,7 +44,7 @@ const Articles: FunctionComponent = () => { const handleFollowUnfollow = (userId: string, following: boolean) => { following ? createOnFollowUserMutation.mutate(userId) : createFollowUserMutation.mutate(userId); }; - + return ( <> @@ -86,11 +87,11 @@ const Articles: FunctionComponent = () => { px={{ base: "6px", md: "10px", lg: "15px" }} py="40px" > - {pinArticles?.map((article: any, index: any) => ( + {pinArticles?.data?.map((article: IArticles, index: number) => ( { {articles && isSuccess && articles?.map((page: any, pageIndex: number) => ( - {page?.data?.articles.map((article: any, index: number) => ( + {page?.data?.articles.map((article: ArticleData, index: number) => ( { )} - + {/* */} diff --git a/src/pages/Articles/show/index.tsx b/src/pages/Articles/show/index.tsx index 38874a4..51c1cd6 100644 --- a/src/pages/Articles/show/index.tsx +++ b/src/pages/Articles/show/index.tsx @@ -88,7 +88,7 @@ const ShowArticle: FunctionComponent = () => { - {truncate(data?.data?.author?.info_details?.bio, 230)} + {truncate(data?.data?.author?.info_details?.bio ?? "", 230)} @@ -159,7 +159,7 @@ const ShowArticle: FunctionComponent = () => { )} - + {data && isSuccess && } ); }; diff --git a/src/pages/FollowPeople/index.tsx b/src/pages/FollowPeople/index.tsx index b8ea4ad..af3cbcb 100644 --- a/src/pages/FollowPeople/index.tsx +++ b/src/pages/FollowPeople/index.tsx @@ -1,33 +1,16 @@ -import { Fragment } from 'react' -import { Link } from 'react-router-dom' -import { Avatar, Box, Card, CardBody, CardHeader, Flex, Heading, Spacer, Spinner, Text } from '@chakra-ui/react' +import { Box, Card, CardBody, CardHeader, Heading, Spinner } from '@chakra-ui/react' +import { Helmet } from 'react-helmet-async' -import { Button } from '@components/index' -import { useUser } from '@context/userContext' -import truncate from '@helpers/truncate' -import { useCreateFollowUser } from '@hooks/user/useCreateFollowUser' -import { useCreateOnFollowUser } from '@hooks/user/useCreateUnFollowUser' -import { useGetAllFollowUsers } from '@hooks/user/useGetFollowUsers' +import { useGetAllFollowUsers } from '@hooks/user/useGetAllUsersToFollow' import { colors } from '../../colors' -import { Helmet } from 'react-helmet-async' +import { FollowSection } from '@components/index' const FollowPeople = () => { - const { user } = useUser() const { people, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = useGetAllFollowUsers(15); - const { createFollowUserMutation } = useCreateFollowUser() - const { createOnFollowUserMutation } = useCreateOnFollowUser(); - - const handleFollowUnfollow = (userId: string, following: boolean) => { - if (following) { - createOnFollowUserMutation.mutate(userId) - } else { - createFollowUserMutation.mutate(userId) - } - } - + return ( <> - + People to follow | learn-hub @@ -44,77 +27,13 @@ const FollowPeople = () => { )} - {people && people?.map((page: any, index: number) => ( - - {page?.data?.users?.map((person: any, index: number) => ( - - - - - - - - - {person?.fullname} - - - - - {user && ( - - )} - - {!user && ( - - - - )} - - - - - {truncate(person?.bio, 150)} - - - ))} - - ))} - - {hasNextPage && ( - - - - )} + @@ -122,4 +41,4 @@ const FollowPeople = () => { ) } -export default FollowPeople \ No newline at end of file +export default FollowPeople; \ No newline at end of file diff --git a/src/pages/Home/Authenticated/ForYou/index.tsx b/src/pages/Home/Authenticated/ForYou/index.tsx index 8385730..a6df334 100644 --- a/src/pages/Home/Authenticated/ForYou/index.tsx +++ b/src/pages/Home/Authenticated/ForYou/index.tsx @@ -9,6 +9,7 @@ import { useCreateSaveArticle } from '@hooks/article/useCreateSaveArticles' import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' import { useCreateFollowUser } from '@hooks/user/useCreateFollowUser' import { useCreateOnFollowUser } from '@hooks/user/useCreateUnFollowUser' +import { ArticleData } from 'src/types' const ForYou = () => { const { user } = useUser(); @@ -46,7 +47,7 @@ const ForYou = () => { {articles && isSuccess && articles?.map((page: any, pageIndex: number) => ( - {page?.data?.articles.map((article: any, index: number) => ( + {page?.data?.articles.map((article: ArticleData, index: number) => ( { const { data: articles, isLoading, isSuccess } = useGetArticles(6) @@ -29,7 +30,7 @@ const HomeArticles: FunctionComponent = () => { px={{ base: "6px", md: "10px", lg: "15px" }} py="40px" > - {articles?.map((article: any, index: any) => ( + {articles?.data?.map((article: IArticles, index: number) => ( { const { data: threads, isLoading, isSuccess } = useGetThreads(8) @@ -28,7 +29,7 @@ const HomeThreads: FunctionComponent = () => { px={{ base: "6px", md: "10px", lg: "15px" }} py="40px" > - {threads?.map((thread: any, index: any) => ( + {threads?.data?.map((thread: IThreads, index: number) => ( = ({ titleValue = '', @@ -22,7 +22,7 @@ const ThreadForm: FunctionComponent = ({ const handleSubmitThread = (event: FormEvent) => { event.preventDefault(); - const threadData: IThread = { + const threadData: IThreadRequest = { title, content }; diff --git a/src/pages/Threads/index.tsx b/src/pages/Threads/index.tsx index 6d003dd..df782eb 100644 --- a/src/pages/Threads/index.tsx +++ b/src/pages/Threads/index.tsx @@ -113,7 +113,12 @@ const Threads: FunctionComponent = () => { - + {/* */} diff --git a/src/pages/Threads/show/index.tsx b/src/pages/Threads/show/index.tsx index 769a74c..4ecf016 100644 --- a/src/pages/Threads/show/index.tsx +++ b/src/pages/Threads/show/index.tsx @@ -24,7 +24,7 @@ const ShowThread: FunctionComponent = () => { const { data, isLoading, isSuccess, error } = useGetSingleThread(id!); - const truncateLenght = useBreakpointValue({base: 45, md: 30, lg: 70}); + const truncateLenght = useBreakpointValue({ base: 45, md: 30, lg: 70 }); if (error) return ; @@ -81,7 +81,12 @@ const ShowThread: FunctionComponent = () => { - + {/* */} diff --git a/src/pages/Users/FollowingUsers/index.tsx b/src/pages/Users/FollowingUsers/index.tsx new file mode 100644 index 0000000..ae4925f --- /dev/null +++ b/src/pages/Users/FollowingUsers/index.tsx @@ -0,0 +1,134 @@ +import { FunctionComponent, useEffect, useState } from 'react' +import { + Box, + Card, + CardBody, + Container, + Heading, + Tab, + TabIndicator, + TabList, + TabPanel, + TabPanels, + Tabs, + Text +} from '@chakra-ui/react' + +import { colors } from '../../../colors' +import FollowSection from '@components/FollowSection' +import { useGetUsersFollowers, useGetUsersFollowings } from '@hooks/user/useGetFollowingUsers' +import Loading from '@components/Loading' +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { useUser } from '@context/userContext' + +const FollowingUsers: FunctionComponent = () => { + const location = useLocation(); + const navigate = useNavigate(); + + const [tabIndex, setTabIndex] = useState(0); + + useEffect(() => { + if (location.pathname.includes("/me/users/followings")) { + setTabIndex(1); + } else if (location.pathname.includes("/me/users/followers")) { + setTabIndex(0); + } + }, [location]); + + const handleTabsChange = (index: number) => { + setTabIndex(index); + navigate(index === 1 ? "/me/users/followings" : "/me/users/followers"); + } + + const { user } = useUser(); + const { + people: followings, + isLoading: isFollowingLoading, + isSuccess: isFollowingSuccess, + hasNextPage: hasNextPageFollowings, + isFetchingNextPage: isFetchingNextPageFollowings, + fetchNextPage: fetchNextPageFollowings + } = useGetUsersFollowings(15); + + const { + people: followers, + isLoading: isFollowersLoading, + isSuccess: isFollowersSuccess, + hasNextPage: hasNextPageFollowers, + isFetchingNextPage: isFetchingNextPageFollowers, + fetchNextPage: fetchNextPageFollowers + } = useGetUsersFollowers(15); + + const isFollowersEmpty = !followers || followers.every((page: any) => !page?.data?.followers?.length); + const isFollowingsEmpty = !followings || followings.every((page: any) => !page?.data?.followings?.length); + + return ( + + + + Back + + + + {isFollowingLoading && isFollowersLoading && } + + handleTabsChange(index)} + > + + followers + followings + + + + + {isFollowersEmpty && isFollowersSuccess && ( + + No followers found + + )} + {followers && isFollowersSuccess && ( + + )} + + + + {isFollowingsEmpty && isFollowingSuccess && ( + + You're not following any users yet! + + )} + + {followings && isFollowingSuccess && ( + + )} + + + + + + + + ) +} + +export default FollowingUsers; \ No newline at end of file diff --git a/src/pages/Users/Profile/index.tsx b/src/pages/Users/Profile/index.tsx index 3b5191c..1cd172d 100644 --- a/src/pages/Users/Profile/index.tsx +++ b/src/pages/Users/Profile/index.tsx @@ -79,11 +79,16 @@ const Profile: FunctionComponent = () => { {user?.data?.state}, {user?.data?.country} - {`${user?.data?.followers} ${user?.data?.followers! > 1 ? 'Followers' : 'Follower'}`} + + + {`${user?.data?.followers} ${user?.data?.followers! > 1 ? 'Followers' : 'Follower'}`} + {" - "} - {user?.data?.followings} following + + {user?.data?.followings} following + @@ -180,11 +185,7 @@ const Profile: FunctionComponent = () => { - {/* - - */} - - + diff --git a/src/pages/Users/SavedArticles/index.tsx b/src/pages/Users/SavedArticles/index.tsx index 253e9b0..6563cbf 100644 --- a/src/pages/Users/SavedArticles/index.tsx +++ b/src/pages/Users/SavedArticles/index.tsx @@ -1,109 +1,141 @@ +import { FunctionComponent, Fragment } from 'react' import { Link } from 'react-router-dom' import { Avatar, Box, Container, Flex, Heading, Text } from '@chakra-ui/react' import { Helmet } from 'react-helmet-async' -import { ArticlesCard, Skeleton } from '@components/index' +import { ArticlesCard, Button, Skeleton } from '@components/index' import { useUser } from '@context/userContext' import { useSavedArticles } from '@hooks/article/useGetSavedArticles' import { colors } from '../../../colors' import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' +import { ArticleData } from 'src/types' +import { useCreateFollowUser } from '@hooks/user/useCreateFollowUser' +import { useCreateOnFollowUser } from '@hooks/user/useCreateUnFollowUser' -const SavedArticles = () => { +const SavedArticles: FunctionComponent = () => { const { user } = useUser(); - const { data: savedArticles, isLoading } = useSavedArticles(); - const { deleteSaveArticleMutation} = useDeleteSaveArticle() + const { articles, isLoading, isSuccess, fetchNextPage, isFetchingNextPage, hasNextPage } = useSavedArticles(25); + const { deleteSaveArticleMutation } = useDeleteSaveArticle(); + const { createFollowUserMutation } = useCreateFollowUser() + const { createOnFollowUserMutation } = useCreateOnFollowUser(); const handleUnSaveArticle = (articleId: string) => { - deleteSaveArticleMutation.mutate(articleId); + deleteSaveArticleMutation.mutate(articleId); } + const handleFollowUnfollow = (userId: string, following: boolean) => { + following ? createOnFollowUserMutation.mutate(userId) : createFollowUserMutation.mutate(userId); + }; + return ( <> - + {`${user?.data?.fullname} - My reading lists | learn-hub`} - - - - - - {user?.data?.fullname} - + + + + + + {user?.data?.fullname} + - - Reading Lists - + + Reading Lists + - - {isLoading && } + + {isLoading && } - {savedArticles && savedArticles?.map((article: any, index: number) => ( - handleUnSaveArticle(article?.id)} - /> - ))} + {articles && isSuccess && articles?.map((page: any, pageIndex: number) => ( + + {page?.data?.articles?.map((article: ArticleData, index: number) => ( + handleFollowUnfollow(article?.author?.id, article?.author?.is_following)} + saveUnsavedArticle={() => handleUnSaveArticle(article?.id)} + /> + ))} + + ))} - {savedArticles?.length === 0 && ( - Your saving lists of articles will appear here! - )} + {hasNextPage && ( + + + + )} + - - - - + + + - {user?.data?.fullname} - {user?.data?.bio} - - Edit Profile - + {user?.data?.fullname} + {user?.data?.bio} + + Edit Profile + + - - + ) } diff --git a/src/pages/Users/show/articles.tsx b/src/pages/Users/show/articles.tsx index 84d243a..73deb71 100644 --- a/src/pages/Users/show/articles.tsx +++ b/src/pages/Users/show/articles.tsx @@ -10,6 +10,7 @@ import { useCreateSaveArticle } from '@hooks/article/useCreateSaveArticles' import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' import { useCreateFollowUser } from '@hooks/user/useCreateFollowUser' import { useCreateOnFollowUser } from '@hooks/user/useCreateUnFollowUser' +import { ArticleData } from 'src/types' const PublicUserArticles: FunctionComponent = () => { const { username } = useParams(); @@ -64,7 +65,7 @@ const PublicUserArticles: FunctionComponent = () => { {articles && isSuccess && articles?.map((page: any, pageIndex: number) => ( - {page?.data?.articles.map((article: any, index: number) => ( + {page?.data?.articles.map((article: ArticleData, index: number) => ( { const { data, isLoading } = useGetPublicUser(username!) const { createFollowUserMutation } = useCreateFollowUser() const { createOnFollowUserMutation } = useCreateOnFollowUser(); + const { isOpen, onOpen, onClose } = useDisclosure(); - const handleFollowUnfollow = (userId: string, is_following: boolean|undefined) => { - if (is_following) { - createOnFollowUserMutation.mutate(userId) - } else { - createFollowUserMutation.mutate(userId) - } + const handleFollowUnfollow = (userId: string, is_following: boolean | undefined) => { + is_following ? createOnFollowUserMutation.mutate(userId) : createFollowUserMutation.mutate(userId); } if (isLoading) return @@ -109,7 +107,6 @@ const ShowUserPublicPosts: FunctionComponent = () => { {`${userData?.followers} ${userData?.followers! > 1 ? 'Followers' : 'Follower'}`} - {userData?.profile_headlines} @@ -128,16 +125,15 @@ const ShowUserPublicPosts: FunctionComponent = () => { )} {!user && ( - - - + )} )} @@ -148,6 +144,8 @@ const ShowUserPublicPosts: FunctionComponent = () => { + + ) } diff --git a/src/services/articles/index.ts b/src/services/articles/index.ts index 5e6dd00..c870a97 100644 --- a/src/services/articles/index.ts +++ b/src/services/articles/index.ts @@ -9,80 +9,72 @@ import { DELETE_SAVE_ARTICLE_ENDPOINT, EDIT_ARTICLE_ENDPOINT, GET_ALL_ARTICLES_ENDPOINT, + GET_PAGINATED_ARTICLES_ENDPOINT, GET_PINNED_ARTICLES_ENDPOINT, GET_RECOMMENDED_ARTICLES_ENDPOINT, GET_RELATED_AUTHOR_ARTICLES_ENDPOINT, GET_SAVED_ARTICLES_ENDPOINT, GET_SINGLE_ARTICLE_ENDPOINT } from '@api/index' -import { IArticle } from 'src/types' +import { handleResponse, getWithPagination } from '@utils/apiHelpers' +import { Article, IArticleRequest, IArticlesResponse, MessageResponse } from 'src/types' -export const createArticle = async (data: IArticle) => { - const response = await axiosInstance.post(CREATE_ARTICLE_ENDPOINT, data); - return response.data; +export const createArticle = async (data: IArticleRequest) => { + return await handleResponse(axiosInstance.post(CREATE_ARTICLE_ENDPOINT, data)); } -export const editArticle = async ({ data, id }: { data: IArticle, id: any }) => { - const response = await axiosInstance.patch(`${EDIT_ARTICLE_ENDPOINT}/${id}`, data); - return response.data; -} - -export const getAllArticles = async (limit: number) => { - const response = await axiosInstance.get(`${GET_ALL_ARTICLES_ENDPOINT}?limit=${limit}`); - return response.data.data; -} - -export const getSingleArticle = async (id: string) => { - const response = await axiosInstance.get(`${GET_SINGLE_ARTICLE_ENDPOINT}/${id}`); - return response.data; +export const editArticle = async ({ data, id }: { data: IArticleRequest, id: string }) => { + return await handleResponse(axiosInstance.patch(`${EDIT_ARTICLE_ENDPOINT}/${id}`, data)); } export const deleteArticle = async (id: string) => { - const response = await axiosInstance.delete(`${DELETE_ARTICLE_ENDPOINT}/${id}`); - return response.data; + return await handleResponse(axiosInstance.delete(`${DELETE_ARTICLE_ENDPOINT}/${id}`)); } export const createArticleComment = async ({ data, id }: { data: any, id: any }) => { - const response = await axiosInstance.post(`${CREATE_ARTICLE_COMMENT_ENDPOINT}/${id}/comments`, data); - return response.data; + return await handleResponse(axiosInstance.post(`${CREATE_ARTICLE_COMMENT_ENDPOINT}/${id}/comments`, data)); } export const createArticleLike = async ({ id }: { id: string }) => { - const response = await axiosInstance.post(`${ARTICLE_LIKE_ENDPOINT}/${id}/likes`); - return response.data; + return await handleResponse(axiosInstance.post(`${ARTICLE_LIKE_ENDPOINT}/${id}/likes`)); } export const createArticleDisLike = async ({ id }: { id: string }) => { - const response = await axiosInstance.delete(`${ARTICLE_DISLIKE_ENDPOINT}/${id}/dislikes`); - return response.data; + return await handleResponse(axiosInstance.delete(`${ARTICLE_DISLIKE_ENDPOINT}/${id}/dislikes`)); } -export const getRecommentedArticles = async (limit: number) => { - const response = await axiosInstance.get(`${GET_RECOMMENDED_ARTICLES_ENDPOINT}?limit=${limit}`); - return response.data.data; +export const createSaveArticle = async (article_id: string) => { + return await handleResponse(axiosInstance.post(`${CREATE_SAVE_ARTICLE_ENDPOINT}/${article_id}`)); } -export const getPinnedArticles = async () => { - const response = await axiosInstance.get(GET_PINNED_ARTICLES_ENDPOINT); - return response.data.data; +export const deleteSaveArticle = async (article_id: string) => { + return await handleResponse(axiosInstance.delete(`${DELETE_SAVE_ARTICLE_ENDPOINT}/${article_id}`)); } -export const createSaveArticle = async (article_id: string) => { - const response = await axiosInstance.post(`${CREATE_SAVE_ARTICLE_ENDPOINT}/${article_id}`); - return response.data; -}; +export const getSingleArticle = async (id: string) => { + return await handleResponse
(axiosInstance.get(`${GET_SINGLE_ARTICLE_ENDPOINT}/${id}`)); +} -export const deleteSaveArticle = async (article_id: string) => { - const response = await axiosInstance.delete(`${DELETE_SAVE_ARTICLE_ENDPOINT}/${article_id}`); - return response.data; +export const getPinnedArticles = async () => { + return await handleResponse(axiosInstance.get(GET_PINNED_ARTICLES_ENDPOINT)); } -export const getSavedArticles = async () => { - const response = await axiosInstance.get(GET_SAVED_ARTICLES_ENDPOINT); - return response.data.data; +export const getRecommentedArticles = async (limit: number, page: number) => { + return await getWithPagination(GET_RECOMMENDED_ARTICLES_ENDPOINT, limit, page); +} + +export const getSavedArticles = async (limit: number, page: number) => { + return await getWithPagination(GET_SAVED_ARTICLES_ENDPOINT, limit, page); } export const getRelatedArticlesArticles = async (id: string) => { - const response = await axiosInstance.get(`${GET_RELATED_AUTHOR_ARTICLES_ENDPOINT}/${id}/arthored-related-articles`); - return response.data.data; + return await handleResponse(axiosInstance.get(`${GET_RELATED_AUTHOR_ARTICLES_ENDPOINT}/${id}/arthored-related-articles`)); +} + +export const getArticles = async (limit: number) => { + return await handleResponse(axiosInstance.get(`${GET_ALL_ARTICLES_ENDPOINT}?limit=${limit}`)); } + +export const getAllArticles = async (limit: number, page: number) => { + return await getWithPagination(GET_PAGINATED_ARTICLES_ENDPOINT, limit, page); +} \ No newline at end of file diff --git a/src/services/auth/index.tsx b/src/services/auth/index.tsx index d100639..6a33767 100644 --- a/src/services/auth/index.tsx +++ b/src/services/auth/index.tsx @@ -4,6 +4,7 @@ import { LOGOUT_ENDPOINT, REGISTER_ENDPOINT, } from '@api/index' +import { handleResponse } from '@utils/apiHelpers' import { ISignup, ISignin, @@ -11,17 +12,14 @@ import { MessageResponse } from 'src/types' -export const signupUser = async (data: ISignup): Promise => { - const response = await axiosInstance.post(REGISTER_ENDPOINT, data); - return response.data; +export const signupUser = (data: ISignup) => { + return handleResponse(axiosInstance.post(REGISTER_ENDPOINT, data)); } -export const signinUser = async (data: ISignin): Promise => { - const response = await axiosInstance.post(LOGIN_ENDPOINT, data); - return response.data; +export const signinUser = (data: ISignin) => { + return handleResponse(axiosInstance.post(LOGIN_ENDPOINT, data)); } -export const signoutUser = async () => { - const response = await axiosInstance.post(LOGOUT_ENDPOINT, null); - return response.data +export const signoutUser = () => { + return handleResponse(axiosInstance.post(LOGOUT_ENDPOINT, null)); } \ No newline at end of file diff --git a/src/services/threads/index.ts b/src/services/threads/index.ts index 9fdbf04..bc61355 100644 --- a/src/services/threads/index.ts +++ b/src/services/threads/index.ts @@ -8,45 +8,43 @@ import { CREATE_THREAD_COMMENT_ENDPOINT, THREAD_DISLIKE_ENDPOINT, THREAD_LIKE_ENDPOINT, + GET_PAGINATED_THREADS_ENDPOINT, } from '@api/index' -import { IThread } from 'src/types' +import { handleResponse, getWithPagination } from '@utils/apiHelpers' +import { IThreadRequest, IThreadResponse, MessageResponse, Thread } from 'src/types' -export const createThread = async (data: IThread) => { - const response = await axiosInstance.post(CREATE_THREAD_ENDPOINT, data); - return response.data; +export const createThread = async (data: IThreadRequest) => { + return await handleResponse(axiosInstance.post(CREATE_THREAD_ENDPOINT, data)); } -export const editThread = async ({ data, id }: { data: IThread, id: string }) => { - const response = await axiosInstance.patch(`${EDIT_THREAD_ENDPOINT}/${id}`, data); - return response.data; +export const editThread = async ({ data, id }: { data: IThreadRequest, id: string }) => { + return await handleResponse(axiosInstance.patch(`${EDIT_THREAD_ENDPOINT}/${id}`, data)); } -export const getAllThreads = async (limit: number) => { - const response = await axiosInstance.get(`${GET_ALL_THREADS_ENDPOINT}?limit=${limit}`); - return response.data.data; +export const deleteThread = async (id: string) => { + return await handleResponse(axiosInstance.delete(`${DELETE_THREAD_ENDPOINT}/${id}`)); } -export const getSingleThread = async (id: string) => { - const response = await axiosInstance.get(`${GET_SINGLE_THREAD_ENDPOINT}/${id}`); - return response.data; +export const createThreadComment = async ({ data, id }: { data: any, id: string }) => { + return await handleResponse(axiosInstance.post(`${CREATE_THREAD_COMMENT_ENDPOINT}/${id}/comments`, data)); } -export const deleteThread = async (id: string) => { - const response = await axiosInstance.delete(`${DELETE_THREAD_ENDPOINT}/${id}`); - return response.data; +export const createThreadLike = async ({ id }: { id: string }) => { + return await handleResponse(axiosInstance.post(`${THREAD_LIKE_ENDPOINT}/${id}/likes`)); } -export const createThreadComment = async ({ data, id }: { data: any, id: string }) => { - const response = await axiosInstance.post(`${CREATE_THREAD_COMMENT_ENDPOINT}/${id}/comments`, data); - return response.data; +export const createThreadDisLike = async ({ id }: { id: string }) => { + return await handleResponse(axiosInstance.delete(`${THREAD_DISLIKE_ENDPOINT}/${id}/dislikes`)); } -export const createThreadLike = async ({id}: {id: string}) => { - const response = await axiosInstance.post(`${THREAD_LIKE_ENDPOINT}/${id}/likes`); - return response.data; +export const getSingleThread = async (id: string) => { + return await handleResponse(axiosInstance.get(`${GET_SINGLE_THREAD_ENDPOINT}/${id}`)); } -export const createThreadDisLike = async ({id}: {id: string}) => { - const response = await axiosInstance.delete(`${THREAD_DISLIKE_ENDPOINT}/${id}/dislikes`); - return response.data; +export const getThreads = async (limit: number) => { + return await handleResponse(axiosInstance.get(`${GET_ALL_THREADS_ENDPOINT}?limit=${limit}`)); } + +export const getAllThreads = async (limit: number, page: number) => { + return await getWithPagination(GET_PAGINATED_THREADS_ENDPOINT, limit, page); +} \ No newline at end of file diff --git a/src/services/user/index.ts b/src/services/user/index.ts new file mode 100644 index 0000000..96f37fd --- /dev/null +++ b/src/services/user/index.ts @@ -0,0 +1,69 @@ +import { axiosInstance } from '@api/axiosInstance' +import { + DELETE_ACCOUNT_ENDPOINT, + FOLLOW_USERS_ENDPOINT, + GET_ALL_USERS_TO_FOLLOW_ENDPOINT, + GET_FOLLOWERS_USERS_ENDPOINT, + Get_FOLLOWING_USERS_ARTICLES_ENDPOINT, + GET_FOLLOWING_USERS_ENDPOINT, + GET_THREE_USERS_ENDPOINT, + ONFOLLOW_USERS_ENDPOINT, + PUBLIC_USER_ENDPOINT, + UPDATE_PASSWORD_ENDPOINT, + UPDATE_PROFILE_ENDPOINT, + UPLOAD_PROFILE_AVATAR_ENDPOINT, + USER_ENDPOINT, +} from '@api/index' +import { getWithPagination, handleResponse } from '@utils/apiHelpers' +import { + IPassword, + IUser, + IUserProfile, + IUserProfileAvatar, + MessageResponse, + PeopleResponse +} from 'src/types' + +// User-related API calls +export const getUser = async () => { + return await handleResponse(axiosInstance.get(USER_ENDPOINT)); +} +export const getPublicUser = async (username: string) => { + return await handleResponse(axiosInstance.get(`${PUBLIC_USER_ENDPOINT}/${username}`)); +} +// Profile-related API calls +export const updateProfile = async (data: IUserProfile) => { + return await handleResponse(axiosInstance.patch(UPDATE_PROFILE_ENDPOINT, data)); +} +export const updatePassword = async (data: IPassword) => { + return await handleResponse(axiosInstance.patch(UPDATE_PASSWORD_ENDPOINT, data)); +} +export const uploadProfileAvatar = async (data: IUserProfileAvatar) => { + return await handleResponse(axiosInstance.patch(UPLOAD_PROFILE_AVATAR_ENDPOINT, data)); +} +export const deleteAccount = async () => { + return await handleResponse(axiosInstance.delete(DELETE_ACCOUNT_ENDPOINT)); +} +// Follow-related API calls +export const getThreeUsersOnCard = async () => { + return await handleResponse(axiosInstance.get(GET_THREE_USERS_ENDPOINT)); +} +export const getAllUsersToFollow = async (limit: number, page: number) => { + return await getWithPagination(GET_ALL_USERS_TO_FOLLOW_ENDPOINT, limit, page); +} +export const getFollowingUsers = async (limit: number, page: number) => { + return await getWithPagination(GET_FOLLOWING_USERS_ENDPOINT, limit, page); +} +export const getFollowersUsers = async (limit: number, page: number) => { + return await getWithPagination(GET_FOLLOWERS_USERS_ENDPOINT, limit, page); +} +export const getFollowedUsersArticles = async (limit: number, page: number) => { + return await getWithPagination(Get_FOLLOWING_USERS_ARTICLES_ENDPOINT, limit, page); +} +// Follow/unfollow actions +export const createFollowUser = async (id: string) => { + return await handleResponse(axiosInstance.post(`${FOLLOW_USERS_ENDPOINT}/${id}/follow`)); +} +export const createOnFollowUser = async (id: string) => { + return await handleResponse(axiosInstance.post(`${ONFOLLOW_USERS_ENDPOINT}/${id}/unfollow`)); +} \ No newline at end of file diff --git a/src/services/user/index.tsx b/src/services/user/index.tsx deleted file mode 100644 index 9e13dbb..0000000 --- a/src/services/user/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { axiosInstance } from '@api/axiosInstance' -import { - DELETE_ACCOUNT_ENDPOINT, - FOLLOW_USERS_ENDPOINT, - GET_ALL_USERS_ENDPOINT, - GET_THREE_USERS_ENDPOINT, - ONFOLLOW_USERS_ENDPOINT, - PUBLIC_USER_ENDPOINT, - UPDATE_PASSWORD_ENDPOINT, - UPDATE_PROFILE_ENDPOINT, - UPLOAD_PROFILE_AVATAR_ENDPOINT, - USER_ENDPOINT, -} from '@api/index' -import { - IPassword, - IUser, - IUserProfile, - IUserProfileAvatar, - MessageResponse -} from 'src/types' - -export const getUser = async (): Promise => { - const response = await axiosInstance.get(USER_ENDPOINT); - return response.data; -} - -export const updatePassword = async (data: IPassword): Promise => { - const response = await axiosInstance.patch(UPDATE_PASSWORD_ENDPOINT, data); - return response.data; -} - -export const updateProfile = async (data: IUserProfile): Promise => { - const response = await axiosInstance.patch(UPDATE_PROFILE_ENDPOINT, data); - return response.data; -} - -export const uploadProfileAvatar = async (data: IUserProfileAvatar): Promise => { - const response = await axiosInstance.patch(UPLOAD_PROFILE_AVATAR_ENDPOINT, data); - return response.data; -} - -export const deleteAccount = async () => { - const response = await axiosInstance.delete(DELETE_ACCOUNT_ENDPOINT); - return response.data; -} - -export const getPublicUser = async (username: string): Promise => { - const response = await axiosInstance.get(`${PUBLIC_USER_ENDPOINT}/${username}`); - return response.data; -} - -export const getThreeUsersOnCard = async () => { - const response = await axiosInstance.get(GET_THREE_USERS_ENDPOINT); - return response.data.data; -} - -export const getAllFollowUsers = async (limit: number, page: number) => { - const response = await axiosInstance.get(`${GET_ALL_USERS_ENDPOINT}?limit=${limit}&page=${page}`); - const dataResponse = await response.data; - return {...dataResponse, prevOffset: page} -} - -export const createFollowUser = async (id: string) => { - const response = await axiosInstance.post(`${FOLLOW_USERS_ENDPOINT}/${id}/follow`); - return response.data; -} - -export const createOnFollowUser = async (id: string) => { - const response = await axiosInstance.post(`${ONFOLLOW_USERS_ENDPOINT}/${id}/unfollow`); - return response.data; -} \ No newline at end of file diff --git a/src/types/article/index.ts b/src/types/article/index.ts index 87fba50..1442067 100644 --- a/src/types/article/index.ts +++ b/src/types/article/index.ts @@ -1,17 +1,18 @@ import { FormEvent } from 'react' +import { Author, AuthorDetails, CreatedAt } from 'src/types' -export interface IArticle { +export interface IArticleRequest { title: string; content: string; thumbnail: string; } export interface IArticleFormProps { - titleValue?: string; - thumbnailValue?: string; - contentValue?: string; - isEditing?: boolean - id?: any + titleValue: string; + thumbnailValue: string; + contentValue: string; + isEditing: boolean + id?: string; } export interface ArticleActionButtonsProps { @@ -46,4 +47,58 @@ export interface IArticleHeroProps { read_time: string; date: string; followUser: () => void; +} + +export interface Article { + data: ArticleData; +} + +export interface ArticleData { + id: string; + title: string; + slug: string; + content: string; + thumbnail: string; + isOwner: boolean; + read_time: string; + is_saved: boolean; + author: Author; + created_at: CreatedAt; + article_like_counts: number; + article_comment_counts: number; + user_liked_article: boolean; + article_comments: ArticleComment[]; +} + +export interface ArticleComment { + id: string; + article_id: string; + comment: string; + created_at: CreatedAt; + user: CommentUser; +} + + interface CommentUser { + id: string; + fullname: string; + username: string; + avatar: string | null; + profile_headlines: string | null; + followers: string; + followings: string; + is_following: boolean; + info_details: AuthorDetails; +} + +export interface IArticlesResponse { + data: IArticles[]; +} + +export interface IArticles { + id: string; + title: string; + slug: string; + thumbnail: string; + content: string; + created_at: CreatedAt; } \ No newline at end of file diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..a685e5b --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,12 @@ +export interface AuthorDetails { + bio: string | null; + twitter: string | null; + gitHub: string | null; + website: string | null; +} + +export interface CreatedAt { + human: string; + date_time: string; + human_short: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index 74115f8..8574e79 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,5 +3,6 @@ export * from '../types/auth' export * from '../types/article' export * from '../types/thread' export * from '../types/user' +export * from '../types/common' export * from '../types/search' export * from '../types/messageResponse' \ No newline at end of file diff --git a/src/types/thread/index.ts b/src/types/thread/index.ts index befdfe2..dae85b5 100644 --- a/src/types/thread/index.ts +++ b/src/types/thread/index.ts @@ -1,4 +1,6 @@ -export interface IThread { +import { Author, CommentUser, CreatedAt } from 'src/types' + +export interface IThreadRequest { title: string; content: string; } @@ -20,8 +22,46 @@ export interface IThreadRepliesCardProps { } export interface ThreadFormProps { - titleValue?: string; - contentValue?: string; - isEditing?: boolean - id?: any + titleValue: string; + contentValue: string; + isEditing: boolean + id?: string +} + +interface ThreadData { + id: string; + title: string; + slug: string; + content: string; + isOwner: boolean; + author: Author; + thread_comment_counts: number; + thread_like_counts: number; + created_at: CreatedAt; + thread_comments: ThreadComment[]; +}; + +export interface ThreadComment { + id: string; + thread_id: string; + comment: string; + created_at: CreatedAt; + user: CommentUser; } + +export interface Thread { + data: ThreadData; +}; + +export interface IThreadResponse { + data: IThreads[]; +} + +export interface IThreads { + id: string; + title: string; + slug: string + content: string; + author: Author + created_at: CreatedAt; +} \ No newline at end of file diff --git a/src/types/user/index.ts b/src/types/user/index.ts index 83c13d8..af05111 100644 --- a/src/types/user/index.ts +++ b/src/types/user/index.ts @@ -1,3 +1,5 @@ +import { AuthorDetails } from 'src/types' + export interface IUserProps { id: string; fullname: string; @@ -31,3 +33,30 @@ export interface IPassword { password: string; password_confirmation: string; } + +export interface Person { + id: string; + username: string; + fullname: string; + avatar: string; + bio: string; + is_following: boolean; +} + +export interface PeopleResponse { + data: Person[]; +} + +export interface CommentUser { + id: string; + fullname: string; + username: string; + avatar: string; + profileHeadlines: string | null; + followers: string; + followings: string; + isFollowing: boolean; + details: AuthorDetails; +} + +export interface Author extends CommentUser {} diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts new file mode 100644 index 0000000..cbe5b0f --- /dev/null +++ b/src/utils/apiHelpers.ts @@ -0,0 +1,12 @@ +import { axiosInstance } from '@api/axiosInstance' + +export const handleResponse = async (response: Promise<{ data: T }>): Promise => { + const res = await response; + return res.data; +}; + +export const getWithPagination = async (url: string, limit: number, page: number) => { + const response = await axiosInstance.get(`${url}?limit=${limit}&page=${page}`); + const dataResponse = await response.data; + return {...dataResponse, prevOffset: page} +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index f8679b4..bbf339e 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -14,6 +14,7 @@ "@api/*": ["src/api/*"], "@services/*": ["src/services/*"], "@validations/*": ["src/validations/*"], + "@utils/*": ["src/utils/*"], }, "composite": true, diff --git a/vite.config.ts b/vite.config.ts index 5b1b760..8cbbaa1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,8 @@ export default defineConfig({ '@constant': '/src/constant', '@api': '/src/api', '@services': '/src/services', - '@validations': '/src/validations' + '@validations': '/src/validations', + '@utils': '/src/utils', } } })