Compare commits

...

No commits in common. "master" and "main" have entirely different histories.
master ... main

222 changed files with 1 additions and 29329 deletions

View File

@ -1,21 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
.gitignore vendored
View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,3 +1,2 @@
# React + Vite
# frontend_adaptive_learning
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

View File

@ -1,18 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Adaptive English Learning</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

6362
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^43.2.0",
"@ckeditor/ckeditor5-react": "^9.3.1",
"axios": "^1.7.7",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"file-saver": "^2.0.5",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.8.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
"react-loading-skeleton": "^3.5.0",
"react-router-dom": "^6.26.0",
"react-select": "^5.8.1",
"react-sortablejs": "^6.1.4"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"vite": "^5.3.4"
}
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,23 +0,0 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import PublicRoutes from './roles/guest/PublicRoutes';
import UserRoutes from './roles/user/UserRoutes';
import AdminRoutes from './roles/admin/AdminRoutes';
import TeacherRoutes from './roles/teacher/TeacherRoutes';
const App = () => {
return (
<Router>
<Routes>
<Route path="/*" element={<PublicRoutes />} />
<Route path="/learning/*" element={<UserRoutes />} />
<Route path="/portal/*" element={<TeacherRoutes />} />
<Route path="/admin/*" element={<AdminRoutes />} />
</Routes>
</Router>
);
};
export default App;

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,128 +0,0 @@
.audio-player {
height: 50px;
width: 350px;
background: #444;
/* box-shadow: 0 0 20px 0 #000a; */
font-family: arial;
color: white;
font-size: 0.75em;
overflow: hidden;
display: grid;
grid-template-rows: 6px auto;
border: 1px solid #444;
border-radius: 8px;
}
.audio-player .timeline {
background: white;
width: 100%;
position: relative;
cursor: pointer;
/* box-shadow: 0 2px 10px 0 #0008; */
}
.audio-player .timeline .progress {
/* background: coral; */
background: #0090FF;
width: 0%;
height: 100%;
transition: 0.25s;
border-radius: 0;
}
.audio-player .controls {
display: flex;
justify-content: space-between;
align-items: stretch;
padding: 0 20px;
}
.audio-player .controls > * {
display: flex;
justify-content: center;
align-items: center;
}
.audio-player .controls .toggle-play.play {
cursor: pointer;
position: relative;
left: 0;
height: 0;
width: 0;
border: 7px solid #0000;
border-left: 13px solid white;
}
.audio-player .controls .toggle-play.pause {
height: 15px;
width: 20px;
cursor: pointer;
position: relative;
}
.audio-player .controls .toggle-play.pause:before {
position: absolute;
top: 0;
left: 0px;
background: white;
content: "";
height: 15px;
width: 3px;
}
.audio-player .controls .toggle-play.pause:after {
position: absolute;
top: 0;
right: 8px;
background: white;
content: "";
height: 15px;
width: 3px;
}
.audio-player .controls .time {
display: flex;
}
.audio-player .controls .time > * {
padding: 2px;
}
.audio-player .controls .volume-container {
cursor: pointer;
}
.audio-player .controls .volume-container .volume-button {
height: 26px;
display: flex;
align-items: center;
}
.audio-player .controls .volume-container .volume-button .volume {
transform: scale(0.7);
}
.audio-player .controls .volume-container .volume-slider {
position: absolute;
left: -3px;
top: 15px;
z-index: -1;
width: 0;
height: 15px;
background: white;
/* box-shadow: 0 0 20px #000a; */
transition: .25s;
}
.audio-player .controls .volume-container .volume-slider .volume-percentage {
/* background: coral; */
background: #0090FF;
height: 100%;
width: 75%;
}
.audio-player .controls .volume-container:hover .volume-slider {
left: -123px;
width: 120px;
}

View File

@ -1,598 +0,0 @@
.admin-page{
background-color: #F1F5FC;
height: 100vh;
}
.admin-page .min-h-100{
min-height: calc(100vh - 72px);
}
.admin-page .admin-container{
min-height: 100vh;
}
.admin-page .admin-content{
height: calc(100vh - 72px);
overflow: auto;
}
.admin-page .navbar{
height: 72px;
}
.admin-page .navbar .btn-group button{
padding: 8px 12px;
}
.admin-page .navbar .navbar-brand span{
display: block;
font-size: 14px;
}
.admin-page .navbar .navbar-nav a{
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #ffffff;
border-radius: 100%;
color: #ffffff;
margin-right: 20px;
}
.admin-page .navbar .navbar-nav a:hover{
background-color: #ffffff;
color: #0090FF;
}
.admin-page .sidebar {
transition: width 0.3s ease;
/* height: calc(100vh - 72px); */
height: 100vh;
top: 0;
box-shadow: 0px 4px 6px 0px rgba(0, 0, 0, 0.20);
}
.admin-page .sidebar .top-logo{
height: 72px;
}
.admin-page .sidebar .nav-container{
/* height: calc(100vh - 122px); */
height: calc(100vh - 124px);
/* background-color: red; */
}
.admin-page .sidebar .nav-container .menu-box{
height: calc(100% - 66px);
max-height: calc(100% - 52px);
overflow: auto;
padding-right: 4px;
}
.admin-page .sidebar .nav-container .menu-box::-webkit-scrollbar {
width: 2px;
}
.admin-page .sidebar .nav-container .menu-box::-webkit-scrollbar-track {
background: #f1f1f1;
}
.admin-page .sidebar .nav-container .menu-box::-webkit-scrollbar-thumb {
background: #888;
}
.admin-page .sidebar .nav-container .menu-box::-webkit-scrollbar-thumb:hover {
background: #555;
}
.admin-page .sidebar input{
padding: 8px 12px 8px 0px;
margin-bottom: 12px;
background-color: #F1F5FC;
border: none;
}
.admin-page .sidebar input::placeholder{
color: #BDBDBD;
}
.admin-page .sidebar .input-group-text{
padding: 8px 12px 8px 12px;
margin-bottom: 12px;
background-color: #F1F5FC;
border: none;
color: #BDBDBD;
}
.admin-page .sidebar .toggle-sidebar{
transition: width 0.3s ease;
z-index: 99999;
display: block;
width: 30px;
height: 30px;
position: absolute;
top: calc(72px / 2 - 15px);
left: calc(100% - 15px);
padding: 0;
}
.admin-page .sidebar .img-avatar{
width: 72px;
height: 72px;
border-radius: 100%;
border: 3px solid #ffffff;
}
.admin-page .sidebar .toggle-btn {
margin-bottom: 1rem;
width: 100%;
}
.admin-page .sidebar .nav-link {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 12px;
background: #ffffff;
color: #526071;
border: 1px solid #F1F5FC;
font-weight: 500;
}
.admin-page .sidebar .submenu .nav-link{
color: #959EA9;
margin-left: 24px;
border: none;
font-weight: normal;
}
.admin-page .sidebar .nav a:hover,
.admin-page .sidebar .nav a.active{
background-color: #0090FF;
color: #ffffff;
border-color: #0090FF;
}
.admin-page .sidebar .nav .drop-menu:hover{
background-color: #ffffff;
border-color: #0090FF;
color: #0090FF;
}
.admin-page .sidebar .nav a.active-border{
border-color: #0090FF;
color: #0090FF;
}
.admin-page .sidebar.minimized {
width: 80px;
}
.admin-page .sidebar.minimized .toggle-sidebar{
rotate: 180deg;
}
.admin-page .sidebar.minimized .img-avatar{
width: 50px;
height: 50px;
margin-bottom: 16px;
}
.admin-page .sidebar.minimized .display-username{
display: none;
}
.admin-page .sidebar.minimized .nav-link span,
.admin-page .sidebar.minimized .nav-link i.chrevon,
.admin-page .sidebar.minimized .submenu,
.admin-page .sidebar.minimized input{
display: none;
}
.admin-page .sidebar.minimized .nav-link i{
margin-right: 0!important;
}
.admin-page .sidebar.minimized .nav-link {
display: flex;
justify-content: center;
align-items: center;
}
.admin-page .sidebar.minimized .nav-link.drop-menu{
border-color: #F1F5FC!important;
color: #526071!important;
}
.admin-page .sidebar.minimized .input-group-text{
cursor: pointer;
width: 52px;
border-radius: 0.375rem!important;
display: flex;
justify-content: center;
}
.modal-admin .modal-header,
.modal-admin .modal-body{
padding: 16px 32px;
}
.modal-admin .modal-header .modal-title{
font-size: 18px;
color: #526071;
}
.admin-page .page-title{
font-size: 30px;
font-weight: 700;
color: #0090FF;
}
.admin-page .page-title.strip{
position: relative;
}
.admin-page .page-title.strip::after{
content: "";
position: absolute;
height: 20px;
border-bottom: 2px solid #0090FF;
/* width: 400px; */
width: 30vw;
margin-left: 12px;
}
.admin-page .page-desc{
font-size: 18px;
color: #526071;
margin-bottom: 32px;
}
.admin-page .col-tabs-parent{
background-color: #ffffff;
border-radius: 12px;
}
.admin-page .col-tabs{
background-color: #ffffff;
border-radius: 12px;
padding: 8px;
}
.admin-page .form-selector{
padding: 8px 16px;
}
.admin-page .col-tabs .nav-link{
border-radius: 8px;
font-size: 14px;
padding: 8px 16px;
color: #BDBDBD;
border: 1px solid transparent;
}
.admin-page .col-tabs .nav-link.active,
.admin-page .col-tabs .dropdown-toggle.show,
.admin-page .col-tabs .nav-item.dropdown.active a.dropdown-toggle{
background-color: #0090FF;
color: #ffffff;
}
.admin-page .mini-cards{
background: #ffffff;
border-radius: 1rem!important;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.admin-page .cards:not(.combine){
background: #ffffff;
border-radius: 1rem!important;
height: 100%;
border: none!important;
}
.admin-page .cards.combine{
background: #ffffff;
height: 100%;
}
.admin-page .combine.combine-top{
border-top-left-radius: 1rem!important;
border-top-right-radius: 1rem!important;
}
.admin-page .combine.combine-bottom{
border-bottom-left-radius: 1rem!important;
border-bottom-right-radius: 1rem!important;
}
.admin-page .cards .cards-title,
.admin-page .cards .accordion-header.cards-title{
padding: 16px 24px;
border-bottom: 1px solid #F1F5FC;
}
.admin-page .cards .cards-title.hr{
border-bottom: 8px solid #F1F5FC!important;
}
.admin-page .cards .cards-title h4,
.admin-page .accordion.cards .cards-title button,
.admin-page .mini-cards h4{
font-size: 18px;
color: #526071;
font-weight: 600;
margin-bottom: 0;
}
.admin-page .cards.cards-exercise .cards-title{
padding: 12px 24px;
}
.admin-page .cards.cards-exercise .cards-title h4,
.admin-page .accordion.cards-exercise .cards-title button{
font-size: 16px;
}
.admin-page .cards.cards-exercise .cards-title button{
font-size: 12px;
}
.admin-page .cards .cards-title p{
font-size: 12px;
color: #BDBDBD;
margin-bottom: 0;
margin-top: 8px;
}
.admin-page .cards .cards-title a{
text-decoration: none;
font-size: 14px;
}
.admin-page .accordion.cards .accordion-item{
border-radius: 1rem;
border: none;
overflow: hidden;
}
.admin-page .cards .accordion-header.cards-title{
padding: 0;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
border-bottom-left-radius: 0!important;
border-bottom-right-radius: 0!important;
}
.admin-page .accordion.cards .cards-title button{
padding: 16px 24px;
border-radius: 1rem;
background-color: #ffffff!important;
outline: none!important;
box-shadow: none!important;
}
.admin-page .cards .cards-body{
padding: 16px 24px;
}
.admin-page .cards .cards-body table{
margin-bottom: 0;
}
.admin-page .cards .cards-body table th{
background-color: #0090FF;
font-size: 14px;
color: #ffffff;
border: 0;
}
.admin-page .cards .cards-body table th:first-of-type{
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.admin-page .cards .cards-body table th:last-of-type{
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
.admin-page .cards .cards-body table tr th:first-child,
.admin-page .cards .cards-body table tr td:first-child{
text-align: center;
}
.admin-page .cards .cards-body table tbody tr:last-of-type td{
border: 0;
}
.admin-page .cards .cards-body table td{
font-size: 12px;
color: #526071;
border-color: #F1F5FC;
}
.admin-page .cards .cards-body table th,
.admin-page .cards .cards-body table td{
padding: 10px 8px;
vertical-align: middle;
}
.admin-page .cards .cards-body table td.action-col .btn{
border-radius: 100%;
background-color: #E2F2FF;
border: none!important;
margin: 0 4px;
}
.admin-page .cards .cards-body table td.action-col .btn-view{
color: #526071;
}
.admin-page .cards .cards-body table td.action-col .btn.btn-edit{
color: #0090FF;
}
.admin-page .cards .cards-body table td.action-col .btn.btn-delete{
color: #E9342D;
}
.admin-page .cards .cards-body table td.action-col .btn-view:hover{
background-color: #526071;
color: #ffffff;
}
.admin-page .cards .cards-body table td.action-col .btn.btn-edit:hover{
background-color: #0090FF;
color: #ffffff;
}
.admin-page .cards .cards-body table td.action-col .btn.btn-delete:hover{
background-color: #E9342D;
color: #ffffff;
}
.admin-page .cards .cards-body .table-input-search{
margin-bottom: 16px;
/* border-color: #BDBDBD; */
}
.admin-page .cards .cards-body form .input-group-icon .input-group-text,
.modal-admin .modal-body form .input-group-icon .input-group-text,
.admin-page .col-tabs-parent .form-selector .input-group-text{
background-color: #ffffff;
color: #BDBDBD;
border-right: 0;
padding-right: 6px;
}
.admin-page .cards .cards-body form small,
.modal-admin .modal-body form small{
font-size: 12px;
color: #526071;
}
.admin-page .cards .cards-body form .input-group-icon input,
.admin-page .cards .cards-body form .input-group-icon select,
.modal-admin .modal-body form .input-group-icon input,
.modal-admin .modal-body form .input-group-icon select,
.admin-page .col-tabs-parent .form-selector select{
border-left: 0;
padding-left: 6px;
}
.admin-page .cards .cards-body form .input-group-icon input::placeholder,
.admin-page .cards .cards-body form .input-group-icon input::placeholder,
.admin-page .cards .cards-body form textarea::placeholder,
.admin-page .cards .cards-body form textarea::placeholder{
color: #BDBDBD;
}
.admin-page .cards .cards-body form .input-group-icon.disabled .input-group-text,
.admin-page .cards .cards-body form .input-group-icon.disabled input,
.modal-admin .modal-body form .input-group-icon.disabled .input-group-text,
.modal-admin .modal-body form .input-group-icon.disabled input{
background-color: #F2F2F2;
color: #333333;
}
.admin-page .cards .cards-body form .input-group-icon select:required:invalid,
.modal-admin .modal-body form .input-group-icon select:required:invalid,
.admin-page .col-tabs-parent .form-selector select:invalid{
color: #BDBDBD;
}
.admin-page .cards .cards-body form .input-group-icon select,
.admin-page .cards .cards-body form .input-group-icon select option,
.modal-admin .modal-body form .input-group-icon select,
.modal-admin .modal-body form .input-group-icon select option,
.admin-page .col-tabs-parent .form-selector select,
.admin-page .col-tabs-parent .form-selector select option{
color: #212529;
}
.admin-dashboard .student-display{
display: flex;
align-items: center;
margin-bottom: 16px;
}
.admin-dashboard .student-display img{
width: 40px;
height: 40px;
object-fit: cover;
margin-right: 8px;
}
.admin-dashboard .student-display .student-identity{
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.admin-dashboard .student-display .student-identity h5{
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.admin-dashboard .student-display .student-identity h6{
font-size: 12px;
font-weight: 400;
margin: 0;
color: #526071;
}
.admin-dashboard .student-display .student-level{
font-size: 14px;
font-weight: 400;
color: #959EA9;
margin-bottom: 0;
margin-left: auto;
}
.react-select-group__control{
border-radius: 0.375rem!important;
border-left: none!important;
border-top-left-radius: 0!important;
border-bottom-left-radius: 0!important;
border-color: #dee2e6!important;
}
.react-select-group__placeholder{
color: #BDBDBD!important;
}
.group-react-select{
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
}
/* audio.no-control::-webkit-media-controls{
display:none !important;
} */
.ck-rounded-corners .ck.ck-editor__top .ck-sticky-panel .ck-sticky-panel__content, .ck.ck-menu-bar{
border-top-left-radius: 0.375rem!important;
border-top-right-radius: 0.375rem!important;
}
.ck.ck-editor__main > .ck-editor__editable{
border-bottom-left-radius: 0.375rem!important;
border-bottom-right-radius: 0.375rem!important;
min-height: 20vh;
}

View File

@ -1,455 +0,0 @@
body {
font-family: "Inter", sans-serif;
margin: 0;
/* display: flex;
place-items: center; */
min-width: 320px;
min-height: 100vh;
color: #333333;
}
.w-fit{
width: fit-content;
}
.w-screen{
width: 100vw;
}
.w-75{
width: 75%;
}
.h-fit{
height: fit-content;
}
.h-screen{
height: 100vh;
}
.h-screen-nav{
height: 100vh;
padding-top: 58px;
}
.pt-nav{
padding-top: 58px;
}
.pt-nav-1{
padding-top: calc(1.5rem + 58px);
}
.mb-45{
margin-bottom: 32px;
}
.ratio-1{
aspect-ratio: 1/1;
width: auto;
height: auto;
}
.lh-normal{
line-height: normal;
}
.items-center{
align-items: center;
justify-content: center;
}
.fs-7{
font-size: 0.88rem!important;
}
.fs-14p{
font-size: 14px;
}
.fs-12p{
font-size: 12px;
}
.fw-500{
font-weight: 500;
}
.rounded-35{
border-radius: 12px;
}
.text-gd{
display: inline-block;
color: transparent;
background-clip: text;
background-image: linear-gradient(to right, #5674ED, #34C3F9);
}
.text-blue{
color: #0090FF;
}
.text-blue-50{
color: #93C5FD;
}
.text-red{
color: #E9342D;
}
.text-green{
color: #00BC65;
}
.text-muted-50{
color: #A3A3A3;
}
.text-grey{
color: #959EA9;
}
.text-navy{
color: #526071;
}
.bg-gd {
background-image: linear-gradient(to right, #5674ED, #34C3F9);
}
.bg-blue{
background-color: #0090FF;
}
.bg-blue-50{
background-color: #EFF6FF;
}
.bg-red{
background-color: #E9342D;
}
.bg-ts{
background: transparent;
}
.bg-blur{
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
background: #ffffff20;
}
.bg-grey{
background-color: grey;
}
.bg-light-grey{
background-color: lightgrey;
}
.btn-gd {
background-image: linear-gradient(to right, #5674ED, #34C3F9);
color: #ffffff;
border: none;
}
.btn-gd:hover{
background-image: linear-gradient(to left, #5674ED, #34C3F9);
color: #fafafa;
}
.btn-gd:disabled{
color: #ffffff;
background-color: #ffffff34;
}
.btn-check:checked+.btn.btn-gd{
color: #ffffff;
}
.btn-outline-muted{
background-color: #ffffff00;
border-color: #A3A3A3;
color: #A3A3A3;
}
.btn-outline-muted:hover{
background-color: #A3A3A3;
border-color: #A3A3A3;
color: #ffffff;
}
.btn-white{
background-color: #ffffff;
border: none;
outline: none;
}
.btn-white:hover, .btn-white:active {
background-color: #fcfcfc!important;
border-color: #fcfcfc!important;
color: #000000!important;
}
.btn-ts{
background-color: #ffffff00;
border: none;
outline: none;
}
.btn-blue {
background-color: #0090FF;
border-color: #0090FF;
color: #ffffff;
}
.btn-blue:hover, .btn-blue:active {
background-color: #008cf7!important;
border-color: #008cf7!important;
color: #ffffff!important;
}
.btn-red {
background-color: #E9342D;
border-color: #E9342D;
color: #ffffff;
}
.btn-red:hover, .btn-red:active {
background-color: #d82721!important;
border-color: #d82721!important;
color: #ffffff!important;
}
.btn-white-blue{
background-color: #ffffff;
border-color: #ffffff;
color: #0090FF;
}
.btn-white-blue:hover{
background-color: #ffffff;
border-color: #0090FF;
color: #0090FF;
}
.btn-outline-white{
background-color: #ffffff00;
border-color: #ffffff;
color: #ffffff;
}
.btn-outline-white:hover{
background-color: #ffffff00;
border-color: #0090FF;
color: #ffffff;
}
.btn-outline-blue{
background-color: #ffffff00;
border-color: #0090FF;
color: #0090FF;
}
.btn-outline-blue:hover, .btn-outline-blue:active, .btn-outline-blue.show{
background-color: #0091ff0c!important;
border-color: #0090FF!important;
color: #0090FF!important;
}
.btn-white-outline-blue{
background-color: #ffffff;
border-color: #93C5FD;
color: #0090FF;
}
.btn-white-outline-blue:hover, .btn-white-outline-blue:active{
background-color: #0090FF!important;
border-color: #ffffff!important;
color: #ffffff!important;
}
.btn-nope{
background-color: #00000000!important;
box-shadow: none!important;
}
.btn-nope:hover{
background-color: #00000007!important;
}
.btn-nav{
width: 140px;
}
.ts{
color:transparent;
}
.truncate{
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.truncate-2{
-webkit-line-clamp: 2;
}
.truncate-4{
-webkit-line-clamp: 4;
}
.cursor-pointer{
cursor: pointer;
}
.border-dashed{
border-style: dashed;
}
.border-blue{
border-color: #0090FF;
}
.custom-alert{
position: absolute;
top: 70px;
right: 12px;
}
.custom-breadcrumb .breadcrumb{
margin-bottom: 0;
}
.custom-breadcrumb .breadcrumb-item{
padding-left: 12px!important;
}
.custom-breadcrumb .breadcrumb-item::before{
padding-right: 12px!important;
font-weight: normal!important;
}
.custom-breadcrumb .breadcrumb-item:not(.active) a{
text-decoration: none!important;
color: #212529bf!important;
}
.custom-breadcrumb .breadcrumb-item.active{
color: #0090FF;
font-weight: 600;
}
.custom-paginate .page-link{
border: none;
padding: 0 12px;
color: #526071;
}
.custom-paginate .active span{
color: #0090FF;
background: transparent;
}
.custom-paginate .disabled span{
color: #BDBDBD;
background: transparent!important;
}
.custom-paginate .page-item{
display: flex;
justify-content: center;
align-items: center;
}
.custom-paginate .page-item:first-of-type span,
.custom-paginate .page-item:last-of-type span{
font-size: 24px;
}
.drop-zone {
cursor: pointer;
background-color: #00000000;
width: 100%;
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2px dashed #dee2e6;
transition: border-color 0.3s ease-in-out;
}
.drop-zone:not(.active):hover{
background-color: #00000007!important;
}
.drop-zone.dragging,
.drop-zone.active {
background-color: #0cb61518;
border: 2px dashed #00BC65;
}
@media (max-width: 992px) {
.ilustration-img{
height: 50%;
}
}
@media (max-width: 768px) {
.landing-page section:not(:last-of-type){
margin-bottom: 0!important;
}
}
.landing-page section{
padding-top: 3rem;
margin-bottom: 6rem;
}
.landing-page section:first-of-type{
padding-top: 5rem;
}
.landing-page section:last-of-type{
/* padding-bottom: 6rem; */
}
.landing-page .content-con{
padding: 6rem;
}
.landing-page .content-con section:not(:last-of-type){
margin-bottom: 4.5rem;
}
footer .social-icon-box a{
width: 32px;
height: 32px;
background-color: #ffffff;
color: black;
display: flex;
justify-content: center;
align-items: center;
margin: 5px;
border-radius: 100%;
}
footer{
background-image: url('../images/footerDecor.png'), linear-gradient(to right, #5674ED, #34C3F9);
background-size: cover;
background-position: top;
}

View File

@ -1,431 +0,0 @@
.teacher-page .min-h-100{
min-height: calc(100vh - 58px);
}
.teacher-page .dashboard-container{
height: calc(100vh - 58px);
padding-top: 58px;
}
.teacher-page .navbar{
height: 58px;
}
.teacher-page .navbar .navbar-nav a{
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #ffffff;
border-radius: 100%;
color: #ffffff;
margin-right: 16px;
}
.teacher-page .navbar .navbar-nav a:hover{
background-color: #ffffff;
color: #0090FF;
}
.teacher-page .sidebar {
transition: width 0.3s ease;
height: calc(100vh - 58px);
top: 58px;
}
.teacher-page .sidebar .toggle-sidebar{
transition: width 0.3s ease;
z-index: 99999;
display: block;
width: 30px;
height: 30px;
position: absolute;
top: calc(50% - 30px);
left: calc(100% - 12px);
padding: 0;
}
.teacher-page .sidebar .img-avatar{
width: 72px;
height: 72px;
border-radius: 100%;
border: 3px solid #ffffff;
object-fit: cover;
}
.teacher-page .sidebar .toggle-btn {
margin-bottom: 1rem;
width: 100%;
}
.teacher-page .sidebar .nav-link {
display: flex;
align-items: center;
padding: 12px 16px;
}
.teacher-page .sidebar .nav a:hover, .teacher-page .sidebar .nav a.active{
background-color: #ffffff47;
}
.teacher-page .sidebar.minimized {
width: 72px;
}
.teacher-page .sidebar.minimized .toggle-sidebar{
rotate: 180deg;
}
.teacher-page .sidebar.minimized .img-avatar{
width: 50px;
height: 50px;
margin-bottom: 16px;
}
.teacher-page .sidebar.minimized .display-username{
display: none;
}
.teacher-page .sidebar.minimized .nav-link span {
display: none;
}
.teacher-page .sidebar.minimized .nav-link i{
margin-right: 0!important;
}
.teacher-page .sidebar.minimized .nav-link {
display: flex;
justify-content: center;
align-items: center;
}
.teacher-page .home-page .filled-journey .card img{
aspect-ratio: 1/1;
width: 10vw;
height: auto;
max-width: 205px;
max-height: 205px;
object-fit: cover;
}
.teacher-main-layout{
height: calc(100vh - 58px);
overflow-y: auto;
}
.modal-admin .modal-header,
.modal-admin .modal-body{
padding: 16px 32px;
}
.modal-admin .modal-header .modal-title{
font-size: 18px;
color: #526071;
}
.teacher-page .page-title{
font-size: 30px;
font-weight: 700;
color: #0090FF;
}
.teacher-page .page-title.strip{
position: relative;
}
.teacher-page .page-title.strip::after{
content: "";
position: absolute;
height: 20px;
border-bottom: 2px solid #0090FF;
width: 30vw;
margin-left: 12px;
}
.teacher-page .page-desc{
font-size: 18px;
color: #526071;
margin-bottom: 32px;
}
.teacher-page .col-tabs-parent{
background-color: #ffffff;
border-radius: 12px;
}
.teacher-page .col-tabs{
background-color: #ffffff;
border-radius: 12px;
padding: 8px;
}
.teacher-page .form-selector{
padding: 8px 16px;
}
.teacher-page .col-tabs .nav-link{
border-radius: 8px;
font-size: 14px;
padding: 8px 16px;
color: #BDBDBD;
border: 1px solid transparent;
}
.teacher-page .col-tabs .nav-link.active,
.teacher-page .col-tabs .dropdown-toggle.show,
.teacher-page .col-tabs .nav-item.dropdown.active a.dropdown-toggle{
background-color: #0090FF;
color: #ffffff;
}
.teacher-page .cards:not(.combine){
background: #ffffff;
border-radius: 1rem!important;
height: 100%;
border: none!important;
}
.teacher-page .cards.combine{
background: #ffffff;
height: 100%;
}
.teacher-page .combine.combine-top{
border-top-left-radius: 1rem!important;
border-top-right-radius: 1rem!important;
}
.teacher-page .combine.combine-bottom{
border-bottom-left-radius: 1rem!important;
border-bottom-right-radius: 1rem!important;
}
.teacher-page .cards .cards-title,
.teacher-page .cards .accordion-header.cards-title{
padding: 16px 24px;
border-bottom: 1px solid #F1F5FC;
}
.teacher-page .cards .cards-title.hr{
border-bottom: 8px solid #F1F5FC!important;
}
.teacher-page .cards .cards-title h4,
.teacher-page .accordion.cards .cards-title button{
font-size: 18px;
color: #526071;
font-weight: 600;
margin-bottom: 0;
}
.teacher-page .cards.cards-exercise .cards-title{
padding: 12px 24px;
}
.teacher-page .cards.cards-exercise .cards-title h4,
.teacher-page .accordion.cards-exercise .cards-title button{
font-size: 16px;
}
.teacher-page .cards.cards-exercise .cards-title button{
font-size: 12px;
}
.teacher-page .cards .cards-title p{
font-size: 12px;
color: #BDBDBD;
margin-bottom: 0;
margin-top: 8px;
}
.teacher-page .cards .cards-title a{
text-decoration: none;
font-size: 14px;
}
.teacher-page .accordion.cards .accordion-item{
border-radius: 1rem;
border: none;
overflow: hidden;
}
.teacher-page .cards .accordion-header.cards-title{
padding: 0;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
border-bottom-left-radius: 0!important;
border-bottom-right-radius: 0!important;
}
.teacher-page .accordion.cards .cards-title button{
padding: 16px 24px;
border-radius: 1rem;
background-color: #ffffff!important;
outline: none!important;
box-shadow: none!important;
}
.teacher-page .cards .cards-body{
padding: 16px 24px;
}
.teacher-page .cards .cards-body table{
margin-bottom: 0;
}
.teacher-page .cards .cards-body table th{
background-color: #0090FF;
font-size: 14px;
color: #ffffff;
border: 0;
}
.teacher-page .cards .cards-body table th:first-of-type{
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.teacher-page .cards .cards-body table th:last-of-type{
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
.teacher-page .cards .cards-body table tr th:first-child,
.teacher-page .cards .cards-body table tr td:first-child{
text-align: center;
}
.teacher-page .cards .cards-body table tbody tr:last-of-type td{
border: 0;
}
.teacher-page .cards .cards-body table td{
font-size: 12px;
color: #526071;
border-color: #F1F5FC;
}
.teacher-page .cards .cards-body table th,
.teacher-page .cards .cards-body table td{
padding: 10px 8px;
vertical-align: middle;
}
.teacher-page .cards .cards-body table td.action-col .btn{
border-radius: 100%;
background-color: #E2F2FF;
border: none!important;
margin: 0 4px;
}
.teacher-page .cards .cards-body table td.action-col .btn-view{
color: #526071;
}
.teacher-page .cards .cards-body table td.action-col .btn.btn-edit{
color: #0090FF;
}
.teacher-page .cards .cards-body table td.action-col .btn.btn-delete{
color: #E9342D;
}
.teacher-page .cards .cards-body table td.action-col .btn-view:hover{
background-color: #526071;
color: #ffffff;
}
.teacher-page .cards .cards-body table td.action-col .btn.btn-edit:hover{
background-color: #0090FF;
color: #ffffff;
}
.teacher-page .cards .cards-body table td.action-col .btn.btn-delete:hover{
background-color: #E9342D;
color: #ffffff;
}
.teacher-page .cards .cards-body .table-input-search{
margin-bottom: 16px;
/* border-color: #BDBDBD; */
}
.teacher-page .cards .cards-body form .input-group-icon .input-group-text,
.modal-admin .modal-body form .input-group-icon .input-group-text,
.teacher-page .col-tabs-parent .form-selector .input-group-text{
background-color: #ffffff;
color: #BDBDBD;
border-right: 0;
padding-right: 6px;
}
.teacher-page .cards .cards-body form small,
.modal-admin .modal-body form small{
font-size: 12px;
color: #526071;
}
.teacher-page .cards .cards-body form .input-group-icon input,
.teacher-page .cards .cards-body form .input-group-icon select,
.modal-admin .modal-body form .input-group-icon input,
.modal-admin .modal-body form .input-group-icon select,
.teacher-page .col-tabs-parent .form-selector select{
border-left: 0;
padding-left: 6px;
}
.teacher-page .cards .cards-body form .input-group-icon input::placeholder,
.teacher-page .cards .cards-body form .input-group-icon input::placeholder,
.teacher-page .cards .cards-body form textarea::placeholder,
.teacher-page .cards .cards-body form textarea::placeholder{
color: #BDBDBD;
}
.teacher-page .cards .cards-body form .input-group-icon.disabled .input-group-text,
.teacher-page .cards .cards-body form .input-group-icon.disabled input,
.modal-admin .modal-body form .input-group-icon.disabled .input-group-text,
.modal-admin .modal-body form .input-group-icon.disabled input{
background-color: #F2F2F2;
color: #333333;
}
.teacher-page .cards .cards-body form .input-group-icon select:required:invalid,
.modal-admin .modal-body form .input-group-icon select:required:invalid,
.teacher-page .col-tabs-parent .form-selector select:invalid{
color: #BDBDBD;
}
.teacher-page .cards .cards-body form .input-group-icon select,
.teacher-page .cards .cards-body form .input-group-icon select option,
.modal-admin .modal-body form .input-group-icon select,
.modal-admin .modal-body form .input-group-icon select option,
.teacher-page .col-tabs-parent .form-selector select,
.teacher-page .col-tabs-parent .form-selector select option{
color: #212529;
}
.react-select-group__control{
border-radius: 0.375rem!important;
border-left: none!important;
border-top-left-radius: 0!important;
border-bottom-left-radius: 0!important;
border-color: #dee2e6!important;
}
.react-select-group__placeholder{
color: #BDBDBD!important;
}
.group-react-select{
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
}

View File

@ -1,385 +0,0 @@
.dashboard-page .min-h-100{
min-height: calc(100vh - 58px);
}
.dashboard-page .dashboard-container{
height: calc(100vh - 58px);
padding-top: 58px;
}
.dashboard-page .navbar{
height: 58px;
}
.dashboard-page .navbar .navbar-nav a{
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #ffffff;
border-radius: 100%;
color: #ffffff;
margin-right: 16px;
}
.dashboard-page .navbar .navbar-nav a:hover{
background-color: #ffffff;
color: #0090FF;
}
.dashboard-page .sidebar {
transition: width 0.3s ease;
height: calc(100vh - 58px);
top: 58px;
}
.dashboard-page .sidebar .toggle-sidebar{
transition: width 0.3s ease;
z-index: 99999;
display: block;
width: 30px;
height: 30px;
position: absolute;
top: calc(50% - 30px);
left: calc(100% - 12px);
padding: 0;
}
.dashboard-page .sidebar .img-avatar{
width: 72px;
height: 72px;
border-radius: 100%;
border: 3px solid #ffffff;
object-fit: cover;
}
.dashboard-page .sidebar .toggle-btn {
margin-bottom: 1rem;
width: 100%;
}
.dashboard-page .sidebar .nav-link {
display: flex;
align-items: center;
padding: 12px 16px;
}
.dashboard-page .sidebar .nav a:hover, .dashboard-page .sidebar .nav a.active{
background-color: #ffffff47;
}
.dashboard-page .sidebar.minimized {
width: 72px;
}
.dashboard-page .sidebar.minimized .toggle-sidebar{
rotate: 180deg;
}
.dashboard-page .sidebar.minimized .img-avatar{
width: 50px;
height: 50px;
margin-bottom: 16px;
}
.dashboard-page .sidebar.minimized .display-username{
display: none;
}
.dashboard-page .sidebar.minimized .nav-link span {
display: none;
}
.dashboard-page .sidebar.minimized .nav-link i{
margin-right: 0!important;
}
.dashboard-page .sidebar.minimized .nav-link {
display: flex;
justify-content: center;
align-items: center;
}
.dashboard-page .home-page .filled-journey .card img{
aspect-ratio: 1/1;
width: 10vw;
height: auto;
max-width: 205px;
max-height: 205px;
object-fit: cover;
}
.user-main-layout{
height: calc(100vh - 58px);
overflow-y: auto;
}
.exercise-history .nav.nav-pills .nav-item a,
.topic-page .nav.nav-pills .nav-item a,
.setting-page .nav.nav-pills .nav-item a{
color: #959EA9;
font-size: 14px;
}
.exercise-history .nav.nav-pills .nav-item a.active,
.topic-page .nav.nav-pills .nav-item a.active,
.setting-page .nav.nav-pills .nav-item a.active{
background-color: #0090FF;
color: #ffffff;
}
.exercise-history .nav.nav-pills .nav-item a.active,
.setting-page .nav.nav-pills .nav-item a.active{
border-radius: 0.6rem;
font-weight: bold;
}
.exercise-history .submit-time{
color: #959EA9;
font-size: 14px;
font-weight: 400;
}
.learning-page .card{
overflow: hidden;
}
.learning-page .card img{
aspect-ratio: 5/2;
/* max-height: 165px; */
width: 100%;
height: auto;
object-fit: cover;
margin-bottom: 12px;
border-radius: 8px;
}
.learning-page .card .card-body{
display: flex;
flex-direction: column;
justify-content: space-between;
}
.learning-page .card .card-body .card-text{
font-size: 12px;
}
.topic-page .nav.nav-pills .nav-item a{
font-size: 14px;
}
.btn-square-back{
width: 30px;
height: 30px;
padding: 0;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.topic-page .head-title{
background-size: cover;
background-position: center;
}
.topic-page .head-title h6{
color: #737373;
}
.topic-page .head-title p, .topic-page .topic-content p{
font-size: 14px;
}
.exercise-history .custom-breadcrumb .breadcrumb-item:first-of-type{
padding-left: 0!important;
}
.material-page ol.breadcrumb,
.level-page ol.breadcrumb{
margin-bottom: 0;
}
.exercise-page img, .exercise-page video,
.material-page img, .material-page video{
max-height: 180px;
width: auto;
}
.exercise-page .video-box,
.material-page .video-box{
max-width: 520px;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
.exercise-page .number-list .number-label{
color: #959EA9;
display: flex;
}
.exercise-page .number-list .number-label:not(.active):hover{
background-color: #0091ff0c;
}
.exercise-page .number-list .number-label.active{
color: #ffffff;
background-color: #0090FF;
}
.exercise-page .number-list .number-label.answered:not(.active){
color: #0090FF;
}
.exercise-page .review-list .number-label{
color: #959EA9;
display: flex;
}
.exercise-page .review-list .number-label:not(.active):hover{
background-color: #00BC650c;
}
.exercise-page .review-list .number-label.correct.active{
color: #ffffff;
background-color: #00BC65;
}
.exercise-page .review-list .number-label.incorrect.active{
color: #ffffff;
background-color: #E9342D;
}
.exercise-page .review-list .number-label.correct:not(.active){
color: #00BC65;
}
.exercise-page .review-list .number-label.incorrect:not(.active){
color: #E9342D;
}
.exercise-page .correction-label{
width: 55%;
display: flex;
align-items: center;
color: #ffffff;
margin-bottom: 12px;
padding: 8px 16px;
border-radius: 8px;
}
.exercise-page .correction-label.correct{
background-color: #00BC65;
}
.exercise-page .correction-label.incorrect{
background-color: #E9342D;
}
.exercise-page .options .form-check,
.exercise-page .options-tf .form-check,
.exercise-page .options-mp .form-check {
width: fit-content;
}
.exercise-page .options .selected-answer span,
.exercise-page .options-tf .selected-answer span{
background-color: #0090FF;
border: 1px solid #0090FF;
color: #ffffff;
}
.exercise-page .options .option-label,
.exercise-page .options-tf .option-label,
.exercise-page .options-mp .option-label {
width: 35px;
height: 35px;
color: #333333;
border: 1px solid #333333;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.exercise-page .options .option-text,
.exercise-page .options-tf .option-text,
.exercise-page .options-mp .option-text {
height: 35px;
min-width: 200px;
padding: 0 14px;
color: #333333;
border: 1px solid #333333;
display: flex;
align-items: center;
border-radius: 0 14px 14px 14px;
}
/* .exercise-page .options-mp .option-label,
.exercise-page .options-mp .option-text{
border: 2px solid #ffffff;
} */
.level-page .check-icon{
z-index: 1;
position: absolute;
bottom: -40px;
right: -40px;
aspect-ratio: 1/1;
width: 165px;
height: auto;
}
.level-page *:not(.check-icon){
z-index: 2;
}
.level-page .level-con{
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.level-page .level-con .level-label{
padding: 8px;
border-radius: 14px;
border: 1px solid #ffffff;
display: flex;
align-items: center;
color: #ffffff;
}
.level-page .level-con .level-label span{
display: flex;
justify-content: center;
align-items: center;
width: 42px;
height: 42px;
border-radius: 8px;
background-color: #ffffff;
color: #0090FF;
font-size: 24px;
font-weight: bold;
margin-left: 12px;
}
.level-page .level-con.bg-light-grey .level-label span{
color: lightgrey;
}
.level-page .level-con p{
min-height: 55px;
padding-right: 10%;
}

Binary file not shown.

View File

@ -1,23 +0,0 @@
import React from 'react';
import Navbar from './AdminNavbar';
import SideNav from './AdminSideNav';
const AdminLayout = ({ children }) => {
return (
<div className="admin-page">
<div className="container-fluid admin-container">
<div className="row">
<SideNav />
<main className="col p-0 overflow-auto">
<Navbar />
<div className='p-4 admin-content'>
{children}
</div>
</main>
</div>
</div>
</div>
);
};
export default AdminLayout;

View File

@ -1,94 +0,0 @@
import React, { useState } from 'react';
import { Navbar, Container, Nav, SplitButton, Dropdown, ButtonGroup, Button, Modal } from 'react-bootstrap';
import logo from '../../../assets/images/logo-w.png';
import logoutIllustration from '../../../assets/images/illustration/logout.png';
import { Link } from 'react-router-dom';
import useAuth from '../../../roles/guest/auth/hooks/useAuth';
import { MEDIA_URL } from '../../../utils/Constant';
import avatar from '../../../assets/images/default-avatar.jpg';
function validName(fullName) {
const nameArray = fullName.split(" ");
const firstTwoWords = nameArray.slice(0, 2).join(" ");
return firstTwoWords;
}
function getDateNow() {
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const now = new Date();
const dayName = days[now.getDay()];
const day = String(now.getDate()).padStart(2, '0')
const monthName = months[now.getMonth()];
const year = now.getFullYear();
return `${dayName}, ${day} ${monthName} ${year}`;
}
const AdminNavbar = () => {
const { logout } = useAuth();
const { username, picture } = JSON.parse(localStorage.getItem('userData'));
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleLogout = () => {
logout();
};
return (
<Navbar bg="ts" className='border-bottom'>
<Navbar.Brand href="#" className='px-4 d-flex items-center'>
<div>
<span className='fw-bold text-navy'>{getDateNow()}</span>
<span className='fw-light text-grey'>CMS Smart English Adaptive Learning System</span>
</div>
</Navbar.Brand>
<Navbar.Toggle aria-controls="navbar-nav" />
<Navbar.Collapse id="navbar-nav">
<Dropdown as={ButtonGroup} align='end' className='ms-auto me-4 rounded rounded-35'>
<Button variant="white" className='d-flex'>
{/* <i className="me-2 bi bi-person-circle"></i> */}
<img src={picture ? `${MEDIA_URL}/avatar/${picture}` : avatar} alt="profile"
style={{
objectFit:"cover",
height:"24px",
width:"24px",
borderRadius:"100%"
}}
className='me-2'
/>
<span className='truncate text-start' style={{maxWidth:"112px"}}>{validName(username)}</span>
</Button>
<Dropdown.Toggle split variant="white" className='border-start' id="dropdown-split-basic" />
<Dropdown.Menu>
<Dropdown.Item as={Link} to={'/admin/profile'}>Profile</Dropdown.Item>
<Dropdown.Item onClick={handleShow}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Navbar.Collapse>
<Modal show={show} onHide={handleClose} centered>
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-dark'>Time to <span className='text-red'>logout</span>?</h4>
<img src={logoutIllustration} alt="" />
<p className='my-3 text-muted fw-light'>Confirm logout? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="red" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
</Navbar>
);
};
export default AdminNavbar;

View File

@ -1,145 +0,0 @@
import React, { useState } from 'react';
import { Nav, Button, Collapse, Form, InputGroup } from 'react-bootstrap';
import { NavLink } from 'react-router-dom';
import logo from '../../../assets/images/logo.png'
import logos from '../../../assets/images/logo-s.png'
const AdminSideNav = () => {
const [open1, setOpen1] = useState(false);
const [open2, setOpen2] = useState(false);
const [open3, setOpen3] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
const toggleMinimize = () => {
setIsMinimized(!isMinimized);
if (!isMinimized) {
setOpen1(false);
setOpen2(false);
setOpen3(false);
}
};
return (
<nav id="sidebarMenu" className={`col-md-3 col-lg-2 sticky-top d-md-block bg-white shadow-end sidebar sidebar-admin ${isMinimized ? 'minimized' : ''}`}>
{/* <Button variant='white-outline-blue' className="toggle-sidebar" onClick={toggleMinimize}>
<i className="chrevon bi bi-chevron-double-left"></i>
</Button> */}
<div className="position-sticky">
<div className="d-flex justify-content-between align-items-center top-logo">
<img src={isMinimized ? logos : logo} alt="" />
<i className="bi bi-caret-left-square text-grey cursor-pointer" style={{ transform: isMinimized ? 'rotate(180deg)' : 'rotate(0deg)'}} onClick={toggleMinimize}></i>
</div>
{/* <Form.Control className='rounded rounded-35'
type="text"
aria-describedby="searchData"
placeholder='search'
/> */}
<InputGroup className="rounded rounded-35">
<InputGroup.Text id="basic-addon1" onClick={isMinimized ? toggleMinimize : () => {}}>
<i className="chrevon bi bi-search"></i>
</InputGroup.Text>
<Form.Control
placeholder="Search"
aria-label="Username"
aria-describedby="basic-addon1"
/>
</InputGroup>
<Nav className="nav-container flex-column">
<div className="menu-box">
<Nav.Link as={NavLink} to="/admin/dashboard" className="rounded rounded-35">
<i className="bi bi-house me-2"></i>
<span className='text-truncate'>Home</span>
</Nav.Link>
<>
<Nav.Link
className={`drop-menu rounded rounded-35 d-flex justify-content-between align-items-center ${open1 ? `active-border` : ` `}`}
onClick={() => {
if (isMinimized) {
toggleMinimize();
setOpen1(!open1);
} else{
setOpen1(!open1);
}
}}
>
<div className={`w-100 d-flex align-items-center ${isMinimized ? `justify-content-center` : `justify-content-start`}`}>
<i className="bi bi-briefcase me-2"></i>
<span className='text-truncate'>Academic</span>
</div>
{open1 ? <i className="chrevon bi bi-chevron-up"></i> : <i className="chrevon bi bi-chevron-down"></i>}
</Nav.Link>
<Collapse className='submenu' in={open1}>
<div>
<Nav.Link as={NavLink} to="student" className="rounded rounded-35">Students</Nav.Link>
<Nav.Link as={NavLink} to="teacher" className="rounded rounded-35">Teachers</Nav.Link>
<Nav.Link as={NavLink} to="class" className="rounded rounded-35">Classes</Nav.Link>
</div>
</Collapse>
</>
<>
<Nav.Link
className={`drop-menu rounded rounded-35 d-flex justify-content-between align-items-center ${open2 ? `active-border` : ` `}`}
onClick={() => {
if (isMinimized) {
toggleMinimize();
setOpen2(!open2);
} else{
setOpen2(!open2);
}
}}
>
<div className={`w-100 d-flex align-items-center ${isMinimized ? `justify-content-center` : `justify-content-start`}`}>
<i className="bi bi-journal-bookmark me-2"></i>
<span className='text-truncate'>Learning</span>
</div>
{open2 ? <i className="chrevon bi bi-chevron-up"></i> : <i className="chrevon bi bi-chevron-down"></i>}
</Nav.Link>
<Collapse className='submenu' in={open2}>
<div>
<Nav.Link as={NavLink} to="section" className="rounded rounded-35">Sections</Nav.Link>
<Nav.Link as={NavLink} to="topic" className="rounded rounded-35">Topics</Nav.Link>
<Nav.Link as={NavLink} to="material" className="rounded rounded-35">Materials</Nav.Link>
<Nav.Link as={NavLink} to="exercise" className="rounded rounded-35">Exercises</Nav.Link>
</div>
</Collapse>
</>
<>
<Nav.Link
className={`drop-menu rounded rounded-35 d-flex justify-content-between align-items-center ${open3 ? `active-border` : ` `}`}
onClick={() => {
if (isMinimized) {
toggleMinimize();
setOpen3(!open3);
} else{
setOpen3(!open3);
}
}}
>
<div className={`w-100 d-flex align-items-center ${isMinimized ? `justify-content-center` : `justify-content-start`}`}>
<i className="bi bi-check2-circle me-2"></i>
<span className='text-truncate'>Monitoring</span>
</div>
{open3 ? <i className="chrevon bi bi-chevron-up"></i> : <i className="chrevon bi bi-chevron-down"></i>}
</Nav.Link>
<Collapse className='submenu' in={open3}>
<div>
<Nav.Link as={NavLink} to="learning-progress" className="rounded rounded-35">Learning Progress</Nav.Link>
<Nav.Link as={NavLink} to="report" className="rounded rounded-35">Issue Report</Nav.Link>
</div>
</Collapse>
</>
</div>
<div className='setting-box'>
<Nav.Link as={NavLink} to="/admin/profile" className="rounded rounded-35">
<i className="bi bi-gear me-2"></i>
<span className='text-truncate'>Settings</span>
</Nav.Link>
</div>
</Nav>
</div>
</nav>
);
};
export default AdminSideNav;

View File

@ -1,105 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Navbar, Nav, Container, Button, Offcanvas } from 'react-bootstrap';
import logo from '../../../assets/images/logo.png';
import logoWhite from '../../../assets/images/logo-w.png';
import { useLocation } from 'react-router-dom';
const MainNav = () => {
const location = useLocation();
const [scrolled, setScrolled] = useState(false);
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleScroll = () => {
if (window.scrollY > 80) {
setScrolled(true);
} else {
setScrolled(false);
}
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
const getNavbarMenu = () => {
if (location.pathname === '/') {
return 'd-block flex-grow-1';
} else{
return 'd-none'
}
};
return (
<>
<Navbar collapseOnSelect expand="lg" className={`position-fixed w-100 ${scrolled ? 'bg-blue' : 'bg-blur'}`} style={{zIndex:"99",top:"0"}}>
<Container>
{/* <Navbar.Brand href="#home" className='text-white'>
<img
alt=""
src={logo}
height="32"
className="d-inline-block align-top"
/>{' '}
</Navbar.Brand> */}
<Navbar.Brand href="/" className='text-white'>
<img
alt=""
src={(location.pathname === '/' || scrolled) ? logoWhite : logo}
height="32"
className="d-inline-block align-top"
/>{' '}
</Navbar.Brand>
<div className={getNavbarMenu()}>
<div className='d-none d-lg-block'>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="w-100 w-lg-75 justify-content-center">
<Nav.Link href="#hero" className='text-white me-3'>Home.</Nav.Link>
<Nav.Link href="#whatis" className='text-white me-3'>What Is.</Nav.Link>
<Nav.Link href="#feature" className='text-white me-3'>Feature.</Nav.Link>
<Nav.Link href="#youget" className='text-white me-3'>You Get.</Nav.Link>
<Nav.Link href="#contacts" className='text-white me-3'>Join Now.</Nav.Link>
<Nav.Link href="/signup" className='text-white me-3 d-block d-md-none'>Sign Up</Nav.Link>
<Nav.Link href="/login" className='text-white me-3 d-block d-md-none'>Login</Nav.Link>
</Nav>
<Nav className='d-flex'>
<Button as='a' href='/signup' variant='white-blue' className='btn-nav mx-1 rounded-4 py-2 fs-6' size='lg'>Sign<span className="ts">_</span>Up</Button>
<Button as='a' href='/login' variant='outline-white' className='btn-nav mx-1 rounded-4 py-2 fs-6' size='lg'>Login</Button>
</Nav>
</Navbar.Collapse>
</div>
<div className='d-flex d-lg-none justify-content-end'>
<Button variant="outline-white" onClick={handleShow}>
<i className="bi bi-list text-white"></i>
</Button>
</div>
</div>
</Container>
</Navbar>
<Offcanvas show={show} onHide={handleClose} placement='end'>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Menu</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body>
<Nav.Link href="#hero" onClick={() => setShow(false)} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>Home.</Nav.Link>
<Nav.Link href="#whatis" onClick={() => setShow(false)} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>What Is.</Nav.Link>
<Nav.Link href="#feature" onClick={() => setShow(false)} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>Feature.</Nav.Link>
<Nav.Link href="#youget" onClick={() => setShow(false)} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>You Get.</Nav.Link>
<Nav.Link href="#contacts" onClick={() => setShow(false)} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top border-bottom text-blue'>Join Now.</Nav.Link>
<div className="d-flex w-100 justify-content-between mt-2">
<Nav.Link href="/signup" onClick={() => setShow(false)} className='fs-3 w-50 text-center me-1 py-2 border-top text-black bg-warning rounded-3'>Sign Up</Nav.Link>
<Nav.Link href="/login" onClick={() => setShow(false)} className='fs-3 w-50 text-center ms-1 py-2 border-top border-bottom text-white bg-blue rounded-3'>Login</Nav.Link>
</div>
</Offcanvas.Body>
</Offcanvas>
</>
);
};
export default MainNav;

View File

@ -1,22 +0,0 @@
import React from 'react';
import Navbar from './TeacherNavbar';
import SideNav from './TeacherSideNav';
const TeacherLayout = ({ children }) => {
return (
<div className="teacher-page h-screen">
<Navbar />
<div className="container-fluid dashboard-container">
<div className="row min-h-100">
<SideNav />
<main className="col p-4 overflow-auto bg-light teacher-main-layout">
{children}
</main>
</div>
</div>
</div>
);
};
export default TeacherLayout;

View File

@ -1,91 +0,0 @@
import React, { useState } from 'react';
import { Navbar, Container, Nav, Modal, Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import logo from '../../../assets/images/logo.png';
import logoW from '../../../assets/images/logo-w.png';
import logoutIllustration from '../../../assets/images/illustration/logout.png';
import useAuth from '../../../roles/guest/auth/hooks/useAuth';
import Report from '../../../roles/user/report/views/Report';
import { unSlugify } from '../../../utils/Constant';
const TeacherNavbar = () => {
const { logout } = useAuth();
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const [showReport, setShowReport] = useState(false);
const handleCloseReport = () => setShowReport(false);
const handleShowReport = () => setShowReport(true);
const handleLogout = () => {
logout();
};
return (
<>
<Navbar bg='blue' expand="md" fixed="top">
<Container fluid className='px-0'>
<Navbar.Brand as={Link} to="/learning" className='col-md-3 col-lg-2 d-flex items-center'>
<img
alt="logo"
src={logoW}
height="32"
/>
</Navbar.Brand>
<Navbar.Toggle aria-controls="navbar-nav" />
<div className="navbar-title col-md-6 col-lg-8">
</div>
<Navbar.Collapse id="navbar-nav" className='col-md-3 col-lg-2 d-flex items-center'>
<Nav className="d-flex">
<OverlayTrigger
placement='bottom'
overlay={
<Tooltip id='t-report'>report</Tooltip>
}
>
<Nav.Link onClick={handleShowReport}>
<i className="bi bi-headset"></i>
</Nav.Link>
</OverlayTrigger>
<OverlayTrigger
placement='bottom'
overlay={
<Tooltip id='t-logout'>Logout</Tooltip>
}
>
<Nav.Link onClick={handleShow}>
<i className="bi bi-box-arrow-right"></i>
</Nav.Link>
</OverlayTrigger>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
<Modal show={show} onHide={handleClose} centered>
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-dark'>Time to <span className='text-danger'>logout</span>?</h4>
<img src={logoutIllustration} alt="" />
<p className='my-3 text-muted fw-light'>Confirm logout? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="danger" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
<Modal centered show={showReport} onHide={handleCloseReport} size='lg'>
<Report onClose={handleCloseReport}/>
</Modal>
</>
);
};
export default TeacherNavbar;

View File

@ -1,67 +0,0 @@
import React, { useState } from 'react';
import { Nav, Button } from 'react-bootstrap';
import { NavLink } from 'react-router-dom';
import avatar from '../../../assets/images/default-avatar.jpg';
import { MEDIA_URL } from '../../../utils/Constant';
function validName(fullName) {
const nameArray = fullName.split(" ");
const firstTwoWords = nameArray.slice(0, 2).join(" ");
return firstTwoWords;
}
const TeacherSideNav = () => {
const { username, picture } = JSON.parse(localStorage.getItem('userData'));
const [isMinimized, setIsMinimized] = useState(false);
const toggleMinimize = () => {
setIsMinimized(!isMinimized);
};
return (
<nav id="sidebarMenu" className={`col-md-3 col-lg-2 sticky-top d-none d-md-block bg-blue sidebar ${isMinimized ? 'minimized' : ''}`}>
<Button variant='white-outline-blue' className="toggle-sidebar" onClick={toggleMinimize}>
<i className="bi bi-chevron-double-left"></i>
</Button>
<div className="position-sticky pt-4">
<div className="d-flex flex-column items-center mb-3 border-bottom">
<img src={picture ? `${MEDIA_URL}/avatar/${picture}` : avatar} alt=""
className="img-avatar"
height={72} width={72}
style={{objectFit:'cover'}}
/>
<h6 className='display-username text-white text-center truncate truncate-2 my-3'>{validName(username)}</h6>
</div>
<Nav className="flex-column">
<Nav.Link as={NavLink} to="home" className="mb-2 text-white rounded">
<i className="bi bi-house-fill me-2"></i>
<span className='text-truncate'>Home</span>
</Nav.Link>
<Nav.Link as={NavLink} to="class" className="mb-2 text-white rounded">
<i className="bi bi-easel2-fill me-2"></i>
<span className='text-truncate'>Class</span>
</Nav.Link>
<Nav.Link as={NavLink} to="student" className="mb-2 text-white rounded">
<i className="bi bi-people-fill me-2"></i>
<span className='text-truncate'>Student</span>
</Nav.Link>
<Nav.Link as={NavLink} to="monitoring" className="mb-2 text-white rounded">
<i className="bi bi-kanban-fill me-2"></i>
<span className='text-truncate'>Monitoring</span>
</Nav.Link>
<Nav.Link as={NavLink} to="feedback" className="mb-2 text-white rounded">
<i className="bi bi-chat-left-text-fill me-2"></i>
<span className='text-truncate'>Feedback</span>
</Nav.Link>
<Nav.Link as={NavLink} to="setting" className="mb-2 text-white rounded">
<i className="bi bi-gear-fill me-2"></i>
<span className='text-truncate'>Setting</span>
</Nav.Link>
</Nav>
</div>
</nav>
);
};
export default TeacherSideNav;

View File

@ -1,68 +0,0 @@
import React from 'react';
import Navbar from './UserNavbar';
import SideNav from './UserSideNav';
import { useLocation } from 'react-router-dom';
const UserLayout = ({ children }) => {
const location = useLocation();
const getSideNav = () => {
let showIt = true;
switch (location.pathname) {
case '/learning/home':
showIt = true;
break;
case '/learning/home/':
showIt = true;
break;
case '/learning/module':
showIt = true;
break;
case '/learning/module/':
showIt = true;
break;
case '/learning/history':
showIt = true;
break;
case '/learning/history/':
showIt = true;
break;
case '/learning/setting':
showIt = true;
break;
case '/learning/setting/':
showIt = true;
break;
default:
showIt = false;
break;
}
return showIt;
};
return (
<div className="dashboard-page h-screen">
<Navbar />
<div className="container-fluid dashboard-container">
<div className="row min-h-100">
{/* <SideNav /> */}
{getSideNav() ? <SideNav /> : ""}
<main className="col p-4 overflow-auto bg-light user-main-layout">
{children}
</main>
</div>
</div>
</div>
);
};
export default UserLayout;

View File

@ -1,163 +0,0 @@
import React, { useState } from 'react';
import { Navbar, Container, Nav, Modal, Button, OverlayTrigger, Tooltip, Offcanvas } from 'react-bootstrap';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import logo from '../../../assets/images/logo.png';
import logoW from '../../../assets/images/logo-w.png';
import logoutIllustration from '../../../assets/images/illustration/logout.png';
import useAuth from '../../../roles/guest/auth/hooks/useAuth';
import { headerSection, headerTopic, headerLevel } from '../../../utils/Constant';
import Report from '../../../roles/user/report/views/Report';
import { unSlugify } from '../../../utils/Constant';
const UserNavbar = () => {
const { user, logout } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const pathSegments = location.pathname.split('/');
const hasTopicAndLevel = pathSegments.length == 7 && pathSegments[4] && pathSegments[5];
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const [showMenu, setShowMenu] = useState(false);
const handleCloseMenu = () => setShowMenu(false);
const handleShowMenu = () => setShowMenu(true);
const [showReport, setShowReport] = useState(false);
const handleCloseReport = () => setShowReport(false);
const handleShowReport = () => setShowReport(true);
const handleLogout = () => {
logout();
};
const lastSegment = location.pathname.split('/').filter(Boolean).pop();
const bgBlue = () => {
let blue = true;
switch (lastSegment) {
case 'exercise':
blue = false;
break;
default:
blue = true;
break;
}
return blue;
};
return (
<>
<Navbar bg={bgBlue() ? 'blue' : 'white'} expand="md" fixed="top">
<Container fluid className='px-md-0'>
<Navbar.Brand as={Link} to="/learning" className='col-md-3 col-lg-2 d-flex items-center'>
<img
alt="logo"
src={bgBlue() ? logoW : logo}
height="32"
/>
</Navbar.Brand>
{/* <Navbar.Toggle className='custom-toggle shadow-none border-white' /> */}
<Button variant="btn-outline-white" onClick={handleShowMenu} className='border py-0 d-block d-md-none'>
<i className="bi bi-list text-white fs-3"></i>
</Button>
<div className="d-none d-md-block navbar-title col-md-6 col-lg-8">
{hasTopicAndLevel ? (
lastSegment === 'review' ? (
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
{headerSection} : {headerTopic}
<span className='text-dark'> - {headerLevel}</span>
</h5>
):(
lastSegment === 'exercise' ? (
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
{`${unSlugify(pathSegments[3])} : ${unSlugify(pathSegments[4])} `}
<span className='text-dark'> - {unSlugify(pathSegments[5])}</span>
</h5>
):(
<h5 className={`text-center m-0 ${bgBlue() ? 'text-white' : 'text-blue'}`}>
{unSlugify(pathSegments[4])}
</h5>
)
)
):('')}
</div>
<Navbar.Collapse id="navbar-nav" className='col-md-3 col-lg-2 d-none d-md-flex items-center'>
<Nav className={bgBlue() ? 'd-flex' : 'd-none'}>
<OverlayTrigger
placement='bottom'
overlay={
<Tooltip id='t-report'>report</Tooltip>
}
>
<Nav.Link onClick={handleShowReport}>
<i className="bi bi-headset"></i>
</Nav.Link>
</OverlayTrigger>
<OverlayTrigger
placement='bottom'
overlay={
<Tooltip id='t-logout'>Logout</Tooltip>
}
>
<Nav.Link onClick={handleShow}>
<i className="bi bi-box-arrow-right"></i>
</Nav.Link>
</OverlayTrigger>
{/* <Nav.Link href="#">
<i className="bi bi-headset"></i>
</Nav.Link>
<Nav.Link href="#" onClick={handleShow}>
<i className="bi bi-box-arrow-right"></i>
</Nav.Link> */}
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
<Modal show={show} onHide={handleClose} centered>
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-dark'>Time to <span className='text-danger'>logout</span>?</h4>
<img src={logoutIllustration} alt="" />
<p className='my-3 text-muted fw-light'>Confirm logout? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="danger" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
<Modal centered show={showReport} onHide={handleCloseReport} size='lg'>
<Report onClose={handleCloseReport}/>
</Modal>
<Offcanvas show={showMenu} onHide={handleCloseMenu} placement={'end'}>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Menu</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body className='h-100'>
<Nav.Link as={Link} to="home" onClick={() => handleCloseMenu()} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>Home.</Nav.Link>
<Nav.Link as={Link} to="module" onClick={() => handleCloseMenu()} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>Learning.</Nav.Link>
<Nav.Link as={Link} to="history" onClick={() => handleCloseMenu()} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>History.</Nav.Link>
<Nav.Link as={Link} to="setting" onClick={() => handleCloseMenu()} style={{fontSize:"3.5vh"}} className='py-3 ps-3 border-top text-blue'>Setting.</Nav.Link>
<div className="d-flex w-100 justify-content-between mt-2">
<Nav.Link onClick={() => (handleCloseMenu(), handleShowReport())} className='fs-3 w-50 text-center me-1 py-2 border-top text-black bg-warning rounded-3'>Report</Nav.Link>
<Nav.Link onClick={() => (handleCloseMenu(), handleShow())} className='fs-3 w-50 text-center ms-1 py-2 border-top border-bottom text-white bg-red rounded-3'>Logout</Nav.Link>
</div>
</Offcanvas.Body>
</Offcanvas>
</>
);
};
export default UserNavbar;

View File

@ -1,59 +0,0 @@
import React, { useState } from 'react';
import { Nav, Button } from 'react-bootstrap';
import { NavLink } from 'react-router-dom';
import avatar from '../../../assets/images/default-avatar.jpg';
import { MEDIA_URL } from '../../../utils/Constant';
function validName(fullName) {
const nameArray = fullName.split(" ");
const firstTwoWords = nameArray.slice(0, 2).join(" ");
return firstTwoWords;
}
const UserSideNav = () => {
const { username, picture } = JSON.parse(localStorage.getItem('userData'));
const [isMinimized, setIsMinimized] = useState(false);
const toggleMinimize = () => {
setIsMinimized(!isMinimized);
};
return (
<nav id="sidebarMenu" className={`col-md-3 col-lg-2 sticky-top d-none d-md-block bg-blue sidebar ${isMinimized ? 'minimized' : ''}`}>
<Button variant='white-outline-blue' className="toggle-sidebar" onClick={toggleMinimize}>
<i className="bi bi-chevron-double-left"></i>
</Button>
<div className="position-sticky pt-4">
<div className="d-flex flex-column items-center mb-3 border-bottom">
<img src={picture ? `${MEDIA_URL}/avatar/${picture}` : avatar} alt=""
className="img-avatar"
height={72} width={72}
style={{objectFit:'cover'}}
/>
<h6 className='display-username text-white text-center text-capitalize truncate truncate-2 my-3'>{validName(username)}</h6>
</div>
<Nav className="flex-column">
<Nav.Link as={NavLink} to="home" className="mb-2 text-white rounded">
<i className="bi bi-house me-2"></i>
<span className='text-truncate'>Home</span>
</Nav.Link>
<Nav.Link as={NavLink} to="module" className="mb-2 text-white rounded">
<i className="bi bi-book-half me-2"></i>
<span className='text-truncate'>Learning</span>
</Nav.Link>
<Nav.Link as={NavLink} to="history" className="mb-2 text-white rounded">
<i className="bi bi-clock-history me-2"></i>
<span className='text-truncate'>History</span>
</Nav.Link>
<Nav.Link as={NavLink} to="setting" className="mb-2 text-white rounded">
<i className="bi bi-gear-fill me-2"></i>
<span className='text-truncate'>Setting</span>
</Nav.Link>
</Nav>
</div>
</nav>
);
};
export default UserSideNav;

View File

@ -1,101 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import audio from '../../assets/audio/sample.mp3'
import '../../assets/styles/Components/AudioPlayer.css';
const AudioPlayer = ({ progressControl = false }, audioSrc, title = "audio") => {
const audioRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const togglePlay = () => {
const audio = audioRef.current;
if (isPlaying) {
audio.pause();
} else {
audio.play();
}
setIsPlaying(!isPlaying);
};
const handleTimeUpdate = () => {
const audio = audioRef.current;
setCurrentTime(audio.currentTime);
setProgress((audio.currentTime / audio.duration) * 100);
};
const handleLoadedMetadata = () => {
const audio = audioRef.current;
setDuration(audio.duration);
};
const handleEnded = () => {
setIsPlaying(false);
setProgress(0);
setCurrentTime(0);
};
const formatTime = (time) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
};
useEffect(() => {
const audio = audioRef.current;
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
audio.addEventListener('ended', handleEnded);
return () => {
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
audio.removeEventListener('ended', handleEnded);
};
}, []);
return (
<div className="audio-player">
<audio ref={audioRef} onTimeUpdate={handleTimeUpdate}>
<source src={audio} type="audio/mp3" />
Your browser does not support the audio tag.
</audio>
<div
className="timeline"
onClick={(e) => {
if (!progressControl) {
const audio = audioRef.current;
const rect = e.target.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickRatio = clickX / rect.width;
audio.currentTime = audio.duration * clickRatio;
}
}}
>
<div className="progress" style={{ width: `${progress}%` }}></div>
</div>
<div className="controls">
<div className="play-container" onClick={togglePlay}>
<div className={`toggle-play ${isPlaying ? 'pause' : 'play'}`}></div>
</div>
<div className="time">
<div className="current">{formatTime(currentTime)}</div>
<div className="divider">/</div>
<div className="length">{formatTime(duration)}</div>
</div>
<div className="name">{title}</div>
<div className="volume-container">
<div className="volume-button">
<div className="volume icono-volumeMedium"></div>
</div>
<div className="volume-slider">
<div className="volume-percentage"></div>
</div>
</div>
</div>
</div>
);
};
export default AudioPlayer;

File diff suppressed because one or more lines are too long

View File

@ -1,32 +0,0 @@
import React from 'react';
const Button = ({ children, onClick, type = 'button', className = '', variant = '', size='', disabled }) => {
let buttonClasses = '';
if (size === 'sm') {
buttonClasses += 'font-medium rounded-md text-xs text-center px-3 py-2 focus:outline-none ';
} else {
buttonClasses += 'font-medium rounded-md text-sm text-center px-5 py-2.5 focus:outline-none ';
}
switch (variant) {
case 'primary':
buttonClasses += 'text-white bg-blue-700 hover:bg-blue-800';
break;
case 'gd':
buttonClasses += 'text-white bg-gradient-to-r from-blue-primary to-teal-primary hover:bg-gradient-to-bl ';
break;
default:
buttonClasses += 'text-gray-900 bg-white hover:bg-gray-100';
break;
}
return (
<button type={type} className={`${className} ${buttonClasses}`} onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
export default Button;

View File

@ -1,158 +0,0 @@
import React, { useState } from "react";
import Mammoth from "mammoth";
import AudioPlayer from "./AudioPlayer";
const DocImporter = () => {
const [questions, setQuestions] = useState([]);
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (file) {
try {
const arrayBuffer = await file.arrayBuffer();
const { value } = await Mammoth.extractRawText({ arrayBuffer });
// const parsedQuestions = parseQuestions(value);
console.log(value);
const parsedQuestions = parseQuestionsNew(value);
setQuestions(parsedQuestions);
} catch (error) {
console.error("Error reading .docx file", error);
}
}
};
const parseQuestionsNew = (text) => {
const questionsArray = [];
const questions = text.split("#Question");
if (questions.length >= 5) {
questions.forEach((questionText) => {
if (questionText.trim()) {
const parts = questionText.trim().split("\n").map(line => line.trim());
const number = parts.findIndex(part => part.startsWith("#Question:"));
const question = parts.findIndex(part => part.startsWith("Question:"));
const optionA = parts.findIndex(part => part.startsWith("Option A:"));
const optionB = parts.findIndex(part => part.startsWith("Option B:"));
const optionC = parts.findIndex(part => part.startsWith("Option C:"));
const optionD = parts.findIndex(part => part.startsWith("Option D:"));
const optionE = parts.findIndex(part => part.startsWith("Option E:"));
const correct = parts.findIndex(part => part.startsWith("Correct:"));
const weight = parts.findIndex(part => part.startsWith("Weight:"));
const imageUrlIndex = parts.findIndex(part => part.startsWith("Image URL:"));
const audioUrlIndex = parts.findIndex(part => part.startsWith("Audio URL:"));
const videoUrlIndex = parts.findIndex(part => part.startsWith("Video URL:"));
const questionData = {
Number: parts[0]?.split(": ")[1],
Question: question !== -1 ? parts[question].split(": ")[1] || "Tidak ada" : "Tidak ada",
// Option: [
// optionA !== -1 ? parts[optionA].split(": ")[1] || "Tidak ada" : "Tidak ada",
// optionB !== -1 ? parts[optionB].split(": ")[1] || "Tidak ada" : "Tidak ada",
// optionC !== -1 ? parts[optionC].split(": ")[1] || "Tidak ada" : "Tidak ada",
// optionD !== -1 ? parts[optionD].split(": ")[1] || "Tidak ada" : "Tidak ada",
// optionE !== -1 ? parts[optionE].split(": ")[1] || "Tidak ada" : "Tidak ada"
// ],
OptionA: optionA !== -1 ? parts[optionA].split(": ")[1] || "Tidak ada" : "Tidak ada",
OptionB: optionB !== -1 ? parts[optionB].split(": ")[1] || "Tidak ada" : "Tidak ada",
OptionC: optionC !== -1 ? parts[optionC].split(": ")[1] || "Tidak ada" : "Tidak ada",
OptionD: optionD !== -1 ? parts[optionD].split(": ")[1] || "Tidak ada" : "Tidak ada",
OptionE: optionE !== -1 ? parts[optionE].split(": ")[1] || "Tidak ada" : "Tidak ada",
Correct: correct !== -1 ? parts[correct].split(": ")[1] || "Tidak ada" : "Tidak ada",
Weight: weight !== -1 ? parts[weight].split(": ")[1] || "Tidak ada" : "Tidak ada",
imageUrl: imageUrlIndex !== -1 ? parts[imageUrlIndex].split(": ")[1] || "Tidak ada" : "Tidak ada",
audioUrl: audioUrlIndex !== -1 ? parts[audioUrlIndex].split(": ")[1] || "Tidak ada" : "Tidak ada",
videoUrl: videoUrlIndex !== -1 ? parts[videoUrlIndex].split(": ")[1] || "Tidak ada" : "Tidak ada",
};
questionsArray.push(questionData);
}
});
}
// console.info(questionsArray);
return questionsArray;
};
const parseQuestions = (text) => {
const questionsArray = [];
const questions = text.split("Nomor");
questions.forEach((questionText) => {
if (questionText.trim()) {
const parts = questionText.trim().split("\n").map(line => line.trim());
const imageUrlIndex = parts.findIndex(part => part.startsWith("Image URL:"));
const tipeSoalIndex = parts.findIndex(part => part.startsWith("Tipe Soal:"));
const cleanChoice = (choice) => choice.replace(/^[A-Z]\. /, '');
const question = {
nomor: parts[0]?.split(": ")[1],
pertanyaan: parts[2]?.split(": ")[1],
pilihan: [
cleanChoice(parts[4]) || "Tidak ada",
cleanChoice(parts[6]) || "Tidak ada",
cleanChoice(parts[8]) || "Tidak ada",
cleanChoice(parts[10]) || "Tidak ada"
],
jawabanBenar: parts[12]?.split(": ")[1],
imageUrl: imageUrlIndex !== -1 ? parts[imageUrlIndex].split(": ")[1] || "Tidak ada" : "Tidak ada",
tipeSoal: tipeSoalIndex !== -1 ? parts[tipeSoalIndex].split(": ")[1] || "Tidak ada" : "Tidak ada"
};
questionsArray.push(question);
}
});
console.info(questionsArray);
return questionsArray;
};
return (
<div>
<h2>Import Soal dari File .docx</h2>
<input type="file" accept=".docx" onChange={handleFileChange} />
{questions.length > 0 && (
<div>
<h3>Soal yang diimpor:</h3>
<ul>
{questions.map((data, index) => (
<li key={index} className="mb-4">
<h4>Soal {data.Number}</h4>
<h5>Weight : {data.Weight}</h5>
{/* <p><strong>Url Gambar</strong> {data.imageUrl}</p> */}
{data.imageUrl !== "Tidak ada" && (
<img src={data.imageUrl} alt={`Image for soal ${data.number}`} style={{ width: '100px', height: 'auto' }} />
)}
{data.audioUrl !== "Tidak ada" && (
<AudioPlayer audioSrc={data.audioUrl} />
)}
{data.videoUrl !== "Tidak ada" && (
<video src={data.videoUrl} controls className='w-50'></video>
)}
<p className="mb-1"><strong>Pertanyaan:</strong> {data.Question}</p>
<p className="mb-1"><strong>Pilihan Jawaban:</strong></p>
<ol type="A">
{/* {data.pilihan.map((choice, i) => (
<li key={i}>{choice}</li>
))} */}
<li key={0}>{data.OptionA}</li>
<li key={1}>{data.OptionB}</li>
<li key={2}>{data.OptionC}</li>
<li key={3}>{data.OptionD}</li>
{data.OptionE !== "Tidak ada" && (
<li key={4}>{data.OptionE}</li>
)}
</ol>
<p className="mb-1"><strong>Jawaban Benar:</strong> {data.Correct}</p>
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default DocImporter;

View File

@ -1,79 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Pagination } from 'react-bootstrap';
const TablePaginate = ({ totalPages, currentPage, onPageChange }) => {
const [pageNumbers, setPageNumbers] = useState([]);
useEffect(() => {
const visiblePages = getVisiblePages(currentPage, totalPages);
setPageNumbers(visiblePages);
}, [totalPages, currentPage]);
const getVisiblePages = (current, total) => {
setPageNumbers([]);
let pages = [];
if (total <= 7) {
for (let i = 1; i <= total; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push(2);
if (current < 3 && current > total - 2) {
pages.push("...");
}else{
if (current == 3) {
pages.push(3);
}else if (current > 3) {
pages.push("...");
}
if (current > 3 && current < total - 2) {
pages.push(current);
}
if (current == total - 2) {
pages.push(total - 2);
}else if(current < total - 2){
pages.push("...");
}
}
pages.push(total - 1);
pages.push(total);
}
return pages;
};
return (
<Pagination className="custom-paginate justify-content-center m-0">
<Pagination.Prev
disabled={currentPage === 1}
onClick={() => onPageChange(currentPage - 1)}
/>
{pageNumbers.map((number, index) => (
number === "..." ? (
<Pagination.Ellipsis disabled key={index} />
) : (
<Pagination.Item
key={index}
active={number === currentPage}
onClick={() => onPageChange(number)}
>
{number}
</Pagination.Item>
)
))}
<Pagination.Next
disabled={currentPage === totalPages}
onClick={() => onPageChange(currentPage + 1)}
/>
</Pagination>
);
};
export default TablePaginate;

View File

@ -1,65 +0,0 @@
import React from 'react';
import { Modal, Button, Spinner, ModalBody } from 'react-bootstrap';
import create from '../../../assets/images/illustration/submitExercise.png';
import cue from '../../../assets/images/illustration/successModal.png';
import ask from '../../../assets/images/illustration/IllustrationForgot.png'
const ModalOperation = ({
show,
handleClose,
title,
description,
loading,
successMessage,
confirmAction,
handleConfirm
}) => {
return (
<Modal show={show} onHide={handleClose} centered>
{loading?(
<Modal.Body className='p-4 d-flex flex-column items-center'>
<div className='d-flex' style={{margin:"5vh 0"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</div>
</Modal.Body>
):(
confirmAction?(
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-dark'><span className='text-red'>Delete</span> This Data</h4>
<img src={ask} alt="" className="w-50"/>
{/* <p className='my-3 text-muted fw-light text-center'>Once deleted, this cannot be restored. Do you wish to go ahead?</p> */}
<p className='my-3 text-muted fw-light text-center'>{description}</p>
<div className="w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>Cancel</Button>
<Button variant="red" className="py-2 px-5 mx-1 rounded-35" onClick={handleConfirm}>Delete</Button>
</div>
</Modal.Body>
):(
title === "ERROR"?(
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-red'>ERROR!</h4>
<img src={ask} alt="" className="w-50"/>
<p className='my-3 text-muted fw-light text-center'>{successMessage}</p>
<Button variant="red" className="w-100 py-2 px-5 rounded-35" onClick={handleClose}>Close</Button>
</Modal.Body>
):(
<Modal.Body className='p-4 d-flex flex-column items-center'>
<h4 className='mb-4 fw-bold text-dark'>Data <span className='text-blue'> Successfully</span> {title}!</h4>
<img src={title === 'Created'? create : cue } alt="" className="w-50"/>
<p className='my-3 text-muted fw-light text-center'>{successMessage}</p>
<Button variant="gd" className="w-100 py-2 px-5 rounded-35" onClick={handleClose}>Got It</Button>
</Modal.Body>
)
)
)}
</Modal>
);
};
export default ModalOperation;

View File

@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.css';
import './assets/styles/index.css';
import 'react-loading-skeleton/dist/skeleton.css';
import App from './App.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(
//<React.StrictMode>
<App />
//</React.StrictMode>,
);

View File

@ -1,84 +0,0 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import AdminLayout from '../../components/layout/admin/AdminLayout';
import NotFound from './NotFound';
import Dashboard from './dashboard/views/Dashboard';
import ManageStudents from './manage_students/views/ManageStudents';
import ManageTeachers from './manage_teachers/views/ManageTeachers';
import ManageClasses from './manage_classes/views/ManageClasses';
import ClassDetail from './manage_classes/views/ClassDetail';
import ManageSections from './manage_section/views/ManageSections';
import ManageTopics from './manage_topics/views/ManageTopics';
import ManageMaterials from './manage_materials/views/ManageMaterials';
import EditorMaterial from './manage_materials/views/EditorMaterial';
import ManageExercises from './manage_exercises/views/ManageExercises';
import OldManageExercises from './manage_exercises/views/OldManageExercises';
import ExerciseDetail from './manage_exercises/views/ExerciseDetail';
import UpdateExercises from './manage_exercises/views/UpdateExercise';
import ManageProgress from './manage_progress/views/ManageProgress';
import StudentProgress from './manage_progress/views/StudentProgress';
import ClassProgress from './manage_progress/views/ClassProgress';
import ManageReports from './manage_reports/views/ManageReports';
import Setting from './setting/views/Setting';
import Review from '../user/review/views/Review';
import '../../assets/styles/admin.css';
import ProtectedRoute from '../../utils/ProtectedRoute';
const AdminRoutes = () => {
return (
<AdminLayout>
<Routes>
<Route element={<ProtectedRoute role="admin" />}>
<Route path="*" element={<NotFound/>} />
<Route path="/" element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="student" element={<ManageStudents />} />
<Route path="teacher" element={<ManageTeachers />} />
<Route path="class" element={<ManageClasses />} />
<Route path="class/class-detail" element={<ClassDetail />} />
<Route path="class/class-detail/:classId" element={<ClassDetail />} />
<Route path="section" element={<ManageSections />} />
<Route path="topic" element={<ManageTopics />} />
<Route path="material" element={<ManageMaterials />} />
<Route path="material/update-material/:materialId" element={<EditorMaterial />} />
<Route path="exercise" element={<ManageExercises />} />
<Route path="exercise/old" element={<OldManageExercises />} />
<Route path="exercise/exercise-detail/:levelId" element={<ExerciseDetail />} />
<Route path="exercise/update-exercise/:levelId" element={<UpdateExercises />} />
<Route path="learning-progress" element={<ManageProgress />} />
<Route path="learning-progress/s/:progressId" element={<StudentProgress />} />
<Route path="learning-progress/c/:progressId" element={<ClassProgress />} />
<Route path="report" element={<ManageReports />} />
<Route path="profile" element={<Setting />} />
<Route path="review/s/:stdLearning" element={<Review />} />
</Route>
{/* <Route path="*" element={<NotFound/>} />
<Route path="/" element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="student" element={<ManageStudents />} />
<Route path="teacher" element={<ManageTeachers />} />
<Route path="class" element={<ManageClasses />} />
<Route path="class/class-detail" element={<ClassDetail />} />
<Route path="section" element={<ManageSections />} />
<Route path="topic" element={<ManageTopics />} />
<Route path="material" element={<ManageMaterials />} />
<Route path="exercise" element={<ManageExercises />} />
<Route path="exercise/exercise-detail" element={<ExerciseDetail />} />
<Route path="learning-progress" element={<ManageProgress />} />
<Route path="learning-progress/student" element={<StudentProgress />} />
<Route path="report" element={<ManageReports />} />
<Route path="setting" element={<Setting />} /> */}
</Routes>
</AdminLayout>
);
};
export default AdminRoutes;

View File

@ -1,11 +0,0 @@
import React from 'react';
const NotFound = () => {
return (
<div className='pt-nav'>
<h1>Not Found</h1>
</div>
);
};
export default NotFound;

View File

@ -1,94 +0,0 @@
import { useState, useEffect } from 'react';
import dashboardService from '../services/serviceDashboard';
const useDashboard = () => {
const [totalStudent, setTotalStudent] = useState("-");
const [totalTeacher, setTotalTeacher] = useState("-");
const [reports, setReports] = useState([]);
const [loadingReports, setLoadingReports] = useState(true);
const [activity, setActivity] = useState([]);
const [loadingActivity, setLoadingActivity] = useState(true);
const [error, setError] = useState(null);
const fetchTotalStudent = async () => {
try {
const data = await dashboardService.fetchTotalStudent("", "", 1, 0);
setTotalStudent(data.payload.totalStudents);
} catch (err) {
setError(err);
}
};
const fetchTotalTeacher = async () => {
try {
const data = await dashboardService.fetchTotalTeacher("", "", 1, 0);
setTotalTeacher(data.payload.totalTeachers);
} catch (err) {
setError(err);
}
};
const fetchActivity = async () => {
setLoadingActivity(true);
try {
const data = await dashboardService.fetchActivity("", "", 1, 5);
setActivity(data.payload.studentActivities);
} catch (err) {
setError(err);
} finally {
setLoadingActivity(false);
}
};
const fetchReport = async () => {
setLoadingReports(true);
try {
const data = await dashboardService.fetchReport("", "", 1, 5);
setReports(data.payload.reports);
} catch (err) {
setError(err);
} finally {
setLoadingReports(false);
}
};
useEffect(() => {
fetchTotalStudent(),
fetchTotalTeacher(),
fetchActivity();
fetchReport();
}, []);
function formatLocalDate(isoDate) {
const date = new Date(isoDate);
const options = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Jakarta'
};
return new Intl.DateTimeFormat('id-ID', options).format(date)
.replace(/\//g, '-')
.replace(/\./g, ':') + ' WIB';
}
return {
error,
totalStudent,
totalTeacher,
reports,
activity,
loadingReports,
loadingActivity,
formatLocalDate
};
};
export default useDashboard;

View File

@ -1,48 +0,0 @@
import axiosInstance from '../../../../utils/axiosInstance';
const fetchTotalStudent = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/user/student?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching reports:', error);
throw error;
}
};
const fetchTotalTeacher = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/user/teacher?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching reports:', error);
throw error;
}
};
const fetchActivity = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/stdLearning/activities?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching reports:', error);
throw error;
}
};
const fetchReport = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/report?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching reports:', error);
throw error;
}
};
export default{
fetchTotalStudent,
fetchTotalTeacher,
fetchReport,
fetchActivity
};

View File

@ -1,156 +0,0 @@
import React from 'react';
import { Table, Row, Col, Card, Button, Spinner } from 'react-bootstrap';
import { NavLink, Link } from 'react-router-dom';
import useDashboard from '../hooks/useDashboard';
function validName(text) {
const words = text.trim().split(" ");
return words.slice(0, 2).join(" ");
}
const AdminDashboard = () => {
const { username } = JSON.parse(localStorage.getItem('userData'));
const {
error,
totalStudent,
totalTeacher,
reports,
activity,
loadingActivity,
loadingReports,
formatLocalDate
} = useDashboard();
return (
<div className='admin-dashboard'>
<h2 className='page-title'>Hi, {validName(username)}!</h2>
<p className='page-desc'>Together, we can make every learning journey smarter and more adaptive.</p>
<Row className='mb-45'>
<Col md={4} xxl={2}>
<div className="mini-cards p-4 mb-3">
<h4 className='m-0'>All Student</h4>
<hr />
<h1 className='m-0 fw-bold text-blue'>{totalStudent}</h1>
</div>
<div className="mini-cards p-4">
<h4 className='m-0'>All Teacher</h4>
<hr />
<h1 className='m-0 fw-bold text-blue'>{totalTeacher}</h1>
</div>
</Col>
<Col md={8} xxl={10}>
<div className="cards">
<div className="cards-title">
<h4>Recent Student Activities</h4>
</div>
<div className="cards-body">
<Table hover>
<thead>
<tr>
<th className='text-start'>Full Name</th>
<th className='text-center'>Class</th>
<th>Topic</th>
<th>Level</th>
<th className='text-center'>Score</th>
<th className='text-center'>Review</th>
</tr>
</thead>
<tbody>
{loadingActivity?(
<tr>
<td colSpan={6} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
activity.length > 0?(
activity.map((data, index) => (
<tr key={index}>
<td className='text-start'>{data.NAME_USERS}</td>
<td className='text-center'>{data.NAME_CLASS ? data.NAME_CLASS : '-'}</td>
<td>{data.NAME_TOPIC}</td>
<td>{data.NAME_LEVEL}</td>
<td className='text-center'>{data.SCORE}</td>
<td className='text-center action-col'>
<Link className='btn btn-sm btn-view' to={`/admin/review/s/${data.ID_STUDENT_LEARNING}`}><i className="bi bi-eye"></i></Link>
</td>
</tr>
))
):(
<tr>
<td colSpan={6} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
</div>
</div>
</Col>
</Row>
<Row className='mb-45'>
<Col sm={12}>
<div className="cards">
<div className="cards-title d-flex justify-content-between align-items-center">
<h4>Issue Reports</h4>
<NavLink to="/admin/report" className="text-blue">See more</NavLink>
</div>
<div className="cards-body">
<Table hover>
<thead>
<tr>
<th>No</th>
<th>Email Address</th>
<th>Full Name</th>
<th>Report</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{loadingReports?(
<tr>
<td colSpan={5} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
reports.length > 0?(
reports.map((data, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{data.USER_EMAIL}</td>
<td>{data.USER_EMAIL}</td>
<td>{data.REPORTS}</td>
<td>{formatLocalDate(data.TIME_REPORT)}</td>
</tr>
))
):(
<tr>
<td colSpan={5} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default AdminDashboard;

View File

@ -1,37 +0,0 @@
import { useState, useEffect } from 'react';
import classService from '../services/serviceClasses';
const useClassDetail = (classId) => {
const [classData, setClassData] = useState([]);
const [nameClass, setClassName] = useState('class');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const getNameClass = await classService.getClassById(classId);
setClassName(getNameClass.payload.NAME_CLASS);
const data = await classService.getClassStudent(classId);
if ([data.payload].length > 0) {
setClassData(data.payload);
} else {
setClassData([]);
}
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [classId]);
return { classData, nameClass, loading, error };
};
export default useClassDetail;

View File

@ -1,265 +0,0 @@
import { useState, useEffect } from 'react';
import classService from '../services/serviceClasses';
const useClasses = () => {
const [classes, setClasses] = useState([]);
const [freeStudent, setStudentOption] = useState([]);
const [studentSlug, setStudentSlug] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedClass, setSelectedClass] = useState(null);
const [formData, setFormData] = useState({
className: '',
capacity: '',
});
const [assignStudents, setAssignStudents] = useState([]);
const [assignClass, setAssignClass] = useState('');
const [show, setShow] = useState(false);
const [showAssign, setShowAssign] = useState(false);
const [showLoader, setShowLoader] = useState(false);
const [loaderState, setLoaderState] = useState({ loading: false, successMessage: '', title: '', description: '', confirmAction: false });
const resetForm = () =>{
setFormData({
className: '',
capacity: '',
});
}
const handleFormChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const assignStudentChange = (selectedOptions) => {
setAssignStudents(selectedOptions);
};
const assignClassChange = (e) => {
setAssignClass(e.target.value);
};
const handleShow = (data) => {
setSelectedClass(data);
setFormData({
className: data?.NAME_CLASS || '',
capacity: data?.TOTAL_STUDENT || '',
});
setShow(true);
};
const handleClose = () => {
setShow(false);
setShowAssign(false);
resetForm();
setSelectedClass(null)
};
const handleShowAssign = () => setShowAssign(true);
const handleCloseLoader = () => setShowLoader(false);
const handleShowLoader = (title, description, loading = false, successMessage = '', confirmAction = false) => {
setLoaderState({ title, description, loading, successMessage, confirmAction });
setShowLoader(true);
};
const fetchDataStudent = async () => {
setLoading(true);
try {
const studentArray = {};
const studentForOption = [];
const dataStudent = await classService.fetchDataFreeStudent();
dataStudent.payload.map((s, index) => {
studentArray[s.NISN] = s.NAME_USERS;
studentForOption[index] = {value: s.NISN, label: `${s.NISN} - ${s.NAME_USERS}`}
});
setStudentSlug(studentArray);
setStudentOption(studentForOption);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const createClass = async () => {
const newClass ={
NAME_CLASS: formData.className,
TOTAL_STUDENT: formData.capacity,
};
handleShowLoader('Created', '', true)
try {
const createdClass = await classService.createData(newClass);
setClasses((prevClasses) => [...prevClasses, createdClass.payload]);
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your new entry has been successfully created and saved.'
}));
resetForm();
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.response.data.message
}));
}
};
const editClass = async () => {
const id = selectedClass.ID_CLASS;
const data = formData;
const updateClassData ={
NAME_CLASS: data.className,
TOTAL_STUDENT: parseInt(data.capacity),
};
handleClose();
handleShowLoader('Updated', '', true);
try {
const classData = await classService.updateData(id, updateClassData);
setClasses((prevClasses) =>
prevClasses.map((s) => (s.ID_CLASS === id ? classData.payload : s))
);
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your data has been successfully updated.'
}));
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.response.data.message
}));
}
};
const deleteClass = async (id) => {
handleShowLoader('Deleted', 'Are you sure you want to delete this class?', false, '', true);
const confirmDelete = async () => {
handleCloseLoader();
handleShowLoader('Deleted', '', true);
try {
await classService.deleteData(id);
} catch (err) {
setError(err);
}finally{
setClasses((prevClasses) => prevClasses.filter((s) => s.ID_CLASS !== id));
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your data has been successfully deleted.'
}));
}
}
setLoaderState(prev => ({ ...prev, handleConfirm: confirmDelete }));
};
const assignStudentToClass = async () => {
handleClose();
handleShowLoader('Updated', '', true)
const selectedStudent = assignStudents.map((selectedData) => ({
NAME_USERS: studentSlug[selectedData.value],
NISN: selectedData.value
}));
const assignData = {
NAME_CLASS: assignClass,
STUDENTS: selectedStudent
};
try {
await classService.studentToClass(assignData);
} catch (err) {
setError(err);
}finally{
fetchDataStudent();
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your data has been successfully updated.'
}));
}
};
const [search, setSearch] = useState("");
const [sort, setSort] = useState("");
const [page, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(7);
const [totalPages, setTotalPages] = useState(0);
const [totalData, setTotalData] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const data = await classService.fetchData(search, sort, page, limit);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setClasses(data.payload.classes);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSerachChange = () => {
fetchData();
setCurrentPage(1);
}
const handlePageChange = (pages) => {
setCurrentPage(pages);
}
const handleLimitsChange = (e) => {
setLimit(e.target.value);
setCurrentPage(1);
}
useEffect(() => {
fetchDataStudent();
}, []);
useEffect(() => {
fetchData();
}, [page, limit]);
return {
classes,
freeStudent,
loading,
error,
formData,
show,
showAssign,
showLoader,
loaderState,
createClass,
editClass,
deleteClass,
assignStudentToClass,
handleFormChange,
assignStudentChange,
assignClassChange,
resetForm,
handleShow,
handleShowAssign,
handleClose,
handleCloseLoader,
totalPages,
totalData,
setSearch,
page,
handlePageChange,
handleLimitsChange,
handleSerachChange
};
};
export default useClasses;

View File

@ -1,93 +0,0 @@
import axiosInstance from '../../../../utils/axiosInstance';
const fetchData= async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/class/admin?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching class:', error);
throw error;
}
};
const fetchDataFreeStudent = async () => {
try {
const response = await axiosInstance.get(`/class/student/unassigned`);
return response.data;
} catch (error) {
console.error('Error fetching class:', error);
throw error;
}
};
const getClassById = async (id) => {
try {
const response = await axiosInstance.get(`/class/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching class with ID ${id}:`, error);
throw error;
}
};
const getClassStudent = async (id) => {
try {
const response = await axiosInstance.get(`/class/student/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching class with ID ${id}:`, error);
throw error;
}
}
const createData = async (classData) => {
try {
const response = await axiosInstance.post(`/class`, classData);
return response.data;
} catch (error) {
console.error('Error adding class:', error);
throw error;
}
};
const updateData = async (id, classData) => {
try {
const response = await axiosInstance.put(`/class/update/${id}`, classData);
return response.data;
} catch (error) {
console.error(`Error updating class with ID ${id}:`, error);
throw error;
}
};
const deleteData = async (id) => {
try {
const response = await axiosInstance.delete(`/class/delete/${id}`);
return response.data;
} catch (error) {
console.error(`Error deleting class with ID ${id}:`, error);
throw error;
}
};
const studentToClass = async (asignData) => {
try {
const response = await axiosInstance.post(`/class/student/update`, asignData);
return response.data;
} catch (error) {
console.error(`Error asign student to class :`, error);
throw error;
}
};
export default{
fetchData,
fetchDataFreeStudent,
getClassById,
getClassStudent,
createData,
updateData,
deleteData,
studentToClass
};

View File

@ -1,90 +0,0 @@
import React from 'react';
import { Table, Row, Col, Button, Form, Dropdown, DropdownButton, Breadcrumb, Spinner } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom';
import useClassDetail from '../hooks/useClassDetail';
const ClassDetail = () => {
const { classId } = useParams();
const { classData, nameClass, loading, error } = useClassDetail(classId);
if (error) {
return <>{error}</>;
}
return (
<div className='admin-teachers'>
<Row className='mb-45'>
<Col className="d-flex align-items-center breadcrumb-con">
<Button as={Link} className='btn btn-blue btn-square-back' to='/admin/class'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="#">Academic</Breadcrumb.Item>
<Breadcrumb.Item href="/admin/class" className='text-capitalize'>Classes</Breadcrumb.Item>
<Breadcrumb.Item active>Class Details</Breadcrumb.Item>
</Breadcrumb>
</Col>
</Row>
<Row className='mb-45'>
<Col>
<div className="cards">
<div className="cards-title">
<h4>{nameClass}</h4>
</div>
<div className="cards-body">
<div className="d-flex">
<Form.Control
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search'
/>
</div>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th>Student Name</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={5} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
classData.length > 0 ?(
classData.map((data, index) => (
<tr key={data.ID}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<td>{data.NAME_USERS}</td>
</tr>
))
):(
<tr>
<td colSpan={3} style={{height:'20vh'}}>
<h3>This class is empty</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default ClassDetail;

View File

@ -1,312 +0,0 @@
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { Table, Row, Col, Nav, Tab, Button, Form, InputGroup, Spinner, Modal } from 'react-bootstrap';
import Select from 'react-select';
import useClasses from '../hooks/useClasses';
import ModalOperation from '../../../../components/ui/adminMessageModal/ModalOperation';
import TablePaginate from '../../../../components/ui/TablePaginate';
const ManageClasses = () => {
const {
classes,
freeStudent,
loading,
error,
formData,
show,
showAssign,
showLoader,
loaderState,
createClass,
editClass,
deleteClass,
assignStudentToClass,
handleFormChange,
assignStudentChange,
assignClassChange,
resetForm,
handleShow,
handleShowAssign,
handleClose,
handleCloseLoader,
page,
totalData,
totalPages,
setSearch,
handlePageChange,
handleLimitsChange,
handleSerachChange
} = useClasses();
if (error) {
return (<>{error}</>);
}
return (
<div className='admin-teachers'>
<div className="d-flex justify-content-between">
<h2 className='page-title strip'>Classes</h2>
<Button variant="outline-blue" type="button" className='py-2 bg-white' onClick={handleShowAssign}>
<i className="bi bi-person-fill-add me-2"></i>Assign Student to Class
</Button>
</div>
<p className='page-desc'>Description of Classes.</p>
<Tab.Container id="left-tabs-example" defaultActiveKey="detail" onSelect={resetForm}>
<Row className='mb-45'>
<Col xs={12}>
<Nav variant="pills" className='col-tabs'>
<Nav.Item>
<Nav.Link eventKey="detail">View Entries</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="create">Create Data</Nav.Link>
</Nav.Item>
</Nav>
</Col>
</Row>
<Row className='mb-45'>
<Col xs={12} className='col-tabs-content'>
<Tab.Content>
<Tab.Pane eventKey="detail">
<div className="cards">
<div className="cards-title">
<h4>Class List</h4>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSerachChange(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearch(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>Class Name</th>
<th className='text-center'>Capacity</th>
<th className='text-center'>Action</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={5} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
classes.length > 0 ?(
classes.map((data, index) => (
<tr key={data.ID_CLASS}>
<td>{index + 1}</td>
<td>{data.NAME_CLASS}</td>
<td>{data.TOTAL_STUDENT}</td>
<td className='text-center action-col d-flex justify-content-center'>
<NavLink className='btn btn-sm btn-view' to={`class-detail/${data.ID_CLASS}`}><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={() => handleShow(data)}>
<i className="bi bi-pencil-square"></i>
</Button>
<Button size='sm' className='btn-delete' onClick={() => deleteClass(data.ID_CLASS)}>
<i className="bi bi-trash3"></i>
</Button>
</td>
</tr>
))
):(
<tr>
<td colSpan={5} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
<div className="mt-2 w-100 d-flex justify-content-between align-items-center">
<div className='d-flex align-items-center'>
<small className="me-2">Item per page</small>
<Form.Select
size='sm'
className='py-0 px-1 me-2'
aria-label="Default select example"
defaultValue='7'
onChange={handleLimitsChange}
style={{ width: '50px' }}
>
<option value="7">7</option>
<option value="10">10</option>
<option value="20">20</option>
</Form.Select>
<small>of {totalData}</small>
</div>
<TablePaginate
totalPages={totalPages}
currentPage={page}
onPageChange={handlePageChange}
/>
</div>
</div>
</div>
</Tab.Pane>
<Tab.Pane eventKey="create">
<div className="cards">
<div className="cards-title">
<h4>Add Class Data</h4>
</div>
<div className="cards-body">
<Form onSubmit={(e) => { e.preventDefault(); createClass(); }}>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridClass" className='col-12 col-sm-6'>
<Form.Label>Class Name<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-easel2"></i></InputGroup.Text>
<Form.Control
name="className"
value={formData.className || ''}
onChange={handleFormChange}
placeholder="Enter Class Name"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridCapacity" className='col-12 col-sm-6'>
<Form.Label>Class Capacity<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-person"></i></InputGroup.Text>
<Form.Control type='number'
name="capacity"
value={formData.capacity || ''}
onChange={handleFormChange}
placeholder="Enter Class Capacity"
/>
</InputGroup>
</Form.Group>
</Row>
<div className="d-flex justify-content-end">
<Button variant="outline-blue" type="reset" className='ms-auto py-2 rounded-35' onClick={resetForm}>
reset
</Button>
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
Add
</Button>
</div>
</Form>
</div>
</div>
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
<Modal show={show} onHide={handleClose} className='modal-admin' size='lg' centered>
<Modal.Header closeButton>
<Modal.Title>Update Class Data</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={(e) => { e.preventDefault(); editClass(); }}>
<Form.Group as={Col} controlId="updateGridName" className='mb-3'>
<Form.Label>Class Name<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-easel2"></i></InputGroup.Text>
<Form.Control
name="className"
value={formData.className || ''}
onChange={handleFormChange}
placeholder="Enter Class Name"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="updateGridCapacity" className='mb-3'>
<Form.Label>Class Capacity<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-person"></i></InputGroup.Text>
<Form.Control type='number'
name="capacity"
value={formData.capacity || ''}
onChange={handleFormChange}
placeholder="Enter Class Capacity"
/>
</InputGroup>
</Form.Group>
<div className="d-flex justify-content-end">
<Button variant="blue" type="submit" className='py-2 px-5 w-100 rounded-35'>
Update
</Button>
</div>
</Form>
</Modal.Body>
</Modal>
<Modal show={showAssign} onHide={handleClose} className='modal-admin' size='lg'>
<Modal.Header closeButton>
<Modal.Title>Assign Student to Class</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={(e) => { e.preventDefault(); assignStudentToClass(); }}>
<Form.Group as={Col} controlId="formGridEmail" className='mb-3'>
<Form.Label>Class<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-person"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" required defaultValue="" onChange={assignClassChange}>
<option value="" disabled hidden>Select Class</option>
{
classes.map((data, index) => (
<option key={data.ID_CLASS} value={data.NAME_CLASS}>{data.NAME_CLASS}</option>
))
}
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword" className='mb-3'>
<Form.Label>Student<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-person-vcard"></i></InputGroup.Text>
<Select
className='group-react-select'
classNamePrefix='react-select-group'
required={true}
isMulti
options={freeStudent}
onChange={assignStudentChange}
placeholder="Select Students"
backspaceRemovesValue={true}
closeMenuOnSelect={false}
menuPosition="fixed"
/>
</InputGroup>
</Form.Group>
<div className="d-flex justify-content-end">
<Button variant="blue" type="submit" className='py-2 px-5 w-100 rounded-35'>
Add
</Button>
</div>
</Form>
</Modal.Body>
</Modal>
<ModalOperation
show={showLoader}
handleClose={handleCloseLoader}
title={loaderState.title}
description={loaderState.description}
loading={loaderState.loading}
successMessage={loaderState.successMessage}
confirmAction={loaderState.confirmAction}
handleConfirm={loaderState.handleConfirm}
/>
</div>
);
};
export default ManageClasses;

View File

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import exerciseService from '../services/serviceExercises';
const useExercise = () => {
const [levels, setLevels] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [sort, setSort] = useState("");
const [page, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(7);
const [totalPages, setTotalPages] = useState(0);
const [totalData, setTotalData] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const data = await exerciseService.fetchData(search, sort, page, limit);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setLevels(data.payload.levels);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSerachChange = () => {
fetchData();
setCurrentPage(1);
}
const handlePageChange = (pages) => {
setCurrentPage(pages);
}
const handleLimitsChange = (e) => {
setLimit(e.target.value);
setCurrentPage(1);
}
useEffect(() => {
fetchData();
}, [page, limit]);
return {
levels,
loading,
error,
page,
totalData,
totalPages,
setSearch,
handlePageChange,
handleLimitsChange,
handleSerachChange,
};
};
export default useExercise;

View File

@ -1,450 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import exerciseService from '../services/serviceExercises';
import { MEDIA_URL } from '../../../../utils/Constant';
function videoUrlChecker(url) {
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const googleDriveRegex = /https?:\/\/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)\/(?:view|preview)/;
if (youtubeRegex.test(url)) {
return 'Youtube';
} else if (googleDriveRegex.test(url)) {
return 'Gdrive';
} else {
return 'none';
}
}
const useUpdateExercises = (levelId) => {
const [exerciseData, setExerciseData] = useState([]);
const [isUpdated, setIsUpdated] = useState([]);
const [levelName, setLevelName] = useState('Level');
const [sectionName, setSectionName] = useState('section');
const [topicName, setTopicName] = useState('topic');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const mediaPath = `${MEDIA_URL}/exercise`;
const [formData, setFormData] = useState({
ID_ADMIN_EXERCISE: "",
ID_LEVEL: "",
TITLE: "",
QUESTION: "",
QUESTION_TYPE: "",
SCORE_WEIGHT: "",
AUDIO: "",
IMAGE: "",
VIDEO: "",
multipleChoices: "",
trueFalse: "",
matchingPairs: {},
});
const [imageContainer, setImageContainer] = useState("");
const [audioContainer, setAudioContainer] = useState("");
const [selectedQuestion, setSelectedQuestion] = useState(false);
const [show, setShow] = useState(false);
const [showLoader, setShowLoader] = useState(false);
const [loaderState, setLoaderState] = useState({ loading: false, successMessage: '', title: '', description: '', confirmAction: false });
const resetForm = () =>{
setSelectedQuestion(false);
setFormData({
ID_ADMIN_EXERCISE:"",
ID_LEVEL:"",
TITLE:"",
QUESTION:"",
QUESTION_TYPE:"",
SCORE_WEIGHT:"",
AUDIO:'',
IMAGE:'',
VIDEO:'',
multipleChoices:"",
trueFalse:"",
matchingPairs:{},
});
}
const handleCloseLoader = () => setShowLoader(false);
const handleShowLoader = (title, description, loading = false, successMessage = '', confirmAction = false) => {
setLoaderState({ title, description, loading, successMessage, confirmAction });
setShowLoader(true);
};
const [mediaPreview, setMediaPreview] = useState(null);
const handleFormChangeMedia = (e) => {
const file = e.target.files[0];
if (file) {
const mediaUrl = URL.createObjectURL(file);
setMediaPreview(mediaUrl);
}
setFormData({ ...formData, [e.target.name]: file });
};
const [selectedQuestionNumber, setSelectedQuestionNumber] = useState(null);
const handleShow = (data) => {
setImageContainer("");
setAudioContainer("");
setSelectedQuestion(true);
setSelectedQuestionNumber(data+1);
setFormData({
ID_ADMIN_EXERCISE: exerciseData[data].ID_ADMIN_EXERCISE || "",
ID_LEVEL: exerciseData[data].ID_LEVEL || "",
TITLE: exerciseData[data].TITLE || "",
QUESTION: exerciseData[data].QUESTION || "",
QUESTION_TYPE: exerciseData[data].QUESTION_TYPE || "",
SCORE_WEIGHT: exerciseData[data].SCORE_WEIGHT || "",
AUDIO: exerciseData[data].AUDIO || '',
IMAGE: exerciseData[data].IMAGE || '',
VIDEO: exerciseData[data].VIDEO || '',
multipleChoices: exerciseData[data].multipleChoices || "",
trueFalse: exerciseData[data].trueFalse || "",
matchingPairs: exerciseData[data].matchingPairs || {},
});
if(exerciseData[data].IMAGE){
setImageContainer(exerciseData[data].IMAGE);
}
if(exerciseData[data].AUDIO){
setAudioContainer(exerciseData[data].AUDIO);
}
setShow(true);
};
const handleClose = () => {
setShow(false);
resetForm();
setSelectedQuestion(null);
setMediaPreview(null);
setImageContainer("");
setAudioContainer("");
};
const handleShowNewData = () => {
setSelectedQuestionNumber(null);
resetForm();
setShow(true);
}
const [validUrl, setValidUrl] = useState('');
const [urlInput, setUrlInput] = useState(null);
const [showUrlModal, setShowUrlModal] = useState(false);
const handleShowUrlModal = () => {setShowUrlModal(true); setValidUrl('')};
const handleCloseUrlModal = () => {setShowUrlModal(false); setValidUrl('')};
const handleUrlInput = (e) => {
setUrlInput(e.target.value)
};
const handleCheckUrl = () => {
const check = videoUrlChecker(urlInput);
if (check === "Gdrive" || check === "Youtube") {
setValidUrl('valid');
}else{
setValidUrl('invalid');
}
};
const handleSetVideoUrl = () => {
setFormData({...formData, VIDEO: urlInput});
handleCloseUrlModal();
};
const handleFormChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleFormChangeAnswer = (e) => {
if (formData.QUESTION_TYPE === "TFQ") {
setFormData({...formData, trueFalse: e.target.value});
}else if (formData.QUESTION_TYPE === "MCQ") {
setFormData({
...formData,
multipleChoices: {
...formData.multipleChoices,
[0]: {
...formData.multipleChoices[0],
[e.target.name]: e.target.value
},
}
});
}else if (formData.QUESTION_TYPE === "MPQ") {
const match = e.target.name.match(/^(\d+)_(.*)/);
const index = match[1];
const side = match[2];
setFormData({
...formData,
matchingPairs: {
...formData.matchingPairs,
[index]: {
...formData.matchingPairs[index],
[side]: e.target.value,
},
},
});
}
};
const handleFormResetMedia = () => {
setFormData({...formData, AUDIO: "", IMAGE: "", VIDEO: ""});
}
const createQuestion = async () => {
const createData = new FormData();
createData.append('exercises[0][ID_LEVEL]', levelId);
createData.append('exercises[0][QUESTION]', formData.QUESTION);
createData.append('exercises[0][SCORE_WEIGHT]', formData.SCORE_WEIGHT);
createData.append('exercises[0][QUESTION_TYPE]', formData.QUESTION_TYPE);
if (formData.VIDEO) {
createData.append('exercises[0][VIDEO]', formData.VIDEO);
}else if (formData.IMAGE) {
createData.append('IMAGE[0]', formData.IMAGE);
}else if (formData.AUDIO) {
createData.append('AUDIO[0]', formData.AUDIO);
}
if (formData.QUESTION_TYPE === "TFQ") {
createData.append('exercises[0][IS_TRUE]', formData.trueFalse);
}else if (formData.QUESTION_TYPE === "MCQ") {
createData.append('exercises[0][OPTION_A]', formData.multipleChoices[0].OPTION_A);
createData.append('exercises[0][OPTION_B]', formData.multipleChoices[0].OPTION_B);
createData.append('exercises[0][OPTION_C]', formData.multipleChoices[0].OPTION_C);
createData.append('exercises[0][OPTION_D]', formData.multipleChoices[0].OPTION_D);
if (formData.multipleChoices[0].OPTION_E) {
createData.append('exercises[0][OPTION_E]', formData.multipleChoices[0].OPTION_E);
}
createData.append('exercises[0][ANSWER_KEY]', formData.multipleChoices[0].ANSWER_KEY);
}else if (formData.QUESTION_TYPE === "MPQ") {
Object.keys(formData.matchingPairs).forEach((key, newIndex) => {
createData.append(`exercises[0][PAIRS][${newIndex}][LEFT_PAIR]`, formData.matchingPairs[key].LEFT_PAIR);
createData.append(`exercises[0][PAIRS][${newIndex}][RIGHT_PAIR]`, formData.matchingPairs[key].RIGHT_PAIR);
});
}
handleShowLoader('Created', '', true);
try {
const create = await exerciseService.createData(createData);
setExerciseData((prevQuestion) => [...prevQuestion, create.payload]);
handleClose();
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Question successfully created.'
}));
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.response.data.message
}));
}
};
const updateQuestion = async () => {
const id = formData.ID_ADMIN_EXERCISE;
const updateData = new FormData();
updateData.append('ID_LEVEL', levelId);
updateData.append('QUESTION', formData.QUESTION);
updateData.append('SCORE_WEIGHT', formData.SCORE_WEIGHT);
if (formData.VIDEO) {
updateData.append('VIDEO', formData.VIDEO);
}else if (formData.IMAGE) {
updateData.append('IMAGE', formData.IMAGE);
}else if (formData.AUDIO) {
updateData.append('AUDIO', formData.AUDIO);
}
if (formData.VIDEO === '') {
updateData.append('VIDEO', "");
}
if (formData.IMAGE === '') {
updateData.append('IMAGE', "");
}
if (formData.AUDIO === '') {
updateData.append('AUDIO', "");
}
if (formData.QUESTION_TYPE === "TFQ") {
if (typeof formData.trueFalse === 'object') {
updateData.append('IS_TRUE', formData.trueFalse[0].IS_TRUE);
} else {
updateData.append('IS_TRUE', formData.trueFalse);
}
}else if (formData.QUESTION_TYPE === "MCQ") {
updateData.append('OPTION_A', formData.multipleChoices[0].OPTION_A);
updateData.append('OPTION_B', formData.multipleChoices[0].OPTION_B);
updateData.append('OPTION_C', formData.multipleChoices[0].OPTION_C);
updateData.append('OPTION_D', formData.multipleChoices[0].OPTION_D);
if (formData.multipleChoices[0].OPTION_E) {
updateData.append('OPTION_E', formData.multipleChoices[0].OPTION_E);
}
updateData.append('ANSWER_KEY', formData.multipleChoices[0].ANSWER_KEY);
}else if (formData.QUESTION_TYPE === "MPQ") {
Object.keys(formData.matchingPairs).forEach((key, newIndex) => {
updateData.append(`PAIRS[${newIndex}][LEFT_PAIR]`, formData.matchingPairs[key].LEFT_PAIR);
updateData.append(`PAIRS[${newIndex}][RIGHT_PAIR]`, formData.matchingPairs[key].RIGHT_PAIR);
});
}
//media checker delete
if (imageContainer != '' && imageContainer != formData.IMAGE) {
await exerciseService.deleteMedia(id, 'image');
setImageContainer('');
setAudioContainer('');
}
if (audioContainer != '' && audioContainer != formData.AUDIO) {
await exerciseService.deleteMedia(id, 'audio');
setImageContainer('');
setAudioContainer('');
}
handleShowLoader('Updated', '', true);
try {
const update = await exerciseService.updateData(id, updateData);
setExerciseData((prevQuestion) =>
prevQuestion.map((s) => (s.ID_ADMIN_EXERCISE === id ? update.payload : s))
);
handleClose();
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Question successfully updated.'
}));
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.message
}));
}
};
const deleteQuestion = async (index) => {
const id = exerciseData[index].ID_ADMIN_EXERCISE;
handleShowLoader('Deleted', `Are you sure you want to delete Question ${index+1}?`, false, '', true);
const confirmDelete = async () => {
handleCloseLoader();
handleShowLoader('Deleted', '', true)
try {
await exerciseService.deleteData(id);
setExerciseData((prevSections) => prevSections.filter((s) => s.ID_ADMIN_EXERCISE !== id));
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: `Question has been successfully deleted.`
}));
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.message
}));
}
}
setLoaderState(prev => ({ ...prev, handleConfirm: confirmDelete }));
};
const sortingQuestion = async () => {
const id = levelId;
const sortingData = {
ID_LEVEL: levelId,
exercises: exerciseData.map(item => ({
ID_ADMIN_EXERCISE: item.ID_ADMIN_EXERCISE,
TITLE: item.TITLE
}))
};
handleShowLoader('Updated', '', true);
try {
await exerciseService.sortingData(sortingData);
// console.log(sorting.payload);
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Exercise successfully updated.'
}));
} catch (err) {
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.message
}));
}
};
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const setUpdated = [];
const data = await exerciseService.getExerciseByLevelId(levelId);
// console.log(data.EXERCISES);
setLevelName(data.NAME_LEVEL);
setSectionName(data.NAME_SECTION);
setTopicName(data.NAME_TOPIC);
setExerciseData(data.EXERCISES);
for (let i = 0; i < data.EXERCISES.length; i++) {
setUpdated[i] = false;
if (i === data.EXERCISES.length-1) {
setIsUpdated(setUpdated);
}
}
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [levelId]);
return {
exerciseData,
setExerciseData,
formData,
levelName,
sectionName,
topicName,
loading,
error,
mediaPath,
mediaPreview,
handleFormChange,
handleFormChangeAnswer,
handleFormChangeMedia,
handleFormResetMedia,
showLoader,
handleCloseLoader,
loaderState,
createQuestion,
updateQuestion,
deleteQuestion,
sortingQuestion,
selectedQuestion,
selectedQuestionNumber,
show,
handleShow,
handleClose,
handleShowNewData,
validUrl,
handleCheckUrl,
handleUrlInput,
showUrlModal,
handleShowUrlModal,
handleCloseUrlModal,
handleSetVideoUrl
};
};
export default useUpdateExercises;

View File

@ -1,125 +0,0 @@
import axiosInstance from '../../../../utils/axiosInstance';
const getSoalNumber = (title) => {
const match = title.match(/\d+$/);
return match ? parseInt(match[0], 10) : 0;
};
const fetchData= async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/level/admin?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching levels:', error);
throw error;
}
};
const getLevelById = async (id) => {
try {
const response = await axiosInstance.get(`/level/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching level with ID ${id}:`, error);
throw error;
}
};
const getExerciseByLevelId = async (id) => {
try {
const response = await axiosInstance.get(`/exercise/admin/level/${id}`);
const sortedData = response.data.payload.EXERCISES.sort((a, b) => {
return getSoalNumber(a.TITLE) - getSoalNumber(b.TITLE);
});
return {NAME_LEVEL: response.data.payload.NAME_LEVEL ,NAME_SECTION: response.data.payload.NAME_SECTION ,NAME_TOPIC: response.data.payload.NAME_TOPIC , EXERCISES: sortedData};
} catch (error) {
console.error(`Error fetching level with ID ${id}:`, error);
throw error;
}
};
const createData = async (questionData) => {
try {
const response = await axiosInstance.post(`/exercises`, questionData);
const question = response.data.payload[0];
let editedResponse = "";
if (question.QUESTION_TYPE === "MPQ") {
editedResponse = {...question, matchingPairs: question.questionDetails};
}else if (question.QUESTION_TYPE === "MCQ") {
editedResponse = {...question, multipleChoices: [question.questionDetails]};
}else if (question.QUESTION_TYPE === "TFQ") {
editedResponse = {...question, trueFalse: [question.questionDetails]};
}
return {payload: editedResponse, message: response.data.message, status: response.data.statusCode,};
} catch (error) {
console.error('Error adding level:', error);
throw error;
}
};
const updateData = async (id, questionData) => {
try {
const response = await axiosInstance.put(`/exercise/${id}`, questionData);
const question = response.data.payload;
let editedResponse = "";
if (question.QUESTION_TYPE === "MPQ") {
editedResponse = {...question, matchingPairs: question.matchingPairs};
}else if (question.QUESTION_TYPE === "MCQ") {
editedResponse = {...question, multipleChoices: [question.multipleChoices]};
}else if (question.QUESTION_TYPE === "TFQ") {
editedResponse = {...question, trueFalse: [question.trueFalse]};
}
return {payload: editedResponse, message: response.data.message, status: response.data.statusCode,};
// return response.data;
} catch (error) {
console.error(`Error updating level with ID ${id}:`, error);
throw error;
}
};
const sortingData = async (questionData) => {
try {
const response = await axiosInstance.put(`/exercise/title`, questionData);
return response.data;
} catch (error) {
console.error(`Error updating level with ID ${id}:`, error);
throw error;
}
};
const deleteData = async (id) => {
try {
const response = await axiosInstance.delete(`/exercise/${id}`);
return response.data;
} catch (error) {
console.error(`Error deleting question with ID ${id}:`, error);
throw error;
}
};
const deleteMedia = async (id, fileType) => {
try {
const response = await axiosInstance.post(`/exercise/file/${id}`, {fileType});
return response.data;
} catch (error) {
console.error(`Error deleting question with ID ${id}:`, error);
throw error;
}
};
export default{
fetchData,
getExerciseByLevelId,
getLevelById,
createData,
updateData,
sortingData,
deleteData,
deleteMedia
};

View File

@ -1,83 +0,0 @@
import React from 'react';
import { Table, Row, Col, Button, Form, Dropdown, DropdownButton, Breadcrumb } from 'react-bootstrap';
import { Link } from 'react-router-dom';
const ExerciseDetail = () => {
return (
<div className='admin-teachers'>
<Row className='mb-45'>
<Col className="d-flex align-items-center breadcrumb-con">
<Button as={Link} className='btn btn-blue btn-square-back' to='/admin/exercise'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="#">Academic</Breadcrumb.Item>
<Breadcrumb.Item href="/admin/exercise" className='text-capitalize'>Exercise</Breadcrumb.Item>
<Breadcrumb.Item active>Exercise Details</Breadcrumb.Item>
</Breadcrumb>
</Col>
</Row>
<Row className='mb-45'>
<Col>
<div className="cards">
<div className="cards-title">
<h4>Class A</h4>
</div>
<div className="cards-body">
<div className="d-flex">
<Form.Control
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search'
/>
<DropdownButton title="Sort by" variant='ts'>
<Dropdown.Item href="#/action-1">NISN</Dropdown.Item>
<Dropdown.Item href="#/action-2">Student Name</Dropdown.Item>
</DropdownButton>
</div>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th>Student Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>9075550101</td>
<td>Courtney Henry</td>
</tr>
<tr>
<td>2</td>
<td>4055550128</td>
<td>Eleanor Pena</td>
</tr>
<tr>
<td>3</td>
<td>2095550104</td>
<td>Annette Black</td>
</tr>
<tr>
<td>4</td>
<td>4065550120</td>
<td>Marvin McKinney</td>
</tr>
<tr>
<td>5</td>
<td>7045550127</td>
<td>Leslie Alexander</td>
</tr>
</tbody>
</Table>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default ExerciseDetail;

View File

@ -1,253 +0,0 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Table, Row, Col, Nav, Tab, Button, Form, InputGroup, OverlayTrigger, Tooltip, Spinner } from 'react-bootstrap';
import useExercises from '../hooks/useExercises';
import TablePaginate from '../../../../components/ui/TablePaginate';
const ManageExercises = () => {
const {
levels,
loading,
error,
page,
totalData,
totalPages,
setSearch,
handlePageChange,
handleLimitsChange,
handleSerachChange,
} = useExercises();
return (
<div className='admin-teachers'>
<h2 className='page-title strip'>Exercise</h2>
<p className='page-desc'>Description of Exercise.</p>
<Tab.Container id="left-tabs-example" defaultActiveKey="detail">
{/* <Row className='mb-45'>
<Col xs={12}>
<Nav variant="pills" className='col-tabs'>
<Nav.Item>
<Nav.Link eventKey="detail">View Entries</Nav.Link>
</Nav.Item>
<Nav.Item>
<OverlayTrigger overlay={<Tooltip id="tooltip-disabled">Select the level below </Tooltip>}>
<span className="d-inline-block">
<Nav.Link disabled onClick={(e)=>{e.preventDefault();}}>Create Data</Nav.Link>
</span>
</OverlayTrigger>
</Nav.Item>
</Nav>
</Col>
</Row> */}
<Row className='mb-45'>
<Col xs={12} className='col-tabs-content'>
<Tab.Content>
<Tab.Pane eventKey="detail">
<div className="cards">
<div className="cards-title">
<h4>Exercise List</h4>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSerachChange(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearch(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>Section</th>
<th>Topic</th>
<th className='text-center'>Level</th>
<th className='text-center'>Action</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={5} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
levels.length > 0 ?(
levels.map((level, index) => (
<tr key={level.ID_LEVEL}>
<td>{index + 1}</td>
<td>{level.NAME_SECTION}</td>
<td>{level.NAME_TOPIC}</td>
<td className='text-center'>{level.NAME_LEVEL}</td>
<td className='text-center action-col d-flex justify-content-center'>
<Link className='btn btn-sm btn-edit' to={`update-exercise/${level.ID_LEVEL}`}>
<i className="bi bi-pencil-square"></i>
</Link>
</td>
</tr>
))
):(
<tr>
<td colSpan={5} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
<div className="mt-2 w-100 d-flex justify-content-between align-items-center">
<div className='d-flex align-items-center'>
<small className="me-2">Item per page</small>
<Form.Select
size='sm'
className='py-0 px-1 me-2'
aria-label="Default select example"
defaultValue='7'
onChange={handleLimitsChange}
style={{ width: '50px' }}
>
<option value="7">7</option>
<option value="10">10</option>
<option value="20">20</option>
</Form.Select>
<small>of {totalData}</small>
</div>
<TablePaginate
totalPages={totalPages}
currentPage={page}
onPageChange={handlePageChange}
/>
</div>
</div>
</div>
</Tab.Pane>
<Tab.Pane eventKey="create">
<div className="cards">
<div className="cards-title">
<h4>Add Material Data</h4>
</div>
<div className="cards-body">
<Form>
<Row className='mb-2'>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Section<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-book"></i></InputGroup.Text>
<Form.Select aria-label="section select" defaultValue="" required>
<option value="" disabled hidden>Choose Section</option>
<option value="1">Grammar</option>
<option value="2">Listening</option>
<option value="3">Reading</option>
<option value="4">Vocabulary</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Topic<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-card-list"></i></InputGroup.Text>
<Form.Select aria-label="topic select" defaultValue="" required>
<option value="" disabled hidden>Choose Topic</option>
<option value="1">Talking about Self</option>
<option value="2">Congratulating & Complimenting Others</option>
<option value="3">Talking About Intentions</option>
<option value="4">Presenting Information</option>
<option value="5">Describing a Place</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Level<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-bar-chart"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Level</option>
<option value="0">Pretest</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
</Form.Select>
</InputGroup>
</Form.Group>
</Row>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Material Content<sup className='text-red fw-bold'>*</sup></Form.Label>
<Form.Control as="textarea" rows={5} className='mb-2' placeholder='Type the level description here...' />
</Form.Group>
</Row>
<Row className='mb-4'>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Audio File (optional)</Form.Label>
<InputGroup className="input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-music-note-beamed"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Audio"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Image File (optional)</Form.Label>
<InputGroup className="input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-image"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Image"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Video File (optional)</Form.Label>
<InputGroup className="input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-film"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Video"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 10 MB</small>
</Form.Group>
</Row>
<div className="d-flex justify-content-end">
<Button variant="outline-blue" type="reset" className='ms-auto py-2 rounded-35'>
reset
</Button>
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
Add
</Button>
</div>
</Form>
</div>
</div>
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
</div>
);
};
export default ManageExercises;

View File

@ -1,488 +0,0 @@
import React, { useState } from 'react';
import { Table, Row, Col, Nav, NavDropdown, Tab, Button, Form, InputGroup, Dropdown, DropdownButton, Accordion, Modal } from 'react-bootstrap';
import { NavLink } from 'react-router-dom';
const OldManageExercises = () => {
const [show, setShow] = useState(false);
const [viewData, setView] = useState(true);
const [dropDownTitle, setDropdownTitle] = useState('Create Data');
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const toggleViewData = () => setView(true);
const toggleCreateData = () => setView(false);
return (
<div className='admin-teachers'>
<h2 className='page-title strip'>Exercises</h2>
<p className='page-desc'>Description of Exercises.</p>
<Tab.Container id="left-tabs-example" defaultActiveKey="detail">
<Row className='mb-45'>
<Col xs={12}>
<div className='w-100 col-tabs-parent'>
<Nav variant="pills" className={`col-tabs ${viewData ? `border-0` : `border-bottom`}`}>
<Nav.Item>
<Nav.Link eventKey="detail" onClick={toggleViewData}>View Entries</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="create" onClick={toggleCreateData}>Create Data</Nav.Link>
</Nav.Item>
<NavDropdown title={dropDownTitle} id="nav-dropdown" className={`ms-2 ${!viewData ? `active` : ``}`}>
<NavDropdown.Item eventKey="create-multiple" onClick={() => { toggleCreateData(); setDropdownTitle('Multiple Choice'); }}>
Create Multiple Choice
</NavDropdown.Item>
<NavDropdown.Item eventKey="create-truefalse" onClick={() => { toggleCreateData(); setDropdownTitle('True / False'); }}>
Create True False
</NavDropdown.Item>
<NavDropdown.Item eventKey="create-matching" onClick={() => { toggleCreateData(); setDropdownTitle('Matching Pairs'); }}>
Create Matching Pairs
</NavDropdown.Item>
</NavDropdown>
</Nav>
<Form className={`form-selector ${viewData ? `d-none` : `d-block`}`}>
<Row>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Section<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-book"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Section</option>
<option value="1">Grammar</option>
<option value="2">Listening</option>
<option value="3">Reading</option>
<option value="4">Vocabulary</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Topic<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-card-list"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Topic</option>
<option value="1">Talking about Self</option>
<option value="2">Congratulating & Complimenting Others</option>
<option value="3">Talking About Intentions</option>
<option value="4">Presenting Information</option>
<option value="5">Describing a Place</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Level<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-bar-chart"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Level</option>
<option value="0">Pretest</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
</Form.Select>
</InputGroup>
</Form.Group>
</Row>
</Form>
</div>
</Col>
</Row>
<Row className='mb-45'>
<Col xs={12} className='col-tabs-content'>
<Tab.Content>
<Tab.Pane eventKey="detail">
<div className="cards">
<div className="cards-title">
<h4>Exercise List</h4>
</div>
<div className="cards-body">
<div className="d-flex">
<Form.Control
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search'
/>
<DropdownButton title="Sort by" variant='ts'>
<Dropdown.Item href="#/action-1">Section</Dropdown.Item>
<Dropdown.Item href="#/action-2">Topic</Dropdown.Item>
<Dropdown.Item href="#/action-3">Level</Dropdown.Item>
</DropdownButton>
</div>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>Section</th>
<th>Topic</th>
<th className='text-center'>Level</th>
<th className='text-center'>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>Pretest</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>2</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>1</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>3</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>2</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>4</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>3</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>5</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>4</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>6</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>5</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
<tr>
<td>7</td>
<td>Listening</td>
<td>Talking about Self</td>
<td className='text-center'>6</td>
<td className='text-center action-col'>
<NavLink className='btn btn-sm btn-view' to='exercise-detail'><i className="bi bi-eye"></i></NavLink>
<Button size='sm' className='btn-edit' onClick={handleShow}><i className="bi bi-pencil-square"></i></Button>
<Button size='sm' className='btn-delete'><i className="bi bi-trash3"></i></Button>
</td>
</tr>
</tbody>
</Table>
</div>
</div>
</Tab.Pane>
<Tab.Pane eventKey="create-multiple">
<Accordion defaultActiveKey="0" alwaysOpen className='cards mb-45'>
<Accordion.Item eventKey="0">
<Accordion.Header className='cards-title'>Question 1</Accordion.Header>
<Accordion.Body className='cards-body'>
<Form>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Question<sup className='text-red fw-bold'>*</sup></Form.Label>
<Form.Control as="textarea" rows={3} className='mb-2' placeholder='Type the Question here...' />
</Form.Group>
</Row>
<Row className='mb-2'>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Audio File (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-music-note-beamed"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Audio"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Image File (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-image"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Image"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>URL Video (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-film"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Video"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
</Form.Group>
</Row>
<Row>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option A<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Option A"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option B<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Option B"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option C<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Option C"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option D<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Option D"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option E (Optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Option E"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-6 col-sm-3'>
<Form.Label>Answer Key<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-key"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Answer Key</option>
<option value="0">A</option>
<option value="1">B</option>
<option value="2">C</option>
<option value="3">D</option>
<option value="4">E</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-6 col-sm-3'>
<Form.Label>Weight<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-lightbulb"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Weight"
/>
</InputGroup>
</Form.Group>
</Row>
<div className="d-flex justify-content-end">
<Button variant="outline-blue" type="reset" className='ms-auto py-2 rounded-35'>
reset
</Button>
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
Add
</Button>
</div>
</Form>
</Accordion.Body>
</Accordion.Item>
</Accordion>
</Tab.Pane>
<Tab.Pane eventKey="create-truefalse">
<Accordion defaultActiveKey="0" alwaysOpen className='cards mb-45'>
<Accordion.Item eventKey="0">
<Accordion.Header className='cards-title'>Question 1</Accordion.Header>
<Accordion.Body className='cards-body'>
<Form>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Question<sup className='text-red fw-bold'>*</sup></Form.Label>
<Form.Control as="textarea" rows={3} className='mb-2' placeholder='Type the Question here...' />
</Form.Group>
</Row>
<Row className='mb-2'>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Audio File (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-music-note-beamed"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Audio"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Image File (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-image"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Image"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>URL Video (optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-film"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Video"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
</Form.Group>
</Row>
<Row>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Answer Key<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-key"></i></InputGroup.Text>
<Form.Select aria-label="teacher select" defaultValue="" required>
<option value="" disabled hidden>Choose Answer Key</option>
<option value="0">true</option>
<option value="1">false</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Weight<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-lightbulb"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Weight"
/>
</InputGroup>
</Form.Group>
</Row>
<div className="d-flex justify-content-end">
<Button variant="outline-blue" type="reset" className='ms-auto py-2 rounded-35'>
reset
</Button>
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
Add
</Button>
</div>
</Form>
</Accordion.Body>
</Accordion.Item>
</Accordion>
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
<Modal show={show} onHide={handleClose} className='modal-admin' size='lg' centered>
<Modal.Header closeButton>
<Modal.Title>Update Teacher Data</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Section Name<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-book"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Section Name"
aria-label="NIP"
aria-describedby="basic-addon1"
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} controlId="formGridPassword">
<Form.Label>Thumbnail<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1" className='border-dashed'><i className="bi bi-cloud-arrow-up"></i></InputGroup.Text>
<Form.Control
placeholder="Choose Image"
aria-label="fullname"
aria-describedby="basic-addon1"
className='border-dashed'
/>
</InputGroup>
<small>Ensure the file size is no larger than 5 MB</small>
</Form.Group>
</Row>
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Section Description<sup className='text-red fw-bold'>*</sup></Form.Label>
<Form.Control as="textarea" rows={3} placeholder='Type the section description here...' />
</Form.Group>
</Row>
<div className="d-flex justify-content-end">
<Button variant="blue" type="submit" className='py-2 px-5 w-100 rounded-35'>
Update
</Button>
</div>
</Form>
</Modal.Body>
</Modal>
</div>
);
};
export default OldManageExercises;

View File

@ -1,761 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { Table, Row, Col, Button, Form, Modal, InputGroup, Accordion, Breadcrumb, Spinner } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom';
import useUpdateExercises from '../hooks/useUpdateExercises';
import ModalOperation from '../../../../components/ui/adminMessageModal/ModalOperation';
import {ReactSortable } from 'react-sortablejs';
import VideoPlayer from './components/VideoPlayer';
const mpColors = ['#FC6454', '#FBD025', '#46E59A', '#0090FF','#E355D5'];
const UpdateExercises = () => {
const { levelId } = useParams();
const {
loading,
error,
exerciseData,
setExerciseData,
formData,
levelName,
sectionName,
topicName,
mediaPath,
mediaPreview,
handleFormChange,
handleFormChangeAnswer,
handleFormChangeMedia,
handleFormResetMedia,
updateMaterials,
showLoader,
handleCloseLoader,
loaderState,
createQuestion,
updateQuestion,
deleteQuestion,
sortingQuestion,
selectedQuestion,
selectedQuestionNumber,
show,
handleShow,
handleClose,
handleShowNewData,
validUrl,
handleCheckUrl,
handleUrlInput,
showUrlModal,
handleShowUrlModal,
handleCloseUrlModal,
handleSetVideoUrl
} = useUpdateExercises(levelId);
const triggerFileInput = (e) => {
e.target.previousElementSibling.click();
};
return (
<div className='admin-teachers'>
<Row className='mb-45'>
<Col className="d-flex align-items-center breadcrumb-con">
<Button as={Link} className='btn btn-blue btn-square-back' to='/admin/material'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="#">Learning</Breadcrumb.Item>
<Breadcrumb.Item href="/admin/exercise" className='text-capitalize'>Exercise</Breadcrumb.Item>
<Breadcrumb.Item active>Update Exercise</Breadcrumb.Item>
</Breadcrumb>
</Col>
</Row>
<Row className='mb-0'>
<Col>
<div className="cards rounded-top combine combine-top">
<div className="cards-title d-flex">
<h4>{sectionName} : {topicName} - <span className='text-blue'>{levelName}</span></h4>
</div>
</div>
</Col>
</Row>
<Row className='mb-45'>
<Col className='h-fit'>
{loading?(
<div className='w-100 d-flex justify-content-center align-items-center' style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</div>
):(
<>
<Col className='mb-3 sticky-top' style={{top:'-25px'}}>
<div className="cards shadow-sm combine combine-bottom">
<div className="cards-title border-0 d-flex justify-content-between align-items-center">
<h4 className='mb-0'>{exerciseData.length} Question</h4>
<Button size='sm' variant='blue' className='px-5 rounded-3' onClick={sortingQuestion}>Save</Button>
</div>
</div>
</Col>
<ReactSortable
list={exerciseData}
setList={setExerciseData}
handle=".handle"
>
{exerciseData.map((data, index) => (
<div className="cards cards-exercise mb-3" key={data.ID_ADMIN_EXERCISE}>
<div className="cards-title d-flex justify-content-between">
<div className="d-flex align-items-center">
<span style={{ cursor: 'grab' }} className='handle'><i className="bi bi-list me-3" /></span>
<h4>{index+1}</h4>
<h4>. {data.QUESTION_TYPE}</h4>
</div>
<div className="d-flex align-items-center">
<h4 className='mb-0 me-3'>{data.SCORE_WEIGHT} point</h4>
<Button size='sm' variant='outline-warning' className='me-2 border' onClick={() => handleShow(index)}><i className="bi bi-pencil me-1"></i>edit</Button>
<Button size='sm' variant='outline-danger' className='border' onClick={() => deleteQuestion(index)}><i className="bi bi-trash3"></i></Button>
</div>
</div>
<div className="cards-body">
<Row>
<Col className={`d-flex ${data.AUDIO?"flex-column":"align-items-center"}`}>
{data.IMAGE?(
<img src={`${mediaPath}/image/${data.IMAGE}`} alt=""
className='me-2 bg-light cursor-pointer'
style={{aspectRatio:"3/2",width:"10vw",objectPosition:"center",objectFit:"contain"}}
onClick={() => handleShow(index)}
/>
):(
data.AUDIO?(
<audio controls style={{height:"30px", marginBottom:"8px"}}
src={`${mediaPath}/audio/${data.AUDIO}`}
>
</audio>
):(
data.VIDEO?(
<div className='cursor-pointer' onClick={() => handleShow(index)}>
<VideoPlayer url={data.VIDEO} con="main"/>
</div>
):(
<></>
)
)
)}
<p className='m-0 fs-14p'>{data.QUESTION}</p>
</Col>
</Row>
<span className='fs-12p'>Answer Key :</span>
{data.QUESTION_TYPE === "TFQ"?(
<Row>
<Col md={6} className='d-flex align-items-center'>
{data.trueFalse[0].IS_TRUE === 1 || data.trueFalse[0].IS_TRUE === "1" ?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>True</span>
</Col>
<Col md={6} className='d-flex align-items-center'>
{data.trueFalse[0].IS_TRUE === 0 || data.trueFalse[0].IS_TRUE === "0" ?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>False</span>
</Col>
</Row>
):(
data.QUESTION_TYPE === "MCQ"?(
<Row>
<Col md={6} className='d-flex align-items-center'>
{data.multipleChoices[0].ANSWER_KEY === "A"?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>
{data.multipleChoices[0].OPTION_A}
</span>
</Col>
<Col md={6} className='d-flex align-items-center'>
{data.multipleChoices[0].ANSWER_KEY === "B"?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>
{data.multipleChoices[0].OPTION_B}
</span>
</Col>
<Col md={6} className='d-flex align-items-center'>
{data.multipleChoices[0].ANSWER_KEY === "C"?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>
{data.multipleChoices[0].OPTION_C}
</span>
</Col>
<Col md={6} className='d-flex align-items-center'>
{data.multipleChoices[0].ANSWER_KEY === "D"?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>
{data.multipleChoices[0].OPTION_D}
</span>
</Col>
{data.multipleChoices[0].OPTION_E?(
<Col md={6} className='d-flex align-items-center'>
{data.multipleChoices[0].ANSWER_KEY === "E"?(
<i className="bi bi-check text-success fs-5 me-1"></i>
):(
<i className="bi bi-x text-red fs-5 me-1"></i>
)}
<span className='fs-14p'>
{data.multipleChoices[0].OPTION_E}
</span>
</Col>
):(
<></>
)}
</Row>
):(
<div className='mt-1'>
{data.matchingPairs.map((dataMp, index) => (
<Row key={index}>
<Col md={6} className='px-5 d-flex align-items-center'>
<div className='mb-2 py-1 px-3 w-100 rounded-pill' style={{backgroundColor: mpColors[index]}}>
<span className='fs-14p text-white'>{dataMp.LEFT_PAIR}</span>
</div>
</Col>
<Col md={6} className='px-5 d-flex align-items-center'>
<div className='mb-2 py-1 px-3 w-100 rounded-pill' style={{backgroundColor: mpColors[index]}}>
<span className='fs-14p text-white'>{dataMp.RIGHT_PAIR}</span>
</div>
</Col>
</Row>
))}
</div>
)
)}
</div>
</div>
))}
</ReactSortable>
<div className='w-100 mt-4 p-3 bg-white rounded-4'>
<Button variant='blue' className='py-2 px-5 w-100 rounded-35' onClick={() => handleShowNewData()}>
+ add question
</Button>
</div>
</>
)}
</Col>
</Row>
<Modal show={show} onHide={handleClose} className='modal-admin' scrollable fullscreen="lg-down" size='xl' centered backdrop="static">
<Modal.Header closeButton>
<Modal.Title>Question Editor</Modal.Title>
</Modal.Header>
<Modal.Body className='pt-0'>
<Form onSubmit={(e) => { e.preventDefault(); selectedQuestion?updateQuestion():createQuestion(); }}>
<div className="sticky-top py-2 px-3 mb-3 rounded-bottom shadow-sm bg-white d-flex justify-content-between align-items-center">
<h4 className='m-0'>{selectedQuestionNumber ? `Question ${selectedQuestionNumber}` : "New Question"}</h4>
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
Save
</Button>
</div>
{selectedQuestion?(
<></>
):(
<Row className='mb-2'>
<Form.Group as={Col} className='mb-2 col-6 col-lg-3'>
<Form.Label>Question Type<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-braces"></i></InputGroup.Text>
<Form.Select aria-label="teacher select"
required
name='QUESTION_TYPE'
defaultValue={formData.QUESTION_TYPE}
onChange={handleFormChange}
>
<option value="" disabled hidden>Question Type</option>
<option value="MCQ">Multiple Choice</option>
<option value="TFQ">True / False</option>
<option value="MPQ">Matching Pairs</option>
</Form.Select>
</InputGroup>
</Form.Group>
</Row>
)}
<Row className="mb-2">
<Form.Group as={Col} controlId="formGridEmail">
<Form.Label>Question<sup className='text-red fw-bold'>*</sup></Form.Label>
<Form.Control
required
as="textarea"
rows={3}
className='mb-2'
placeholder='Type the Question here...'
name='QUESTION'
value={formData.QUESTION || ''}
onChange={handleFormChange}
/>
</Form.Group>
</Row>
<Row className='mb-2'>
<Col xs={12}>
<Form.Label>Media (optional)</Form.Label>
{formData.IMAGE?(
<div className='mb-2 w-100 rounded-3 d-flex flex-column justify-content-start align-items-center' style={{border: "2px dashed #dee2e6"}}>
<Button variant='red' size='sm' className='ms-auto rounded-3'
onClick={()=>{handleFormResetMedia();}}
style={{marginBottom:"-3%"}}
>
<i className="bi bi-trash3"></i>
</Button>
{mediaPreview?(
<img src={mediaPreview} alt="" className='my-2 bg-light' style={{aspectRatio:"3/2",height:"35vh",width:"auto",objectPosition:"center",objectFit:"contain"}}/>
):(
<img src={`${mediaPath}/image/${formData.IMAGE}`} alt="" className='my-2 bg-light' style={{aspectRatio:"3/2",height:"35vh",width:"auto",objectPosition:"center",objectFit:"contain"}}/>
)}
</div>
):(
formData.AUDIO?(
<div className='mb-2 w-100 rounded-3 d-flex flex-column justify-content-start align-items-center' style={{border: "2px dashed #dee2e6"}}>
<Button variant='red' size='sm' className='ms-auto rounded-3'
onClick={()=>{handleFormResetMedia();}}
style={{marginBottom:"-3%"}}
>
<i className="bi bi-trash3"></i>
</Button>
{mediaPreview?(
<audio controls className='my-2'
src={mediaPreview}
>
</audio>
):(
<audio controls className='my-2'
src={`${mediaPath}/audio/${formData.AUDIO}`}
>
</audio>
)}
</div>
):(
formData.VIDEO?(
<div className='mb-2 w-100 rounded-3 d-flex flex-column justify-content-start align-items-center' style={{border: "2px dashed #dee2e6"}}>
<Button variant='red' size='sm' className='ms-auto rounded-3'
onClick={()=>{handleFormResetMedia();}}
style={{marginBottom:"-3%"}}
>
<i className="bi bi-trash3"></i>
</Button>
<VideoPlayer url={formData.VIDEO} con="modal" />
</div>
):(
<div className='mb-2 w-100 rounded-3 d-flex justify-content-around align-items-center' style={{border: "2px dashed #dee2e6"}}>
<div className='py-2 h-100 d-flex align-items-center'>
<Form.Control type='file' accept="audio/*"
placeholder="Choose Audio"
aria-label="fullname"
aria-describedby="basic-addon1"
className='d-none'
name='AUDIO'
onChange={handleFormChangeMedia}
/>
<Button variant='light'
style={{aspectRatio:"4/2",width:"auto", height:"16vh"}}
onClick={(e) => {e.preventDefault();triggerFileInput(e)}}
>
<i className="fs-1 text-secondary bi bi-music-note-beamed"></i>
</Button>
</div>
<div className='py-2 h-100 d-flex align-items-center'>
<Form.Control type='file' accept="image/*"
placeholder="Choose Image"
aria-label="fullname"
aria-describedby="basic-addon1"
className='d-none'
name='IMAGE'
onChange={handleFormChangeMedia}
/>
<Button variant='light'
style={{aspectRatio:"4/2",width:"auto", height:"16vh"}}
onClick={(e) => {e.preventDefault();triggerFileInput(e)}}
>
<i className="fs-1 text-secondary bi bi-image"></i>
</Button>
</div>
<div className='py-2 h-100 d-flex align-items-center'>
<Button variant='light'
style={{aspectRatio:"4/2",width:"auto", height:"16vh"}}
onClick={handleShowUrlModal}
>
<i className="fs-1 text-secondary bi bi-youtube"></i>
</Button>
</div>
</div>
)
)
)}
</Col>
</Row>
{formData.QUESTION_TYPE=== "TFQ"?(
<Row>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Answer Key<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-key"></i></InputGroup.Text>
<Form.Select
required
name='IS_TRUE'
defaultValue={`${selectedQuestion ? formData.trueFalse[0].IS_TRUE : ""}`}
onChange={handleFormChangeAnswer}
>
<option value="" disabled hidden>Choose Answer Key</option>
<option value="0">false</option>
<option value="1">true</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Weight<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-lightbulb"></i></InputGroup.Text>
<Form.Control type='number' onWheel={() => document.activeElement.blur()} placeholder="Enter Weight"
required
min={0}
name='SCORE_WEIGHT'
defaultValue={formData.SCORE_WEIGHT || ''}
onChange={handleFormChange}
/>
</InputGroup>
</Form.Group>
</Row>
):(
formData.QUESTION_TYPE=== "MCQ"?(
<Row>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option A<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control placeholder="Enter Option A"
required
name='OPTION_A'
defaultValue={formData.multipleChoices[0]?.OPTION_A ? formData.multipleChoices[0].OPTION_A || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option B<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control placeholder="Enter Option B"
required
name='OPTION_B'
defaultValue={formData.multipleChoices[0]?.OPTION_B ? formData.multipleChoices[0].OPTION_B || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option C<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control placeholder="Enter Option C"
required
name='OPTION_C'
defaultValue={formData.multipleChoices[0]?.OPTION_C ? formData.multipleChoices[0].OPTION_C || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option D<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control placeholder="Enter Option D"
required
name='OPTION_D'
defaultValue={formData.multipleChoices[0]?.OPTION_D ? formData.multipleChoices[0].OPTION_D || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Option E (Optional)</Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-question-circle"></i></InputGroup.Text>
<Form.Control placeholder="Enter Option E"
name='OPTION_E'
defaultValue={formData.multipleChoices[0]?.OPTION_E ? formData.multipleChoices[0].OPTION_E || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Col className='col-6 d-none d-sm-block d-lg-none'></Col>
<Form.Group as={Col} className='mb-2 col-6 col-lg-3'>
<Form.Label>Answer Key<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text id="basic-addon1"><i className="bi bi-key"></i></InputGroup.Text>
<Form.Select
required
name='ANSWER_KEY'
defaultValue={formData.multipleChoices[0]?.ANSWER_KEY ? formData.multipleChoices[0].ANSWER_KEY || "" : ""}
onChange={handleFormChangeAnswer}
>
<option value="" disabled hidden>Choose Answer Key</option>
<option value="A"
hidden={formData.multipleChoices[0]?.OPTION_A ? false : true}
>
A
</option>
<option value="B"
hidden={formData.multipleChoices[0]?.OPTION_B ? false : true}
>
B
</option>
<option value="C"
hidden={formData.multipleChoices[0]?.OPTION_C ? false : true}
>
C
</option>
<option value="D"
hidden={formData.multipleChoices[0]?.OPTION_D ? false : true}
>
D
</option>
<option value="E"
hidden={formData.multipleChoices[0]?.OPTION_E ? false : true}
>
E
</option>
<option value="abced"
disabled
hidden={formData.multipleChoices[0] ? true : false}
>
Fill Option First
</option>
</Form.Select>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-6 col-lg-3'>
<Form.Label>Weight<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-lightbulb"></i></InputGroup.Text>
<Form.Control type='number' onWheel={() => document.activeElement.blur()} placeholder="Enter Weight"
required
min={0}
name='SCORE_WEIGHT'
defaultValue={formData.SCORE_WEIGHT || ''}
onChange={handleFormChange}
/>
</InputGroup>
</Form.Group>
</Row>
):(
formData.QUESTION_TYPE=== "MPQ"?(
<Row>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Left Pair<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[0]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Left Pair 1"
name='0_LEFT_PAIR'
defaultValue={formData.matchingPairs[0]?.LEFT_PAIR ? formData.matchingPairs[0].LEFT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Right Pair<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[0]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Right Pair 1"
name='0_RIGHT_PAIR'
defaultValue={formData.matchingPairs[0]?.RIGHT_PAIR ? formData.matchingPairs[0].RIGHT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[1]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Left Pair 2"
name='1_LEFT_PAIR'
defaultValue={formData.matchingPairs[1]?.LEFT_PAIR ? formData.matchingPairs[1].LEFT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[1]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Right Pair 2"
name='1_RIGHT_PAIR'
defaultValue={formData.matchingPairs[1]?.RIGHT_PAIR ? formData.matchingPairs[1].RIGHT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[2]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Left Pair 3"
name='2_LEFT_PAIR'
defaultValue={formData.matchingPairs[2]?.LEFT_PAIR ? formData.matchingPairs[2].LEFT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[2]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Right Pair 3"
name='2_RIGHT_PAIR'
defaultValue={formData.matchingPairs[2]?.RIGHT_PAIR ? formData.matchingPairs[2].RIGHT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[3]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Left Pair 4"
name='3_LEFT_PAIR'
defaultValue={formData.matchingPairs[3]?.LEFT_PAIR ? formData.matchingPairs[3].LEFT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[3]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Right Pair 4"
name='3_RIGHT_PAIR'
defaultValue={formData.matchingPairs[3]?.RIGHT_PAIR ? formData.matchingPairs[3].RIGHT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[4]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Left Pair 5"
name='4_LEFT_PAIR'
defaultValue={formData.matchingPairs[4]?.LEFT_PAIR ? formData.matchingPairs[4].LEFT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text style={{backgroundColor: mpColors[4]}}><i className="text-white bi bi-question-circle"></i></InputGroup.Text>
<Form.Control
placeholder="Enter Right Pair 5"
name='4_RIGHT_PAIR'
defaultValue={formData.matchingPairs[4]?.RIGHT_PAIR ? formData.matchingPairs[4].RIGHT_PAIR || '' : ''}
onChange={handleFormChangeAnswer}
/>
</InputGroup>
</Form.Group>
<Form.Group as={Col} className='mb-2 col-12 col-sm-6'>
<Form.Label>Weight<sup className='text-red fw-bold'>*</sup></Form.Label>
<InputGroup className="mb-2 input-group-icon">
<InputGroup.Text><i className="bi bi-lightbulb"></i></InputGroup.Text>
<Form.Control type='number' onWheel={() => document.activeElement.blur()}
required
min={0}
placeholder="Enter Weight"
name='SCORE_WEIGHT'
value={formData.SCORE_WEIGHT || ''}
onChange={handleFormChange}
/>
</InputGroup>
</Form.Group>
</Row>
):(
<>
<Form.Label>Answer Key<sup className='text-red fw-bold'>*</sup></Form.Label>
<h3 className='text-center my-3'>Select Question Type</h3>
</>
)
)
)}
</Form>
</Modal.Body>
</Modal>
<Modal show={showUrlModal} className='modal-admin' onHide={handleCloseUrlModal} centered style={{backgroundColor:"#00000099"}}>
<Modal.Header closeButton>
Video URL
</Modal.Header>
<Modal.Body>
<InputGroup>
<Form.Control
placeholder="Enter Video Url"
name='VIDEO'
onChange={handleUrlInput}
/>
{validUrl === "valid"?(
<Button variant="outline-success" className={`${validUrl === "valid" ? "d-block":"d-none"}`} onClick={handleSetVideoUrl}>
Save
</Button>
):(
<Button variant="outline-secondary" className={`${validUrl === "valid" ? "d-none":"d-block"}`} onClick={handleCheckUrl}>
Check
</Button>
)}
</InputGroup>
<small className={`ms-1 ${validUrl === "invalid" ? "text-red" : validUrl === "valid" ? "text-success" : ""}`}>
{validUrl === "invalid" ? "This URL is invalid." : validUrl === "valid" ? "This URL is valid" : "The URL will be checked for validity before use."}
</small>
</Modal.Body>
</Modal>
<ModalOperation
show={showLoader}
handleClose={handleCloseLoader}
title={loaderState.title}
description={loaderState.description}
loading={loaderState.loading}
successMessage={loaderState.successMessage}
confirmAction={loaderState.confirmAction}
handleConfirm={loaderState.handleConfirm}
/>
</div>
);
};
export default UpdateExercises;

View File

@ -1,81 +0,0 @@
import React from "react";
function VideoPlayer({ url, con }) {
const getVideoData = (url) => {
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const googleDriveRegex = /https?:\/\/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)\/(?:view|preview)/;
if (youtubeRegex.test(url)) {
const videoId = url.match(youtubeRegex)[1];
return {
source: "YouTube",
embedUrl: `https://www.youtube.com/embed/${videoId}`,
thumbnailUrl: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
};
} else if (googleDriveRegex.test(url)) {
const fileId = url.match(googleDriveRegex)[1];
return {
source: "Google Drive",
embedUrl: `https://drive.google.com/file/d/${fileId}/preview`,
// thumbnailUrl: `https://drive.google.com/thumbnail?id=${fileId}`,
thumbnailUrl: `https://lh3.googleusercontent.com/d/${fileId}=s220?authuser=0`
};
} else {
return { source: "Unknown", embedUrl: null, thumbnailUrl: null };
}
};
const videoData = getVideoData(url);
if (videoData.source === "Unknown") {
return <p className="text-red">invalid URL.</p>;
}
return (
con === 'main'?(
<div className="me-2"
style={{
aspectRatio:"3/2",
width:"10vw",
position: "relative",
display: "flex",
justifyContent:"center",
alignItems:"center",
backgroundColor:"#000000",
overflow:"hidden",
}}
>
<img
src={videoData.thumbnailUrl}
alt={`${videoData.source} video thumbnail`}
style={{ width: "100%", height:"100%", objectFit:"cover", display:"block", opacity:"0.6" }}
/>
<div style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
{videoData.source === "YouTube" ? <i className="bi bi-youtube fs-1 text-white"></i> : <i className="bi bi-google fs-2 text-white"></i>}
</div>
</div>
):(
<div style={{width:"30vw", height:"fit-content"}}>
<iframe
src={videoData.embedUrl}
title={`${videoData.source} video player`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{
height:"auto"
}}
className="w-100 bg-secondary"
></iframe>
</div>
)
);
}
export default VideoPlayer;

View File

@ -1,65 +0,0 @@
import React from "react";
function VideoPlayer({ url }) {
const getVideoData = (url) => {
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const googleDriveRegex = /https?:\/\/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)\/(?:view|preview)/;
if (youtubeRegex.test(url)) {
const videoId = url.match(youtubeRegex)[1];
return {
source: "YouTube",
thumbnailUrl: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
videoLink: `https://www.youtube.com/watch?v=${videoId}`,
};
} else if (googleDriveRegex.test(url)) {
const fileId = url.match(googleDriveRegex)[1];
return {
source: "Google Drive",
thumbnailUrl: `https://drive.google.com/thumbnail?id=${fileId}`,
videoLink: `https://drive.google.com/file/d/${fileId}/view`,
};
} else {
return { source: "Unknown", thumbnailUrl: null, videoLink: null };
}
};
const videoData = getVideoData(url);
if (videoData.source === "Unknown") {
return <p>URL video tidak valid. Harap masukkan URL YouTube atau Google Drive yang benar.</p>;
}
return (
<div className="video-thumbnail me-2"
style={{
aspectRatio:"3/2",
width:"10vw",
position: "relative",
display: "flex",
justifyContent:"center",
alignItems:"center",
backgroundColor:"#000000",
overflow:"hidden",
}}
>
<a href={videoData.videoLink} target="_blank" rel="noopener noreferrer">
<img
src={videoData.thumbnailUrl}
alt={`${videoData.source} video thumbnail`}
style={{ width: "100%", height:"auto", display:"block", opacity:"0.6" }}
/>
<div style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
{videoData.source === "YouTube" ? <i className="bi bi-youtube fs-1 text-white"></i> : <i className="bi bi-google fs-2 text-white"></i>}
</div>
</a>
</div>
);
}
export default VideoPlayer;

View File

@ -1,415 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import materialService from '../services/serviceMaterials';
import { ButtonView } from 'ckeditor5';
import { MEDIA_URL } from '../../../../utils/Constant';
const useTest = (materialId) => {
const [editorData, setEditorData] = useState('');
const [levelName, setLevelName] = useState('Level');
const [sectionName, setSectionName] = useState('section');
const [topicName, setTopicName] = useState('topic');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const [isLayoutReady, setIsLayoutReady] = useState(false);
const mediaPath = `${MEDIA_URL}/level/`;
const [show, setShow] = useState(false);
const [showLoader, setShowLoader] = useState(false);
const [loaderState, setLoaderState] = useState({ loading: false, successMessage: '', title: '', description: '', confirmAction: false });
const handleCloseLoader = () => setShowLoader(false);
const handleShowLoader = (title, description, loading = false, successMessage = '', confirmAction = false) => {
setLoaderState({ title, description, loading, successMessage, confirmAction });
setShowLoader(true);
};
const handleEditorChange = (event, editor) => {
const data = editor.getData();
setEditorData(data);
};
const mediaUpload = async (type, file) => {
setShow(true);
const mediaData = new FormData();
mediaData.append(`${type}[0]`, file);
try {
const upload = await materialService.uploadMedia(materialId, mediaData);
const fileName = upload.payload[`${type}[0]`];
const url = `${mediaPath}${type.toLowerCase()}/${fileName}`
setShow(false);
return url;
} catch (err) {
return err;
}
};
class MyUploadAdapter {
constructor(loader, apiUrl) {
this.loader = loader;
this.apiUrl = apiUrl;
}
upload() {
return this.loader.file
.then(file => new Promise(async (resolve, reject) => {
try {
const imageUrl = await mediaUpload('IMAGE', file);
resolve({
default: imageUrl
});
} catch (error) {
reject(error.message);
}
}));
}
// Handle abort saat upload
abort() {
// Implementasikan jika ingin support untuk membatalkan upload
}
}
function CustomUploadAdapterPlugin (editor) {
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new MyUploadAdapter(loader, 'https://');
};
}
// function CustomMediaPlugin (editor) {
// // Plugin untuk Audio
// editor.ui.componentFactory.add('audioUpload', locale => {
// const view = new ButtonView(locale);
// const audioIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-music" viewBox="0 0 16 16"><path d="M11 6.64a1 1 0 0 0-1.243-.97l-1 .25A1 1 0 0 0 8 6.89v4.306A2.6 2.6 0 0 0 7 11c-.5 0-.974.134-1.338.377-.36.24-.662.628-.662 1.123s.301.883.662 1.123c.364.243.839.377 1.338.377s.974-.134 1.338-.377c.36-.24.662-.628.662-1.123V8.89l2-.5z"/><path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/></svg>`
// view.set({
// label: '',
// icon: audioIcon,
// withText: false,
// tooltip: 'Upload Audio'
// });
// view.on('execute', () => {
// const input = document.createElement('input');
// input.type = 'file';
// input.accept = 'audio/*';
// input.onchange = async () => {
// const file = input.files[0];
// if (file) {
// try {
// const audioUrl = await mediaUpload('AUDIO', file);
// editor.model.change(writer => {
// const audioElement = writer.createElement('audio', {
// src: audioUrl
// });
// editor.model.insertContent(audioElement, editor.model.document.selection);
// });
// } catch (error) {
// console.error('Error uploading audio:', error);
// }
// }
// };
// input.click();
// });
// return view;
// });
// // Plugin untuk Image
// editor.ui.componentFactory.add('imageUpload', locale => {
// const view = new ButtonView(locale);
// view.set({
// label: 'image',
// withText: true,
// tooltip: 'Upload Image'
// });
// view.on('execute', () => {
// const input = document.createElement('input');
// input.type = 'file';
// input.accept = 'image/*'; // Menerima file gambar
// input.onchange = async () => {
// const file = input.files[0];
// if (file) {
// try {
// const imageUrl = await mediaUpload('IMAGE', file);
// editor.model.change(writer => {
// const imageElement = writer.createElement('image', {
// src: imageUrl
// });
// editor.model.insertContent(imageElement, editor.model.document.selection);
// });
// } catch (error) {
// console.error('Error uploading image:', error);
// }
// }
// };
// input.click();
// });
// return view;
// });
// // Definisi schema
// editor.model.schema.register('audio', {
// allowWhere: '$block',
// allowAttributes: ['src'],
// isObject: true,
// isBlock: true
// });
// editor.model.schema.register('image', {
// allowWhere: '$block',
// allowAttributes: ['src'],
// isObject: true,
// isBlock: true
// });
// // Konversi untuk audio
// editor.conversion.for('upcast').elementToElement({
// model: 'audio',
// view: {
// name: 'audio',
// attributes: {
// src: true
// }
// }
// });
// editor.conversion.for('dataDowncast').elementToElement({
// model: 'audio',
// view: (modelElement, { writer }) => {
// const audioElement = writer.createEmptyElement('audio', {
// controls: 'controls',
// src: modelElement.getAttribute('src')
// });
// return audioElement;
// }
// });
// editor.conversion.for('editingDowncast').elementToElement({
// model: 'audio',
// view: (modelElement, { writer }) => {
// const audioElement = writer.createEmptyElement('audio', {
// controls: 'controls',
// src: modelElement.getAttribute('src')
// });
// return audioElement;
// }
// });
// // Konversi untuk image
// editor.conversion.for('upcast').elementToElement({
// model: 'image',
// view: {
// name: 'img',
// attributes: {
// src: true
// }
// }
// });
// editor.conversion.for('dataDowncast').elementToElement({
// model: 'image',
// view: (modelElement, { writer }) => {
// const imageElement = writer.createEmptyElement('img', {
// src: modelElement.getAttribute('src')
// });
// return imageElement;
// }
// });
// editor.conversion.for('editingDowncast').elementToElement({
// model: 'image',
// view: (modelElement, { writer }) => {
// const imageElement = writer.createEmptyElement('img', {
// src: modelElement.getAttribute('src')
// });
// return imageElement;
// }
// });
// }
function CustomMediaPlugin(editor) {
// Plugin untuk Image
editor.ui.componentFactory.add('imageUpload', locale => {
const view = new ButtonView(locale);
view.set({
label: 'image',
withText: true,
tooltip: 'Upload Image'
});
view.on('execute', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async () => {
const file = input.files[0];
if (file) {
try {
const imageUrl = await mediaUpload('IMAGE', file); // Upload image
editor.model.change(writer => {
const imageHtml = `<img src="${imageUrl}" alt="Image" style="width:200px;height:200px;" />`;
editor.model.insertContent(writer.createRawElement('span', {}, function(domElement) {
domElement.innerHTML = imageHtml;
}));
});
} catch (error) {
console.error('Error uploading image:', error);
}
}
};
input.click();
});
return view;
});
// Plugin untuk Audio
editor.ui.componentFactory.add('audioUpload', locale => {
const view = new ButtonView(locale);
const audioIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-music" viewBox="0 0 16 16">
<path d="M11 6.64a1 1 0 0 0-1.243-.97l-1 .25A1 1 0 0 0 8 6.89v4.306A2.6 2.6 0 0 0 7 11c-.5 0-.974.134-1.338.377-.36.24-.662.628-.662 1.123s.301.883.662 1.123c.364.243.839.377 1.338.377s.974-.134 1.338-.377c.36-.24.662-.628.662-1.123V8.89l2-.5z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>`;
view.set({
label: '',
icon: audioIcon,
withText: false,
tooltip: 'Upload Audio'
});
view.on('execute', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'audio/*';
input.onchange = async () => {
const file = input.files[0];
if (file) {
try {
const audioUrl = await mediaUpload('AUDIO', file);
const audioHtml = `<audio controls src="${audioUrl}"></audio>`;
const viewFragment = editor.data.processor.toView(audioHtml);
const modelFragment = editor.data.toModel(viewFragment);
editor.model.insertContent(modelFragment, editor.model.document.selection);
} catch (error) {
console.error('Error uploading audio:', error);
}
}
};
input.click();
});
return view;
});
// Konversi Audio sebagai elemen HTML biasa
}
const fetchData = async () => {
setLoading(true);
try {
const data = await materialService.getLevelById(materialId);
// console.log(data.payload);
setEditorData(data.payload.CONTENT);
setLevelName(data.payload.NAME_LEVEL);
setSectionName(data.payload.NAME_SECTION);
setTopicName(data.payload.NAME_TOPIC);
setLoading(false);
} catch (error) {
setError(error);
}
};
useEffect(() => {
fetchData();
setIsLayoutReady(true);
return () => setIsLayoutReady(false);
}, [materialId]);
const handleSubmit = async () => {
if (!editorData) {
alert('Content is empty');
return;
}
const updateData = new FormData();
updateData.append('CONTENT', editorData);
handleShowLoader('Updated', '', true);
try {
const update = await materialService.updateData(materialId, updateData);
setEditorData(update.payload.CONTENT);
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your data has been successfully updated.'
}));
} catch (err) {
// setError(err);
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.message
}));
}
};
return {
loading,
error,
show,
editorData,
levelName,
sectionName,
topicName,
handleEditorChange,
editorContainerRef,
editorRef,
isLayoutReady,
mediaUpload,
MyUploadAdapter,
CustomUploadAdapterPlugin,
CustomMediaPlugin,
handleSubmit,
showLoader,
handleCloseLoader,
loaderState,
};
};
export default useTest;

View File

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import levelService from '../services/serviceMaterials';
const useMaterials = () => {
const [levels, setLevels] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [sort, setSort] = useState("");
const [page, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(7);
const [totalPages, setTotalPages] = useState(0);
const [totalData, setTotalData] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const data = await levelService.fetchData(search, sort, page, limit);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setLevels(data.payload.levels);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSerachChange = () => {
fetchData();
setCurrentPage(1);
}
const handlePageChange = (pages) => {
setCurrentPage(pages);
}
const handleLimitsChange = (e) => {
setLimit(e.target.value);
setCurrentPage(1);
}
useEffect(() => {
fetchData();
}, [page, limit]);
return {
levels,
loading,
error,
page,
totalData,
totalPages,
setSearch,
handlePageChange,
handleLimitsChange,
handleSerachChange,
};
};
export default useMaterials;

View File

@ -1,161 +0,0 @@
import { useState, useEffect } from 'react';
import materialService from '../services/serviceMaterials';
import { MEDIA_URL } from '../../../../utils/Constant';
const useUpdateMaterials = (materialId) => {
const [materialData, setMaterialData] = useState([]);
const [levelName, setLevelName] = useState('Level');
const [sectionName, setSectionName] = useState('section');
const [topicName, setTopicName] = useState('topic');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const mediaPath = `${MEDIA_URL}/level`;
const [formData, setFormData] = useState({
content: '',
audio: '',
image: '',
video: ''
});
const [showLoader, setShowLoader] = useState(false);
const [loaderState, setLoaderState] = useState({ loading: false, successMessage: '', title: '', description: '', confirmAction: false });
const handleFormChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const [audioPreview, setAudioPreview] = useState(null);
const [audioPreviewTitle, setAudioPreviewTitle] = useState(null);
const handleFormChangeAudio = (e) => {
setAudioPreviewTitle(e.target.value);
const file = e.target.files[0];
if (file) {
const mediaUrl = URL.createObjectURL(file);
setAudioPreview(mediaUrl);
}
setFormData({ ...formData, [e.target.name]: file });
};
const [imagePreview, setImagePreview] = useState(null);
const [imagePreviewTitle, setImagePreviewTitle] = useState('');
const handleFormChangeImage = (e) => {
setImagePreviewTitle(e.target.value);
const file = e.target.files[0];
if (file) {
const mediaUrl = URL.createObjectURL(file);
setImagePreview(mediaUrl);
}
setFormData({ ...formData, [e.target.name]: file });
};
const handleRemoveMedia = (type) =>{
setFormData({ ...formData, [type]: '' });
if (type === 'image') {
setImagePreview('');
setImagePreviewTitle('');
setMaterialData({...materialData, IMAGE:""});
}else{
setAudioPreview('');
setAudioPreviewTitle('');
setMaterialData({...materialData, AUDIO:""});
}
};
const handleCloseLoader = () => setShowLoader(false);
const handleShowLoader = (title, description, loading = false, successMessage = '', confirmAction = false) => {
setLoaderState({ title, description, loading, successMessage, confirmAction });
setShowLoader(true);
};
const updateMaterials = async () => {
const id = materialData.ID_LEVEL;
const updateData = new FormData();
updateData.append('CONTENT', formData.content,);
if (formData.audio) {
updateData.append('AUDIO', formData.audio);
}
if (formData.image) {
updateData.append('IMAGE', formData.image);
}
if (formData.video) {
updateData.append('VIDEO', formData.video);
}
handleShowLoader('Updated', '', true);
try {
const update = await materialService.updateData(id, updateData);
setMaterialData(update.payload);
setFormData({
content: update.payload.CONTENT,
audio: update.payload.AUDIO,
image: update.payload.IMAGE,
video: update.payload.VIDEO
});
setLoaderState(prev => ({
...prev,
loading: false,
successMessage: 'Your data has been successfully updated.'
}));
} catch (err) {
// setError(err);
setLoaderState(prev => ({
...prev,
title: "ERROR",
loading: false,
successMessage: err.message
}));
}
};
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const data = await materialService.getLevelById(materialId);
setLevelName(data.payload.NAME_LEVEL);
setSectionName(data.payload.NAME_SECTION);
setTopicName(data.payload.NAME_TOPIC);
setMaterialData(data.payload);
setFormData({
content: data.payload.CONTENT,
audio: data.payload.AUDIO,
image: data.payload.IMAGE,
video: data.payload.VIDEO
});
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [materialId]);
return {
materialData,
formData,
levelName,
sectionName,
topicName,
loading,
error,
mediaPath,
audioPreview,
imagePreview,
audioPreviewTitle,
imagePreviewTitle,
handleFormChange,
handleFormChangeAudio,
handleFormChangeImage,
handleRemoveMedia,
updateMaterials,
showLoader,
handleCloseLoader,
loaderState,
};
};
export default useUpdateMaterials;

View File

@ -1,48 +0,0 @@
import axiosInstance from '../../../../utils/axiosInstance';
const fetchData= async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/level/admin?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching levels:', error);
throw error;
}
};
const getLevelById = async (id) => {
try {
const response = await axiosInstance.get(`/level/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching level with ID ${id}:`, error);
throw error;
}
};
const updateData = async (id, materialData) => {
try {
const response = await axiosInstance.put(`/level/${id}`, materialData);
return response.data;
} catch (error) {
console.error(`Error updating level with ID ${id}:`, error);
throw error;
}
};
const uploadMedia = async (id, mediaData) => {
try {
const response = await axiosInstance.post(`/level/file/${id}`, mediaData);
return response.data;
} catch (error) {
console.error("Error uploading media:", error);
throw error;
}
};
export default{
fetchData,
getLevelById,
uploadMedia,
updateData,
};

View File

@ -1,279 +0,0 @@
import React from 'react';
import { Row, Col, Button, Form, Modal, InputGroup, Breadcrumb, Spinner } from 'react-bootstrap';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import {
ClassicEditor,
Base64UploadAdapter,
SimpleUploadAdapter,
GeneralHtmlSupport,
AccessibilityHelp, Alignment, Autoformat, AutoImage, AutoLink, Autosave, BlockQuote, Bold, Essentials, FindAndReplace,FontBackgroundColor, FontColor, FontFamily, FontSize,
Heading, Highlight, HorizontalLine, ImageBlock, ImageCaption, ImageInline, ImageInsert, ImageInsertViaUrl, ImageResize, ImageStyle, ImageTextAlternative, ImageToolbar, ImageUpload, Indent, IndentBlock, Italic,
Link, LinkImage, List, ListProperties, MediaEmbed, Paragraph, PasteFromOffice, RemoveFormat, SelectAll, SpecialCharacters, SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersEssentials, SpecialCharactersLatin, SpecialCharactersMathematical, SpecialCharactersText, Strikethrough, Subscript, Superscript,
Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableToolbar, TextTransformation, TodoList, Underline, Undo,
ButtonView
} from 'ckeditor5';
import 'ckeditor5/ckeditor5.css';
import useEditorMaterial from '../hooks/useEditorMaterial';
import { useParams } from 'react-router-dom';
import { Link as BtnLink } from 'react-router-dom';
import ModalOperation from '../../../../components/ui/adminMessageModal/ModalOperation';
const EditorMaterial = () => {
const { materialId } = useParams();
const {
loading,
error,
show,
editorData,
levelName,
sectionName,
topicName,
handleEditorChange,
editorContainerRef,
editorRef,
isLayoutReady,
CustomUploadAdapterPlugin,
CustomMediaPlugin,
handleSubmit,
showLoader,
loaderState,
handleCloseLoader,
} = useEditorMaterial(materialId);
const editorConfig = {
toolbar: {
items: [
'findAndReplace','undo', 'redo', '|', 'heading', 'fontFamily', 'fontSize', '|', 'bold', 'italic', 'underline', 'fontColor', 'fontBackgroundColor', '|',
'link',
'imageUpload',
'audioUpload',
'mediaEmbed',
'|', 'alignment', 'numberedList', 'bulletedList', 'todoList', 'outdent', 'indent', '|',
'removeFormat', '|',
// 'insertImage',
// 'insertImageViaUrl',
],
shouldNotGroupWhenFull: true
},
plugins: [
CustomMediaPlugin,
// Base64UploadAdapter,
// SimpleUploadAdapter,
GeneralHtmlSupport,
AccessibilityHelp, Alignment, Autoformat, AutoImage, AutoLink, Autosave, BlockQuote, Bold, Essentials, FindAndReplace, FontBackgroundColor, FontColor, FontFamily, FontSize,
Heading, Highlight, HorizontalLine, ImageBlock, ImageCaption, ImageInline, ImageInsert, ImageResize, ImageStyle, ImageTextAlternative, ImageToolbar, ImageUpload, Indent, IndentBlock, Italic,
Link, LinkImage, List, ListProperties, MediaEmbed, Paragraph, PasteFromOffice, RemoveFormat, SelectAll, SpecialCharacters, SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersEssentials, SpecialCharactersLatin, SpecialCharactersMathematical, SpecialCharactersText, Strikethrough, Subscript, Superscript,
Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableToolbar, TextTransformation, TodoList, Underline, Undo
],
htmlSupport: {
allow: [
{
name: 'audio',
attributes: true,
classes: true,
styles: true
}
]
},
extraPlugins: [CustomUploadAdapterPlugin],
removePlugins: ['ImageInsert'],
fontFamily: {
supportAllValues: true
},
fontSize: {
options: [10, 12, 14, 'default', 18, 20, 22],
supportAllValues: true
},
heading: {
options: [
{
model: 'paragraph',
title: 'Paragraph',
class: 'ck-heading_paragraph'
},
{
model: 'heading1',
view: 'h1',
title: 'Heading 1',
class: 'ck-heading_heading1'
},
{
model: 'heading2',
view: 'h2',
title: 'Heading 2',
class: 'ck-heading_heading2'
},
{
model: 'heading3',
view: 'h3',
title: 'Heading 3',
class: 'ck-heading_heading3'
},
{
model: 'heading4',
view: 'h4',
title: 'Heading 4',
class: 'ck-heading_heading4'
},
{
model: 'heading5',
view: 'h5',
title: 'Heading 5',
class: 'ck-heading_heading5'
},
{
model: 'heading6',
view: 'h6',
title: 'Heading 6',
class: 'ck-heading_heading6'
}
]
},
image: {
toolbar: [
'toggleImageCaption',
'imageTextAlternative',
'|',
'imageStyle:inline',
'imageStyle:wrapText',
'imageStyle:breakText',
'|',
'resizeImage'
]
},
mediaEmbed: {
providers: [
{
name: 'youtube',
url: [
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
],
html: match => {
const id = match[0].includes('watch?v=')
? match[0].split('watch?v=')[1]
: match[0].split('youtu.be/')[1];
const embedUrl = `https://www.youtube.com/embed/${id}`;
return `<iframe controls src="${embedUrl}" style="aspect-ratio:3/2;width:40vw;height:auto"></iframe>`;
}
},
{
name: 'googleDrive',
url: [
/https?:\/\/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)\/(?:view|preview)/
],
html: match => {
const id = match[0].split('/d/')[1].split('/')[0];
// const embedUrl = `https://drive.google.com/uc?export=download&id=${id}`;
const embedUrl = `https://drive.google.com/file/d/${id}/preview`;
return `<iframe controls src="${embedUrl}" style="aspect-ratio:3/2;width:40vw;height:auto"></iframe>`;
}
}
]
},
initialData:
editorData,
link: {
addTargetToExternalLinks: true,
defaultProtocol: 'https://',
decorators: {
toggleDownloadable: {
mode: 'manual',
label: 'Downloadable',
attributes: {
download: 'file'
}
}
}
},
list: {
properties: {
styles: true,
startIndex: true,
reversed: true
}
},
menuBar: {
isVisible: true
},
placeholder: 'Type or paste the material here!',
table: {
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
}
};
return (
<div>
<Row className='mb-45'>
<Col className="d-flex align-items-center breadcrumb-con">
<Button as={BtnLink} className='btn btn-blue btn-square-back' to='/admin/material'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="#">Learning</Breadcrumb.Item>
<Breadcrumb.Item href="/admin/material" className='text-capitalize'>Materials</Breadcrumb.Item>
<Breadcrumb.Item active>Update Materials</Breadcrumb.Item>
</Breadcrumb>
</Col>
</Row>
<Row className='mb-45'>
<Col>
<div className="cards">
<div className="cards-title d-flex">
<h4>{sectionName} : {topicName} - <span className='text-blue'>{levelName}</span></h4>
</div>
<div className="cards-body">
<label className='mb-2 fw-500'>Material Content <sup className='text-red fw-bold'>*</sup></label>
<div className="mb-4 main-container">
<div className="editor-container editor-container_classic-editor" ref={editorContainerRef}>
<div className="editor-container__editor">
<div ref={editorRef}>
{isLayoutReady &&
<CKEditor
editor={ClassicEditor}
config={editorConfig}
data={editorData || ""}
onChange={handleEditorChange}
/>
}
</div>
</div>
</div>
</div>
<div className="d-flex justify-content-end">
<Button variant="blue" onClick={handleSubmit} className='ms-2 py-2 px-5 rounded-35'>
Save
</Button>
</div>
</div>
</div>
</Col>
</Row>
<Modal show={show} centered>
<Modal.Body>
<div className='w-100 d-flex justify-content-center align-items-center' style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</div>
</Modal.Body>
</Modal>
<ModalOperation
show={showLoader}
handleClose={handleCloseLoader}
title={loaderState.title}
description={loaderState.description}
loading={loaderState.loading}
successMessage={loaderState.successMessage}
confirmAction={loaderState.confirmAction}
handleConfirm={loaderState.handleConfirm}
/>
</div>
);
};
export default EditorMaterial;

Some files were not shown because too many files have changed in this diff Show More