Compare commits
No commits in common. "master" and "main" have entirely different histories.
|
|
@ -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
|
|
@ -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?
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
18
index.html
|
|
@ -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
41
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
23
src/App.jsx
|
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 455 KiB |
|
Before Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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? We’ll 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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? We’ll 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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? We’ll 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
13
src/main.jsx
|
|
@ -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>,
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div className='pt-nav'>
|
||||
<h1>Not Found</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||