diff --git a/README.md b/README.md
index d2ea91a..0af44df 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,28 @@
## A Forum platform for problem solving, knowledge sharing and community builders, join others for sharing knowledge.
### API Docs [Api Docs](https://learn-hub-backend-api.onrender.com/api/docs)
+
+## Requirements
+
+The following tools are required in order to start the installation.
+
+- Node
+- [Node](https://nodejs.org/en/download/package-manager/)
+
+## Installation
+
+1. Clone this repository with `git clone https://github.com/chimobi-justice/learn-hub.git`
+- Change directories into learn-hub
+- cd learn-hub
+2. Run `npm install` to install the Node dependencies
+3. Create the .env file by duplicating the .env.example file
+- VITE_API_BASE_URL
+- if clone the backend [learn-hub-backend](https://github.com/chimobi-justice/learn-hub-backend.git) repo use the local server url or use the host backend url
+- https://learn-hub-backend-api.onrender.com/api/v1
+4. Run the application
+- npm run dev
+
+
+You can now visit the app in your browser by visiting [http://localhost:5173](http://localhost:5173).
+
+
diff --git a/src/App.tsx b/src/App.tsx
index 36998b7..432698b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -25,10 +25,13 @@ import ArthoredArticles from '@pages/Users/AuthoredViews/Articles'
import ShowArticle from '@pages/Articles/show'
import CreateArticle from '@pages/Articles/create'
+import ShowUserPublicPosts from '@pages/Users/show'
+
import Login from '@pages/Auth/Login'
import Register from '@pages/Auth/Register'
import PrivateRoute from './Route/privateRoute'
+import AuthRoute from './Route/AuthRoute'
const routes = createBrowserRouter(
createRoutesFromElements(
@@ -41,6 +44,8 @@ const routes = createBrowserRouter(
} />
} />
+ } />
+
{/* private route */}
} />} />
} />} />
@@ -54,8 +59,8 @@ const routes = createBrowserRouter(
} />} />
{/* end private route */}
- } />
- } />
+ } />} />
+ } />} />
} />
diff --git a/src/Route/AuthRoute.tsx b/src/Route/AuthRoute.tsx
new file mode 100644
index 0000000..7bf3606
--- /dev/null
+++ b/src/Route/AuthRoute.tsx
@@ -0,0 +1,28 @@
+import { FunctionComponent, ReactElement, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+
+import { useUser } from '@context/userContext'
+
+interface AuthRouteProps {
+ element: ReactElement;
+}
+
+const AuthRoute: FunctionComponent = ({ element }) => {
+ const navigate = useNavigate();
+
+ const getToken = !!localStorage.getItem("ucType_");
+
+ const { user } = useUser();
+
+ useEffect(() => {
+ if (user && getToken) {
+ navigate('/', { replace: true});
+ }
+ }, [user, getToken, navigate]);
+
+ if (getToken) return null;
+
+ return element;
+}
+
+export default AuthRoute;
\ No newline at end of file
diff --git a/src/Route/privateRoute.tsx b/src/Route/privateRoute.tsx
index c9ebb64..a2539fb 100644
--- a/src/Route/privateRoute.tsx
+++ b/src/Route/privateRoute.tsx
@@ -1,8 +1,6 @@
import { FunctionComponent, ReactElement } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
-// import { useUser } from '@context/userContext'
-
interface PrivateRouteProps {
element: ReactElement;
}
diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts
index c0ba157..3010d78 100644
--- a/src/api/axiosInstance.ts
+++ b/src/api/axiosInstance.ts
@@ -36,11 +36,8 @@ axiosInstance.interceptors.response.use(
localStorage.removeItem('ucType_');
location.href = '/auth/login'
}
- throw new Error(error?.response?.data?.message
- || error?.response?.status
- || error?.message
- || 'An unexpected error occurred');
+ return Promise.reject(error);
+
}
- return Promise.reject(new Error('An unexpected error occurred'));
}
);
diff --git a/src/api/endpoints/userEndpoints.ts b/src/api/endpoints/userEndpoints.ts
index 1fd958d..b7169c2 100644
--- a/src/api/endpoints/userEndpoints.ts
+++ b/src/api/endpoints/userEndpoints.ts
@@ -9,3 +9,5 @@ export const UPLOAD_PROFILE_AVATAR_ENDPOINT = `${API_BASE_URL}/users/accounts/up
export const UPDATE_PASSWORD_ENDPOINT = `${API_BASE_URL}/users/accounts/update-password`;
export const DELETE_ACCOUNT_ENDPOINT = `${API_BASE_URL}/users/accounts/delete`;
+
+export const PUBLIC_USER_ENDPOINT = `${API_BASE_URL}/users`;
diff --git a/src/api/types/messageResponse.ts b/src/api/types/messageResponse.ts
index 9f00826..302e1f4 100644
--- a/src/api/types/messageResponse.ts
+++ b/src/api/types/messageResponse.ts
@@ -1,4 +1,3 @@
export interface MessageResponse {
message: string;
- // data: T
}
\ No newline at end of file
diff --git a/src/api/types/user/index.ts b/src/api/types/user/index.ts
index b2d6d99..e8f0069 100644
--- a/src/api/types/user/index.ts
+++ b/src/api/types/user/index.ts
@@ -1,16 +1,18 @@
export interface UserResponse {
- id: string | number;
- fullname: string;
- email: string;
- username: string;
- twitter: string;
- avatar: string;
- gitHub: string;
- website: string;
- headlines: string;
- state: string;
- country: string;
- bio: string;
+ data: {
+ id: string | number;
+ fullname: string;
+ email: string;
+ username: string;
+ twitter: string;
+ avatar: string;
+ gitHub: string;
+ website: string;
+ profile_headlines: string;
+ state: string;
+ country: string;
+ bio: string;
+ }
}
export interface UpdateProfileRequest {
@@ -35,16 +37,8 @@ export interface UpdateProfileAvatarRequest {
avatar: string;
}
-export interface UpdateProfileAvatarResponse {
- message: string;
-}
-
export interface UpdatePasswordRequest {
current_password: string;
password: string;
password_confirmation: string;
}
-
-export interface UpdatePasswordResponse {
- message: string;
-}
diff --git a/src/components/ArticleCard/Articles.tsx b/src/components/ArticleCard/Articles.tsx
index 4586368..2db3070 100644
--- a/src/components/ArticleCard/Articles.tsx
+++ b/src/components/ArticleCard/Articles.tsx
@@ -14,7 +14,7 @@ import { CiEdit } from 'react-icons/ci'
import { MdDeleteOutline } from 'react-icons/md'
import truncate from '@helpers/truncate'
-import { stripTags } from '@helpers/stripTags';
+import { stripTags } from '@helpers/stripTags'
interface IProps {
articleImg: string;
@@ -63,9 +63,9 @@ const ArticlesCard: FunctionComponent = ({
/>
- = ({
fontSize={"14px"}
lineHeight={"1.7em"}
color={"#0009"}
- dangerouslySetInnerHTML={stripTags(truncate(description, 250))}
+ dangerouslySetInnerHTML={stripTags(truncate(description, 200))}
/>
-
- {authorUsername && authorAvatar && (
-
-
-
- )}
-
-
+
+
{authorUsername && (
+
+
+
+ )}
+ {authorUsername && (
{authorUsername}
)}
+
-
- {authorProfileHeadlines && (
- {truncate(authorProfileHeadlines, 60)}
- )}
+
+ {authorProfileHeadlines && (
+ {truncate(authorProfileHeadlines, 60)}
+ )}
- {read_time}
-
+ {read_time}
-
-
+
{isOwner && (
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
index f12efa7..9fc7b1c 100644
--- a/src/components/Footer/index.tsx
+++ b/src/components/Footer/index.tsx
@@ -10,6 +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 { colors } from '../../colors'
@@ -122,6 +123,17 @@ const Footer: FunctionComponent = () => {
LinkedIn
+
+
+ Github
+
+
diff --git a/src/components/HeroSection/index.tsx b/src/components/HeroSection/index.tsx
index 50b7e5f..02f6993 100644
--- a/src/components/HeroSection/index.tsx
+++ b/src/components/HeroSection/index.tsx
@@ -99,6 +99,7 @@ const HeroSection: FunctionComponent = () => {
{
+const NavBarLg: FunctionComponent = () => {
const { user } = useUser();
const { signOutMutation } = useSignOut()
@@ -43,16 +43,20 @@ const NavBarLg: FunctionComponent = () => {
justifyContent="space-between"
py={"20px"}
>
-
-
- Learn Hub
-
-
+
+
+
+ Learn Hub
+
+
+
+
+
{
{
+
{Menu?.map((menu) => (
@@ -145,6 +146,7 @@ const NavBarSm: FunctionComponent = () => {
= ({ author, createdAt }) => (
-
-
-
+
+
+
- By: {author.fullname}
+ By: {truncate(author?.fullname, 20)}
posted {createdAt}
diff --git a/src/components/index.tsx b/src/components/index.tsx
index 380b396..16d8c2a 100644
--- a/src/components/index.tsx
+++ b/src/components/index.tsx
@@ -16,6 +16,7 @@ import TextArea from '@components/TextArea'
import EmptyState from '@components/EmptyState'
import Editor from '@components/Editor'
import ContentBlockContent from '@components/CodeBlockContent'
+import ThreadCard from '@components/ThreadCard'
export {
Alert,
@@ -36,4 +37,5 @@ export {
EmptyState,
Editor,
ContentBlockContent,
+ ThreadCard,
}
\ No newline at end of file
diff --git a/src/constant/Code.ts b/src/constant/Code.ts
deleted file mode 100644
index e3c8ea1..0000000
--- a/src/constant/Code.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-export const CodeHighlight = `
- Creating a reusable Chakra UI Button component
-
- import { FunctionComponent, ReactElement, ReactNode } from 'react'
- import { ButtonProps, Button as ChakraButton } from '@chakra-ui/react'
-
- const borderRadiusMap = {
- lg: '15px',
- md: '10px',
- sm: '5px',
- }
-
- type ButtonRoundedRadius = keyof typeof borderRadiusMap;
-
- type SizeType = 'sm' | 'md' | 'lg' | { base: string, sm?: string, md?: string, lg?: string };
-
- type WidthSize = string | { base: string, sm?: string, md?: string, lg?: string };
-
- interface IProps extends Pick {
- isloading?: boolean;
- isDisable?: boolean;
- loadingText?: string;
- size: SizeType;
- width?: WidthSize;
- variant: "outline" | "solid";
- type: "button" | "submit";
- rounded: ButtonRoundedRadius;
- leftIcon?: ReactElement;
- rightIcon?: ReactElement;
- children: ReactNode;
- onClick?: () => void;
- }
-
- const Button: FunctionComponent = ({
- isloading,
- isDisable,
- loadingText,
- size,
- width,
- variant = "solid",
- type,
- rounded = "sm",
- leftIcon,
- rightIcon,
- onClick,
- children,
- ...rest
- }) => {
- const borderRadius = borderRadiusMap[rounded];
-
- return (
-
- {children}
-
- )
- }
-
- export default Button;
-`;
\ No newline at end of file
diff --git a/src/constant/Menu.ts b/src/constant/Menu.ts
index 56bc851..962cb98 100644
--- a/src/constant/Menu.ts
+++ b/src/constant/Menu.ts
@@ -14,4 +14,9 @@ export const Menu = [
name: "Articles",
url: "articles"
},
+ {
+ id: 4,
+ name: "Discussions ~ coming soon",
+ url: ""
+ },
];
\ No newline at end of file
diff --git a/src/hooks/article/useGetAuthoredArticles.ts b/src/hooks/article/useGetAuthoredArticles.ts
index 7f9ef7b..125bfb7 100644
--- a/src/hooks/article/useGetAuthoredArticles.ts
+++ b/src/hooks/article/useGetAuthoredArticles.ts
@@ -5,10 +5,17 @@ import { GET_ARTHORED_ARTICLES_ENDPOINT } from '@api/index'
export const useGetAuthoredArticles = (limit: number = 0, username: string) => {
const fetchPaginatedArticles = async ({ pageParam = 0 }) => {
- const res = await axiosInstance.get(`${GET_ARTHORED_ARTICLES_ENDPOINT}/${username}?limit=${limit}&page=${pageParam}`)
-
- const dataResponse = await res.data;
- return { ...dataResponse, prevOffset: pageParam };
+ try {
+ const res = await axiosInstance.get(`${GET_ARTHORED_ARTICLES_ENDPOINT}/${username}?limit=${limit}&page=${pageParam}`);
+ const dataStatus = res.status;
+ const dataResponse = await res.data;
+ return { ...dataResponse, dataStatus, prevOffset: pageParam };
+ } catch (error: any) {
+ if (error.response?.status === 404) {
+ return { dataStatus: 404 }; // Handle 404 by returning a custom response
+ }
+ throw error; // Rethrow any other error to be handled by React Query
+ }
};
const {
@@ -24,14 +31,23 @@ export const useGetAuthoredArticles = (limit: number = 0, username: string) => {
initialPageParam: 1,
placeholderData: keepPreviousData,
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
}
})
const articles = articlesResponse?.pages ?? null;
+ const dataStatus = articlesResponse?.pages?.[0]?.dataStatus ?? null;
- return { articles, isLoading, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage }
+ return {
+ articles,
+ isLoading,
+ isSuccess,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ dataStatus
+ }
}
\ No newline at end of file
diff --git a/src/hooks/article/useGetPublicAuthoredArticles.ts b/src/hooks/article/useGetPublicAuthoredArticles.ts
new file mode 100644
index 0000000..d98f464
--- /dev/null
+++ b/src/hooks/article/useGetPublicAuthoredArticles.ts
@@ -0,0 +1,54 @@
+import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
+
+import { axiosInstance } from '@api/axiosInstance'
+import { GET_ARTHORED_ARTICLES_ENDPOINT } from '@api/index'
+
+export const useGetPublicAuthoredArticles = (limit: number = 0, username: string) => {
+ const fetchPublicAuthoredArticles = async ({ pageParam = 0 }) => {
+ try {
+ const res = await axiosInstance
+ .get(`${GET_ARTHORED_ARTICLES_ENDPOINT}/${username}/public?limit=${limit}&page=${pageParam}`);
+ const dataStatus = res.status;
+ const dataResponse = await res.data;
+ return { ...dataResponse, dataStatus, prevOffset: pageParam };
+ } catch (error: any) {
+ if (error.response?.status === 404) {
+ return { dataStatus: 404 }; // Handle 404 by returning a custom response
+ }
+ throw error; // Rethrow any other error to be handled by React Query
+ }
+ };
+
+ const {
+ data: articlesResponse,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
+ isSuccess
+ } = useInfiniteQuery({
+ queryKey: ['public-author-articles', limit, username],
+ queryFn: fetchPublicAuthoredArticles,
+ placeholderData: keepPreviousData,
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (!lastPage?.data?.pagination?.next_page_url) {
+ return undefined;
+ }
+ return lastPage?.data?.pagination?.current_page + 1
+ }
+ })
+
+ const articles = articlesResponse?.pages ?? null;
+ const dataStatus = articlesResponse?.pages?.[0]?.dataStatus ?? null;
+
+ return {
+ articles,
+ isLoading,
+ isSuccess,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ dataStatus
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/thread/useGetAuthoredThreads.ts b/src/hooks/thread/useGetAuthoredThreads.ts
index b34dedc..3a2187d 100644
--- a/src/hooks/thread/useGetAuthoredThreads.ts
+++ b/src/hooks/thread/useGetAuthoredThreads.ts
@@ -5,10 +5,18 @@ import { GET_ARTHORED_THREADS_ENDPOINT } from '@api/index'
export const useGetAuthoredThreads = (limit: number = 0, username: string) => {
const fetchPaginatedArticles = async ({ pageParam = 0 }) => {
- const res = await axiosInstance.get(`${GET_ARTHORED_THREADS_ENDPOINT}/${username}?limit=${limit}&page=${pageParam}`)
-
- const dataResponse = await res.data;
- return { ...dataResponse, prevOffset: pageParam };
+ try {
+ const res = await axiosInstance
+ .get(`${GET_ARTHORED_THREADS_ENDPOINT}/${username}?limit=${limit}&page=${pageParam}`);
+ const dataStatus = res.status;
+ const dataResponse = await res.data;
+ return { ...dataResponse, dataStatus, prevOffset: pageParam };
+ } catch (error: any) {
+ if (error.response?.status === 404) {
+ return { dataStatus: 404 }; // Handle 404 by returning a custom response
+ }
+ throw error; // Rethrow any other error to be handled by React Query
+ }
};
const {
@@ -24,14 +32,23 @@ export const useGetAuthoredThreads = (limit: number = 0, username: string) => {
initialPageParam: 1,
placeholderData: keepPreviousData,
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
}
})
const threads = threadsResponse?.pages ?? null;
+ const dataStatus = threadsResponse?.pages?.[0]?.dataStatus ?? null;
- return { threads, isLoading, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage }
+ return {
+ threads,
+ isLoading,
+ isSuccess,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ dataStatus
+ }
}
\ No newline at end of file
diff --git a/src/hooks/thread/useGetPublicAuthoredThreads.ts b/src/hooks/thread/useGetPublicAuthoredThreads.ts
new file mode 100644
index 0000000..5a76167
--- /dev/null
+++ b/src/hooks/thread/useGetPublicAuthoredThreads.ts
@@ -0,0 +1,54 @@
+import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
+
+import { axiosInstance } from '@api/axiosInstance'
+import { GET_ARTHORED_THREADS_ENDPOINT } from '@api/index'
+
+export const useGetPublicAuthoredThreads = (limit: number = 0, username: string) => {
+ const fetchPublicAuthoredThreads = async ({ pageParam = 0 }) => {
+ try {
+ const res = await axiosInstance
+ .get(`${GET_ARTHORED_THREADS_ENDPOINT}/${username}/public?limit=${limit}&page=${pageParam}`);
+ const dataStatus = res.status;
+ const dataResponse = await res.data;
+ return { ...dataResponse, dataStatus, prevOffset: pageParam };
+ } catch (error: any) {
+ if (error.response?.status === 404) {
+ return { dataStatus: 404 }; // Handle 404 by returning a custom response
+ }
+ throw error; // Rethrow any other error to be handled by React Query
+ }
+ };
+
+ const {
+ data: threadsResponse,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
+ isSuccess
+ } = useInfiniteQuery({
+ queryKey: ['public-author-threads', limit, username],
+ queryFn: fetchPublicAuthoredThreads,
+ placeholderData: keepPreviousData,
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ if (!lastPage?.data?.pagination?.next_page_url) {
+ return undefined;
+ }
+ return lastPage?.data?.pagination?.current_page + 1
+ }
+ })
+
+ const threads = threadsResponse?.pages ?? null;
+ const dataStatus = threadsResponse?.pages?.[0]?.dataStatus ?? null;
+
+ return {
+ threads,
+ isLoading,
+ isSuccess,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ dataStatus
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/user/useGetPublicUser.ts b/src/hooks/user/useGetPublicUser.ts
new file mode 100644
index 0000000..c5fdc3d
--- /dev/null
+++ b/src/hooks/user/useGetPublicUser.ts
@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query'
+
+import { getPublicUser } from '@services/user'
+
+export const useGetPublicUser = (username: string) => {
+ return useQuery({
+ queryKey: ['user', username],
+ queryFn: () => getPublicUser(username),
+ })
+}
\ No newline at end of file
diff --git a/src/pages/Articles/HeroSection/index.tsx b/src/pages/Articles/HeroSection/index.tsx
index 75e12be..63cc774 100644
--- a/src/pages/Articles/HeroSection/index.tsx
+++ b/src/pages/Articles/HeroSection/index.tsx
@@ -58,7 +58,7 @@ const ArticleHeroSection: FunctionComponent = ({
-
+
{authorName}
diff --git a/src/pages/Articles/components/ArticleForm.tsx b/src/pages/Articles/components/ArticleForm.tsx
index 0dd7f1e..328034d 100644
--- a/src/pages/Articles/components/ArticleForm.tsx
+++ b/src/pages/Articles/components/ArticleForm.tsx
@@ -146,7 +146,7 @@ const ArticleForm: FunctionComponent = ({
/>
-
+
= ({
/>
-
+