From 25ae080ed424ce3ebee58522400d659ed893d52d Mon Sep 17 00:00:00 2001 From: justice chimobi Date: Thu, 3 Oct 2024 10:50:58 +0100 Subject: [PATCH] Feat: save, unsaved, articles to reading-list --- index.html | 4 +- package-lock.json | 27 ++++ package.json | 3 + src/Route/index.tsx | 8 +- src/api/endpoints/articleEndpoint.ts | 9 +- src/assets/images/searching.png | Bin 0 -> 36941 bytes src/components/ArticleCard/Articles.tsx | 33 +++- src/components/Card/index.tsx | 4 +- src/components/Editor/index.tsx | 14 +- src/components/Footer/index.tsx | 5 +- src/components/NavBar/navBarLg.tsx | 40 +++-- src/components/NavBar/navBarSm.tsx | 44 +++++- src/components/Search/index.tsx | 124 --------------- .../components/ThreadCardContent.tsx | 2 +- src/components/index.tsx | 2 - src/hooks/article/useCreateSaveArticles.ts | 32 ++++ src/hooks/article/useDeleteSavaArticles.ts | 32 ++++ src/hooks/article/useGetPaginatedArticles.ts | 4 +- src/hooks/article/useGetSavedArticles.ts | 10 ++ src/hooks/search/useGetSearch.ts | 38 ++++- src/main.tsx | 6 +- src/pages/Articles/components/ArticleForm.tsx | 3 + src/pages/Articles/index.tsx | 42 +++-- .../{ForUsers => ForYou}/index.tsx | 25 ++- src/pages/Home/Authenticated/index.tsx | 2 +- src/pages/Home/Public/Articles/index.tsx | 2 +- src/pages/Search/articles.tsx | 66 ++++++++ src/pages/Search/components/loadButton.tsx | 33 ++++ src/pages/Search/index.tsx | 145 ++++++++++++++++++ src/pages/Search/threads.tsx | 64 ++++++++ src/pages/Search/users.tsx | 73 +++++++++ src/pages/Threads/components/threadForm.tsx | 3 + src/pages/Users/SavedArticles/index.tsx | 105 +++++++++++++ .../Users/Settings/UpdateProfile/index.tsx | 15 +- src/pages/Users/Settings/index.tsx | 4 +- src/pages/Users/show/articles.tsx | 22 +++ src/services/articles/index.ts | 22 ++- src/services/search/index.ts | 10 +- src/theme.ts | 6 +- 39 files changed, 886 insertions(+), 197 deletions(-) create mode 100644 src/assets/images/searching.png delete mode 100644 src/components/Search/index.tsx create mode 100644 src/hooks/article/useCreateSaveArticles.ts create mode 100644 src/hooks/article/useDeleteSavaArticles.ts create mode 100644 src/hooks/article/useGetSavedArticles.ts rename src/pages/Home/Authenticated/{ForUsers => ForYou}/index.tsx (77%) create mode 100644 src/pages/Search/articles.tsx create mode 100644 src/pages/Search/components/loadButton.tsx create mode 100644 src/pages/Search/index.tsx create mode 100644 src/pages/Search/threads.tsx create mode 100644 src/pages/Search/users.tsx create mode 100644 src/pages/Users/SavedArticles/index.tsx diff --git a/index.html b/index.html index f1a61b2..8209d45 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,9 @@ + + + learn-hub @@ -11,5 +14,4 @@
- \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 55e7da8..0652506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@fontsource/open-sans": "^5.1.0", + "@fontsource/raleway": "^5.1.0", "@tanstack/react-query": "^5.50.1", "@tanstack/react-query-devtools": "^5.56.2", "axios": "^1.7.2", @@ -23,6 +25,7 @@ "react-quill": "^2.0.0", "react-router-dom": "^6.24.1", "react-toastify": "^10.0.5", + "use-debounce": "^10.0.3", "yup": "^1.4.0" }, "devDependencies": { @@ -2168,6 +2171,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fontsource/open-sans": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz", + "integrity": "sha512-g+mjF8gWUDwck9DrRCkhmFeEj7fskjtKZJKAQguVzSg93lc6ThakTHMRgs0dZfe5qBbktrV839tDrb4bIDyZSA==", + "license": "OFL-1.1" + }, + "node_modules/@fontsource/raleway": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/raleway/-/raleway-5.1.0.tgz", + "integrity": "sha512-g97nQtuEMVlW95xKCZuDun4gSxmBOxf+7yfValqOwATvJL/98RkCacEsBgJlFNtWxO0+FkmoMl3b9kvZpyQ6VA==", + "license": "OFL-1.1" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -5581,6 +5596,18 @@ } } }, + "node_modules/use-debounce": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.3.tgz", + "integrity": "sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/package.json b/package.json index 03cd31f..95bf979 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@fontsource/open-sans": "^5.1.0", + "@fontsource/raleway": "^5.1.0", "@tanstack/react-query": "^5.50.1", "@tanstack/react-query-devtools": "^5.56.2", "axios": "^1.7.2", @@ -25,6 +27,7 @@ "react-quill": "^2.0.0", "react-router-dom": "^6.24.1", "react-toastify": "^10.0.5", + "use-debounce": "^10.0.3", "yup": "^1.4.0" }, "devDependencies": { diff --git a/src/Route/index.tsx b/src/Route/index.tsx index a40ff4d..bd6dda9 100644 --- a/src/Route/index.tsx +++ b/src/Route/index.tsx @@ -25,11 +25,16 @@ import { } from 'react-router-dom' import AuthRoute from './AuthRoute' import PrivateRoute from './privateRoute' +import Search from '@pages/Search' +import SavedArticles from '@pages/Users/SavedArticles' const routes = createBrowserRouter( createRoutesFromElements( }> } /> + + } /> + } /> } /> @@ -47,7 +52,8 @@ const routes = createBrowserRouter( } />} /> } />} /> - } />} /> + } />} /> + } />} /> } />} /> {/* end private route */} diff --git a/src/api/endpoints/articleEndpoint.ts b/src/api/endpoints/articleEndpoint.ts index c7ac57a..46af789 100644 --- a/src/api/endpoints/articleEndpoint.ts +++ b/src/api/endpoints/articleEndpoint.ts @@ -22,4 +22,11 @@ export const ARTICLE_DISLIKE_ENDPOINT = `${API_BASE_URL}/articles`; export const GET_RECOMMENDED_ARTICLES_ENDPOINT = `${API_BASE_URL}/articles/recommented-articles`; -export const GET_PINNED_ARTICLES_ENDPOINT = `${API_BASE_URL}/articles/pinned-articles`; \ No newline at end of file +export const GET_PINNED_ARTICLES_ENDPOINT = `${API_BASE_URL}/articles/pinned-articles`; + +export const CREATE_SAVE_ARTICLE_ENDPOINT = `${API_BASE_URL}/articles/save-article`; + +export const DELETE_SAVE_ARTICLE_ENDPOINT = `${API_BASE_URL}/articles/unsave-article`; + +export const GET_SAVED_ARTICLES_ENDPOINT = `${API_BASE_URL}/articles/saved-articles`; + diff --git a/src/assets/images/searching.png b/src/assets/images/searching.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5bedd9121c63360be4961700143eecb25f8893 GIT binary patch literal 36941 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 D<`xu@ literal 0 HcmV?d00001 diff --git a/src/components/ArticleCard/Articles.tsx b/src/components/ArticleCard/Articles.tsx index 34f98ff..be6057a 100644 --- a/src/components/ArticleCard/Articles.tsx +++ b/src/components/ArticleCard/Articles.tsx @@ -8,10 +8,13 @@ import { Flex, Heading, Image, - Text + Text, + Tooltip } from '@chakra-ui/react' import { CiEdit } from 'react-icons/ci' import { MdDeleteOutline } from 'react-icons/md' +import { BsSave } from 'react-icons/bs' +import { IoSaveSharp } from "react-icons/io5"; import truncate from '@helpers/truncate' import { stripTags } from '@helpers/stripTags' @@ -29,7 +32,9 @@ interface IProps { isOwner?: boolean; onDelete?: () => void; id?: any; - isLoggedIn?: any + isLoggedIn?: any; + is_saved?: boolean; + saveUnsavedArticle?: () => void; } const ArticlesCard: FunctionComponent = ({ @@ -44,7 +49,9 @@ const ArticlesCard: FunctionComponent = ({ isOwner, onDelete, id, - isLoggedIn + isLoggedIn, + is_saved, + saveUnsavedArticle }) => { return ( @@ -89,7 +96,7 @@ const ArticlesCard: FunctionComponent = ({ - + {isOwner && ( @@ -144,7 +151,23 @@ const ArticlesCard: FunctionComponent = ({ - + + {!isOwner && isLoggedIn && !is_saved && ( + + + + + + )} + + {!isOwner && isLoggedIn && is_saved && + + + + + + } + {read_time} diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx index 12e13d7..195e389 100644 --- a/src/components/Card/index.tsx +++ b/src/components/Card/index.tsx @@ -71,14 +71,14 @@ const Card: FunctionComponent = ({ textDecoration: "underline" }} > - {title} + {truncate(title, 21)} diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx index c2e8f53..ae99264 100644 --- a/src/components/Editor/index.tsx +++ b/src/components/Editor/index.tsx @@ -12,7 +12,7 @@ const Editor: FunctionComponent = ({ content, setContent, placehold toolbar: [ [{ 'header': [1, 2, 3, 4, 5, 6, false] }], ['bold', 'italic', 'underline', 'blockquote'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '+1' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }], ['link', 'image', 'code-block'], ], }; @@ -24,13 +24,23 @@ const Editor: FunctionComponent = ({ content, setContent, placehold 'link', 'image', 'code-block' ]; + const handleContentChange = (value: string) => { + const trimmedValue = value.replace(/<(.|\n)*?>/g, '').trim(); + + if (!trimmedValue) { + setContent(""); + } else { + setContent(value); + } + } + return ( diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index 9fc7b1c..4c83928 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -10,7 +10,7 @@ import { } from '@chakra-ui/react' import { FaXTwitter } from 'react-icons/fa6' import { IoLogoLinkedin } from 'react-icons/io' -import { FaGithub } from "react-icons/fa"; +import { FaGithub } from 'react-icons/fa' import { colors } from '../../colors' @@ -83,7 +83,6 @@ const Footer: FunctionComponent = () => { fontWeight={"normal"} spacing={3} > - Account Feedback Contact Us @@ -139,7 +138,7 @@ const Footer: FunctionComponent = () => { - © 2024 Learn Hub. All right reserved. Made in Nigeria. + © 2024 Learn Hub. All right reserved. Made in Nigeria. diff --git a/src/components/NavBar/navBarLg.tsx b/src/components/NavBar/navBarLg.tsx index afa3c28..e70c11f 100644 --- a/src/components/NavBar/navBarLg.tsx +++ b/src/components/NavBar/navBarLg.tsx @@ -15,8 +15,14 @@ import { } from '@chakra-ui/react' import { FaRegUser } from 'react-icons/fa' import { IoIosArrowUp, IoIosArrowDown } from 'react-icons/io' +import { GoPerson } from 'react-icons/go' +import { FiSearch } from 'react-icons/fi' +import { RiArticleFill } from 'react-icons/ri' +import { RiChatThreadLine } from 'react-icons/ri' +import { CiViewList } from 'react-icons/ci' +import { IoSettingsOutline } from 'react-icons/io5' -import { Button, Search } from '@components/index' +import { Button } from '@components/index' import { colors } from '../../colors' import { Menu } from '@constant/Menu' import { useUser } from '@context/userContext' @@ -43,7 +49,6 @@ const NavBarLg: FunctionComponent = () => { justifyContent="space-between" py={"20px"} > - {/* */} { Learn Hub - {/* */} { ))} - - - + + + {user ? ( @@ -107,29 +118,34 @@ const NavBarLg: FunctionComponent = () => { - + - Your Profile + Your Profile - Your Articles + Your Articles - Your Threads + Your Threads + + + + + Reading List - Settings + Settings - Logout + Logout diff --git a/src/components/NavBar/navBarSm.tsx b/src/components/NavBar/navBarSm.tsx index 7efec64..61290cb 100644 --- a/src/components/NavBar/navBarSm.tsx +++ b/src/components/NavBar/navBarSm.tsx @@ -24,9 +24,16 @@ import { IoIosArrowUp, IoIosArrowDown } from 'react-icons/io' +import { FaRegUser } from 'react-icons/fa' +import { GoPerson } from 'react-icons/go' +import { FiSearch } from 'react-icons/fi' +import { RiArticleFill } from 'react-icons/ri' +import { RiChatThreadLine } from 'react-icons/ri' +import { CiViewList } from 'react-icons/ci' +import { IoSettingsOutline } from 'react-icons/io5' import { colors } from '../../colors' -import { Button, Search } from '@components/index' +import { Button } from '@components/index' import { Menu } from '@constant/Menu' import { useUser } from '@context/userContext' import { useSignOut } from '@hooks/auth/useSignOut' @@ -119,7 +126,20 @@ const NavBarSm: FunctionComponent = () => { - + setOpen(false)} + > + + + {Menu?.map((menu) => ( @@ -163,11 +183,11 @@ const NavBarSm: FunctionComponent = () => { setOpen(false)} > - Your Profile + Your Profile { onClick={() => setOpen(false)} > - Your Articles + Your Articles { onClick={() => setOpen(false)} > - Your Theads + Your Theads + + + setOpen(false)} + > + + Reading List { onClick={() => setOpen(false)} > - Settings + Settings - Logout + Logout diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx deleted file mode 100644 index 5f9dd99..0000000 --- a/src/components/Search/index.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { ChangeEvent, FunctionComponent, useEffect, useRef, useState } from 'react' -import { - Box, - Input, - InputGroup, - InputLeftElement, - Modal, - ModalBody, - ModalContent, - ModalOverlay, - Spinner, - Text, - useDisclosure -} from '@chakra-ui/react' -import { FiSearch } from 'react-icons/fi' -import Button from '@components/Button' -import { useGetSearch } from '@hooks/search/useGetSearch' -import { Link } from 'react-router-dom' -import truncate from '@helpers/truncate' - -const Search: FunctionComponent = () => { - const { isOpen, onOpen, onClose } = useDisclosure() - - const initialRef = useRef(null) - const finalRef = useRef(null) - - const [searchQuery, setSearchQuery] = useState('') - const [searchArr, setSearchArr] = useState([]); - - const { data, isLoading, isError } = useGetSearch(searchQuery); - - const handleInputChange = (e: ChangeEvent) => { - const value = e?.target?.value; - setSearchQuery(value); - - if (value.length === 0) { - // Clear the search results if input is cleared - setSearchArr([]); - } - } - - useEffect(() => { - if (searchQuery?.length > 0 && data) { - setSearchArr((prev: any) => - [...prev, ...data?.articles?.data, ...data?.threads?.data] - ) - } - }, [data, searchQuery]) - - return ( - <> - - - - - - - - - - - - - - - {isLoading ? ( - - - - ) : isError ? ( - Error fetching data - ) : ( - - {searchArr?.length > 0 ? ( - searchArr?.map((d: any, index: number) => ( - - - {truncate(d?.title, 200)} - - - )) - ) : ( - No data found! - )} - - )} - - - - - - ) -} - -export default Search; \ No newline at end of file diff --git a/src/components/ThreadCard/components/ThreadCardContent.tsx b/src/components/ThreadCard/components/ThreadCardContent.tsx index dc0b2fe..ab13c0c 100644 --- a/src/components/ThreadCard/components/ThreadCardContent.tsx +++ b/src/components/ThreadCard/components/ThreadCardContent.tsx @@ -21,7 +21,7 @@ const ThreadCardContent: FunctionComponent = ({ isSingleView }) => ( - + {title} diff --git a/src/components/index.tsx b/src/components/index.tsx index 6941230..85a1f2d 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -7,7 +7,6 @@ import Footer from '@components/Footer' import HeroSection from '@components/HeroSection' import NavBar from '@components/NavBar' import NotFound from '@components/NotFound' -import Search from '@components/Search' import Skeleton from '@components/Skeleton' import RecommendTopicCard from '@components/RecommendTopicCard' import FollowCard from '@components/FollowCard' @@ -31,7 +30,6 @@ export { NavBar, NotFound, RecommendTopicCard, - Search, Skeleton, Input, TextArea, diff --git a/src/hooks/article/useCreateSaveArticles.ts b/src/hooks/article/useCreateSaveArticles.ts new file mode 100644 index 0000000..6e65425 --- /dev/null +++ b/src/hooks/article/useCreateSaveArticles.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' + +import { errorNotification, successNotification } from '@helpers/notification' +import { createSaveArticle } from '@services/articles' + +export const useCreateSaveArticle = () => { + const queryClient = useQueryClient(); + + const createSaveArticleMutation = useMutation({ + mutationFn: createSaveArticle, + onSuccess: (data) => { + successNotification(data?.message) + + queryClient.invalidateQueries({ + queryKey: ['articles'] + }); + + queryClient.invalidateQueries({ + queryKey: ['saved-articles'] + }); + + queryClient.invalidateQueries({ + queryKey: ['recommented-articles'] + }); + }, + onError: (error: any) => { + errorNotification(error?.response?.data?.message) + } + }) + + return { createSaveArticleMutation } +} \ No newline at end of file diff --git a/src/hooks/article/useDeleteSavaArticles.ts b/src/hooks/article/useDeleteSavaArticles.ts new file mode 100644 index 0000000..1842542 --- /dev/null +++ b/src/hooks/article/useDeleteSavaArticles.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' + +import { errorNotification, successNotification } from '@helpers/notification' +import { deleteSaveArticle } from '@services/articles' + +export const useDeleteSaveArticle = () => { + const queryClient = useQueryClient(); + + const deleteSaveArticleMutation = useMutation({ + mutationFn: deleteSaveArticle, + onSuccess: (data) => { + successNotification(data?.message) + + queryClient.invalidateQueries({ + queryKey: ['articles'] + }); + + queryClient.invalidateQueries({ + queryKey: ['saved-articles'] + }); + + queryClient.invalidateQueries({ + queryKey: ['recommented-articles'] + }); + }, + onError: (error: any) => { + errorNotification(error?.response?.data?.message) + } + }) + + return { deleteSaveArticleMutation } +} \ No newline at end of file diff --git a/src/hooks/article/useGetPaginatedArticles.ts b/src/hooks/article/useGetPaginatedArticles.ts index 5a40fe7..e65c129 100644 --- a/src/hooks/article/useGetPaginatedArticles.ts +++ b/src/hooks/article/useGetPaginatedArticles.ts @@ -24,10 +24,10 @@ export const useGetPaginatedArticles = (limit: number = 0) => { placeholderData: keepPreviousData, initialPageParam: 1, getNextPageParam: (lastPage) => { - if (!lastPage.data.pagination.next_page_url) { + if (!lastPage?.data?.pagination?.next_page_url) { return undefined; } - return lastPage.data.pagination.current_page + 1 + return lastPage?.data?.pagination?.current_page + 1 } }) diff --git a/src/hooks/article/useGetSavedArticles.ts b/src/hooks/article/useGetSavedArticles.ts new file mode 100644 index 0000000..3647b48 --- /dev/null +++ b/src/hooks/article/useGetSavedArticles.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query' + +import { getSavedArticles } from '@services/articles' + +export const useSavedArticles = () => { + return useQuery({ + queryKey: ['saved-articles'], + queryFn: getSavedArticles, + }) +} \ No newline at end of file diff --git a/src/hooks/search/useGetSearch.ts b/src/hooks/search/useGetSearch.ts index ce1b5bd..567a44d 100644 --- a/src/hooks/search/useGetSearch.ts +++ b/src/hooks/search/useGetSearch.ts @@ -1,11 +1,33 @@ -import { useQuery } from '@tanstack/react-query' +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' -import { getSearch } from '@services/search' +import { fetchSearchResults } from '@services/search' + +export const useGetSearch = (query: any, resource: any) => { + const { + data: dataResponse, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isSuccess, + isError + } = useInfiniteQuery({ + queryKey: ['search', query, resource], + queryFn: ({ pageParam = 1 }) => fetchSearchResults({ query, page: pageParam, resource }), + getNextPageParam: (lastPage) => { + if (!lastPage?.data?.[resource]?.pagination?.next_page_url) { + return undefined; + } + return lastPage?.data?.[resource]?.pagination?.current_page + 1 + }, + placeholderData: keepPreviousData, + initialPageParam: 1, + enabled: !!query + }); + + + const data = dataResponse?.pages ?? null; + + return { data, isLoading, isSuccess, isError, fetchNextPage, hasNextPage, isFetchingNextPage } -export const useGetSearch = (search: string) => { - return useQuery({ - queryKey: ['search', search], - queryFn: () => getSearch(search), - enabled: search?.length > 0, - }) } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 1f5042d..a89398e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,10 +5,12 @@ import './index.css' import 'react-quill/dist/quill.snow.css' import 'react-toastify/dist/ReactToastify.css' import '../node_modules/highlight.js/styles/atom-one-dark.css' +import '@fontsource/raleway/400.css' +import '@fontsource/open-sans/700.css' import { ToastContainer } from 'react-toastify' import { ChakraProvider } from '@chakra-ui/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { Theme } from './theme' import UserContextProvider from '@context/userContext.tsx' diff --git a/src/pages/Articles/components/ArticleForm.tsx b/src/pages/Articles/components/ArticleForm.tsx index 328034d..fee17ab 100644 --- a/src/pages/Articles/components/ArticleForm.tsx +++ b/src/pages/Articles/components/ArticleForm.tsx @@ -69,6 +69,9 @@ const ArticleForm: FunctionComponent = ({ } else { createArticleMutation.mutate(articleData); } + + setContent('') + setTitle('') }; useEffect(() => { diff --git a/src/pages/Articles/index.tsx b/src/pages/Articles/index.tsx index aebaa22..e63e68b 100644 --- a/src/pages/Articles/index.tsx +++ b/src/pages/Articles/index.tsx @@ -11,10 +11,12 @@ import { Alert, Skeleton } from '@components/index' -import { useUser } from '@context/userContext'; -import { useGetPaginatedArticles } from '@hooks/article/useGetPaginatedArticles'; -import { useDeleteArticle } from '@hooks/article/useDeleteArticle'; -import { useGetPinnedArticles } from '@hooks/article/useGetPinnedArticles'; +import { useUser } from '@context/userContext' +import { useGetPaginatedArticles } from '@hooks/article/useGetPaginatedArticles' +import { useDeleteArticle } from '@hooks/article/useDeleteArticle' +import { useGetPinnedArticles } from '@hooks/article/useGetPinnedArticles' +import { useCreateSaveArticle } from '@hooks/article/useCreateSaveArticles' +import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' const Articles: FunctionComponent = () => { const { user } = useUser(); @@ -27,21 +29,39 @@ const Articles: FunctionComponent = () => { isFetchingNextPage } = useGetPaginatedArticles(10) const { deleteArticleMutation } = useDeleteArticle() - const {data: pinArticles } = useGetPinnedArticles(); + const { data: pinArticles } = useGetPinnedArticles(); + const { createSaveArticleMutation } = useCreateSaveArticle(); + const { deleteSaveArticleMutation} = useDeleteSaveArticle() const { isOpen, onOpen, onClose } = useDisclosure(); - const [deletingArticleId, setDeletingArticleId] = useState(null); + const [deleteArticleId, setDeleteArticleId] = useState(null); const handleDelete = (articleId: any) => { - setDeletingArticleId(articleId) + setDeleteArticleId(articleId) onOpen() } const handleDeleteArticle = () => { - if (!deletingArticleId) return; - deleteArticleMutation.mutate(deletingArticleId) + if (!deleteArticleId) return; + deleteArticleMutation.mutate(deleteArticleId) onClose() } + const handleSaveUnsavedArticle = (articleId: string, is_saved: boolean) => { + if (is_saved) { + unsaveArticle(articleId); + } else { + saveArticle(articleId); + } + } + + const saveArticle = (articleId: string) => { + createSaveArticleMutation.mutate(articleId); + }; + + const unsaveArticle = (articleId: string) => { + deleteSaveArticleMutation.mutate(articleId); + }; + return ( { )} - + {pinArticles?.map((article: any, index: number) => ( { authorUsername={article?.author?.username} onDelete={() => handleDelete(article?.id)} isLoggedIn={!!user} + is_saved={article?.is_saved} + saveUnsavedArticle={() => handleSaveUnsavedArticle(article?.id, article?.is_saved)} /> ))} diff --git a/src/pages/Home/Authenticated/ForUsers/index.tsx b/src/pages/Home/Authenticated/ForYou/index.tsx similarity index 77% rename from src/pages/Home/Authenticated/ForUsers/index.tsx rename to src/pages/Home/Authenticated/ForYou/index.tsx index baf0a5e..61701fd 100644 --- a/src/pages/Home/Authenticated/ForUsers/index.tsx +++ b/src/pages/Home/Authenticated/ForYou/index.tsx @@ -2,9 +2,11 @@ import { Fragment, useState } from 'react' import { Box, useDisclosure } from '@chakra-ui/react' import { Alert, ArticlesCard, Button, Skeleton } from '@components/index' +import { useUser } from '@context/userContext' import { useDeleteArticle } from '@hooks/article/useDeleteArticle' import { useGetRecommentedArticles } from '@hooks/article/useGetRecommentedArticles' -import { useUser } from '@context/userContext' +import { useCreateSaveArticle } from '@hooks/article/useCreateSaveArticles' +import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' const ForYou = () => { const { user } = useUser(); @@ -16,8 +18,9 @@ const ForYou = () => { fetchNextPage, isFetchingNextPage } = useGetRecommentedArticles(20) - const { deleteArticleMutation } = useDeleteArticle() + const { createSaveArticleMutation } = useCreateSaveArticle(); + const { deleteSaveArticleMutation} = useDeleteSaveArticle() const { isOpen, onOpen, onClose } = useDisclosure(); const [deleteArticleId, setDeleteArticleId] = useState(null); @@ -32,6 +35,22 @@ const ForYou = () => { onClose() } + const handleSaveUnsavedArticle = (articleId: string, is_saved: boolean) => { + if (is_saved) { + unsaveArticle(articleId); + } else { + saveArticle(articleId); + } + } + + const saveArticle = (articleId: string) => { + createSaveArticleMutation.mutate(articleId); + }; + + const unsaveArticle = (articleId: string) => { + deleteSaveArticleMutation.mutate(articleId); + }; + return ( <> {isLoading && } @@ -53,6 +72,8 @@ const ForYou = () => { authorUsername={article?.author?.username} onDelete={() => handleDelete(article?.id)} isLoggedIn={!!user} + is_saved={article?.is_saved} + saveUnsavedArticle={() => handleSaveUnsavedArticle(article?.id, article?.is_saved)} /> ))} diff --git a/src/pages/Home/Authenticated/index.tsx b/src/pages/Home/Authenticated/index.tsx index 9bfc642..25981e9 100644 --- a/src/pages/Home/Authenticated/index.tsx +++ b/src/pages/Home/Authenticated/index.tsx @@ -13,7 +13,7 @@ import { import { colors } from '../../../colors' import { FollowCard, RecommendTopicCard } from '@components/index' import Following from '@pages/Home/Authenticated/Following' -import ForYou from '@pages/Home/Authenticated/ForUsers' +import ForYou from '@pages/Home/Authenticated/ForYou' const HomeAuthUserPage: FunctionComponent = () => { const [tabIndex, setTabIndex] = useState(0); diff --git a/src/pages/Home/Public/Articles/index.tsx b/src/pages/Home/Public/Articles/index.tsx index df929cb..5385c5f 100644 --- a/src/pages/Home/Public/Articles/index.tsx +++ b/src/pages/Home/Public/Articles/index.tsx @@ -23,7 +23,7 @@ const HomeArticles: FunctionComponent = () => { {articles && isSuccess && ( <> - + {articles?.map((article: any, index: any) => ( void; + isFetching: boolean; +} + +const SearchArticles: FunctionComponent = ({ + articles, + hasMore, + fetchNext, + isFetching + +}) => { + return ( + + {articles?.length > 0 && ( + <> + Articles + + + + + } spacing='4'> + {articles?.map((article: any, index: number) => ( + + + {truncate(article?.title, 200)} + + + + ))} + + + {hasMore && ( + + )} + + + + + )} + + ) +} + +export default SearchArticles; diff --git a/src/pages/Search/components/loadButton.tsx b/src/pages/Search/components/loadButton.tsx new file mode 100644 index 0000000..8737a8b --- /dev/null +++ b/src/pages/Search/components/loadButton.tsx @@ -0,0 +1,33 @@ +import { Box } from "@chakra-ui/react"; +import Button from "@components/Button"; +import { FunctionComponent } from "react"; +import { FaChevronDown } from "react-icons/fa6"; + +interface LoadButtonProps { + hasMore: boolean; + fetchNext: () => void; + isFetching: boolean; +} + +const LoadButton: FunctionComponent = ({ + hasMore, + fetchNext, + isFetching +}) => { + return ( + + + + ) +} + +export default LoadButton; \ No newline at end of file diff --git a/src/pages/Search/index.tsx b/src/pages/Search/index.tsx new file mode 100644 index 0000000..0958803 --- /dev/null +++ b/src/pages/Search/index.tsx @@ -0,0 +1,145 @@ +import { ChangeEvent, FunctionComponent, useEffect, useState } from 'react' +import { Box, Container, Image, Input, InputGroup, InputLeftElement, Spinner, Text } from '@chakra-ui/react' +import { FiSearch } from 'react-icons/fi' +import { useDebounce } from 'use-debounce' + +import { useGetSearch } from '@hooks/search/useGetSearch' +import SearchUsers from '@pages/Search/users' +import SearchArticles from '@pages/Search/articles' +import SearchThreads from '@pages/Search/threads' +import SearchImg from '@assets/images/searching.png' + +const Search: FunctionComponent = () => { + const [searchQuery, setSearchQuery] = useState('') + const [searchArr, setSearchArr] = useState({ + users: [], + articles: [], + threads: [] + }); + const [value] = useDebounce(searchQuery, 1000); + + const { + data: usersData, + fetchNextPage: fetchNextUsers, + hasNextPage: hasMoreUsers, + isFetchingNextPage: isFetchingNextUsers, + isLoading: isLoadingUser, + } = useGetSearch(value, 'users'); + + const { + data: articlesData, + fetchNextPage: fetchNextArticles, + hasNextPage: hasMoreArticles, + isFetchingNextPage: isFetchingNextArticles, + isLoading: isLoadingArticles, + } = useGetSearch(value, 'articles'); + + const { + data: threadsData, + fetchNextPage: fetchNextThreads, + hasNextPage: hasMoreThreads, + isFetchingNextPage: isFetchingNextThreads, + isLoading: isLoadingThreads, + } = useGetSearch(value, 'threads'); + + const handleInputChange = (e: ChangeEvent) => { + const value = e?.target?.value; + setSearchQuery(value); + if (value?.length === 0) { + // Clear the search results if input is cleared + setSearchArr({ + users: [], + articles: [], + threads: [] + }); + } + } + + useEffect(() => { + // Only update searchArr when there is a valid search term + if (value.length > 0) { + setSearchArr({ + users: usersData?.flatMap((page) => page?.data?.users?.data) || [], + articles: articlesData?.flatMap((page) => page?.data?.articles?.data) || [], + threads: threadsData?.flatMap((page) => page?.data?.threads?.data) || [], + }); + } else { + // Clear the search results when the search term is empty + setSearchArr({ + users: [], + articles: [], + threads: [], + }); + } + }, [usersData, articlesData, threadsData, value]); + + return ( + + + + + + + + + + + {searchQuery?.length === 0 && ( + + Waiting for search. + + + + )} + + {isLoadingUser && isLoadingArticles && isLoadingThreads && ( + + + + )} + + + + + + + + + + ) +} + +export default Search; \ No newline at end of file diff --git a/src/pages/Search/threads.tsx b/src/pages/Search/threads.tsx new file mode 100644 index 0000000..808290e --- /dev/null +++ b/src/pages/Search/threads.tsx @@ -0,0 +1,64 @@ +import { FunctionComponent } from 'react' +import { Link } from 'react-router-dom' +import { Box, Card, CardBody, Stack, StackDivider, Text } from '@chakra-ui/react' + +import truncate from '@helpers/truncate' +import LoadButton from '@pages/Search/components/loadButton' + +interface SearchThreadsProps { + threads: any; + hasMore: boolean; + fetchNext: () => void; + isFetching: boolean; +} + +const SearchThreads: FunctionComponent = ({ + threads, + hasMore, + fetchNext, + isFetching +}) => { + return ( + + {threads?.length > 0 && ( + <> + Threads + + + + + } spacing='4'> + {threads?.map((thread: any, index: number) => ( + + + {truncate(thread?.title, 200)} + + + ))} + + + {hasMore && ( + + )} + + + + + )} + + ) +} + +export default SearchThreads; diff --git a/src/pages/Search/users.tsx b/src/pages/Search/users.tsx new file mode 100644 index 0000000..965666c --- /dev/null +++ b/src/pages/Search/users.tsx @@ -0,0 +1,73 @@ +import { FunctionComponent } from 'react' +import { Link } from 'react-router-dom' +import { Avatar, Box, Card, CardBody, Flex, Heading, Stack, StackDivider, Text } from '@chakra-ui/react' + +import truncate from '@helpers/truncate' +import LoadButton from '@pages/Search/components/loadButton' + +interface SearchUsersProps { + users: any; + hasMore: boolean; + fetchNext: () => void; + isFetching: boolean; +} + +const SearchUsers: FunctionComponent = ({ + users, + hasMore, + fetchNext, + isFetching +}) => { + return ( + + {users?.length > 0 && ( + <> + Users + + + + + } spacing='4'> + {users?.map((user: any, index: number) => ( + + + + + + + {user?.fullname} + + + + {truncate(user?.bio, 190)} + + + + ))} + + + {hasMore && ( + + )} + + + + + )} + + ) +} + +export default SearchUsers; diff --git a/src/pages/Threads/components/threadForm.tsx b/src/pages/Threads/components/threadForm.tsx index bde00ac..ebd639c 100644 --- a/src/pages/Threads/components/threadForm.tsx +++ b/src/pages/Threads/components/threadForm.tsx @@ -38,6 +38,9 @@ const ThreadForm: FunctionComponent = ({ } else { createThreadMutation.mutate(threadData); } + + setContent('') + setTitle('') }; useEffect(() => { diff --git a/src/pages/Users/SavedArticles/index.tsx b/src/pages/Users/SavedArticles/index.tsx new file mode 100644 index 0000000..57195f7 --- /dev/null +++ b/src/pages/Users/SavedArticles/index.tsx @@ -0,0 +1,105 @@ +import { Link } from 'react-router-dom' +import { Avatar, Box, Container, Flex, Heading, Text } from '@chakra-ui/react' + +import { ArticlesCard } from '@components/index' +import { useUser } from '@context/userContext' +import { useSavedArticles } from '@hooks/article/useGetSavedArticles' +import { colors } from '../../../colors' +import { useDeleteSaveArticle } from '@hooks/article/useDeleteSavaArticles' + +const SavedArticles = () => { + const { user } = useUser(); + const { data: savedArticles } = useSavedArticles(); + const { deleteSaveArticleMutation} = useDeleteSaveArticle() + + const handleUnSaveArticle = (articleId: string) => { + deleteSaveArticleMutation.mutate(articleId); + } + + return ( + + + + + + {user?.data?.fullname} + + + + Reading Lists + + + + {savedArticles && savedArticles?.map((article: any, index: number) => ( + handleUnSaveArticle(article?.id)} + /> + ))} + + {savedArticles?.length === 0 && ( + Your saving lists of articles will appear here! + )} + + + + + + + + {user?.data?.fullname} + {/* 0 Follower */} + + {user?.data?.bio} + + + Edit Profile + + + + + + ) +} + +export default SavedArticles; \ No newline at end of file diff --git a/src/pages/Users/Settings/UpdateProfile/index.tsx b/src/pages/Users/Settings/UpdateProfile/index.tsx index eda3e85..5108ab8 100644 --- a/src/pages/Users/Settings/UpdateProfile/index.tsx +++ b/src/pages/Users/Settings/UpdateProfile/index.tsx @@ -9,7 +9,8 @@ import { FormLabel, IconButton, SimpleGrid, - Spinner + Spinner, + Text } from '@chakra-ui/react' import { Formik, Field } from 'formik' import { FaCamera } from 'react-icons/fa6' @@ -85,7 +86,7 @@ const UpdateProfile: FunctionComponent = () => { { /> )} + + + + The profile image must be a file of type: jpg, png, jpeg, JPG, PNG. + { - Settings + + profile \ Settings + { const { username } = useParams(); @@ -20,6 +22,8 @@ const PublicUserArticles: FunctionComponent = () => { isFetchingNextPage } = useGetPublicAuthoredArticles(20, username!); const { deleteArticleMutation } = useDeleteArticle() + const { createSaveArticleMutation } = useCreateSaveArticle(); + const { deleteSaveArticleMutation } = useDeleteSaveArticle() const { isOpen, onOpen, onClose } = useDisclosure(); const [deleteArticleId, setDeleteArticleId] = useState(null); @@ -34,6 +38,22 @@ const PublicUserArticles: FunctionComponent = () => { onClose() } + const handleSaveUnsavedArticle = (articleId: string, is_saved: boolean) => { + if (is_saved) { + unsaveArticle(articleId); + } else { + saveArticle(articleId); + } + } + + const saveArticle = (articleId: string) => { + createSaveArticleMutation.mutate(articleId); + }; + + const unsaveArticle = (articleId: string) => { + deleteSaveArticleMutation.mutate(articleId); + }; + return ( <> {isLoading && } @@ -55,6 +75,8 @@ const PublicUserArticles: FunctionComponent = () => { authorUsername={article?.author?.username} onDelete={() => handleDelete(article?.id)} isLoggedIn={!!user} + is_saved={article?.is_saved} + saveUnsavedArticle={() => handleSaveUnsavedArticle(article?.id, article?.is_saved)} /> ))} diff --git a/src/services/articles/index.ts b/src/services/articles/index.ts index 5048343..ad38b90 100644 --- a/src/services/articles/index.ts +++ b/src/services/articles/index.ts @@ -5,11 +5,14 @@ import { ArticleRequest, CREATE_ARTICLE_COMMENT_ENDPOINT, CREATE_ARTICLE_ENDPOINT, + CREATE_SAVE_ARTICLE_ENDPOINT, DELETE_ARTICLE_ENDPOINT, + DELETE_SAVE_ARTICLE_ENDPOINT, EDIT_ARTICLE_ENDPOINT, GET_ALL_ARTICLES_ENDPOINT, GET_PINNED_ARTICLES_ENDPOINT, GET_RECOMMENDED_ARTICLES_ENDPOINT, + GET_SAVED_ARTICLES_ENDPOINT, GET_SINGLE_ARTICLE_ENDPOINT } from '@api/index' @@ -43,12 +46,12 @@ export const createArticleComment = async ({ data, id }: { data: any, id: any }) return response.data; } -export const createArticleLike = async ({id}: {id: string}) => { +export const createArticleLike = async ({ id }: { id: string }) => { const response = await axiosInstance.post(`${ARTICLE_LIKE_ENDPOINT}/${id}/likes`); return response.data; } -export const createArticleDisLike = async ({id}: {id: string}) => { +export const createArticleDisLike = async ({ id }: { id: string }) => { const response = await axiosInstance.delete(`${ARTICLE_DISLIKE_ENDPOINT}/${id}/dislikes`); return response.data; } @@ -62,3 +65,18 @@ export const getPinnedArticles = async () => { const response = await axiosInstance.get(GET_PINNED_ARTICLES_ENDPOINT); return response.data.data; } + +export const createSaveArticle = async (article_id: string) => { + const response = await axiosInstance.post(`${CREATE_SAVE_ARTICLE_ENDPOINT}/${article_id}`); + return response.data; +}; + +export const deleteSaveArticle = async (article_id: string) => { + const response = await axiosInstance.delete(`${DELETE_SAVE_ARTICLE_ENDPOINT}/${article_id}`); + return response.data; +} + +export const getSavedArticles = async () => { + const response = await axiosInstance.get(GET_SAVED_ARTICLES_ENDPOINT); + return response.data.data; +} diff --git a/src/services/search/index.ts b/src/services/search/index.ts index 8e81d91..99bd3af 100644 --- a/src/services/search/index.ts +++ b/src/services/search/index.ts @@ -1,7 +1,11 @@ import { axiosInstance } from '@api/axiosInstance' import { SEARCH_ENDPOINT } from '@api/endpoints/searchEndpoints' -export const getSearch = async (search: string) => { - const response = await axiosInstance.get(`${SEARCH_ENDPOINT}?search=${search}`); - return response.data.data; +export const fetchSearchResults = async ({ query, page, resource }: any) => { + const response = await axiosInstance.get(`${SEARCH_ENDPOINT}`, { + params: { search: query, page, resource } + }); + + const dataResponse = await response.data; + return { ...dataResponse, prevOffset: page }; } \ No newline at end of file diff --git a/src/theme.ts b/src/theme.ts index b1b0b8f..b4a18fa 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -6,12 +6,16 @@ export const Theme = extendTheme({ body: { background: "#fafafa", color: "#000", - fontFamily: "DM SAN, Arial, sans-serif", height: "100vh", } }) }, + fonts: { + heading: `'Open Sans', sans-serif`, + body: `'Raleway', sans-serif`, + }, + components: { Button: { baseStyle: {