Skip to content

Commit

Permalink
FEAT: get user public posts, finish protected/public route & user det…
Browse files Browse the repository at this point in the history
…ails
  • Loading branch information
chimobi-justice committed Sep 26, 2024
1 parent ce2a536 commit ce93a8b
Show file tree
Hide file tree
Showing 44 changed files with 764 additions and 239 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).


9 changes: 7 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -41,6 +44,8 @@ const routes = createBrowserRouter(
<Route index path='/articles' element={<Articles />} />
<Route index path='/articles/:slug/:id' element={<ShowArticle />} />

<Route index path='/user/:username' element={<ShowUserPublicPosts />} />

{/* private route */}
<Route path="/articles/new" element={<PrivateRoute element={<CreateArticle />} />} />
<Route path="/articles/edit/:id" element={<PrivateRoute element={<EditArticle />} />} />
Expand All @@ -54,8 +59,8 @@ const routes = createBrowserRouter(
<Route path="/me/settings/account/edit" element={<PrivateRoute element={<ProfileEdit />} />} />
{/* end private route */}

<Route index path='/auth/login' element={<Login />} />
<Route index path='/auth/register' element={<Register />} />
<Route path="/auth/login" element={<AuthRoute element={<Login />} />} />
<Route path="/auth/register" element={<AuthRoute element={<Register />} />} />

<Route index path='*' element={<NotFound />} />
</Route>
Expand Down
28 changes: 28 additions & 0 deletions src/Route/AuthRoute.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthRouteProps> = ({ 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;
2 changes: 0 additions & 2 deletions src/Route/privateRoute.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
7 changes: 2 additions & 5 deletions src/api/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
);
2 changes: 2 additions & 0 deletions src/api/endpoints/userEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
1 change: 0 additions & 1 deletion src/api/types/messageResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export interface MessageResponse {
message: string;
// data: T
}
34 changes: 14 additions & 20 deletions src/api/types/user/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
}
40 changes: 19 additions & 21 deletions src/components/ArticleCard/Articles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,9 +63,9 @@ const ArticlesCard: FunctionComponent<IProps> = ({
/>
</Box>

<Box
width={{ base: "100%", md: "65%" }}
p={"5px"}
<Box
width={{ base: "100%", md: "65%" }}
p={"10px"}
display={"flex"}
gap={2}
justifyContent={"space-between"}
Expand All @@ -88,38 +88,36 @@ const ArticlesCard: FunctionComponent<IProps> = ({
fontSize={"14px"}
lineHeight={"1.7em"}
color={"#0009"}
dangerouslySetInnerHTML={stripTags(truncate(description, 250))}
dangerouslySetInnerHTML={stripTags(truncate(description, 200))}
/>

<Flex flex="1" gap={2} alignItems="center" flexWrap="wrap" my={"12px"}>
{authorUsername && authorAvatar && (
<Link to={`/user/${authorUsername}`}>
<Avatar size={"xs"} name={authorFullname} src={authorAvatar} />
</Link>
)}

<Box>
<Flex flex="1" gap={2} my={"12px"} flexDirection={{ base: "column", md: "row" }}>
<Box display={"flex"} gap={3} alignItems={"center"}>
{authorUsername && (
<Link to={`/user/${authorUsername}`}>
<Avatar size={"xs"} name={authorFullname} src={authorAvatar} />
</Link>
)}

{authorUsername && (
<Heading size="xs" fontSize={"13px"}>
<Link to={`/user/${authorUsername}`}>
{authorUsername}
</Link>
</Heading>
)}
</Box>

<Box display={"flex"} gap={3}>
{authorProfileHeadlines && (
<Text fontSize={"12px"} color={"#0009"}>{truncate(authorProfileHeadlines, 60)}</Text>
)}
<Box display={"flex"} gap={3} alignItems={"center"}>
{authorProfileHeadlines && (
<Text fontSize={"12px"} color={"#0009"}>{truncate(authorProfileHeadlines, 60)}</Text>
)}

<Text fontSize={"12px"} color={"#0009"}>{read_time}</Text>
</Box>
<Text fontSize={"12px"} color={"#0009"}>{read_time}</Text>
</Box>

</Flex>
</Box>

{isOwner && (
<Flex p={"5px"} gap={2}>
<Box>
Expand Down
12 changes: 12 additions & 0 deletions src/components/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -122,6 +123,17 @@ const Footer: FunctionComponent = () => {
<IoLogoLinkedin style={{ fontSize: "20px" }} />LinkedIn
</Link>
</ListItem>
<ListItem>
<Link
to="https://github.com/chimobi-justice/learn-hub"
target='_blank'
style={{
display: "flex",
gap: "3px"
}}>
<FaGithub style={{ fontSize: "20px" }} />Github
</Link>
</ListItem>
</List>
</Box>
</SimpleGrid>
Expand Down
1 change: 1 addition & 0 deletions src/components/HeroSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const HeroSection: FunctionComponent = () => {
<Box
width={{ base: "100%", lg: "50%" }}
mt={{ base: "25px", lg: "2px" }}
display={{ base: "none", md: "block" }}
>
<Image
src={HeroImg}
Expand Down
29 changes: 17 additions & 12 deletions src/components/NavBar/navBarLg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import {
import { FaRegUser } from 'react-icons/fa'
import { IoIosArrowUp, IoIosArrowDown } from 'react-icons/io'

import Button from '@components/Button'
import { Button, Search } from '@components/index'
import { colors } from '../../colors'
import { Menu } from '@constant/Menu'
import { useUser } from '@context/userContext'
import { useSignOut } from '@hooks/auth/useSignOut'

const NavBarLg: FunctionComponent = () => {
const NavBarLg: FunctionComponent = () => {
const { user } = useUser();
const { signOutMutation } = useSignOut()

Expand All @@ -43,16 +43,20 @@ const NavBarLg: FunctionComponent = () => {
justifyContent="space-between"
py={"20px"}
>
<Link to="/">
<Heading
fontStyle={"italic"}
fontWeight={"bold"}
as="h4"
color={colors.primary}
>
Learn <Text as="span" color={"#000"}>Hub</Text>
</Heading>
</Link>
<Box display={"flex"} gap={4} alignItems={"center"}>
<Link to="/">
<Heading
fontStyle={"italic"}
fontWeight={"bold"}
as="h4"
color={colors.primary}
>
Learn <Text as="span" color={"#000"}>Hub</Text>
</Heading>
</Link>

<Search />
</Box>

<Box
display={"inline-flex"}
Expand Down Expand Up @@ -84,6 +88,7 @@ const NavBarLg: FunctionComponent = () => {
<Avatar
size={"sm"}
name={user?.data?.fullname}
src={user?.data?.avatar}
/>
<Box>
<Text
Expand Down
4 changes: 3 additions & 1 deletion src/components/NavBar/navBarSm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from 'react-icons/io'

import { colors } from '../../colors'
import { Button } from '@components/index'
import { Button, Search } from '@components/index'
import { Menu } from '@constant/Menu'
import { useUser } from '@context/userContext'
import { useSignOut } from '@hooks/auth/useSignOut'
Expand Down Expand Up @@ -119,6 +119,7 @@ const NavBarSm: FunctionComponent = () => {
</Box>

<Box p={"10px"}>
<Search />
<List alignItems="start" mt="2rem" w="100%">
{Menu?.map((menu) => (
<ListItem w="100%" mb="20px" key={menu.id}>
Expand All @@ -145,6 +146,7 @@ const NavBarSm: FunctionComponent = () => {
<Avatar
size={"sm"}
name={user?.data?.fullname}
src={user?.data?.avatar}
/>
<Box>
<Text
Expand Down
9 changes: 5 additions & 4 deletions src/components/ThreadCard/components/ThreadCardHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FunctionComponent } from 'react'
import { Link } from 'react-router-dom'
import { Avatar, Flex, Text } from '@chakra-ui/react'
import truncate from '@helpers/truncate';

interface ThreadCardHeaderProps {
author: {
Expand All @@ -12,12 +13,12 @@ interface ThreadCardHeaderProps {
}

const ThreadCardHeader: FunctionComponent<ThreadCardHeaderProps> = ({ author, createdAt }) => (
<Flex flex="1" gap={2} alignItems="center" flexWrap="wrap">
<Link to={`/user/${author.username}`}>
<Avatar size="xs" name={author.fullname} src={author.avatar} />
<Flex flex="1" gap={2} alignItems="center" flexWrap="wrap" mt={"15px"}>
<Link to={`/user/${author?.username}`}>
<Avatar size="xs" name={author?.fullname} src={author?.avatar} />
</Link>
<Text fontSize="12px" color="#0009" _hover={{ textDecoration: 'underline' }}>
<Link to={`/user/${author.username}`}>By: {author.fullname}</Link>
<Link to={`/user/${author?.username}`}>By: {truncate(author?.fullname, 20)}</Link>
</Text>
<Text fontSize="12px" fontWeight="300" color="#0009">
<Text as="span" color="#000" mr="2px">posted</Text> {createdAt}
Expand Down
Loading

0 comments on commit ce93a8b

Please sign in to comment.