all
21
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# 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?
|
||||||
94
README.md
|
|
@ -1,93 +1,3 @@
|
||||||
# frontend-adaptive-learning
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
|
||||||
|
|
||||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
|
||||||
|
|
||||||
## Add your files
|
|
||||||
|
|
||||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
|
||||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd existing_repo
|
|
||||||
git remote add origin https://gitlab.com/profile-image/kedaireka/polinema-adapative-learning/frontend-adaptive-learning.git
|
|
||||||
git branch -M main
|
|
||||||
git push -uf origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integrate with your tools
|
|
||||||
|
|
||||||
- [ ] [Set up project integrations](https://gitlab.com/profile-image/kedaireka/polinema-adapative-learning/frontend-adaptive-learning/-/settings/integrations)
|
|
||||||
|
|
||||||
## Collaborate with your team
|
|
||||||
|
|
||||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
|
||||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
||||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
||||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
||||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
|
||||||
|
|
||||||
## Test and Deploy
|
|
||||||
|
|
||||||
Use the built-in continuous integration in GitLab.
|
|
||||||
|
|
||||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
|
||||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
||||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
||||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
||||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
# Editing this README
|
|
||||||
|
|
||||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
|
||||||
|
|
||||||
## Suggestions for a good README
|
|
||||||
|
|
||||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
||||||
|
|
||||||
## Name
|
|
||||||
Choose a self-explaining name for your project.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
||||||
|
|
||||||
## Badges
|
|
||||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
||||||
|
|
||||||
## Visuals
|
|
||||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
State if you are open to contributions and what your requirements are for accepting them.
|
|
||||||
|
|
||||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
|
||||||
|
|
||||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
|
||||||
|
|
||||||
## Authors and acknowledgment
|
|
||||||
Show your appreciation to those who have contributed to the project.
|
|
||||||
|
|
||||||
## License
|
|
||||||
For open source projects, say how it is licensed.
|
|
||||||
|
|
||||||
## Project status
|
|
||||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
||||||
|
|
|
||||||
18
index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!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>
|
||||||
6149
package-lock.json
generated
Normal file
39
package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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
public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
23
src/App.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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;
|
||||||
BIN
src/assets/audio/sample.mp3
Normal file
BIN
src/assets/images/Decoration.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/Decoration2.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/images/decorFooter.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
src/assets/images/default-avatar.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/images/feature1.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/images/feature2.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/feature3.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/feature4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/images/footerDecor.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/images/illustration/Illustration.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/assets/images/illustration/Illustration2.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/images/illustration/IllustrationForgot.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/images/illustration/IllustrationLogin.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/images/illustration/IllustrationRegister.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/assets/images/illustration/changePass.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/images/illustration/checkIcon.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src/assets/images/illustration/emptyJourney.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/illustration/emptyStudent.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/images/illustration/grammar.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
src/assets/images/illustration/listening.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
src/assets/images/illustration/logout.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/illustration/material-audio.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/illustration/material-image.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/images/illustration/reading.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
src/assets/images/illustration/report.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/images/illustration/resultDown.png
Normal file
|
After Width: | Height: | Size: 455 KiB |
BIN
src/assets/images/illustration/resultFinish.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
src/assets/images/illustration/resultJump.png
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
src/assets/images/illustration/resultStay.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
src/assets/images/illustration/service.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
src/assets/images/illustration/submitExercise.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/illustration/successModal.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/assets/images/illustration/topic.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/images/illustration/vocabulary.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
src/assets/images/loading.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
src/assets/images/logo-w.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
128
src/assets/styles/Components/AudioPlayer.css
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
598
src/assets/styles/admin.css
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
407
src/assets/styles/index.css
Normal file
|
|
@ -0,0 +1,407 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-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-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
424
src/assets/styles/teacher.css
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
.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% - 15px);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
326
src/assets/styles/user.css
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
.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% - 15px);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 .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%;
|
||||||
|
}
|
||||||
BIN
src/assets/video/sample.mp4
Normal file
23
src/components/layout/admin/AdminLayout.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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;
|
||||||
94
src/components/layout/admin/AdminNavbar.jsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
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 { API_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 ? `${API_URL}/uploads/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;
|
||||||
144
src/components/layout/admin/AdminSideNav.jsx
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
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={logo} alt="" />
|
||||||
|
<i className="bi bi-caret-left-square text-grey cursor-pointer" 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;
|
||||||
76
src/components/layout/guest/MainNav.jsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Navbar, Nav, Container, Button } 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 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 w-100';
|
||||||
|
} else{
|
||||||
|
return 'd-none'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar collapseOnSelect expand="md" 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()}>
|
||||||
|
<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-none d-lg-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>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainNav;
|
||||||
22
src/components/layout/teacher/TeacherLayout.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
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">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeacherLayout;
|
||||||
91
src/components/layout/teacher/TeacherNavbar.jsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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;
|
||||||
59
src/components/layout/teacher/TeacherSideNav.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
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 { API_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 ? `${API_URL}/uploads/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 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="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="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;
|
||||||
68
src/components/layout/user/UserLayout.jsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
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">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserLayout;
|
||||||
131
src/components/layout/user/UserNavbar.jsx
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
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 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 [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-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 aria-controls="navbar-nav" />
|
||||||
|
<div className="navbar-title col-md-6 col-lg-8">
|
||||||
|
{hasTopicAndLevel ? (
|
||||||
|
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-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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserNavbar;
|
||||||
59
src/components/layout/user/UserSideNav.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
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 { API_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 ? `${API_URL}/uploads/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 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;
|
||||||
101
src/components/ui/AudioPlayer.jsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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;
|
||||||
382
src/components/ui/AudioSvg.jsx
Normal file
32
src/components/ui/Button.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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;
|
||||||
158
src/components/ui/DocImporter.jsx
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
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;
|
||||||
79
src/components/ui/TablePaginate.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
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;
|
||||||
65
src/components/ui/adminMessageModal/ModalOperation.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
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'>{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'>{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
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
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>,
|
||||||
|
);
|
||||||
79
src/roles/admin/AdminRoutes.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
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 '../../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>
|
||||||
|
|
||||||
|
{/* <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;
|
||||||
11
src/roles/admin/NotFound.jsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className='pt-nav'>
|
||||||
|
<h1>Not Found</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
94
src/roles/admin/dashboard/hooks/useDashboard.jsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
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.monitorings);
|
||||||
|
} 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;
|
||||||
48
src/roles/admin/dashboard/services/serviceDashboard.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
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(`/monitoring/progress?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
|
||||||
|
};
|
||||||
152
src/roles/admin/dashboard/views/Dashboard.jsx
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Table, Row, Col, Card, Button, Spinner } from 'react-bootstrap';
|
||||||
|
import { NavLink } 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>No</th>
|
||||||
|
<th>NISN</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Section</th>
|
||||||
|
<th>Topic</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loadingActivity?(
|
||||||
|
<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>
|
||||||
|
):(
|
||||||
|
activity.length > 0?(
|
||||||
|
activity.map((data, index) => (
|
||||||
|
<tr index>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>{data.NISN}</td>
|
||||||
|
<td>{data.NAME_USERS}</td>
|
||||||
|
<td>{data.NAME_SECTION}</td>
|
||||||
|
<td>{data.NAME_TOPIC}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
):(
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} 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;
|
||||||
37
src/roles/admin/manage_classes/hooks/useClassDetail.jsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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;
|
||||||
257
src/roles/admin/manage_classes/hooks/useClasses.jsx
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
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]);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
}finally{
|
||||||
|
resetForm();
|
||||||
|
setLoaderState(prev => ({
|
||||||
|
...prev,
|
||||||
|
loading: false,
|
||||||
|
successMessage: 'Your new entry has been successfully created and saved.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
}finally{
|
||||||
|
setLoaderState(prev => ({
|
||||||
|
...prev,
|
||||||
|
loading: false,
|
||||||
|
successMessage: 'Your data has been successfully updated.'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
93
src/roles/admin/manage_classes/services/serviceClasses.jsx
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
90
src/roles/admin/manage_classes/views/ClassDetail.jsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
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;
|
||||||
304
src/roles/admin/manage_classes/views/ManageClasses.jsx
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
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.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'>
|
||||||
|
<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>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</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;
|
||||||
62
src/roles/admin/manage_exercises/hooks/useExercises.jsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
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;
|
||||||
419
src/roles/admin/manage_exercises/hooks/useUpdateExercises.jsx
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import exerciseService from '../services/serviceExercises';
|
||||||
|
import { API_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 = `${API_URL}/uploads/exercise`;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
ID_ADMIN_EXERCISE: "",
|
||||||
|
ID_LEVEL: "",
|
||||||
|
TITLE: "",
|
||||||
|
QUESTION: "",
|
||||||
|
QUESTION_TYPE: "",
|
||||||
|
SCORE_WEIGHT: "",
|
||||||
|
AUDIO: "",
|
||||||
|
IMAGE: "",
|
||||||
|
VIDEO: "",
|
||||||
|
multipleChoices: "",
|
||||||
|
trueFalse: "",
|
||||||
|
matchingPairs: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 || {},
|
||||||
|
});
|
||||||
|
setShow(true);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setShow(false);
|
||||||
|
resetForm();
|
||||||
|
setSelectedQuestion(null);
|
||||||
|
setMediaPreview(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
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") {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
114
src/roles/admin/manage_exercises/services/serviceExercises.jsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default{
|
||||||
|
fetchData,
|
||||||
|
getExerciseByLevelId,
|
||||||
|
getLevelById,
|
||||||
|
createData,
|
||||||
|
updateData,
|
||||||
|
sortingData,
|
||||||
|
deleteData,
|
||||||
|
};
|
||||||
83
src/roles/admin/manage_exercises/views/ExerciseDetail.jsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
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;
|
||||||
245
src/roles/admin/manage_exercises/views/ManageExercises.jsx
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
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>Level</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.map((level, index) => (
|
||||||
|
<tr key={level.ID_LEVEL}>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>{level.NAME_SECTION}</td>
|
||||||
|
<td>{level.NAME_TOPIC}</td>
|
||||||
|
<td>{level.NAME_LEVEL}</td>
|
||||||
|
<td className='text-center action-col'>
|
||||||
|
<Link className='btn btn-sm btn-edit' to={`update-exercise/${level.ID_LEVEL}`}>
|
||||||
|
<i className="bi bi-pencil-square"></i>
|
||||||
|
</Link>
|
||||||
|
</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;
|
||||||
488
src/roles/admin/manage_exercises/views/OldManageExercises.jsx
Normal file
|
|
@ -0,0 +1,488 @@
|
||||||
|
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;
|
||||||
803
src/roles/admin/manage_exercises/views/UpdateExercise.jsx
Normal file
|
|
@ -0,0 +1,803 @@
|
||||||
|
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/material" 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'>
|
||||||
|
|
||||||
|
{/* <div className="cards">
|
||||||
|
<div className="cards-title d-flex justify-content-between">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<span style={{ cursor: 'grab' }}><i className="bi bi-list me-3" /></span>
|
||||||
|
<h4>1</h4>
|
||||||
|
<h4>.Multiple Choice</h4>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex">
|
||||||
|
<Button size='sm' variant='outline-warning' className='me-2 border'><i className="bi bi-pencil me-1"></i>edit</Button>
|
||||||
|
<Button size='sm' variant='outline-danger' className='border'><i className="bi bi-trash3"></i></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cards-body">
|
||||||
|
<Row className='mb-2'>
|
||||||
|
<Col className='d-flex align-items-center'>
|
||||||
|
<img src="https://picsum.photos/300/200" alt="" className='me-2 bg-light' style={{aspectRatio:"1/1",width:"5vw",objectPosition:"center",objectFit:"contain"}}/>
|
||||||
|
<p className='m-0 fs-14p'>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum assumenda tenetur nostrum, consequatur mollitia ea dignissimos adipisci rerum nesciunt laboriosam laborum ut atque beatae facilis nam illum impedit quia eius.</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<span className='fs-12p'>Question Choices</span>
|
||||||
|
<Row>
|
||||||
|
<Col md={6} className='d-flex align-items-center'>
|
||||||
|
<i className="bi bi-check text-success fs-5 me-1"></i><span className='fs-14p'>gatau</span>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} className='d-flex align-items-center'>
|
||||||
|
<i className="bi bi-x text-red fs-5 me-1"></i><span className='fs-14p'>ya ini</span>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} className='d-flex align-items-center'>
|
||||||
|
<i className="bi bi-x text-red fs-5 me-1"></i><span className='fs-14p'>ya itu</span>
|
||||||
|
</Col>
|
||||||
|
<Col md={6} className='d-flex align-items-center'>
|
||||||
|
<i className="bi bi-x text-red fs-5 me-1"></i><span className='fs-14p'>gatau ini</span>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{/* <div className="py-4 bg-white d-flex justify-content-end">
|
||||||
|
<Button variant="blue" type="submit" className='ms-2 py-2 px-5 rounded-35'>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
</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;
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
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;
|
||||||
415
src/roles/admin/manage_materials/hooks/useEditorMaterial.jsx
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import materialService from '../services/serviceMaterials';
|
||||||
|
import { ButtonView } from 'ckeditor5';
|
||||||
|
import { API_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 = `${API_URL}/uploads/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;
|
||||||
|
|
||||||
62
src/roles/admin/manage_materials/hooks/useMaterials.jsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
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;
|
||||||
161
src/roles/admin/manage_materials/hooks/useUpdateMaterials.jsx
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import materialService from '../services/serviceMaterials';
|
||||||
|
import { API_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 = `${API_URL}/uploads/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;
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
279
src/roles/admin/manage_materials/views/EditorMaterial.jsx
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
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;
|
||||||
252
src/roles/admin/manage_materials/views/ManageMaterials.jsx
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Table, Row, Col, Nav, Tab, Button, Form, InputGroup, Spinner, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||||
|
import useMaterials from '../hooks/useMaterials';
|
||||||
|
import ModalOperation from '../../../../components/ui/adminMessageModal/ModalOperation';
|
||||||
|
import TablePaginate from '../../../../components/ui/tablePaginate';
|
||||||
|
|
||||||
|
const ManageMaterials = () => {
|
||||||
|
const {
|
||||||
|
levels,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
page,
|
||||||
|
totalData,
|
||||||
|
totalPages,
|
||||||
|
setSearch,
|
||||||
|
handlePageChange,
|
||||||
|
handleLimitsChange,
|
||||||
|
handleSerachChange,
|
||||||
|
} = useMaterials();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='admin-teachers'>
|
||||||
|
<h2 className='page-title strip'>Materials</h2>
|
||||||
|
<p className='page-desc'>Description of Materials.</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>Material 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>Level</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.map((level, index) => (
|
||||||
|
<tr key={level.ID_LEVEL}>
|
||||||
|
<td>{index + 1}</td>
|
||||||
|
<td>{level.NAME_SECTION}</td>
|
||||||
|
<td>{level.NAME_TOPIC}</td>
|
||||||
|
<td>{level.NAME_LEVEL}</td>
|
||||||
|
<td className='text-center action-col'>
|
||||||
|
{/* <Button size='sm' className='btn-edit' onClick={() => handleShow(level)}>
|
||||||
|
<i className="bi bi-pencil-square"></i>
|
||||||
|
</Button> */}
|
||||||
|
<Link className='btn btn-sm btn-edit' to={`update-material/${level.ID_LEVEL}`}>
|
||||||
|
<i className="bi bi-pencil-square"></i>
|
||||||
|
</Link>
|
||||||
|
{/* <Button size='sm' className='btn-edit' onClick={() => handleShow(level)}>
|
||||||
|
<i className="bi bi-pencil-square"></i>
|
||||||
|
</Button> */}
|
||||||
|
</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 ManageMaterials;
|
||||||
275
src/roles/admin/manage_materials/views/UpdateMaterials.jsx
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Table, Row, Col, Button, Form, Modal, InputGroup, Breadcrumb, Spinner } from 'react-bootstrap';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import useUpdateMaterials from '../hooks/useUpdateMaterials';
|
||||||
|
import ModalOperation from '../../../../components/ui/adminMessageModal/ModalOperation';
|
||||||
|
|
||||||
|
const UpdateMaterials = () => {
|
||||||
|
const { materialId } = useParams();
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
materialData,
|
||||||
|
formData,
|
||||||
|
levelName,
|
||||||
|
sectionName,
|
||||||
|
topicName,
|
||||||
|
mediaPath,
|
||||||
|
audioPreview,
|
||||||
|
imagePreview,
|
||||||
|
audioPreviewTitle,
|
||||||
|
imagePreviewTitle,
|
||||||
|
handleFormChange,
|
||||||
|
handleFormChangeAudio,
|
||||||
|
handleFormChangeImage,
|
||||||
|
handleRemoveMedia,
|
||||||
|
updateMaterials,
|
||||||
|
showLoader,
|
||||||
|
handleCloseLoader,
|
||||||
|
loaderState,
|
||||||
|
|
||||||
|
} = useUpdateMaterials(materialId);
|
||||||
|
|
||||||
|
const [viewImg, setMediaImg] = useState(false);
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [mediaLoading, setMediaLoad] = useState(true);
|
||||||
|
|
||||||
|
const handleClose = () => {setShow(false);setMediaLoad(true);};
|
||||||
|
const handleViewMedia = (p) => {
|
||||||
|
setMediaImg(p);
|
||||||
|
setShow(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMediaLoaded = () => {
|
||||||
|
setMediaLoad(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
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/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">
|
||||||
|
{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>
|
||||||
|
):(
|
||||||
|
<Form onSubmit={(e) => { e.preventDefault(); updateMaterials(); }}>
|
||||||
|
<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 meterial content here...'
|
||||||
|
value={formData.content || ''}
|
||||||
|
onChange={handleFormChange}
|
||||||
|
name='content'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</Row>
|
||||||
|
<Row className='mb-4'>
|
||||||
|
<Form.Group as={Col}>
|
||||||
|
<Form.Label>
|
||||||
|
Audio File (optional)
|
||||||
|
{materialData.AUDIO || audioPreview ?(
|
||||||
|
<span className='ms-2 text-blue cursor-pointer' onClick={(e) => { e.preventDefault(); handleViewMedia(false); }}>
|
||||||
|
view media
|
||||||
|
</span>
|
||||||
|
|
||||||
|
):(
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</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 type='file' accept="audio/*"
|
||||||
|
placeholder="Choose Audio"
|
||||||
|
aria-label="fullname"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
className='border-dashed d-none'
|
||||||
|
onChange={handleFormChangeAudio}
|
||||||
|
name='audio'
|
||||||
|
/>
|
||||||
|
<Form.Control readOnly onClick={(e) => {e.preventDefault();triggerFileInput(e)}}
|
||||||
|
placeholder="Choose Audio"
|
||||||
|
aria-label="fullname"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
className='border-dashed cursor-pointer'
|
||||||
|
value={audioPreviewTitle ? audioPreviewTitle : formData.audio || ''}
|
||||||
|
onChange={(e)=>{e.preventDefault();}}
|
||||||
|
/>
|
||||||
|
{audioPreview || formData.audio ?(
|
||||||
|
<Button variant="outline-danger" className='fs-14p' onClick={(e) => { e.preventDefault(); handleRemoveMedia('audio'); }}>
|
||||||
|
remove
|
||||||
|
</Button>
|
||||||
|
):(<></>)}
|
||||||
|
</InputGroup>
|
||||||
|
<small>Ensure the file size is no larger than 5 MB</small>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group as={Col}>
|
||||||
|
<Form.Label>
|
||||||
|
Image File (optional)
|
||||||
|
{materialData.IMAGE || imagePreview ?(
|
||||||
|
<span className='ms-2 text-blue cursor-pointer' onClick={(e) => { e.preventDefault(); handleViewMedia(true); }}>
|
||||||
|
view media
|
||||||
|
</span>
|
||||||
|
|
||||||
|
):(
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</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 type='file' accept="image/*"
|
||||||
|
placeholder="Choose Image"
|
||||||
|
aria-label="fullname"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
className='border-dashed d-none'
|
||||||
|
onChange={handleFormChangeImage}
|
||||||
|
name='image'
|
||||||
|
/>
|
||||||
|
<Form.Control readOnly onClick={(e) => {e.preventDefault();triggerFileInput(e)}}
|
||||||
|
placeholder="Choose Image"
|
||||||
|
aria-label="fullname"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
className='border-dashed cursor-pointer'
|
||||||
|
value={imagePreviewTitle ? imagePreviewTitle : formData.image || ''}
|
||||||
|
onChange={(e)=>{e.preventDefault();}}
|
||||||
|
/>
|
||||||
|
{imagePreview || formData.image ?(
|
||||||
|
<Button variant="outline-danger" className='fs-14p' onClick={(e) => { e.preventDefault(); handleRemoveMedia('image'); }}>
|
||||||
|
remove
|
||||||
|
</Button>
|
||||||
|
):(<></>)}
|
||||||
|
</InputGroup>
|
||||||
|
<small>Ensure the file size is no larger than 5 MB</small>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group as={Col}>
|
||||||
|
<Form.Label>Video Link (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 type='url'
|
||||||
|
placeholder="Enter Video URL"
|
||||||
|
aria-label="fullname"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
className='border-dashed'
|
||||||
|
value={formData.video || ''}
|
||||||
|
onChange={handleFormChange}
|
||||||
|
name='video'
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<small>Make sure the Link is valid</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>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Modal show={show} onHide={handleClose} className='modal-admin' centered>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>View Media</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{mediaLoading?(
|
||||||
|
<div className='py-3 d-flex justify-content-center align-items-center'>
|
||||||
|
<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>
|
||||||
|
):(
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{viewImg?(
|
||||||
|
imagePreview?(
|
||||||
|
<img
|
||||||
|
className='w-100'
|
||||||
|
src={imagePreview}
|
||||||
|
alt="material image"
|
||||||
|
onLoad={handleMediaLoaded}
|
||||||
|
style={{ display: mediaLoading ? 'none' : 'block' }}
|
||||||
|
/>
|
||||||
|
):(
|
||||||
|
<img
|
||||||
|
className='w-100'
|
||||||
|
src={`${mediaPath}/image/${materialData.IMAGE}`}
|
||||||
|
alt="material image"
|
||||||
|
onLoad={handleMediaLoaded}
|
||||||
|
style={{ display: mediaLoading ? 'none' : 'block' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
):(
|
||||||
|
audioPreview?(
|
||||||
|
<div className='w-100 d-flex justify-content-center align-items-center'>
|
||||||
|
<audio controls
|
||||||
|
src={audioPreview}
|
||||||
|
onCanPlay={handleMediaLoaded}
|
||||||
|
style={{ display: mediaLoading ? 'none' : 'block' }}
|
||||||
|
>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
):(
|
||||||
|
<div className='w-100 d-flex justify-content-center align-items-center'>
|
||||||
|
<audio controls
|
||||||
|
src={`${mediaPath}/audio/${materialData.AUDIO}`}
|
||||||
|
onCanPlay={handleMediaLoaded}
|
||||||
|
style={{ display: mediaLoading ? 'none' : 'block' }}
|
||||||
|
>
|
||||||
|
</audio>
|
||||||
|
</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 UpdateMaterials;
|
||||||
142
src/roles/admin/manage_progress/hooks/useProgress.jsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import progressService from '../services/serviceProgress';
|
||||||
|
|
||||||
|
const useProgress = () => {
|
||||||
|
const [students, setStudents] = useState([]);
|
||||||
|
const [classes, setClasses] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const [searchStudent, setSearchStudent] = useState("");
|
||||||
|
const [sortStudent, setSortStudent] = useState("");
|
||||||
|
const [pageStudent, setCurrentPageStudent] = useState(1);
|
||||||
|
const [limitStudent, setLimitStudent] = useState(20);
|
||||||
|
const [totalPagesStudent, setTotalPagesStudent] = useState(0);
|
||||||
|
const [totalDataStudent, setTotalDataStudent] = useState(null);
|
||||||
|
|
||||||
|
const [searchClass, setSearchClass] = useState("");
|
||||||
|
const [sortClass, setSortClass] = useState("");
|
||||||
|
const [pageClass, setCurrentPageClass] = useState(1);
|
||||||
|
const [limitClass, setLimitClass] = useState(1000);
|
||||||
|
const [totalPagesClass, setTotalPagesClass] = useState(0);
|
||||||
|
const [totalDataClass, setTotalDataClass] = useState(null);
|
||||||
|
|
||||||
|
const [selectedId, setSelectedId] = useState('');
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [loadingModal, setLoadingModal] = useState(true);
|
||||||
|
|
||||||
|
const fetchDataStudents = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const student = await progressService.fetchDataStudent(searchStudent, sortStudent, pageStudent, limitStudent);
|
||||||
|
setStudents(student.payload.monitorings);
|
||||||
|
setTotalPagesStudent(student.payload.totalPages);
|
||||||
|
setTotalDataStudent(student.payload.totalItems);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataClasses = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const classes = await progressService.fetchDataClass(searchClass, sortClass, pageClass, limitClass);
|
||||||
|
setClasses(classes.payload.classes);
|
||||||
|
setTotalDataClass(classes.payload.totalItems);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChangeStudents = () => {
|
||||||
|
fetchDataStudents();
|
||||||
|
setCurrentPageStudent(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChangeStudents = (pages) => {
|
||||||
|
setCurrentPageStudent(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLimitsChangeStudents = (e) => {
|
||||||
|
setLimitStudent(e.target.value);
|
||||||
|
setCurrentPageStudent(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchChangeClasses = () => {
|
||||||
|
fetchDataClasses();
|
||||||
|
setCurrentPageClass(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChangeClasses = (pages) => {
|
||||||
|
setCurrentPageClass(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLimitsChangeClasses = (e) => {
|
||||||
|
setLimitClass(e.target.value);
|
||||||
|
setCurrentPageClass(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDataStudents();
|
||||||
|
}, [pageStudent, limitStudent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDataClasses();
|
||||||
|
}, [pageClass, limitClass]);
|
||||||
|
|
||||||
|
const getClassTopic = async (id) => {
|
||||||
|
setLoadingModal(true);
|
||||||
|
try {
|
||||||
|
const classes = await progressService.getTopicByClass(id, '', '', '', 1000);
|
||||||
|
setSelected(classes.payload.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}finally{
|
||||||
|
setLoadingModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShow = (data) => {
|
||||||
|
setSelectedId(data);
|
||||||
|
getClassTopic(data);
|
||||||
|
setShow(true);
|
||||||
|
};
|
||||||
|
const handleClose = () => {setShow(false); setSelected([])};
|
||||||
|
|
||||||
|
return {
|
||||||
|
students,
|
||||||
|
classes,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
totalPagesStudent,
|
||||||
|
totalDataStudent,
|
||||||
|
setSearchStudent,
|
||||||
|
pageStudent,
|
||||||
|
handlePageChangeStudents,
|
||||||
|
handleLimitsChangeStudents,
|
||||||
|
handleSearchChangeStudents,
|
||||||
|
|
||||||
|
totalPagesClass,
|
||||||
|
totalDataClass,
|
||||||
|
setSearchClass,
|
||||||
|
pageClass,
|
||||||
|
handlePageChangeClasses,
|
||||||
|
handleLimitsChangeClasses,
|
||||||
|
handleSearchChangeClasses,
|
||||||
|
|
||||||
|
selectedId,
|
||||||
|
selected,
|
||||||
|
show,
|
||||||
|
loadingModal,
|
||||||
|
handleShow,
|
||||||
|
handleClose,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProgress;
|
||||||
101
src/roles/admin/manage_progress/hooks/useProgressClass.jsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import progressService from '../services/serviceProgress';
|
||||||
|
|
||||||
|
const useProgressClass = (progressId) => {
|
||||||
|
const [progress, setProgress] = useState([]);
|
||||||
|
const [section, setSection] = useState('section');
|
||||||
|
const [topic, setTopic] = useState('topic');
|
||||||
|
const [name, setName] = useState('class');
|
||||||
|
|
||||||
|
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(20);
|
||||||
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
const [totalData, setTotalData] = useState(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const [classId, topicId] = progressId.split("&");
|
||||||
|
|
||||||
|
const dataId = {
|
||||||
|
ID_CLASS: classId,
|
||||||
|
ID_TOPIC: topicId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await progressService.fetchDataClassProgress(dataId, search, sort, page, limit);
|
||||||
|
setProgress(data.payload.levels);
|
||||||
|
setTotalPages(data.payload.totalPages);
|
||||||
|
setTotalData(data.payload.totalItems);
|
||||||
|
|
||||||
|
setSection(data.payload.NAME_SECTION)
|
||||||
|
setTopic(data.payload.NAME_TOPIC)
|
||||||
|
setName(data.payload.NAME_CLASS)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = () => {
|
||||||
|
fetchData();
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (pages) => {
|
||||||
|
setCurrentPage(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLimitsChange = (e) => {
|
||||||
|
setLimit(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [page, limit, progressId]);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
progress,
|
||||||
|
section,
|
||||||
|
topic,
|
||||||
|
name,
|
||||||
|
nisn,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
totalPages,
|
||||||
|
totalData,
|
||||||
|
setSearch,
|
||||||
|
page,
|
||||||
|
handlePageChange,
|
||||||
|
handleLimitsChange,
|
||||||
|
handleSearchChange,
|
||||||
|
formatLocalDate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProgressClass;
|
||||||
97
src/roles/admin/manage_progress/hooks/useProgressStudent.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import progressService from '../services/serviceProgress';
|
||||||
|
|
||||||
|
const useProgressStudent = (monitoringId) => {
|
||||||
|
const [progress, setProgress] = useState([]);
|
||||||
|
const [section, setSection] = useState('section');
|
||||||
|
const [topic, setTopic] = useState('topic');
|
||||||
|
const [name, setName] = useState('name');
|
||||||
|
const [nisn, setNisn] = useState('nisn');
|
||||||
|
|
||||||
|
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(20);
|
||||||
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
const [totalData, setTotalData] = useState(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await progressService.fetchDataStudentProgress(monitoringId, search, sort, page, limit);
|
||||||
|
setProgress(data.payload.levels);
|
||||||
|
console.log(data.payload);
|
||||||
|
setTotalPages(data.payload.totalPages);
|
||||||
|
setTotalData(data.payload.totalItems);
|
||||||
|
|
||||||
|
setSection(data.payload.NAME_SECTION)
|
||||||
|
setTopic(data.payload.NAME_TOPIC)
|
||||||
|
setName(data.payload.NAME_USERS)
|
||||||
|
setNisn(data.payload.NISN)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = () => {
|
||||||
|
fetchData();
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (pages) => {
|
||||||
|
setCurrentPage(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLimitsChange = (e) => {
|
||||||
|
setLimit(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [page, limit, monitoringId]);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
progress,
|
||||||
|
section,
|
||||||
|
topic,
|
||||||
|
name,
|
||||||
|
nisn,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
totalPages,
|
||||||
|
totalData,
|
||||||
|
setSearch,
|
||||||
|
page,
|
||||||
|
handlePageChange,
|
||||||
|
handleLimitsChange,
|
||||||
|
handleSearchChange,
|
||||||
|
formatLocalDate
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProgressStudent;
|
||||||
70
src/roles/admin/manage_progress/services/serviceProgress.jsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import axiosInstance from '../../../../utils/axiosInstance';
|
||||||
|
|
||||||
|
const fetchDataStudent = async (search, sort, page, limit) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/monitoring/progress?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching progress:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataClass = 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 progress:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStudentById = async (id) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/progress/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching progress with student ID ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTopicByClass = async (id, search, sort, page, limit) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/monitoring/class/${id}?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching progress:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataStudentProgress = async (id, search, sort, page, limit) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/monitoring/progress/${id}?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching progress:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataClassProgress = async (dataId, search, sort, page, limit) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(`/monitoring/class?search=${search}&sort=${sort}&page=${page}&limit=${limit}`, dataId);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching progress:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default{
|
||||||
|
fetchDataStudent,
|
||||||
|
fetchDataClass,
|
||||||
|
getStudentById,
|
||||||
|
getTopicByClass,
|
||||||
|
fetchDataStudentProgress,
|
||||||
|
fetchDataClassProgress
|
||||||
|
};
|
||||||