diff --git a/.env b/.env new file mode 100644 index 0000000..fc17da7 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://learn-hub-backend.test/api/v1 \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..2d1bf5f --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +VITE_API_BASE_URL= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 60dd285..9eda637 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@tanstack/react-query": "^5.50.1", + "axios": "^1.7.2", + "formik": "^2.4.6", "framer-motion": "^11.2.13", "highlight.js": "^11.10.0", "react": "^18.3.1", @@ -19,7 +21,9 @@ "react-icons": "^5.2.1", "react-markdown": "^9.0.1", "react-quill": "^2.0.0", - "react-router-dom": "^6.24.1" + "react-router-dom": "^6.24.1", + "react-toastify": "^10.0.5", + "yup": "^1.4.0" }, "devDependencies": { "@types/react": "^18.3.3", @@ -2610,6 +2614,15 @@ "@types/estree": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/lodash": { "version": "4.17.6", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", @@ -2987,6 +3000,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -3154,6 +3182,14 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3172,6 +3208,17 @@ "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", @@ -3301,6 +3348,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3333,6 +3388,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3907,6 +3970,67 @@ "node": ">=10" } }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/framer-motion": { "version": "11.2.13", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.13.tgz", @@ -4584,6 +4708,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5350,6 +5479,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -5624,6 +5772,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5906,6 +6064,18 @@ } } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -6259,11 +6429,21 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6289,6 +6469,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -6718,6 +6903,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e37557d..301ead1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@tanstack/react-query": "^5.50.1", + "axios": "^1.7.2", + "formik": "^2.4.6", "framer-motion": "^11.2.13", "highlight.js": "^11.10.0", "react": "^18.3.1", @@ -21,7 +23,9 @@ "react-icons": "^5.2.1", "react-markdown": "^9.0.1", "react-quill": "^2.0.0", - "react-router-dom": "^6.24.1" + "react-router-dom": "^6.24.1", + "react-toastify": "^10.0.5", + "yup": "^1.4.0" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/src/App.tsx b/src/App.tsx index 4af8306..a28fc26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ import UserViews from '@pages/Users/UserViews' import Login from '@pages/Auth/Login' import Register from '@pages/Auth/Register' +import PrivateRoutes from './Route/privateRoute' const routes = createBrowserRouter( createRoutesFromElements( @@ -29,15 +30,17 @@ const routes = createBrowserRouter( } /> } /> } /> - } /> } /> } /> - } /> - } /> - } /> - } /> + {/* private route */} + } />} /> + } />} /> + } />} /> + } />} /> + } />} /> + {/* end private route */} } /> } /> @@ -50,7 +53,7 @@ const routes = createBrowserRouter( const App = () => { return ( - + ) } diff --git a/src/Route/privateRoute.tsx b/src/Route/privateRoute.tsx new file mode 100644 index 0000000..7edae13 --- /dev/null +++ b/src/Route/privateRoute.tsx @@ -0,0 +1,19 @@ +import { FunctionComponent, ReactElement } from "react"; +import { Navigate, useLocation } from "react-router-dom"; + +interface PrivateRouteProps { + element: ReactElement; +} + +const PrivateRoutes: FunctionComponent = ({ element }) => { + const location = useLocation(); + const isAuthenticated = !!localStorage.getItem("ucType_"); + + return isAuthenticated ? ( + element + ) : ( + + ) +} + +export default PrivateRoutes \ No newline at end of file diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts new file mode 100644 index 0000000..0743a06 --- /dev/null +++ b/src/api/axiosInstance.ts @@ -0,0 +1,38 @@ +import axios from 'axios' + +import { API_BASE_URL } from '@api/constant' + +axios.defaults.baseURL = API_BASE_URL; + +export const axiosInstance = axios.create({ + baseURL: API_BASE_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + 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); + } +); + +axiosInstance.interceptors.response.use( + response => response, + error => { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.message || 'An unexpected error occurred'); + } + return Promise.reject(new Error('An unexpected error occurred')); + } +); diff --git a/src/api/constant.ts b/src/api/constant.ts new file mode 100644 index 0000000..acee7f3 --- /dev/null +++ b/src/api/constant.ts @@ -0,0 +1 @@ +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL \ No newline at end of file diff --git a/src/api/endpoints/authEndpoints.ts b/src/api/endpoints/authEndpoints.ts new file mode 100644 index 0000000..c70bf19 --- /dev/null +++ b/src/api/endpoints/authEndpoints.ts @@ -0,0 +1,8 @@ +import { API_BASE_URL } from '@api/constant' + +export const REGISTER_ENDPOINT = `${API_BASE_URL}/auth/register`; + +export const LOGIN_ENDPOINT = `${API_BASE_URL}/auth/login`; + +export const LOGOUT_ENDPOINT = `${API_BASE_URL}/auth/logout`; + diff --git a/src/api/endpoints/userEndpoints.ts b/src/api/endpoints/userEndpoints.ts new file mode 100644 index 0000000..75bec35 --- /dev/null +++ b/src/api/endpoints/userEndpoints.ts @@ -0,0 +1,3 @@ +import { API_BASE_URL } from '@api/constant' + +export const USER_ENDPOINT = `${API_BASE_URL}/user`; \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..2fdc0c5 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,7 @@ +// Re-export endpoints +export * from '@api/endpoints/authEndpoints' +export * from '@api/endpoints/userEndpoints' + +// Re-export types +export * from '@api/types/auth' +export * from '@api/types/user' diff --git a/src/api/types/auth/index.ts b/src/api/types/auth/index.ts new file mode 100644 index 0000000..1cd5c76 --- /dev/null +++ b/src/api/types/auth/index.ts @@ -0,0 +1,19 @@ +export interface SignupRequest { + fullname: string; + email: string; + password: string; +} + +export interface SignupResponse { + message: string; +} + +export interface SigninRequest { + email: string; + password: string; +} + +export interface SigninResponse { + message: string; + access_token: string; +} \ No newline at end of file diff --git a/src/api/types/user/index.ts b/src/api/types/user/index.ts new file mode 100644 index 0000000..47e5f5c --- /dev/null +++ b/src/api/types/user/index.ts @@ -0,0 +1,5 @@ +export interface UserResponse { + id: string | number; + fullname: string; + email: string; +} diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 5a6eae7..d35978e 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from 'react' +import { ChangeEvent, FocusEvent, FunctionComponent } from 'react' import { Input as ChakraInput } from '@chakra-ui/react' import { colors } from '../../colors' @@ -9,8 +9,8 @@ interface IProps { name: string; width?: string; placeholder: string; - onBlur?: () => void; - onChange?: () => void; + onBlur?: (event: FocusEvent) => void; + onChange?: (event: ChangeEvent) => void; } const Input: FunctionComponent = ({ @@ -18,7 +18,9 @@ const Input: FunctionComponent = ({ value, name, width, - placeholder + placeholder, + onBlur, + onChange }) => { return ( @@ -38,7 +40,8 @@ const Input: FunctionComponent = ({ fontSize: "14px", fontWeight: 400, }} - + onBlur={onBlur} + onChange={onChange} /> ) } diff --git a/src/components/NavBar/navBarLg.tsx b/src/components/NavBar/navBarLg.tsx index 731fae5..acbac5d 100644 --- a/src/components/NavBar/navBarLg.tsx +++ b/src/components/NavBar/navBarLg.tsx @@ -19,9 +19,16 @@ import { IoIosArrowUp, IoIosArrowDown } from 'react-icons/io' import Button from '@components/Button' import { colors } from '../../colors' import { Menu } from '@constant/Menu' +import { useUser } from '@context/userContext' +import { useSignOut } from '@hooks/auth/useSignOut' -const NavBarLg: FunctionComponent = () => { - const isLoggedIn = false; +const NavBarLg: FunctionComponent = () => { + const { user } = useUser(); + const { signOutMutation } = useSignOut() + + const handleLoggedOut = () => { + signOutMutation.mutate(); + }; return ( { ))} - - {isLoggedIn ? ( + {user ? ( {({ isOpen }) => ( @@ -75,15 +81,15 @@ const NavBarLg: FunctionComponent = () => { - Justice Chimobi + {user?.data?.fullname} @@ -107,7 +113,7 @@ const NavBarLg: FunctionComponent = () => { - + Logout @@ -116,7 +122,6 @@ const NavBarLg: FunctionComponent = () => { ) : ( - - - + {user && ( + + + + + + )} diff --git a/src/pages/Auth/Login/index.tsx b/src/pages/Auth/Login/index.tsx index 2d35547..7d5d7a1 100644 --- a/src/pages/Auth/Login/index.tsx +++ b/src/pages/Auth/Login/index.tsx @@ -8,13 +8,38 @@ import { CardBody, CardHeader, Heading, - Text + Text, + FormErrorMessage } from '@chakra-ui/react' import { Button, Input } from '@components/index' import { colors } from '../../../colors' +import { useSignin } from '@hooks/auth/useSignin' +import { SigninRequest } from '@api/index' +import { useFormik } from 'formik' +import { signInvalidateSchema } from '@validations/signin' const Login: FunctionComponent = () => { + const { signinMutation } = useSignin(); + + const _handleSignin = (values: SigninRequest) => { + signinMutation.mutate({ + email: values.email, + password: values.password, + }) + } + + const formilk = useFormik({ + initialValues: { + email: '', + password: '', + }, + onSubmit: _handleSignin, + validationSchema: signInvalidateSchema, + }); + + const { handleChange, handleBlur, handleSubmit, errors, values } = formilk; + return ( { Welcome back - - - Email address - - - +
+ + + Email address + + {errors.email && ( + {errors.email} + )} + + - - - Password - - - + + + Password + + {errors.password && ( + {errors.password} + )} + + - - - - - - Don't have an account? {" "} - - + + + + + Don't have an account? {" "} + + + Sign Up + + + +
diff --git a/src/pages/Auth/Register/index.tsx b/src/pages/Auth/Register/index.tsx index 23438ba..e016c78 100644 --- a/src/pages/Auth/Register/index.tsx +++ b/src/pages/Auth/Register/index.tsx @@ -9,12 +9,39 @@ import { CardHeader, Heading, Text, + FormErrorMessage, } from '@chakra-ui/react' +import { useFormik } from 'formik' import { Button, Input } from '@components/index' import { colors } from '../../../colors' +import { useSignup } from '@hooks/auth/useSignup' +import { signUpvalidateSchema } from '@validations/signup' +import { SignupRequest } from '@api/index' const Register: FunctionComponent = () => { + const { signupMutation } = useSignup(); + + const _handleSignup = (values: SignupRequest) => { + signupMutation.mutate({ + fullname: values.fullname, + email: values.email, + password: values.password, + }) + } + + const formilk = useFormik({ + initialValues: { + fullname: '', + email: '', + password: '', + }, + onSubmit: _handleSignup, + validationSchema: signUpvalidateSchema, + }); + + const { handleChange, handleBlur, handleSubmit, errors, values } = formilk; + return ( { Sign Up - - - Full Name - - - - - - Email address - - - +
+ + + Full Name + + {errors.fullname && ( + {errors.fullname} + )} + + + + + Email address + + {errors.email && ( + {errors.email} + )} + + - - - Password - - - + + + Password + + {errors.password && ( + {errors.password} + )} + + - - - - - Already have an account? {" "} - - + + + + Already have an account? {" "} + + + Sign In + + + +
diff --git a/src/pages/Forum/index.tsx b/src/pages/Forum/index.tsx index 96b4527..9fcba15 100644 --- a/src/pages/Forum/index.tsx +++ b/src/pages/Forum/index.tsx @@ -21,8 +21,11 @@ import truncate from '@helpers/truncate' import { Button, Search, FollowCard, RecommendTopicCard } from '@components/index' import DiscussionCard from '@pages/Forum/components/discussionCard' import AvatarPic from '@assets/images/avatar.jpg' +import { useUser } from '@context/userContext' const Forum: FunctionComponent = () => { + const { user } = useUser(); + const data = [1, 2, 3, 4, 5, 6, 7, 8, 9]; return ( @@ -32,20 +35,22 @@ const Forum: FunctionComponent = () => { > Forum - - - - - + {user && ( + + + + + + )} => { + const response = await axiosInstance.post(REGISTER_ENDPOINT, data); + return response.data; +} + +export const signinUser = async (data: SigninRequest): Promise => { + const response = await axiosInstance.post(LOGIN_ENDPOINT, data); + return response.data; +} + +export const signoutUser = async () => { + const response = await axiosInstance.post(LOGOUT_ENDPOINT, null); + return response.data +} \ No newline at end of file diff --git a/src/services/user/index.tsx b/src/services/user/index.tsx new file mode 100644 index 0000000..b68f918 --- /dev/null +++ b/src/services/user/index.tsx @@ -0,0 +1,7 @@ +import { axiosInstance } from '@api/axiosInstance' +import { USER_ENDPOINT, UserResponse } from '@api/index' + +export const getUser = async (): Promise => { + const response = await axiosInstance.get(USER_ENDPOINT); + return response.data; +} \ No newline at end of file diff --git a/src/validations/signin.ts b/src/validations/signin.ts new file mode 100644 index 0000000..f5c5742 --- /dev/null +++ b/src/validations/signin.ts @@ -0,0 +1,9 @@ +import * as Yup from 'yup' + +export const signInvalidateSchema = () => Yup.object({ + email: Yup.string() + .email("Invalid email address") + .required("Required"), + password: Yup.string() + .required("Required") +}); \ No newline at end of file diff --git a/src/validations/signup.ts b/src/validations/signup.ts new file mode 100644 index 0000000..e6e3a70 --- /dev/null +++ b/src/validations/signup.ts @@ -0,0 +1,12 @@ +import * as Yup from 'yup' + +export const signUpvalidateSchema = Yup.object({ + fullname: Yup.string().required("Required"), + email: Yup.string() + .email("Invalid email address") + .required("Required"), + password: Yup.string() + .required("Required") + .min(8, "Password must be more than 8") + .max(15, "Password should be atleast 15 characters"), +}); \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index ea323c0..f8679b4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -10,7 +10,10 @@ "@helpers/*": ["src/helpers/*"], "@context/*": ["src/context/*"], "@constant/*": ["src/constant/*"], - "@pages/*": ["src/pages/*"] + "@pages/*": ["src/pages/*"], + "@api/*": ["src/api/*"], + "@services/*": ["src/services/*"], + "@validations/*": ["src/validations/*"], }, "composite": true, diff --git a/vite.config.ts b/vite.config.ts index 1753031..5b1b760 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,10 @@ export default defineConfig({ '@errorBoundary': '/src/ErrorBoundary', '@helpers': '/src/helpers', '@context': '/src/context', - '@constant': '/src/constant' + '@constant': '/src/constant', + '@api': '/src/api', + '@services': '/src/services', + '@validations': '/src/validations' } } })