This commit is contained in:
Dimas Atmodjo 2024-10-31 09:32:14 +07:00
parent e3915b4fab
commit 65868d4b4f
212 changed files with 27586 additions and 92 deletions

21
.eslintrc.cjs Normal file
View 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
View 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?

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View 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
View 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
View 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;
}

View 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
View 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

Binary file not shown.

View 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;

View 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? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="red" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
</Navbar>
);
};
export default AdminNavbar;

View File

@ -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;

View 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;

View 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;

View 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? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="danger" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
<Modal centered show={showReport} onHide={handleCloseReport} size='lg'>
<Report onClose={handleCloseReport}/>
</Modal>
</>
);
};
export default TeacherNavbar;

View File

@ -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;

View 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;

View 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? Well be here when you return.</p>
<div className="mt-4 w-100 d-flex justify-content-center">
<Button variant="outline-muted" className="py-2 px-5 mx-1 rounded-35" onClick={handleClose}>No, I'll stay</Button>
<Button variant="danger" className="py-2 px-5 mx-1 rounded-35" onClick={handleLogout}>Yes, I'm done</Button>
</div>
</Modal.Body>
</Modal>
<Modal centered show={showReport} onHide={handleCloseReport} size='lg'>
<Report onClose={handleCloseReport}/>
</Modal>
</>
);
};
export default UserNavbar;

View 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;

View 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;

File diff suppressed because one or more lines are too long

View 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;

View 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;

View 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;

View 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
View 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>,
);

View 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;

View File

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

View 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;

View 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
};

View 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;

View 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;

View 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;

View 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
};

View 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;

View 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;

View 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;

View 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;

View 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,
};

View 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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -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,
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
};

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