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.test.tsx b/src/App.test.tsx deleted file mode 100644 index 2a68616d9846ed7d3bfb9f28ca1eb4d51b2c2f84..0000000000000000000000000000000000000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(<App />); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); 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/components/modals/material.tsx b/src/components/modals/material.tsx index 8bf7a8a751f23cb9f5e1a3ab24f7fca7df0be370..28de400cdd88e080037b1e858caae4590e4763c1 100644 --- a/src/components/modals/material.tsx +++ b/src/components/modals/material.tsx @@ -1,211 +1,599 @@ -import { Box, Text, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, FormControl, FormLabel, Input, Textarea, ModalFooter, ButtonGroup, Button } from "@chakra-ui/react"; +import { + Box, + Text, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + FormControl, + FormLabel, + Input, + Textarea, + ModalFooter, + ButtonGroup, + Button, +} from "@chakra-ui/react"; +import { ChangeEvent, createRef, useEffect, useState } from "react"; import { BiError } from "react-icons/bi"; +import { axiosConfig } from "../../utils/axios"; +import axios from "axios"; +import config from "../../config/config"; +import Loading from "../loading/Loading"; -{ /* Modal Edit */ } -interface EditMaterialModalProps { +{/* Modal Add Material*/ } +interface AddMaterialModalProps { isOpen: boolean; onClose: () => void; - title: string; - description: string; - handleEdit: () => void; + successAdd: () => void; + moduleId: number; } -export function EditMaterialModal({ +export function AddMaterialModal({ isOpen, onClose, - title, - description, - handleEdit, -}: EditMaterialModalProps) { + successAdd, + moduleId, +}: AddMaterialModalProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const newAxiosInstance = axios.create(axiosConfig()); + const [fileType, setFileType] = useState(""); + const [fileName, setFileName] = useState(""); + const [selectedFile, setSelectedFile] = useState<File | null>(null); + const [isAllValid, setIsAllValid] = useState({ + title: false, + description: false, + file: false, + }); + const handleAddMaterial = async () => { + try { + setIsLoading(true); + try { + upload(); + } catch (error) { + console.error('Error uploading:', error); + } finally { + const response = await newAxiosInstance.post(`${config.REST_API_URL}/material/`, { + title: title, + description: description, + source_type: fileType, + material_path: fileName, + modul_id: moduleId, + }); + + console.log('Material added successfully:', response.data.message); + + // Clear the form after successful submission if needed + setTitle(''); + setDescription(''); + setIsLoading(false); + successAdd(); // Refresh new data without reloading page + } + } catch (error) { + console.error('Error adding material:', error); + } finally { + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + file: false, + })); + } + // window.location.reload(); // refresh to see new material added (should change to not reloading) + }; + + const upload = () => { + const formData = new FormData() + if (selectedFile) { + formData.append('file', selectedFile) + } + newAxiosInstance.post(`${config.REST_API_URL}/material/upload`, formData) + .then(res => { }) + .catch(er => console.log(er)) + } + + const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { + if (event.target.files) { + const file = event.target.files?.[0]; + + if (file) { + // const name = file.name; + // const type = file.type; + setSelectedFile(file); + if (file.type.startsWith('video')) { + setFileType('VIDEO'); + } else { + setFileType('PDF'); + } + + setFileName(file.name.replace(/\s/g, '')); + + setIsAllValid({ ...isAllValid, file: true }); + } else { + setIsAllValid({ ...isAllValid, file: false }); + } + } else { + setSelectedFile(null); + setIsAllValid({ ...isAllValid, file: false }); + } + }; + + const handleClose = () => { + setTitle(""); + setDescription(""); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + file: false, + })); + onClose(); + }; + + const checkTitle = () => { + setTitle((prevTitle) => { + const isValid = prevTitle.trim().length > 0; + setIsAllValid((prev) => ({ ...prev, title: isValid })); + return prevTitle; + }); + }; + + const checkDescription = () => { + setDescription((prevDescription) => { + const isValid = prevDescription.trim().length > 0; + setIsAllValid((prev) => ({ ...prev, description: isValid })); + return prevDescription; + }); + }; + + const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setTitle(e.target.value); + checkTitle(); + }; + + const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + setDescription(e.target.value); + checkDescription(); + }; + return ( - <Modal isOpen={isOpen} onClose={onClose}> - <ModalOverlay /> - <ModalContent> - <ModalHeader bg="#d78dff" textAlign={"center"}> - Edit Material - </ModalHeader> - <ModalCloseButton /> - <ModalBody> - <FormControl> - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - Material Title - </FormLabel> - <Input - isRequired - variant="outline" - bg="white" - borderRadius="15px" - mb="5" - fontSize="sm" - placeholder={title} - size="lg" - /> - - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - Material Description - </FormLabel> - <Textarea - isRequired - h="50" - maxHeight={"150"} - bg="white" - borderRadius="15px" - mb="5" - fontSize="sm" - placeholder={description} - size="lg" - /> - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - Material File - </FormLabel> - <Input - fontSize="sm" - border="none" - type="file" - accept="image/*" - size="lg" - /> - </FormControl> - </ModalBody> - - <ModalFooter justifyContent={"center"}> - <ButtonGroup> - <Button colorScheme="gray" flex="1" onClick={onClose}> - Cancel - </Button> - <Button colorScheme="purple" flex="1" ml={3} onClick={handleEdit}> - Edit - </Button> - </ButtonGroup> - </ModalFooter> - </ModalContent> - </Modal> + <> + <Loading loading={isLoading} /> + <Modal isOpen={isOpen} onClose={handleClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader bg="#d78dff" textAlign={"center"}> + Add New Material + </ModalHeader> + <ModalCloseButton /> + <ModalBody> + <FormControl> + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material Title + </FormLabel> + <Input + isRequired + variant="outline" + bg="white" + borderRadius="15px" + mb="5" + fontSize="sm" + size="lg" + placeholder={"Insert Title Here"} + value={title} + onChange={handleTitleChange} + /> + + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material Description + </FormLabel> + <Textarea + isRequired + h="50" + maxHeight={"150"} + bg="white" + borderRadius="15px" + mb="5" + fontSize="sm" + size="lg" + placeholder={"Insert Description Here"} + value={description} + onChange={handleDescriptionChange} + /> + + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material File + </FormLabel> + <Input + fontSize="sm" + border="none" + type="file" + accept=".pdf, video/*" + size="lg" + onChange={handleFileChange} + /> + </FormControl> + </ModalBody> + + <ModalFooter justifyContent={"center"}> + <ButtonGroup> + <Button colorScheme="gray" flex="1" onClick={handleClose}> + Cancel + </Button> + <Button colorScheme="purple" flex="1" ml={3} + onClick={handleAddMaterial} + isDisabled={ + !( + isAllValid.title && + isAllValid.description && + isAllValid.file + ) + } + > + Add + </Button> + </ButtonGroup> + </ModalFooter> + </ModalContent> + </Modal> + </> ); } -{ /* Modal Delete */ } -interface DeleteMaterialModalProps { +{ /* Modal Edit Material */ } +interface EditMaterialModalProps { isOpen: boolean; onClose: () => void; - course_id: number; - handleDelete: () => void; + successEdit: () => void; + materialId: number } -export function DeleteMaterialModal({ +export function EditMaterialModal({ isOpen, onClose, - course_id, - handleDelete, -}: DeleteMaterialModalProps) { + successEdit, + materialId, +}: EditMaterialModalProps) { + const [editedTitle, setEditedTitle] = useState(''); + const [editedDescription, setEditedDescription] = useState(''); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const newAxiosInstance = axios.create(axiosConfig()); + const [selectedFile, setSelectedFile] = useState<File | null>(null); + const [fileType, setFileType] = useState(""); + const [fileName, setFileName] = useState(""); + const [oldFile, setOldFile] = useState(""); + const [isAllValid, setIsAllValid] = useState({ + title: false, + description: false, + file: false, + }); + + useEffect(() => { // Fetch Data to Get Material Detail + const fetchData = async () => { + try { + setIsLoading(true); + const res = await newAxiosInstance.get(`${config.REST_API_URL}/material/${materialId}`); + if (res.data.status === 200) { + setEditedTitle(res.data.data.title); + setEditedDescription(res.data.data.description); + setTitle(res.data.data.title); + setDescription(res.data.data.description); + setOldFile(res.data.data.material_path); + } else { } + setIsLoading(false); + } catch (error) { + console.error('Error fetching material data:', error); + } + }; + fetchData(); + }, [materialId]); + + const handleEditMaterial = async () => { + try { + setIsLoading(true); + try { + if (isAllValid.file) { + upload(); + } else { + setFileName(oldFile); + } + } catch (error) { + console.error('Error uploading:', error); + } finally { + const response = await newAxiosInstance.put(`${config.REST_API_URL}/material/${materialId}`, { + title: title, + description: description, + source_type: fileType, + material_path: fileName, + }); + + console.log('Material edited successfully:', response.data.message); + setIsLoading(false); + successEdit(); // Refresh new data without reloading page + } + } catch (error) { + console.error('Error editing material:', error); + } finally { + // window.location.reload(); // refresh to see new material added (should change to not reloading) + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + file: false, + })); + } + }; + + const upload = () => { + const formData = new FormData() + if (oldFile) { + // Make a delete request to remove the old file + newAxiosInstance.delete(`${config.REST_API_URL}/material/deleteFile/${oldFile}`) + .then(() => { + console.log('Old file deleted successfully'); + }) + .catch((error) => { + console.error('Error deleting old file:', error); + }); + } + if (selectedFile) { + formData.append('file', selectedFile) + } + newAxiosInstance.post(`${config.REST_API_URL}/material/upload`, formData) + .then(res => { }) + .catch(er => console.log(er)) + } + + const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { + if (event.target.files) { + const file = event.target.files?.[0]; + + if (file) { + // const name = file.name; + // const type = file.type; + setSelectedFile(file); + if (file.type.startsWith('video')) { + setFileType('VIDEO'); + } else { + setFileType('PDF'); + } + + setFileName(file.name.replace(/\s/g, '')); + + setIsAllValid({ ...isAllValid, file: true }); + } else { + setIsAllValid({ ...isAllValid, file: false }); + } + } else { + setSelectedFile(null); + setIsAllValid({ ...isAllValid, file: false }); + } + }; + + const handleClose = () => { + setTitle(editedTitle); + setDescription(editedDescription); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + file: false, + })); + onClose(); + }; + + const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.target.value.length > 0) { + setTitle(e.target.value); + setIsAllValid(prevState => ({ + ...prevState, + title: true, + })); + } else { + setTitle(editedTitle); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + })); + } + }; + + const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + if (e.target.value.length > 0) { + setDescription(e.target.value); + setIsAllValid(prevState => ({ + ...prevState, + description: true, + })); + } else { + setDescription(editedDescription); + setIsAllValid(prevState => ({ + ...prevState, + description: false, + })); + } + }; + return ( - <Modal isOpen={isOpen} onClose={onClose}> - <ModalOverlay /> - <ModalContent> - <ModalHeader textAlign={"center"}>Delete Material</ModalHeader> - <ModalCloseButton /> - <ModalBody textAlign={"center"}> - <Box - display="flex" - flexDirection="column" - alignItems="center" - justifyContent="center" - > - <Text as={BiError} fontSize={"150px"} color="red" /> - <Text>Are you sure want to delete this Material?</Text> - </Box> - </ModalBody> - - <ModalFooter justifyContent={"center"}> - <ButtonGroup> - <Button colorScheme="gray" flex="1" onClick={onClose}> - Cancel - </Button> - <Button colorScheme="red" flex="1" ml={3} onClick={handleDelete}> - Delete - </Button> - </ButtonGroup> - </ModalFooter> - </ModalContent> - </Modal> + <> + <Loading loading={isLoading} /> + <Modal isOpen={isOpen} onClose={handleClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader bg="#d78dff" textAlign={"center"}> + Edit Material + </ModalHeader> + <ModalCloseButton /> + <ModalBody> + <FormControl> + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material Title + </FormLabel> + <Input + isRequired + variant="outline" + bg="white" + borderRadius="15px" + mb="5" + fontSize="sm" + placeholder={title} + size="lg" + // value={editedTitle} + onChange={handleTitleChange} + /> + + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material Description + </FormLabel> + <Textarea + isRequired + h="50" + maxHeight={"150"} + bg="white" + borderRadius="15px" + mb="5" + fontSize="sm" + placeholder={description} + size="lg" + // value={editedDescription} + onChange={handleDescriptionChange} + /> + + <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> + Material File + </FormLabel> + <Input + fontSize="sm" + border="none" + type="file" + accept=".pdf, video/*" + size="lg" + onChange={handleFileChange} + /> + </FormControl> + </ModalBody> + + <ModalFooter justifyContent={"center"}> + <ButtonGroup> + <Button colorScheme="gray" flex="1" onClick={handleClose}> + Cancel + </Button> + <Button colorScheme="purple" flex="1" ml={3} + onClick={handleEditMaterial} + isDisabled={ + !( + isAllValid.title || + isAllValid.description || + isAllValid.file + ) + } + > + Edit + </Button> + </ButtonGroup> + </ModalFooter> + </ModalContent> + </Modal> + </> ); } -{/* Modal Add Material*/} -interface AddMaterialModalProps { +{ /* Modal Delete */ } +interface DeleteMaterialModalProps { isOpen: boolean; onClose: () => void; - handleAddMaterial: () => void; + successDelete: () => void; + materialId: number; } -export function AddMaterialModal({ +export function DeleteMaterialModal({ isOpen, onClose, - handleAddMaterial, -}: AddMaterialModalProps) { + successDelete, + materialId, +}: DeleteMaterialModalProps) { + const [isLoading, setIsLoading] = useState(false); + const [oldFile, setOldFile] = useState(""); + const newAxiosInstance = axios.create(axiosConfig()); + + useEffect(() => { // Fetch Data to Get Material Detail + const fetchData = async () => { + try { + setIsLoading(true); + const res = await newAxiosInstance.get(`${config.REST_API_URL}/material/${materialId}`); + if (res.data.status === 200) { + setOldFile(res.data.data.material_path); + } else { } + setIsLoading(false); + } catch (error) { + console.error('Error fetching material data:', error); + } + }; + fetchData(); + }, [materialId]); + + const handleDeleteMaterial = async () => { + try { + setIsLoading(true); + // Make a delete request to remove the old file + newAxiosInstance.delete(`${config.REST_API_URL}/material/deleteFile/${oldFile}`) + .then(() => { + console.log('Old file deleted successfully'); + }) + .catch((error) => { + console.error('Error deleting old file:', error); + }); + + const response = await newAxiosInstance.delete(`${config.REST_API_URL}/material/${materialId}`); + + console.log('Material Deleted successfully:', response.data.message); + + setIsLoading(false); + successDelete(); // Refresh new data without reloading page + } catch (error) { + console.error('Error deleting material:', error); + } + // window.location.reload(); // refresh to see new material added (should change to not reloading) + }; return ( - <Modal isOpen={isOpen} onClose={onClose}> - <ModalOverlay /> - <ModalContent> - <ModalHeader bg="#d78dff" textAlign={"center"}> - Add New Material - </ModalHeader> - <ModalCloseButton /> - <ModalBody> - <FormControl> - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - Material Title - </FormLabel> - <Input - isRequired - variant="outline" - bg="white" - borderRadius="15px" - mb="5" - fontSize="sm" - size="lg" - placeholder={"Insert Title Here"} - /> - - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - Material Description - </FormLabel> - <Textarea - isRequired - h="50" - maxHeight={"150"} - bg="white" - borderRadius="15px" - mb="5" - fontSize="sm" - size="lg" - placeholder={"Insert Description Here"} - /> - - <FormLabel ms="4px" fontSize="sm" fontWeight="bold"> - File - </FormLabel> - <Input - isRequired - fontSize="sm" - border="none" - type="file" - accept="image/*" - size="lg" - /> - </FormControl> - </ModalBody> - - <ModalFooter justifyContent={"center"}> - <ButtonGroup> - <Button colorScheme="gray" flex="1" onClick={onClose}> - Cancel - </Button> - <Button colorScheme="purple" flex="1" ml={3} onClick={handleAddMaterial}> - Add - </Button> - </ButtonGroup> - </ModalFooter> - </ModalContent> - </Modal> + <> + <Loading loading={isLoading} /> + <Modal isOpen={isOpen} onClose={onClose}> + <ModalOverlay /> + <ModalContent> + <ModalHeader textAlign={"center"}>Delete Material</ModalHeader> + <ModalCloseButton /> + <ModalBody textAlign={"center"}> + <Box + display="flex" + flexDirection="column" + alignItems="center" + justifyContent="center" + > + <Text as={BiError} fontSize={"150px"} color="red" /> + <Text>Are you sure want to delete this material?</Text> + </Box> + </ModalBody> + + <ModalFooter justifyContent={"center"}> + <ButtonGroup> + <Button colorScheme="gray" flex="1" onClick={onClose}> + Cancel + </Button> + <Button colorScheme="red" flex="1" ml={3} + onClick={handleDeleteMaterial} + > + Delete + </Button> + </ButtonGroup> + </ModalFooter> + </ModalContent> + </Modal> + </> ); -} \ No newline at end of file +} diff --git a/src/components/modals/module.tsx b/src/components/modals/module.tsx index 139448cefbe7533547afc9ca0ca1940eadc7bb70..7cb4e9162c7a53d1bf20d0dadd8ab0a8176f35c1 100644 --- a/src/components/modals/module.tsx +++ b/src/components/modals/module.tsx @@ -60,19 +60,25 @@ export function AddModuleModal({ setDescription(''); setIsLoading(false); successAdd(); // Refresh new data without reloading page - setIsAllValid({ ...isAllValid, title: false }); - setIsAllValid({ ...isAllValid, description: false }); } catch (error) { console.error('Error adding module:', error); } // window.location.reload(); // refresh to see new module added (should change to not reloading) + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + })); }; const handleClose = () => { setTitle(""); setDescription(""); - setIsAllValid({ ...isAllValid, title: false }); - setIsAllValid({ ...isAllValid, description: false }); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + })); onClose(); }; @@ -226,42 +232,59 @@ export function EditModuleModal({ }); console.log('Module edited successfully:', response.data.message); - setIsLoading(false); successEdit(); // Refresh new data without reloading page - setIsAllValid({ ...isAllValid, title: false }); - setIsAllValid({ ...isAllValid, description: false }); } catch (error) { console.error('Error editing module:', error); } // window.location.reload(); // refresh to see new module added (should change to not reloading) + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + })); }; const handleClose = () => { setTitle(editedTitle); setDescription(editedDescription); - setIsAllValid({ ...isAllValid, title: false }); - setIsAllValid({ ...isAllValid, description: false }); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + description: false, + })); onClose(); }; const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.value.length > 0) { setTitle(e.target.value); - setIsAllValid({ ...isAllValid, title: true }); + setIsAllValid(prevState => ({ + ...prevState, + title: true, + })); } else { setTitle(editedTitle); - setIsAllValid({ ...isAllValid, title: false }); + setIsAllValid(prevState => ({ + ...prevState, + title: false, + })); } }; const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { if (e.target.value.length > 0) { setDescription(e.target.value); - setIsAllValid({ ...isAllValid, description: true }); + setIsAllValid(prevState => ({ + ...prevState, + description: true, + })); } else { setDescription(editedDescription); - setIsAllValid({ ...isAllValid, description: false }); + setIsAllValid(prevState => ({ + ...prevState, + description: false, + })); } }; @@ -320,9 +343,9 @@ export function EditModuleModal({ <Button colorScheme="purple" flex="1" ml={3} onClick={handleEditModule} isDisabled={ - !( - isAllValid.title || - isAllValid.description + !(( + (isAllValid.title && isAllValid.description) || + (isAllValid.title || isAllValid.description)) ) } > @@ -393,7 +416,7 @@ export function DeleteModuleModal({ Cancel </Button> <Button colorScheme="red" flex="1" ml={3} - onClick={handleDeleteModule} + onClick={handleDeleteModule} > Delete </Button> diff --git a/src/components/navbar/LogoutPopup.tsx b/src/components/navbar/LogoutPopup.tsx index 3d6bce4b0a71b432644a56accadae01525afd1f7..aae220123d1ec28e15372cb29bf8b6bf2e2b0084 100644 --- a/src/components/navbar/LogoutPopup.tsx +++ b/src/components/navbar/LogoutPopup.tsx @@ -9,23 +9,51 @@ import { Button, Box, Text, + useToast, } from "@chakra-ui/react"; +import axios from "axios"; import React from "react"; import { BiSad } from "react-icons/bi"; +import { axiosConfig } from "../../utils/axios"; +import { useNavigate } from "react-router-dom"; +import config from "../../config/config"; interface LogoutDialogProps { isOpen: boolean; onClose: () => void; } -export const LogoutDialog = ({ isOpen, onClose }: LogoutDialogProps)=>{ - const cancelRef = React.useRef<HTMLButtonElement | null>(null); +export const LogoutDialog = ({ isOpen, onClose }: LogoutDialogProps) => { + const axiosInstance = axios.create(axiosConfig()); + const toast = useToast(); + const navigate = useNavigate(); + const handleLogout = () => { - // Handle the Logout action here, e.g., send an API request to update the data - // You can use the Logout and Logout state variables - // to send the updated data. - // After Logout is complete, close the modal. + axiosInstance.post(`${config.REST_API_URL}/auth/logout`).then((res) => { + if (res.status === 200) { + toast({ + title: "Logout Success!", + description: "You have been logged out!", + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + navigate("/login"); + } else { + toast({ + title: "Logout failed!", + description: "Your logout request has failed!", + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + } + }); + onClose(); }; + const cancelRef = React.useRef<HTMLButtonElement | null>(null); return ( <AlertDialog leastDestructiveRef={cancelRef} @@ -52,11 +80,11 @@ export const LogoutDialog = ({ isOpen, onClose }: LogoutDialogProps)=>{ <Button colorScheme="gray" ref={cancelRef} onClick={onClose} flex="1"> Cancel </Button> - <Button colorScheme="red" ml={3} flex="1" onClick={onClose}> + <Button colorScheme="red" ml={3} flex="1" onClick={handleLogout}> Logout </Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); -} \ No newline at end of file +}; diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx index 5d116e734aecb1dcbb5b010ee6ea49538c3dbe62..d78a32c5407b920215a316f24940d743aa4930a4 100644 --- a/src/components/navbar/Navbar.tsx +++ b/src/components/navbar/Navbar.tsx @@ -8,26 +8,44 @@ import { } from "react-icons/bi"; import { Outlet } from "react-router-dom"; import { Provider, Container, Item, Profile, Sidenav } from "."; +import axios from "axios"; +import { axiosConfig } from "../../utils/axios"; +import config from "../../config/config"; +import { useEffect, useState } from "react"; export default function Navbar() { + const [isAdmin, setIsAdmin] = useState(false); + const axiosInstance = axios.create(axiosConfig()); + const checkAdmin = () => { + axiosInstance + .get(`${config.REST_API_URL}/user/isAdmin`) + .then((res) =>{ + const response = res["data"]; + const {data} = response; + setIsAdmin(data) + }); + }; + useEffect(()=>{ + checkAdmin(); + },[]) const pict: Profile = { image_path: "defaultprofile.jpg", label: "username", role: "admin", to: "/profile", }; - - const navItems: Item[] = [ + const adminItems: Item[] = [ + { icon: BiUserPlus, label: "Upgrade Request", to: "/admin/request" }, + { icon: BiBookAdd, label: "Premium Courses", to: "/admin/courses" }, + { icon: BiGroup, label: "Premium Users", to: "/admin/users" }, + ]; + const teacherItems: Item[] = [ { icon: BiHome, label: "Home", to: "/course?page=1" }, - { icon: BiUserPlus, label: "Upgrade Request", to: "/request" }, - { icon: BiBookAdd, label: "Premium Courses", to: "/premium-courses" }, - { icon: BiGroup, label: "Premium Users", to: "/premium-users" }, // { icon: BiLogOut, label: "Logout", to: "logout" } ]; - return ( <Provider> - <Container sidenav={<Sidenav navItems={navItems} pict={pict} />}> + <Container sidenav={<Sidenav navItems={isAdmin ? adminItems : teacherItems} pict={!isAdmin ? pict : undefined} />}> <Outlet /> </Container> </Provider> diff --git a/src/components/navbar/items/items.tsx b/src/components/navbar/items/items.tsx index e4d53f84064a6666cf4705a44427d26f7f86402f..490507f4e7d4266b8d75b656964ff40d335c6707 100644 --- a/src/components/navbar/items/items.tsx +++ b/src/components/navbar/items/items.tsx @@ -1,5 +1,5 @@ import { IconType } from "react-icons"; -import { NavLink } from "react-router-dom"; +import { NavLink, useNavigate } from "react-router-dom"; import { List, ListItem, @@ -12,11 +12,15 @@ import { Avatar, WrapItem, Button, + useToast, } from "@chakra-ui/react"; import { BiLogOut } from "react-icons/bi"; import { useState } from "react"; import { LogoutDialog } from "../LogoutPopup"; +import axios from "axios"; +import { axiosConfig } from "../../../utils/axios"; +import config from "../../../config/config"; export interface Item { icon: IconType; @@ -34,15 +38,19 @@ export interface Profile { export interface ItemsProps { navItems: Item[]; mode?: "semi" | "over"; - pict: Profile; + pict?: Profile; } export function Items({ navItems, mode = "semi", pict }: ItemsProps) { + const axiosInstance = axios.create(axiosConfig()); + const toast = useToast(); + const navigate = useNavigate(); const [isModalLogoutOpen, setIsModalLogoutOpen] = useState(false); const openModalLogout = () => { setIsModalLogoutOpen(true); }; const closeModalLogout = () => { + setIsModalLogoutOpen(false); }; @@ -97,23 +105,26 @@ export function Items({ navItems, mode = "semi", pict }: ItemsProps) { return ( <List spacing={3}> <LogoutDialog isOpen={isModalLogoutOpen} onClose={closeModalLogout} /> - <Tooltip - label={pict.label} - placement="right" - bg="purple.500" - color="white" - > - <NavLink to={pict.to} style={{ textDecoration: "none" }}> - <Avatar - src={pict.image_path} - name={pict.label} - size="l" - bg="transparent" - _hover={{ cursor: "pointer" }} - /> - <Text>{pict.role}</Text> - </NavLink> - </Tooltip> + {pict && ( + <Tooltip + label={pict.label} + placement="right" + bg="purple.500" + color="white" + > + <NavLink to={pict.to} style={{ textDecoration: "none" }}> + <Avatar + src={pict.image_path} + name={pict.label} + size="l" + bg="transparent" + _hover={{ cursor: "pointer" }} + /> + <Text>{pict.role}</Text> + </NavLink> + </Tooltip> + )} + {navItems.map((item, index) => sidebarItemInSemiMode(item, index))} <Tooltip label={"Logout"} @@ -135,30 +146,33 @@ export function Items({ navItems, mode = "semi", pict }: ItemsProps) { return ( <List spacing={3}> <LogoutDialog isOpen={isModalLogoutOpen} onClose={closeModalLogout} /> - <WrapItem> - <Link - display="block" - as={NavLink} - to={pict.to} - _focus={{ bg: "gray.100" }} - _hover={{ - bg: "gray.200", - }} - _activeLink={{ bg: "purple.500", color: "white" }} - w="full" - borderRadius="md" - > - <Flex alignItems="center" p={2}> - <Avatar name={pict.label} src={pict.image_path} /> - <Flex display={"block"}> - <Text ml={2} fontWeight={"bold"}> - {pict.label} - </Text> - <Text ml={2}>{pict.role}</Text> + {pict && ( + <WrapItem> + <Link + display="block" + as={NavLink} + to={pict.to} + _focus={{ bg: "gray.100" }} + _hover={{ + bg: "gray.200", + }} + _activeLink={{ bg: "purple.500", color: "white" }} + w="full" + borderRadius="md" + > + <Flex alignItems="center" p={2}> + <Avatar name={pict.label} src={pict.image_path} /> + <Flex display={"block"}> + <Text ml={2} fontWeight={"bold"}> + {pict.label} + </Text> + <Text ml={2}>{pict.role}</Text> + </Flex> </Flex> - </Flex> - </Link> - </WrapItem> + </Link> + </WrapItem> + )} + {navItems.map((item, index) => sidebarItemInOverMode(item, index))} <WrapItem> <Button diff --git a/src/components/navbar/sidenav/sidenav.tsx b/src/components/navbar/sidenav/sidenav.tsx index 22cfd125b91ed6f5b235f3420beee4a779367979..9965f7abd6fac348a2dac27f46e5ff5b3f317484 100644 --- a/src/components/navbar/sidenav/sidenav.tsx +++ b/src/components/navbar/sidenav/sidenav.tsx @@ -17,10 +17,10 @@ import { BiMenu, BiLogOut, BiSad } from "react-icons/bi"; export interface SidenavProps { navItems: Item[]; - pict: Profile; + pict?: Profile; } -export function Sidenav({ navItems, pict }: SidenavProps) { +export function Sidenav({ navItems, pict}: SidenavProps) { const { isOpen, onClose, onOpen } = useSidenav(); return ( diff --git a/src/index.tsx b/src/index.tsx index 5c222535504329d7b40f90dddde0feed0d5cf25c..a6a7d3d26daff547e4237433402535f280a597c0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; -import reportWebVitals from './reportWebVitals'; import { BrowserRouter } from 'react-router-dom'; import { ChakraProvider } from "@chakra-ui/react"; import { PrimeReactProvider } from 'primereact/api'; @@ -18,9 +17,4 @@ root.render( </BrowserRouter> </ChakraProvider> </PrimeReactProvider> -); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +); \ No newline at end of file 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/Materials.tsx b/src/pages/Materials.tsx index 2329c08a0db100598cbb96c6d57dc49e1977f1fa..86d1e123f25c5ad7f05a120274e89e353f72146c 100644 --- a/src/pages/Materials.tsx +++ b/src/pages/Materials.tsx @@ -28,11 +28,13 @@ import { import { useParams } from "react-router-dom"; import { Modules, Materials } from "../types" import { EditModuleModal, AddModuleModal, DeleteModuleModal } from "../components/modals/module"; -// import { } from "../components/modals/material"; +import { EditMaterialModal, AddMaterialModal, DeleteMaterialModal } from "../components/modals/material"; import { axiosConfig } from "../utils/axios"; import config from "../config/config"; import axios from "axios"; import ReactPlayer from "react-player"; +import { Document, Page } from 'react-pdf' +import 'pdfjs-dist/build/pdf.worker.entry'; import PdfViewer from "../components/pdfviewer/pdfviewer"; import Loading from "../components/loading/Loading"; @@ -88,6 +90,10 @@ const ModuleMaterials = () => { }, [refreshModule]); const handleClickModule = (id: number) => { + setIdSelectedModules((prevId) => { + // console.log(prevId); // This will log the previous state + return id; + }); getMaterials(id); } @@ -124,7 +130,7 @@ const ModuleMaterials = () => { } // HANDLING DELETE MODULE - const [isModalDeleteOpen, setIsModalDeleteModuleOpen] = useState(false); + const [isModalDeleteModuleOpen, setIsModalDeleteModuleOpen] = useState(false); const handleOpenDeleteModule = (id: number) => { setIdSelectedModules(id); openModalDeleteModule(); @@ -145,16 +151,23 @@ const ModuleMaterials = () => { const initialMaterials: Materials[] = []; const [materials, setMaterials] = useState(initialMaterials); + const [refreshMaterial, setRefreshMaterial] = useState(false); + const [idSelectedMaterials, setIdSelectedMaterials] = useState(0); + + // FETCH DATA FROM SERVER const getMaterials = async (module_id: number) => { try { setIsLoading(true); - const res = await newAxiosInstance.get(`${config.REST_API_URL}/modul/course/${module_id}`); - const MaterialsData: Materials[] = res.data.data.map((module: any) => { + const res = await newAxiosInstance.get(`${config.REST_API_URL}/material/module/${module_id}`); + // console.log(res); + const MaterialsData: Materials[] = res.data.data.map((material: any) => { return { - id: module.id, - title: module.title, - description: module.description, - module_id: module.module_id, + id: material.id, + title: material.title, + description: material.description, + source_type: material.source_type, + material_path: material.material_path, + module_id: material.modul_id, }; }); setMaterials(MaterialsData); @@ -165,6 +178,53 @@ const ModuleMaterials = () => { } } + // HANDLING ADD MATERIAL + const [isModalAddMaterialOpen, setIsModalAddMaterialOpen] = useState(false); + const openModalAddMaterial = () => { + setIsModalAddMaterialOpen(true); + }; + const closeModalAddMaterial = () => { + setIsModalAddMaterialOpen(false); + }; + const successAddMaterial = () => { + setIsModalAddMaterialOpen(false); + getMaterials(idSelectedModules); + } + + // HANDLING EDIT MATERIAL + const [isModalEditMaterialOpen, setIsModalEditMaterialOpen] = useState(false); + const handleOpenEditMaterial = (id: number) => { + setIdSelectedMaterials(id); + openModalEditMaterial(); + } + const openModalEditMaterial = () => { + setIsModalEditMaterialOpen(true); + }; + const closeModalEditMaterial = () => { + setIsModalEditMaterialOpen(false); + }; + const successEditMaterial = () => { + setIsModalEditMaterialOpen(false); + getMaterials(idSelectedModules); + } + + // HANDLING DELETE MATERIAL + const [isModalDeleteMaterialOpen, setIsModalDeleteMaterialOpen] = useState(false); + const handleOpenDeleteMaterial = (id: number) => { + setIdSelectedMaterials(id); + openModalDeleteMaterial(); + } + const openModalDeleteMaterial = () => { + setIsModalDeleteMaterialOpen(true); + }; + const closeModalDeleteMaterial = () => { + setIsModalDeleteMaterialOpen(false); + }; + const successDeleteMaterial = () => { + setIsModalDeleteMaterialOpen(false); + getMaterials(idSelectedModules); + } + return ( <Container overflow="auto" maxW={"100vw"} maxH={"100vh"}> <Loading loading={isLoading} /> @@ -181,10 +241,9 @@ const ModuleMaterials = () => { onClose={closeModalEditModule} successEdit={successEditModule} moduleId={idSelectedModules} - /> - + /> <DeleteModuleModal - isOpen={isModalDeleteOpen} + isOpen={isModalDeleteModuleOpen} onClose={closeModalDeleteModule} successDelete={successDeleteModule} moduleId={idSelectedModules} @@ -192,12 +251,24 @@ const ModuleMaterials = () => { {/* --------------- MATERIAL POPUPS -------------------- */} - {/* <AddMaterialModal - isOpen={isModalAddOpen} - onClose={closeModalAdd} - handleAdd={handleAdd} - /> */} - + <AddMaterialModal + isOpen={isModalAddMaterialOpen} + onClose={closeModalAddMaterial} + successAdd={successAddMaterial} + moduleId={idSelectedModules} + /> + <EditMaterialModal + isOpen={isModalEditMaterialOpen} + onClose={closeModalEditMaterial} + successEdit={successEditMaterial} + materialId={idSelectedMaterials} + /> + <DeleteMaterialModal + isOpen={isModalDeleteMaterialOpen} + onClose={closeModalDeleteMaterial} + successDelete={successDeleteMaterial} + materialId={idSelectedMaterials} + /> <HStack align="start" justify="center"> <VStack maxW="20%" maxH="95vh" mt="1rem"> @@ -285,87 +356,137 @@ const ModuleMaterials = () => { ></Icon> </VStack> <VStack w="80%" h="95vh" mt="1rem" bg="white" > - {materials.length > 0 ? ( - <Box w="100%" h="94%" overflow={"hidden"}> - <Box w="full" p="3" px="8"> - <Text align="left" fontWeight={"bold"}> - Materials - </Text> - <Divider mt="1" borderColor="black.500" borderWidth="1" /> - </Box> - <Box + {idSelectedModules != 0 ? ( + <> + {materials.length > 0 ? ( + <Box w="100%" h="94%" overflow={"hidden"}> + <Box w="full" p="3" px="8"> + <Text align="left" fontWeight={"bold"}> + Materials + </Text> + <Divider mt="1" borderColor="black.500" borderWidth="1" /> + </Box> + <Box + px="10" + w="100%" + h="95%" + overflow="auto" + css={{ + "::-webkit-scrollbar": { + width: "10px", + }, + "::-webkit-scrollbar-thumb": { + background: "rgb(206, 207, 211)", + }, + "::-webkit-scrollbar-track": { + background: "rgba(255, 255, 255, 0.8)", + }, + }}> + {materials + .sort((a, b) => a.id - b.id) + .map((item) => ( + <Accordion key={item.id} allowToggle> + <AccordionItem> + <AccordionButton bg="#f0f0f0" borderRadius={"5"}> + <Box flex="1" textAlign="left"> + {item.title} + </Box> + <AccordionIcon /> + <Icon + as={BiSolidEdit} + fontSize={"18"} + color={"#564c95"} + _hover={{ color: "green" }} + cursor={"pointer"} + onClick={(e) => { + e.stopPropagation(); // Stop event propagation + handleOpenEditMaterial(item.id); + }} + ></Icon> + <Icon + as={BiSolidTrash} + fontSize={"18"} + color={"#564c95"} + _hover={{ color: "red" }} + cursor={"pointer"} + onClick={(e) => { + e.stopPropagation(); // Stop event propagation + handleOpenDeleteMaterial(item.id); + }} + ></Icon> + </AccordionButton> + {item.source_type === "PDF" ? ( + <AccordionPanel> + <Text align="left" fontSize={"md"} fontWeight={"bold"}>Deskripsi Materi:</Text> + <Text mb="5" align="left" fontSize={"sm"}>{item.description}</Text> + <div> + <iframe + src={`http://localhost:8000/file/${item.material_path}`} + width="100%" + height="500px" /> + </div> + </AccordionPanel> + ) : ( + <AccordionPanel> + <Text align="left" fontSize={"md"} fontWeight={"bold"}>Deskripsi Materi:</Text> + <Text mb="5" align="left" fontSize={"sm"}>{item.description}</Text> + <div> + <ReactPlayer + url={`http://localhost:8000/file/${item.material_path}`} + width="100%" + height="500px" + controls + /> + </div> + </AccordionPanel> + )} + </AccordionItem> + <Divider mt="3" borderColor="transparent" /> + </Accordion> + ))} + </Box> + </Box> + ) : (<Box px="5" w="100%" - h="95%" - overflow="auto" - css={{ - "::-webkit-scrollbar": { - width: "3px", - }, - "::-webkit-scrollbar-thumb": { - background: "rgb(206, 207, 211)", - }, - "::-webkit-scrollbar-track": { - background: "rgba(255, 255, 255, 0.8)", - }, - }}> - {materials - .sort((a, b) => a.id - b.id) - .map((item) => ( - <Accordion allowToggle> - <AccordionItem> - <AccordionButton bg="#f0f0f0" borderRadius={"5"}> - <Box flex="1" textAlign="left"> - {item.title} - </Box> - <AccordionIcon /> - <Icon - as={BiSolidEdit} - fontSize={"18"} - color={"#564c95"} - _hover={{ color: "green" }} - cursor={"pointer"} - // onClick={() => openModalEditModule(item.id, item.title, item.description)} - ></Icon> - <Icon - as={BiSolidTrash} - fontSize={"18"} - color={"#564c95"} - _hover={{ color: "red" }} - cursor={"pointer"} - // onClick={() => openModalDelete(item.id)} - ></Icon> - </AccordionButton> - <AccordionPanel> - <Text align="left" fontSize={"sm"}>{item.description}</Text> - <PdfViewer pdfPath={"/Quiz-1.pdf"} /> - </AccordionPanel> - </AccordionItem> - <Divider mt="3" borderColor="transparent" /> - </Accordion> - ))} + h="94%"> + <Box w="full" p="3"> + <Text align="left" fontWeight={"bold"}> + No Material Available for This Module + </Text> + <Divider mt="1" borderColor="black.500" borderWidth="1" /> + </Box> + </Box> + ) + } + < Icon + as={BiPlusCircle} + fontSize={"26"} + color={"#564c95"} + _hover={{ color: "green" }} + cursor={"pointer"} + onClick={() => openModalAddMaterial()} + ></Icon> + </> + ) : ( + <Box + px="5" + w="100%" + h="94%" + display="flex" + alignItems="center" + justifyContent="center" + > + <Box w="full" p="3"> + <Text align="center" fontWeight={"bold"} fontSize={"30"}> + Welcome To This Course! + </Text> + <Text align="center" fontSize={"24"}> + Please Select a Module to See the Materials + </Text> </Box> </Box> - ) : (<Box - px="5" - w="100%" - h="94%"> - <Box w="full" p="3"> - <Text align="left" fontWeight={"bold"}> - No Material Available for This Module - </Text> - <Divider mt="1" borderColor="black.500" borderWidth="1" /> - </Box> - </Box> )} - <Icon - as={BiPlusCircle} - fontSize={"26"} - color={"#564c95"} - _hover={{ color: "green" }} - cursor={"pointer"} - // onClick={() => openModalAdd(item.module_id, item.title)} - ></Icon> </VStack> </HStack> </Container > 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/react-app-env.d.ts b/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc6b2c932dfe5d0418fc667b86c18b9fc..0000000000000000000000000000000000000000 --- a/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="react-scripts" /> diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts deleted file mode 100644 index 49a2a16e0fbc7636ee16bf907257a5282b856493..0000000000000000000000000000000000000000 --- a/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReportHandler } from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; 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/setupTests.ts b/src/setupTests.ts deleted file mode 100644 index 8f2609b7b3e0e3897ab3bcaad13caf6876e48699..0000000000000000000000000000000000000000 --- a/src/setupTests.ts +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/src/types.tsx b/src/types.tsx index 0d1dc70260556a7ad834af4bffcb95b006d3c6d8..6bbad07669a5906726e0666fe43e77525a9beb6a 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -13,6 +13,10 @@ export type Modules = { course_id: number; }; +export enum Status{ + SUCCESS = "success", + ERROR = "error" +} export type Materials = { id: number; title: string; 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"