diff --git a/package.json b/package.json index 730d8f68939dceea01b0870e32658be0515528e5..1a5e14a8b1cc33cd73ebfdddd6998f4de1bd8b7e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@types/react-dom": "^18.0.0", "axios": "^1.6.1", "framer-motion": "^10.16.4", + "js-cookie": "^3.0.5", "loader-utils": "3.2.1", "primereact": "^10.0.9", "react": "^18.2.0", @@ -54,6 +55,7 @@ ] }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "autoprefixer": "^10.4.16", "postcss": "^8.4.31" } diff --git a/src/App.tsx b/src/App.tsx index bcf346db6ee11d191f97b713738ecdd9138d7147..a53b35ea233b9c443a9635bd31e9ba94cc2fb9d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,42 +1,65 @@ import React, { useEffect } from "react"; import "./App.css"; import { Routes, Route, useNavigate, Navigate } from "react-router-dom"; -import { Container } from "@chakra-ui/react"; import Home from "./pages/Home"; import Login from "./pages/Login"; import Register from "./pages/Register"; -import Courses from "./pages/admin/PremiumCourses"; +import { CoursesList } from "./pages/admin/PremiumCourses"; import Users from "./pages/admin/PremiumUsers"; import Request from "./pages/admin/Request"; import AdminRegister from "./pages/admin/AdminRegister"; import Profile from "./pages/Profile"; import Materials from "./pages/Materials"; -import Navbar from "./components/navbar/Navbar"; -import { Layout } from "./components/layout"; import NotFound from "./pages/NotFound"; +import { AdminLayout } from "./components/layout/AdminLayout"; +import { TeacherLayout } from "./components/layout/TeacherLayout"; function App() { + // const LoggedInRoutes = (children) => { + + // }; return ( <Routes> - <Route - path="/" - element={<Navigate to="/course?page=1" replace />} - /> + <Route path="/" element={<Navigate to="/course?page=1" replace />} /> + <Route path="/admin"> + <Route + path="register" + element={ + <AdminLayout redirect="/not-found"> + <AdminRegister /> + </AdminLayout> + } + /> + <Route + path="courses" + element={ + <AdminLayout redirect="/not-found" children={<CoursesList />} /> + } + /> + <Route + path="request" + element={<AdminLayout redirect="/not-found" children={<Request />} />} + /> + <Route + path="users" + element={<AdminLayout redirect="/not-found" children={<Users />} />} + /> + </Route> {/* Contoh react router */} - <Route path="/course" element={<Layout children={<Home />} />} /> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> - <Route path="/admin/register" element={<AdminRegister />} /> - <Route path="/profile" element={<Layout children={<Profile />} />} /> - <Route path="/request" element={<Layout children={<Request />} />} /> + + <Route + path="/course" + element={<TeacherLayout redirect="/not-found" children={<Home />} />} + /> <Route - path="/premium-courses" - element={<Layout children={<Courses />} />} + path="/profile" + element={<TeacherLayout redirect="/not-found" children={<Profile />} />} /> - <Route path="/premium-users" element={<Layout children={<Users />} />} /> <Route path="/materials/:course_id" - element={<Layout children={<Materials />} />} + element={<TeacherLayout children={<Materials />} />} /> <Route path="*" element={<NotFound />} /> </Routes> diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2b2a0483f517d0d77577ffe18f709010e0442df8 --- /dev/null +++ b/src/components/layout/AdminLayout.tsx @@ -0,0 +1,44 @@ +import { Container } from "@chakra-ui/react"; +import Navbar from "../navbar/Navbar"; +import React, { useEffect } from "react"; +import axios from "axios"; +import { axiosConfig } from "../../utils/axios"; +import { useNavigate } from "react-router-dom"; +import config from "../../config/config"; + +interface AdminLayoutProps { + redirect: string; + children: React.ReactNode; +} + +export const AdminLayout = ({ redirect, children }: AdminLayoutProps) => { + const axiosInstance = axios.create(axiosConfig()); + const navigate = useNavigate(); + const isAdmin = () => { + axiosInstance + .get(`${config.REST_API_URL}/user/isAdmin`) + .then((res) =>{ + const response = res["data"]; + const {status, data} = response; + if(!data || status !== 200){ + navigate("/not-found") + } + }); + }; + useEffect(()=>{ + isAdmin(); + },[]) + return ( + <Container + maxW={"100vw"} + maxH={"100vh"} + display={"flex"} + flexDirection={"row"} + className="App" + bg={"#ffeaff"} + > + <Navbar /> + {children} + </Container> + ); +}; diff --git a/src/components/layout/TeacherLayout.tsx b/src/components/layout/TeacherLayout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afcc0237411a07d935f28d84e8729147d255d9ca --- /dev/null +++ b/src/components/layout/TeacherLayout.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from "react"; +import { axiosConfig } from "../../utils/axios"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; +import config from "../../config/config"; +import { Container } from "@chakra-ui/react"; +import Navbar from "../navbar/Navbar"; +interface TeacherLayoutProps { + redirect?: string; + children: React.ReactNode; +} +export const TeacherLayout = ({ redirect, children }: TeacherLayoutProps) => { + const axiosInstance = axios.create(axiosConfig()); + const navigate = useNavigate(); + const isTeacher = () => { + axiosInstance.get(`${config.REST_API_URL}/user/isAdmin`).then((res) => { + const response = res["data"]; + const { status, data } = response; + if (status === 401) { + navigate("/login"); + } + if (data) { + navigate("/admin/users"); + } + }); + }; + useEffect(() => { + isTeacher(); + }, []); + return ( + <Container + maxW={"100vw"} + maxH={"100vh"} + display={"flex"} + flexDirection={"row"} + className="App" + bg={"#ffeaff"} + > + <Navbar /> + {children} + </Container> + ); +}; diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx deleted file mode 100644 index 6f43f57874c6a94f26796bf12bb0e2626008f48e..0000000000000000000000000000000000000000 --- a/src/components/layout/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Container } from "@chakra-ui/react"; -import Navbar from "../navbar/Navbar"; -import React from "react"; - - -interface LayoutProps { - children: React.ReactNode; - } - -export const Layout = ({children} : LayoutProps) => { - return ( - <Container - maxW={"100vw"} - maxH={"100vh"} - display={"flex"} - flexDirection={"row"} - className="App" - bg={"#ffeaff"} - > - <Navbar /> - {children} - </Container> - ); -}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index b1f5f832b5c68ec8e0e9a78021c315e1a6cd710c..c0f642a39fefe156f70db510602935e3306e10d3 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -16,11 +16,11 @@ import ReactPaginate from "react-paginate"; import { IconContext } from "react-icons"; import { BiChevronLeftCircle, BiChevronRightCircle } from "react-icons/bi"; import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; -import { axiosConfig } from "../utils/axios"; import axios from "axios"; -import config from "../config/config"; import { Courses } from "../types"; import Loading from "../components/loading/Loading"; +import { axiosConfig } from "../utils/axios"; +import config from "../config/config"; const Home = () => { const { page: pageNumber } = useParams(); @@ -40,7 +40,7 @@ const Home = () => { const getCourses = async (pageNumber: number) => { try { setIsLoading(true); - const res = await newAxiosInstance.get(`${config.REST_API_URL}/course?page=${pageNumber}`); + const res = await newAxiosInstance.get(`${config.REST_API_URL}/course/teacher?page=${pageNumber}`); const {status} = res["data"]; if(status === 401){ toast({ diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index b84aec81170672e8df9178394402f38118ece902..ba9f34bd412cd32a3a770609298dea0eea1f836e 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -15,8 +15,8 @@ import { } from "@chakra-ui/react"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import { BiHide, BiShow } from "react-icons/bi"; -import { axiosConfig } from "../utils/axios"; import config from "../config/config"; +import { axiosConfig } from "../utils/axios"; function Login() { const axiosInstance = axios.create(axiosConfig()); diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 555665f3978ab0a7457e3443839b960f551f8a44..1d913e10a9dd32629769f896d49e5241f89ef359 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Flex, Button, @@ -128,8 +128,7 @@ function Register() { duration: 3000, isClosable: true, position: "top", - }); - + }); } }); } catch (error) { diff --git a/src/pages/admin/AdminRegister.tsx b/src/pages/admin/AdminRegister.tsx index 324c1c6a0777607492dfd75a9c60d8edd38474c3..7b27dd20939cc0bac3aa0b9370915cf37f42ac8a 100644 --- a/src/pages/admin/AdminRegister.tsx +++ b/src/pages/admin/AdminRegister.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Flex, Button, @@ -9,11 +9,136 @@ import { Text, Container, Select, + useToast, + InputRightElement, + InputGroup, } from "@chakra-ui/react"; +import { axiosInstance } from "../../utils/axios"; +import config from "../../config/config"; +import { useNavigate } from "react-router-dom"; +import { BiHide, BiShow } from "react-icons/bi"; +import Cookies from "js-cookie"; -function Register() { +function AdminRegister() { const titleColor = "purple.500"; const textColor = "black"; + const toast = useToast(); + const navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [fullname, setFullname] = useState(""); + const [password, setPassword] = useState(""); + const [isAdmin, setIsAdmin] = useState(false); + const [passwordVisible, setPasswordVisible] = useState(false); + const [usernameError, setUsernameError] = useState(""); + const [isAllValid, setIsAllValid] = useState({ + username: false, + fullname: false, + password: false, + }); + useEffect(() => { + const cookie = Cookies.get("user"); + console.log(cookie); + }, []); + const handleChangeFullname: React.ChangeEventHandler<HTMLInputElement> = ( + e: React.ChangeEvent<HTMLInputElement> + ) => { + setFullname(e.target.value); + checkFullname(); + }; + + const handleChangeUsername: React.ChangeEventHandler<HTMLInputElement> = ( + e: React.ChangeEvent<HTMLInputElement> + ) => { + setUsername(e.target.value); + checkUsername(e.target.value); + }; + + const handleChangePassword: React.ChangeEventHandler<HTMLInputElement> = ( + e: React.ChangeEvent<HTMLInputElement> + ) => { + setPassword(e.target.value); + checkPassword(); + }; + + const checkFullname = () => { + if (fullname.length >= 5) { + setIsAllValid({ ...isAllValid, fullname: true }); + } else { + setIsAllValid({ ...isAllValid, fullname: false }); + } + }; + const checkUsername = (current_username: string) => { + if (current_username.length < 5) { + setUsernameError("Username minimum length is 5"); + setIsAllValid({ ...isAllValid, username: false }); + } else if (current_username.includes(" ")) { + setUsernameError("Username should not have a whitespace"); + setIsAllValid({ ...isAllValid, username: false }); + } else { + try { + axiosInstance + .post(`${config.REST_API_URL}/user/username`, { + username: current_username, + }) + .then((res) => { + const { result } = res["data"]; + if (!result) { + setIsAllValid({ ...isAllValid, username: true }); + } else { + setUsernameError("Username must be unique "); + setIsAllValid({ ...isAllValid, username: false }); + } + }); + } catch (error) { + console.log(error); + } + } + }; + + const checkPassword = () => { + const regex = /^(?=.*\d)(?=.*[A-Z]).{8,}$/; + if (password.length > 8 && regex.test(password)) { + setIsAllValid({ ...isAllValid, password: true }); + } else { + setIsAllValid({ ...isAllValid, password: false }); + } + }; + const handleSubmit = () => { + try { + axiosInstance + .post(`${config.REST_API_URL}/user`, { + username: username, + fullname: fullname, + password: password, + }) + .then((res) => { + const { status } = res["data"]; + console.log(res); + if (status === 200) { + toast({ + title: "Register success!", + description: "Your account has been registered", + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + navigate("/admin/user"); + } else { + toast({ + title: "Register failed!", + description: "Your account can't be registered", + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + } + }); + } catch (error) { + console.log(error); + } + }; return ( <Container display={"flex"} flexDir={"column"}> <Flex @@ -59,7 +184,14 @@ function Register() { type="text" placeholder="Input User Fullname" size="lg" + value={fullname} + onChange={(e) => handleChangeFullname(e)} /> + {fullname && !isAllValid.fullname && ( + <Text color={"red.500"} mb="8px" fontSize={"12px"} ml={"2px"}> + Fullname minimum length is 5 + </Text> + )} <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> Username </FormLabel> @@ -72,28 +204,66 @@ function Register() { type="text" placeholder="Input Username" size="lg" + value={username} + onChange={(e) => handleChangeUsername(e)} /> + {username && !isAllValid.username && ( + <Text color="red.400" fontSize="xs"> + {usernameError} + </Text> + )} <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> Password </FormLabel> - <Input - isRequired - bg="white" - borderRadius="15px" - mb="36px" - fontSize="sm" - type="password" - placeholder="Input Password" - size="lg" - /> - + <InputGroup> + <Input + isRequired + bg="white" + borderRadius="15px" + mb="12px" + fontSize="sm" + type={passwordVisible ? "text" : "password"} + placeholder="Enter your password" + size="lg" + value={password} + onChange={handleChangePassword} + /> + <InputRightElement + display={"flex"} + alignItems={"center"} + justifyContent={"center"} + > + {!passwordVisible ? ( + <BiHide + cursor={"pointer"} + size="24px" + onClick={() => setPasswordVisible(true)} + /> + ) : ( + <BiShow + cursor={"pointer"} + size={"24px"} + onClick={() => setPasswordVisible(false)} + /> + )} + </InputRightElement> + </InputGroup> + {password && !isAllValid.password && ( + <Text color="red.400" fontSize="xs" marginBottom={"12px"}> + Password minimum length is 8 and must contain a Capital letter + and 1 number + </Text> + )} <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> Role </FormLabel> <Select placeholder={"Select Role"}> - <option value="student">Student</option> - <option value="teacher">Teacher</option> - <option value="admin">Admin</option> + <option value="teacher" onClick={() => setIsAdmin(false)}> + Teacher + </option> + <option value="admin" onClick={() => setIsAdmin(true)}> + Admin + </option> </Select> <Button @@ -111,6 +281,7 @@ function Register() { _active={{ bg: "purple.400", }} + onClick={handleSubmit} > Register </Button> @@ -121,5 +292,4 @@ function Register() { </Container> ); } - -export default Register; +export default AdminRegister; diff --git a/src/pages/admin/PremiumCourses.tsx b/src/pages/admin/PremiumCourses.tsx index e1313f02d09511aa816ae0a7edc5951a73983011..7bdf9e0c04039912000022b4e426c7bc268d4eb2 100644 --- a/src/pages/admin/PremiumCourses.tsx +++ b/src/pages/admin/PremiumCourses.tsx @@ -29,7 +29,7 @@ import { DataTable } from "primereact/datatable"; import { Column } from "primereact/column"; import { useDisclosure } from "@chakra-ui/react"; -const CoursesList = () => { +export const CoursesList = () => { type courses = { course_id: number; title: string; @@ -542,6 +542,4 @@ function AddCourseModal({ isOpen, onClose, handleAdd }: AddCourseModalProps) { </ModalContent> </Modal> ); -} - -export default CoursesList; +} \ No newline at end of file diff --git a/src/routes/AdminRoutes.jsx b/src/routes/AdminRoutes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7fd23d3603448e9fbad204a369befa18dde80c38 --- /dev/null +++ b/src/routes/AdminRoutes.jsx @@ -0,0 +1,5 @@ +export const AdminRoutes = ({ children }) => { + +}; + +export const TeacherRoutes = ({ children }) => {}; diff --git a/src/utils/axios.ts b/src/utils/axios.ts index abd303c7259c99bc3e4613beb7962e784578aa96..98818f7129bceb46a011fb1b6cde775fabc09eea 100644 --- a/src/utils/axios.ts +++ b/src/utils/axios.ts @@ -9,7 +9,7 @@ const axiosInstance = axios.create({ // 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Origin': '*', }, - withCredentials: false, + withCredentials: true, }); const axiosConfig = () => { diff --git a/yarn.lock b/yarn.lock index 703795a5fd463f4ff62dee787b612125b81f208b..0e5c4905e328b3cbf4e70eeece5ca04437e6a35b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2972,6 +2972,11 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/js-cookie@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95" + integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.14" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz" @@ -7175,6 +7180,11 @@ jiti@^1.18.2: resolved "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz" integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"