start
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
69
.eslintrc.json
Normal file
69
.eslintrc.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"standard",
|
||||
"prettier"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"plugins": [
|
||||
"react",
|
||||
"react-hooks",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error"
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-single"
|
||||
],
|
||||
"multiline-ternary": "off",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
|
||||
.env*
|
||||
|
||||
# Local Claude Code config — settings.local.json holds per-machine permissions.
|
||||
.claude/
|
||||
16
.prettierrc.json
Normal file
16
.prettierrc.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf",
|
||||
"printWidth": 100,
|
||||
"plugins": [
|
||||
"prettier-plugin-tailwindcss"
|
||||
]
|
||||
}
|
||||
87
package.json
Normal file
87
package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "student_panel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"axios-cache-adapter": "^2.7.3",
|
||||
"classnames": "^2.3.1",
|
||||
"formik": "^2.2.9",
|
||||
"moment-jalaali": "^0.9.4",
|
||||
"prop-types": "^15.8.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-ga": "^3.3.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-toast-notifications": "^2.5.1",
|
||||
"recharts": "^2.1.8",
|
||||
"redux": "^4.1.2",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"trendline": "^0.3.1",
|
||||
"web-vitals": "^2.1.2",
|
||||
"workbox-cacheable-response": "^6.5.3",
|
||||
"workbox-core": "^6.5.3",
|
||||
"workbox-expiration": "^6.5.3",
|
||||
"workbox-precaching": "^6.5.3",
|
||||
"workbox-routing": "^6.5.3",
|
||||
"workbox-strategies": "^6.5.3",
|
||||
"workbox-window": "^6.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.16.5",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.6",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^25.3.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"faker": "^5.5.3",
|
||||
"postcss": "^8.4.5",
|
||||
"prettier": "^2.6.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"tailwindcss": "^3.0.8",
|
||||
"tailwindcss-rtl": "^0.8.0"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
78
public/assets/css/fontiran.css
Normal file
78
public/assets/css/fontiran.css
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
*
|
||||
* Name: IRAN Sans-Serif Font
|
||||
* Version: 5.0
|
||||
* Author: Moslem Ebrahimi (moslemebrahimi.com)
|
||||
* Created on: Dec 25, 2012
|
||||
* Updated on: Sep 01, 2017
|
||||
* Website: http://fontiran.com
|
||||
* Copyright: Commercial/Proprietary Software
|
||||
--------------------------------------------------------------------------------------
|
||||
فونت های ایران سن سریف یک نرم افزار مالکیتی محسوب می شود. جهت آگاهی از قوانین استفاده از این فونت ها لطفا به وب سایت (فونت ایران دات کام) مراجعه نمایید
|
||||
--------------------------------------------------------------------------------------
|
||||
IRAN Sans-serif fonts are considered a proprietary software. To gain information about the laws regarding the use of these fonts, please visit www.fontiran.com
|
||||
--------------------------------------------------------------------------------------
|
||||
This set of fonts are used in this project under the license: (GY3QV)
|
||||
--------------------------------------------------------------------------------------
|
||||
*
|
||||
**/
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Black.eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Black.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum)_Black.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum)_Black.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum)_Black.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Bold.eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Bold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum)_Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Medium.eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Medium.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum)_Medium.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Light.eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_Light.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum)_Light.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum)_Light.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum)_Light.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: IRANSans;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum).eot');
|
||||
src: url('../fonts/eot/IRANSansWeb(FaNum).eot?#iefix') format('embedded-opentype'), /* IE6-8 */
|
||||
url('../fonts/woff2/IRANSansWeb(FaNum).woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/
|
||||
url('../fonts/woff/IRANSansWeb(FaNum).woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||
url('../fonts/ttf/IRANSansWeb(FaNum).ttf') format('truetype');
|
||||
}
|
||||
71
public/assets/css/style.css
Normal file
71
public/assets/css/style.css
Normal file
@@ -0,0 +1,71 @@
|
||||
@import url(./fontiran.css);
|
||||
|
||||
body {
|
||||
font-family: IRANSans !important;
|
||||
font-weight: 300;
|
||||
direction: rtl;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, text, strong, input, textarea {
|
||||
font-family: IRANSans !important;
|
||||
}
|
||||
|
||||
div, span, a, label, button {
|
||||
font-family: IRANSans !important;
|
||||
}
|
||||
|
||||
.preloader-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meter {
|
||||
height: 10px;
|
||||
width: 172px;
|
||||
background: #f3efe6;
|
||||
direction: ltr;
|
||||
border-radius: 5px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 5px;
|
||||
animation: progressBar 1.2s ease-in-out infinite;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes progressBar {
|
||||
0% {
|
||||
width: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
49.99% {
|
||||
width: 100%;
|
||||
background-color: #00B1DA;
|
||||
}
|
||||
50.01% {
|
||||
width: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
background-color: #FB8928
|
||||
}
|
||||
}
|
||||
|
||||
.leading-0 {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.plain-text {
|
||||
unicode-bidi: plaintext;
|
||||
unicode-bidi: -moz-plaintext;
|
||||
}
|
||||
1
public/assets/css/style.min.css
vendored
Normal file
1
public/assets/css/style.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import url(./fontiran.css);@keyframes progressBar{0%,50.01%{width:0;background-color:transparent}49.99%{width:100%;background-color:#00b1da}to{width:100%;background-color:#fb8928}}body{font-weight:300;direction:rtl;margin:0}a,body,button,div,h1,h2,h3,h4,h5,h6,input,label,span,strong,text,textarea{font-family:IRANSans!important}.preloader-container{width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;z-index:9999;flex-direction:column}.meter{height:10px;width:172px;background:#f3efe6;direction:ltr;border-radius:5px;margin:12px 0}.progress{height:100%;display:block;border-radius:5px;animation:progressBar 1.2s ease-in-out infinite;animation-fill-mode:both}.leading-0{line-height:0}.plain-text{unicode-bidi:plaintext;unicode-bidi:-moz-plaintext}
|
||||
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum).eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum).eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Black.eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Black.eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Bold.eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Bold.eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Light.eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Light.eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Medium.eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_Medium.eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot
Normal file
BIN
public/assets/fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum).ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum).ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Black.ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Black.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Light.ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Light.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf
Normal file
BIN
public/assets/fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum).woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum).woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Black.woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Black.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Bold.woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Bold.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Light.woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Light.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Medium.woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_Medium.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff
Normal file
BIN
public/assets/fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum).woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum).woff2
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Black.woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Black.woff2
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Light.woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Light.woff2
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2
Normal file
Binary file not shown.
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2
Normal file
BIN
public/assets/fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2
Normal file
Binary file not shown.
12
public/browserconfig.xml
Normal file
12
public/browserconfig.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="https://medmoshaver.com/icons/ms/icon-70.png"/>
|
||||
<square144x144logo src="https://medmoshaver.com/icons/ms/icon-144.png"/>
|
||||
<square150x150logo src="https://medmoshaver.com/icons/ms/icon-150.png"/>
|
||||
<square310x310logo src="https://medmoshaver.com/icons/ms/icon-310.png"/>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
158
public/index.html
Normal file
158
public/index.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='fa'>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta content='IE=edge' http-equiv='X-UA-Compatible'>
|
||||
<meta
|
||||
content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no'
|
||||
name='viewport'
|
||||
>
|
||||
<meta title='پنل فلش کارت گروه مدمشاور'>
|
||||
<meta
|
||||
content='گروه مد مشاور متشکل از تعدادی از رتبههای برتر آزمون دستیاری میباشد
|
||||
که مشاوره تحصیلی در زمینه آزمون دستیاری و پیش کارورزی ارائه میدهد.'
|
||||
name='description'
|
||||
>
|
||||
<title>پنل فلش کارت گروه مد مشاور</title>
|
||||
|
||||
<!-- Android -->
|
||||
<meta content='#fafafa' name='theme-color'>
|
||||
<meta content='yes' name='mobile-web-app-capable'>
|
||||
|
||||
<!-- iOS -->
|
||||
<meta content='پنل فلش کارت گروه مد مشاور' name='apple-mobile-web-app-title'>
|
||||
<meta content='yes' name='apple-mobile-web-app-capable'>
|
||||
<meta content='default' name='apple-mobile-web-app-status-bar-style'>
|
||||
|
||||
<!-- Windows -->
|
||||
<meta content='#fafafa' name='msapplication-navbutton-color'>
|
||||
<meta content='#fafafa' name='msapplication-TileColor'>
|
||||
<meta content='https://medmoshaver.com/icons/ms/icon-144.png' name='msapplication-TileImage'>
|
||||
<meta content='%PUBLIC_URL%/browserconfig.xml' name='msapplication-config'>
|
||||
|
||||
<!-- Pinned Sites -->
|
||||
<meta content='Application Name' name='application-name'>
|
||||
<meta content='Tooltip Text' name='msapplication-tooltip'>
|
||||
<meta content='/' name='msapplication-starturl'>
|
||||
|
||||
<!-- Tap highlighting -->
|
||||
<meta content='no' name='msapplication-tap-highlight'>
|
||||
|
||||
<!-- UC Mobile Browser -->
|
||||
<meta content='yes' name='full-screen'>
|
||||
<meta content='application' name='browsermode'>
|
||||
|
||||
<!-- Disable night mode for this page -->
|
||||
<meta content='enable/disable' name='nightmode'>
|
||||
|
||||
<!-- Fitscreen -->
|
||||
<!-- <meta name="viewport" content="uc-fitscreen=yes"/>-->
|
||||
|
||||
<!-- Layout mode -->
|
||||
<meta content='fitscreen/standard' name='layoutmode'>
|
||||
|
||||
<!-- imagemode - show image even in text only mode -->
|
||||
<meta content='force' name='imagemode'>
|
||||
|
||||
<!-- Orientation -->
|
||||
<meta content='portrait' name='screen-orientation'>
|
||||
|
||||
<!-- open graph markup -->
|
||||
<meta content='website' property='og:type' />
|
||||
<meta content='Med Moshaver' property='og:site_name' />
|
||||
<meta content='https://flashcard.medmoshaver.com' property='og:url' />
|
||||
<meta content='مد مشاور - مشاوره تحصیلی در زمینه آزمون دستیاری و پیش کارورزی'
|
||||
property='og:title' />
|
||||
<meta content='https://medmoshaver.com/icons/open-graph.png'
|
||||
property='og:image' />
|
||||
<meta content='image/png' property='og:image:type' />
|
||||
<meta content='1200' property='og:image:width' />
|
||||
<meta content='630' property='og:image:height' />
|
||||
<meta
|
||||
content='گروه مد مشاور متشکل از تعدادی از رتبههای برتر آزمون دستیاری میباشد
|
||||
که مشاوره تحصیلی در زمینه آزمون دستیاری و پیش کارورزی ارائه میدهد.'
|
||||
property='og:description'
|
||||
/>
|
||||
|
||||
<!-- Main Link Tags -->
|
||||
<link href='https://medmoshaver.com/icons/favicon/favicon-16.png' rel='icon' sizes='16x16'
|
||||
type='image/png'>
|
||||
<link href='https://medmoshaver.com/icons/favicon/favicon-32.png' rel='icon' sizes='32x32'
|
||||
type='image/png'>
|
||||
<link href='https://medmoshaver.com/icons/favicon/favicon-48.png' rel='icon' sizes='48x48'
|
||||
type='image/png'>
|
||||
<link href='https://medmoshaver.com/icons/favicon/favicon-96.png' rel='icon' sizes='96x96'
|
||||
type='image/png'>
|
||||
|
||||
<!-- iOS -->
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon.png' rel='apple-touch-icon'>
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-76.png' rel='apple-touch-icon' sizes='76x76'>
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-120.png' rel='apple-touch-icon'
|
||||
sizes='120x120'>
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-152.png' rel='apple-touch-icon'
|
||||
sizes='152x152'>
|
||||
|
||||
<!-- Startup Image -->
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-320x480.png' rel='apple-touch-startup-image'>
|
||||
|
||||
<!-- Pinned Tab -->
|
||||
<link color='#fafafa' href='https://medmoshaver.com/icons/icon.svg' rel='mask-icon' size='any'>
|
||||
|
||||
<!-- Android -->
|
||||
<link href='https://medmoshaver.com/icons/android/icon-192.png' rel='icon' sizes='192x192'>
|
||||
<link href='https://medmoshaver.com/icons/android/icon-128.png' rel='icon' sizes='128x128'>
|
||||
|
||||
<!-- Others -->
|
||||
<link href='https://medmoshaver.com/icons/favicon/favicon.ico' rel='shortcut icon'
|
||||
type='image/x-icon'>
|
||||
|
||||
<!-- UC Browser -->
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-57.png' rel='apple-touch-icon-precomposed'
|
||||
sizes='57x57'>
|
||||
<link href='https://medmoshaver.com/icons/iOS/icon-72.png' rel='apple-touch-icon' sizes='72x72'>
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href='%PUBLIC_URL%/manifest.json' rel='manifest' />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link href='%PUBLIC_URL%/assets/css/style.min.css' rel='stylesheet' />
|
||||
|
||||
</head>
|
||||
<body dir='rtl'>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id='root'>
|
||||
<div class='preloader-container'>
|
||||
<img
|
||||
alt='logo'
|
||||
class='preloader-content'
|
||||
src='https://medmoshaver.com/icons/icon.svg'
|
||||
width='168px'
|
||||
height='168px'
|
||||
/>
|
||||
<div class='meter'>
|
||||
<span class='progress'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
0
public/lib/.gitkeep
Normal file
0
public/lib/.gitkeep
Normal file
83
public/manifest.json
Normal file
83
public/manifest.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"short_name": "Medmoshaver FlashCard Panel",
|
||||
"name": "Medmoshaver FlashCard Panel",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"density": "0.75",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"density": "1.0",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"density": "8.0",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "https://medmoshaver.com/icons/pwa/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#fafafa",
|
||||
"background_color": "#fafafa"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
BIN
public/voices/reset-guide.mp3
Normal file
BIN
public/voices/reset-guide.mp3
Normal file
Binary file not shown.
0
src/App.css
Normal file
0
src/App.css
Normal file
75
src/App.js
Normal file
75
src/App.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { lazy, Suspense, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import ReactGA from 'react-ga';
|
||||
import HomePage from './components/home';
|
||||
import LoginPage from './components/login';
|
||||
import RegisterPage from './components/register';
|
||||
import CardPage from './components/card';
|
||||
import LastReviewPage from './components/last-review';
|
||||
import CheckCardPage from './components/check-card';
|
||||
import DecksPage from './components/decks';
|
||||
import StorePage from './components/store';
|
||||
import OrderPage from './components/order';
|
||||
import PaymentVerifyPage from './components/payment-verify';
|
||||
import ForgetPasswordPage from './components/forget-password';
|
||||
import RankingPage from './components/ranking';
|
||||
import TermsAndConditionsPage from './components/termsAndConditions';
|
||||
import DefaultPage from './components/default';
|
||||
import TopStudentScoresPage from './components/top-scores';
|
||||
import Loader from './components/shared/loader';
|
||||
|
||||
ReactGA.initialize(process.env.REACT_APP_GA_TRACKING_ID);
|
||||
|
||||
import('moment-jalaali').then(moment => {
|
||||
moment.default.loadPersian({
|
||||
usePersianDigits: false,
|
||||
dialect: 'persian-modern'
|
||||
});
|
||||
});
|
||||
|
||||
const ScoreChartPage = lazy(() => import('./components/score-chart'));
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(window.location.pathname + window.location.search);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Router basename={process.env.PUBLIC_URL}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={HomePage} />
|
||||
<Route exact path='/login' component={LoginPage} />
|
||||
<Route exact path='/register' component={RegisterPage} />
|
||||
<Route
|
||||
exact
|
||||
path='/cards/ch-:chapterId/:cardType(review|new|archived)'
|
||||
component={CardPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path='/reviews/last/ch-:chapterId/:type(review|new)'
|
||||
component={LastReviewPage}
|
||||
/>
|
||||
<Route exact path='/cards/:cardId/preview' component={CheckCardPage} />
|
||||
<Route exact path='/decks' component={DecksPage} />
|
||||
<Route exact path='/store' component={StorePage} />
|
||||
<Route exact path='/orders/:orderId' component={OrderPage} />
|
||||
<Route exact path='/payments/:paymentId/verify' component={PaymentVerifyPage} />
|
||||
<Route exact path='/forget-password' component={ForgetPasswordPage} />
|
||||
<Route exact path='/ranking/:rankType?' component={RankingPage} />
|
||||
<Route
|
||||
exact
|
||||
path='/top-student-scores/:rankType(weekly|monthly)/:number'
|
||||
component={TopStudentScoresPage}
|
||||
/>
|
||||
<Route exact path='/charts/:scoreType(weekly|monthly)' component={ScoreChartPage} />
|
||||
<Route exact path='/terms-and-conditions' component={TermsAndConditionsPage} />
|
||||
<Route path='*' component={DefaultPage} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
0
src/App.min.css
vendored
Normal file
0
src/App.min.css
vendored
Normal file
18
src/components/card/answer.js
Normal file
18
src/components/card/answer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Answer(props) {
|
||||
return (
|
||||
<Segment id='answer' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>جواب:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.answer }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Answer.propTypes = {
|
||||
answer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Answer;
|
||||
55
src/components/card/card.js
Normal file
55
src/components/card/card.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import OptionContainer from './option-container';
|
||||
import Question from './question';
|
||||
import Answer from './answer';
|
||||
import Reviews from './reviews';
|
||||
import Header from './header';
|
||||
import Footer from './footer';
|
||||
import { CORRECT_COLOR, DEFAULT_COLOR } from './utils';
|
||||
import { updateStudentCardRate } from '../../redux/actions/student-card';
|
||||
|
||||
function Card(props) {
|
||||
const { card } = props;
|
||||
const [color, setColor] = useState(DEFAULT_COLOR);
|
||||
const initialRate = props.card.studentCard?.rate || 0;
|
||||
const [rate, setRate] = useState(initialRate);
|
||||
|
||||
useEffect(async () => {
|
||||
if (rate !== initialRate && card.studentCard) {
|
||||
await props.updateStudentCardRate(card.studentCard.id, rate);
|
||||
}
|
||||
}, [rate]);
|
||||
|
||||
return (
|
||||
<div className='mb-20'>
|
||||
<Header chapter={props.chapter} lessonName={props.card.lesson.name} />
|
||||
<Question
|
||||
cardId={card.id}
|
||||
question={card.question}
|
||||
color={color}
|
||||
rate={rate}
|
||||
setRate={setRate}
|
||||
/>
|
||||
<OptionContainer
|
||||
cardId={card.id}
|
||||
options={card.options}
|
||||
setColor={setColor}
|
||||
rate={rate}
|
||||
rateUpdated={rate !== initialRate}
|
||||
/>
|
||||
{color !== DEFAULT_COLOR && <Reviews color={color} />}
|
||||
{color !== DEFAULT_COLOR && <Answer answer={card.answer} color={color} />}
|
||||
<Footer isAnyOptionSelected={color !== DEFAULT_COLOR} showArchive={color === CORRECT_COLOR} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Card.propTypes = {
|
||||
card: PropTypes.object.isRequired,
|
||||
chapter: PropTypes.object.isRequired,
|
||||
updateStudentCardRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { updateStudentCardRate })(Card);
|
||||
32
src/components/card/container.js
Normal file
32
src/components/card/container.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Card from './card';
|
||||
import Error from './error';
|
||||
import MultiFetcher from '../shared/fetches/multi-fetcher';
|
||||
import { fetchNextCard } from '../../redux/actions/card';
|
||||
import { fetchChapter } from '../../redux/actions/chapter';
|
||||
|
||||
function CardsContainer() {
|
||||
const { chapterId, cardType } = useParams();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const priority = urlSearchParams.get('priority');
|
||||
const rateType = urlSearchParams.get('rate-type');
|
||||
const minRate = rateType ? { starred: 1, active: 0 }[rateType] : null;
|
||||
|
||||
return (
|
||||
<MultiFetcher
|
||||
actions={[
|
||||
fetchNextCard.bind(null, chapterId, cardType, priority, minRate),
|
||||
fetchChapter.bind(null, chapterId, priority, minRate)
|
||||
]}
|
||||
stateSelectors={{
|
||||
card: state => state.card,
|
||||
chapter: state => state.chapter
|
||||
}}
|
||||
renderError={error => <Error error={error} />}
|
||||
>
|
||||
{({ card, chapter }) => <Card card={card} chapter={chapter} />}
|
||||
</MultiFetcher>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardsContainer;
|
||||
80
src/components/card/error.js
Normal file
80
src/components/card/error.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
import ErrorComponent from '../shared/error';
|
||||
|
||||
function Error(props) {
|
||||
const { cardType } = useParams();
|
||||
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const priority = urlSearchParams.get('priority');
|
||||
const rateType = urlSearchParams.get('rate-type');
|
||||
|
||||
const { error } = props;
|
||||
const backURL = `/decks?${urlSearchParams.toString()}`;
|
||||
|
||||
if (error.card && error.card.status === 404) {
|
||||
switch (cardType) {
|
||||
case 'review':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='my-4 text-center text-xl'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'مروری '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
{_(rateType) && <span className='font-bold'>{_(rateType) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-row justify-center'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
|
||||
case 'new':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='text-center'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'جدید '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-col items-center'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
|
||||
case 'archived':
|
||||
return (
|
||||
<Segment>
|
||||
<p className='text-center'>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-bold'>{'آرشیو شده '}</span>
|
||||
{_(priority) && <span className='font-bold'>{_(priority) + ' '}</span>}
|
||||
{_(rateType) && <span className='font-bold'>{_(rateType) + ' '}</span>}
|
||||
<span> دیگری یافت نشد.</span>
|
||||
</p>
|
||||
<div className='my-3 flex flex-col items-center gap-3'>
|
||||
<NavLink to={backURL} color='red' content='بازگشت' />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
function _(text) {
|
||||
return !text ? '' : { starred: 'ستاره دار', active: 'فعال', high: 'ضروری ' }[text];
|
||||
}
|
||||
|
||||
Error.propTypes = {
|
||||
error: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Error;
|
||||
98
src/components/card/footer.js
Normal file
98
src/components/card/footer.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ArrowSmLeftIcon, ArrowSmRightIcon } from '@heroicons/react/outline';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { archiveStudentCard } from '../../redux/actions/student-card';
|
||||
import ConfirmModal from '../shared/confirm-modal';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
import { DEFAULT_DECKS_ROUTE } from '../utils/index';
|
||||
|
||||
function Footer(props) {
|
||||
const history = useHistory();
|
||||
const { chapterId, cardType } = useParams();
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
let previousURL = `/reviews/last/ch-${chapterId}/${cardType}`;
|
||||
let nextURL = `/cards/ch-${chapterId}/${cardType}`;
|
||||
const urlSearchParamsSTR = new URLSearchParams(location.search).toString();
|
||||
if (urlSearchParamsSTR) {
|
||||
previousURL += `?${urlSearchParamsSTR}`;
|
||||
nextURL += `?${urlSearchParamsSTR}`;
|
||||
}
|
||||
|
||||
const archiveText = props.isPermanentArchive ? 'آرشیو' : 'آرشیو موقت';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ConfirmModal
|
||||
showModal={showConfirmModal}
|
||||
content={
|
||||
<p>
|
||||
<span>آیا از </span>
|
||||
<span className='font-medium'>{archiveText}</span>
|
||||
<span> این فلش کارت مطمئن هستید؟</span>
|
||||
</p>
|
||||
}
|
||||
onConfirm={async () => {
|
||||
await props.archiveStudentCard();
|
||||
history.push(nextURL);
|
||||
}}
|
||||
onCancel={() => setShowConfirmModal(false)}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'align-center fixed bottom-0 left-0 flex w-full flex-row-reverse justify-between bg-black bg-opacity-60'
|
||||
}
|
||||
>
|
||||
{props.isAnyOptionSelected ? (
|
||||
<Button color='red' onClick={() => history.push(DEFAULT_DECKS_ROUTE)} className='m-2'>
|
||||
بازگشت
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</Button>
|
||||
) : (
|
||||
<NavLink to={previousURL} color='red' className='m-2'>
|
||||
قبلی
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</NavLink>
|
||||
)}
|
||||
{props.showArchive && (
|
||||
<Button
|
||||
color={props.isPermanentArchive ? 'indigo' : 'blue'}
|
||||
onClick={() => setShowConfirmModal(true)}
|
||||
className='m-2'
|
||||
>
|
||||
{props.isPermanentArchive ? 'آرشیو' : 'آرشیو موقت'}
|
||||
</Button>
|
||||
)}
|
||||
<NavLink
|
||||
replace
|
||||
to={nextURL}
|
||||
color='green'
|
||||
textColor='white'
|
||||
disabled={!props.isAnyOptionSelected}
|
||||
className='m-2'
|
||||
>
|
||||
<ArrowSmRightIcon width={20} height={20} className='stroke-current text-white' />
|
||||
بعدی
|
||||
</NavLink>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
isAnyOptionSelected: PropTypes.bool.isRequired,
|
||||
showArchive: PropTypes.bool.isRequired,
|
||||
archiveStudentCard: PropTypes.func.isRequired,
|
||||
isPermanentArchive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ review }, { showArchive }) {
|
||||
return {
|
||||
isPermanentArchive: !!review.data.studentCard && review.data.studentCard.status === 1,
|
||||
showArchive: showArchive && !!review.data.studentCard && review.data.studentCard.status !== 2
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { archiveStudentCard })(Footer);
|
||||
38
src/components/card/header.js
Normal file
38
src/components/card/header.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Segment from '../shared/segment';
|
||||
import PropTypes from 'prop-types';
|
||||
import useQueryParams from '../../hooks/query-params';
|
||||
|
||||
function Header(props) {
|
||||
const { cardType } = useParams();
|
||||
const { priority } = useQueryParams();
|
||||
|
||||
return (
|
||||
<Segment textAlign='center' color={color[cardType]} backgroundColor={color[cardType]}>
|
||||
<div className='flex flex-wrap items-center justify-around gap-3 xl:justify-between'>
|
||||
<div>
|
||||
<span>فلش کارتهای </span>
|
||||
{priority && <span className='font-medium text-red-600'>{_[priority] + ' '}</span>}
|
||||
<span className={`font-medium text-${color[cardType]}-600`}>{_[cardType]}</span>
|
||||
<span> درس </span>
|
||||
<span className='font-medium'>{props.lessonName}</span>
|
||||
<span> بخش </span>
|
||||
<span className='font-medium'>{props.chapter.name}</span>
|
||||
</div>
|
||||
<div className={`rounded-full bg-${color[cardType]}-500 py-1 px-2.5 text-white`}>
|
||||
<span className='font-medium'>{props.chapter[`${cardType}CardCount`]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
const _ = { review: 'مروری', new: 'جدید', archived: 'آرشیو شده', high: 'ضروری ' };
|
||||
const color = { review: 'blue', new: 'green', archived: 'yellow' };
|
||||
|
||||
Header.propTypes = {
|
||||
lessonName: PropTypes.string.isRequired,
|
||||
chapter: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Header;
|
||||
26
src/components/card/icons/hide.js
Normal file
26
src/components/card/icons/hide.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { LightBulbIcon as OutlineLightBulbIcon } from '@heroicons/react/outline';
|
||||
import { LightBulbIcon as SolidLightBulbIcon } from '@heroicons/react/solid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function HideIcon({ hide, onUpdate }) {
|
||||
return hide ? (
|
||||
<OutlineLightBulbIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-gray-300 hover:text-gray-400'
|
||||
/>
|
||||
) : (
|
||||
<SolidLightBulbIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-yellow-300 hover:text-yellow-400'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
HideIcon.propTypes = {
|
||||
hide: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HideIcon;
|
||||
22
src/components/card/icons/index.js
Normal file
22
src/components/card/icons/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import StarIcon from './star';
|
||||
import HideIcon from './hide';
|
||||
import ReportIcon from './report';
|
||||
|
||||
function Icons({ cardId, rate, setRate }) {
|
||||
return (
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
{rate >= 0 && <StarIcon star={rate > 0} onUpdate={() => setRate(rate => 1 - rate)} />}
|
||||
{rate <= 0 && <HideIcon hide={rate < 0} onUpdate={() => setRate(rate => -1 - rate)} />}
|
||||
<ReportIcon cardId={cardId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icons.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
rate: PropTypes.number,
|
||||
setRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Icons;
|
||||
27
src/components/card/icons/report.js
Normal file
27
src/components/card/icons/report.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import ExclamationIcon from '@heroicons/react/outline/ExclamationIcon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import ReportModal from '../report-modal';
|
||||
|
||||
function ReportIcon({ cardId }) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExclamationIcon
|
||||
width={30}
|
||||
onClick={() => setShowModal(!showModal)}
|
||||
className='text-orange-400 hover:text-orange-500'
|
||||
/>
|
||||
{showModal && (
|
||||
<ReportModal cardId={cardId} showModal={showModal} onClose={() => setShowModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReportIcon.propTypes = {
|
||||
cardId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default ReportIcon;
|
||||
22
src/components/card/icons/star.js
Normal file
22
src/components/card/icons/star.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { StarIcon as OutlineStarIcon } from '@heroicons/react/outline';
|
||||
import { StarIcon as SolidStarIcon } from '@heroicons/react/solid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function StarIcon({ star, onUpdate }) {
|
||||
return star ? (
|
||||
<SolidStarIcon
|
||||
width={30}
|
||||
onClick={onUpdate}
|
||||
className='text-yellow-300 hover:text-yellow-400'
|
||||
/>
|
||||
) : (
|
||||
<OutlineStarIcon width={30} onClick={onUpdate} className='text-gray-300 hover:text-gray-400' />
|
||||
);
|
||||
}
|
||||
|
||||
StarIcon.propTypes = {
|
||||
star: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StarIcon;
|
||||
12
src/components/card/index.js
Normal file
12
src/components/card/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
import CardsContainer from './container';
|
||||
|
||||
function CardPage() {
|
||||
return (
|
||||
<MainLayout withFooter>
|
||||
<CardsContainer />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardPage;
|
||||
60
src/components/card/option-container.js
Normal file
60
src/components/card/option-container.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Option from './option';
|
||||
import { createReview } from '../../redux/actions/review';
|
||||
import { CORRECT_COLOR, DONT_KNOW_COLOR, WRONG_COLOR } from './utils';
|
||||
import { updateStudentCardRate } from '../../redux/actions/student-card';
|
||||
|
||||
class OptionContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedOptionId: null,
|
||||
options: [...props.options, { id: 0, text: 'نمی دانم', isCorrect: false }]
|
||||
};
|
||||
}
|
||||
|
||||
handleSelect = async optionId => {
|
||||
if (this.state.selectedOptionId !== null) return null;
|
||||
this.setState({ selectedOptionId: optionId });
|
||||
if (optionId !== 0) this.setState({ options: [...this.props.options] });
|
||||
const { payload: review } = await this.props.createReview(this.props.cardId, optionId);
|
||||
if (this.props.rateUpdated) {
|
||||
await this.props.updateStudentCardRate(review.studentCard.id, this.props.rate);
|
||||
}
|
||||
this.setState({ selectedOptionId: optionId });
|
||||
this.props.setColor(
|
||||
optionId ? (review.status === 1 ? CORRECT_COLOR : WRONG_COLOR) : DONT_KNOW_COLOR
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id='options' className='space-y-3 px-4'>
|
||||
{this.state.options.map((option, index) => (
|
||||
<Option
|
||||
key={option.id}
|
||||
option={option}
|
||||
index={index}
|
||||
isAnyOptionSelected={this.state.selectedOptionId !== null}
|
||||
isSelectedOption={this.state.selectedOptionId === option.id}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionContainer.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
setColor: PropTypes.func.isRequired,
|
||||
createReview: PropTypes.func.isRequired,
|
||||
rate: PropTypes.number,
|
||||
rateUpdated: PropTypes.bool.isRequired,
|
||||
updateStudentCardRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { createReview, updateStudentCardRate })(OptionContainer);
|
||||
79
src/components/card/option.js
Normal file
79
src/components/card/option.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/solid';
|
||||
import Button from '../shared/buttons/button';
|
||||
|
||||
function Option(props) {
|
||||
const { option, isAnyOptionSelected, isSelectedOption } = props;
|
||||
const color =
|
||||
isAnyOptionSelected && option.isCorrect
|
||||
? 'green'
|
||||
: isAnyOptionSelected && isSelectedOption && option.id
|
||||
? 'red'
|
||||
: option.id
|
||||
? 'gray'
|
||||
: 'yellow';
|
||||
|
||||
return (
|
||||
<Button
|
||||
id={`option ${option.id}`}
|
||||
outline={!isAnyOptionSelected}
|
||||
color={color}
|
||||
colorWeight={300}
|
||||
borderWeight={2}
|
||||
disabled={isAnyOptionSelected}
|
||||
textColor='gray'
|
||||
textColorWeight='600'
|
||||
size='xl'
|
||||
justifyContent={option.id ? 'start' : 'between'}
|
||||
onClick={() => props.onSelect(option.id)}
|
||||
className={classNames('w-full', { [`border-2 border-${color}-300`]: isAnyOptionSelected })}
|
||||
>
|
||||
<span className={classNames('flex', { 'mx-auto': !option.id })}>
|
||||
{option.id !== 0 && (
|
||||
<span className='shrink-0 font-semibold'>{`گزینه ${props.index + 1}:`}</span>
|
||||
)}
|
||||
<span className={classNames('mx-4 text-justify', { 'font-semibold': !option.id })}>
|
||||
{option.text}
|
||||
</span>
|
||||
</span>
|
||||
{isSelectedOption &&
|
||||
(!option.id ? (
|
||||
<ExclamationIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 stroke-current text-yellow-600`}
|
||||
/>
|
||||
) : option.isCorrect ? (
|
||||
<CheckIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 rounded-lg
|
||||
border-2 border-green-500
|
||||
stroke-current text-green-500 ms-auto`}
|
||||
/>
|
||||
) : (
|
||||
<XIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={classNames(
|
||||
`shrink-0 rounded-lg
|
||||
border-2 border-red-500
|
||||
stroke-current text-red-500 ms-auto`
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isAnyOptionSelected: PropTypes.bool.isRequired,
|
||||
isSelectedOption: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Option;
|
||||
32
src/components/card/question.js
Normal file
32
src/components/card/question.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import { DEFAULT_COLOR } from './utils';
|
||||
import Icons from './icons';
|
||||
|
||||
function Question(props) {
|
||||
return (
|
||||
<Segment id='question' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<div>
|
||||
<div className='mb-4 flex w-full items-center '>
|
||||
<div className='flex-1 font-semibold'>سوال:</div>
|
||||
<div className='col-span-1'>
|
||||
<Icons cardId={props.cardId} rate={props.rate} setRate={props.setRate} />
|
||||
</div>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.question }} />
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Question.defaultProps = { color: DEFAULT_COLOR };
|
||||
|
||||
Question.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
question: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
rate: PropTypes.number,
|
||||
setRate: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Question;
|
||||
96
src/components/card/report-modal.js
Normal file
96
src/components/card/report-modal.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../shared/modal';
|
||||
import FormSelect from '../shared/forms/select';
|
||||
import FormTextArea from '../shared/forms/textarea';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { required } from '../shared/forms/validations';
|
||||
import { createReport } from '../../redux/actions/report';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import { useState } from 'react';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function ReportModal(props) {
|
||||
const [error, setError] = useState('');
|
||||
const { addToast } = useToasts();
|
||||
|
||||
async function submit(values) {
|
||||
try {
|
||||
setError('');
|
||||
await props.createReport(values);
|
||||
addToast('با تشکر، گزارش خطای شما ثبت گردید.', { appearance: 'success' });
|
||||
props.onClose();
|
||||
} catch (e) {
|
||||
setError(e.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={props.showModal}
|
||||
onClose={props.onClose}
|
||||
header='فرم گزارش خطا'
|
||||
body={
|
||||
<Formik initialValues={{ card: props.cardId, type: '', description: '' }} onSubmit={submit}>
|
||||
<Form id='report-form' className='flex flex-col gap-3'>
|
||||
{error && (
|
||||
<Segment
|
||||
color='red'
|
||||
backgroundColor='red'
|
||||
className='text-center font-medium text-red-500'
|
||||
>
|
||||
<p>{error}</p>
|
||||
<p>لطفا مدتی بعد مجددا تلاش کنید.</p>
|
||||
</Segment>
|
||||
)}
|
||||
<input type='hidden' id='card' name='card' />
|
||||
<Field
|
||||
required
|
||||
name='type'
|
||||
label='نوع خطا'
|
||||
component={FormSelect}
|
||||
validate={required}
|
||||
options={types}
|
||||
/>
|
||||
<Field
|
||||
required
|
||||
name='description'
|
||||
label='توضیحات'
|
||||
component={FormTextArea}
|
||||
validate={required}
|
||||
rows={5}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
}
|
||||
footer={
|
||||
<div className='mx-3 flex justify-between gap-2'>
|
||||
<Button
|
||||
type='submit'
|
||||
form='report-form'
|
||||
color='green'
|
||||
content='ثبت گزارش'
|
||||
className='grow'
|
||||
/>
|
||||
<Button color='red' content='بازگشت' onClick={props.onClose} className='grow' />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const types = [
|
||||
{ name: 'Scientific', text: 'علمی', value: 'Scientific' },
|
||||
{ name: 'Typo', text: 'نگارشی', value: 'Typo' },
|
||||
{ name: 'Other', text: 'سایر', value: 'Other' }
|
||||
];
|
||||
|
||||
ReportModal.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
showModal: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
createReport: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { createReport })(ReportModal);
|
||||
77
src/components/card/reviews.js
Normal file
77
src/components/card/reviews.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/solid';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Reviews(props) {
|
||||
const { color, studentCard } = props;
|
||||
return (
|
||||
<Segment id='history' color={color} colorWeight={300} borderWeight={2}>
|
||||
<div className='flex flex-wrap justify-around gap-4'>
|
||||
<p>
|
||||
<span className='font-medium'>شناسه فلش کارت: </span>
|
||||
{studentCard.card}
|
||||
</p>
|
||||
{studentCard.status > 0 && (
|
||||
<p>
|
||||
<span className='font-medium'>وضعیت: </span>
|
||||
{statuses[studentCard.status]}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<span className='font-medium'>مرور شماره: </span>
|
||||
{studentCard.reviewCount}
|
||||
</p>
|
||||
<div className='flex flex-wrap justify-center gap-x-2 gap-y-4'>
|
||||
<span className='font-medium'>نتایج: </span>
|
||||
<div>
|
||||
{studentCard.reviews.map(review => (
|
||||
<span key={review.id} className='px-px'>
|
||||
{!review.status ? (
|
||||
<ExclamationIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`inline-flex shrink-0 stroke-current text-yellow-500`}
|
||||
/>
|
||||
) : review.status === 1 ? (
|
||||
<CheckIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`inline-flex shrink-0 rounded-lg
|
||||
border-2 border-green-500
|
||||
stroke-current text-green-500 ms-auto`}
|
||||
/>
|
||||
) : (
|
||||
<XIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={classNames(
|
||||
`inline-flex shrink-0 rounded-lg
|
||||
border-2 border-red-500
|
||||
stroke-current text-red-500 ms-auto`
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
const statuses = ['عادی', 'آرشیو موقت', 'آرشیو دائم'];
|
||||
|
||||
Reviews.propTypes = {
|
||||
studentCard: PropTypes.object.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ review }) {
|
||||
return { studentCard: review.data.studentCard };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Reviews);
|
||||
4
src/components/card/utils.js
Normal file
4
src/components/card/utils.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const DEFAULT_COLOR = 'gray';
|
||||
export const CORRECT_COLOR = 'green';
|
||||
export const WRONG_COLOR = 'red';
|
||||
export const DONT_KNOW_COLOR = 'yellow';
|
||||
18
src/components/check-card/answer.js
Normal file
18
src/components/check-card/answer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import { DEFAULT_COLOR } from './utils';
|
||||
|
||||
function Answer(props) {
|
||||
return (
|
||||
<Segment id='answer' color={DEFAULT_COLOR} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>جواب:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.answer }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Answer.propTypes = {
|
||||
answer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired
|
||||
};
|
||||
|
||||
export default Answer;
|
||||
30
src/components/check-card/card.js
Normal file
30
src/components/check-card/card.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Header from './header';
|
||||
import Question from './question';
|
||||
import OptionContainer from './option-container';
|
||||
import Answer from './answer';
|
||||
import Footer from './footer';
|
||||
|
||||
function Card(props) {
|
||||
const { card } = props;
|
||||
|
||||
return (
|
||||
<div className='mb-20'>
|
||||
<Header
|
||||
cardId={card.id}
|
||||
chapterName={props.card.chapter.name}
|
||||
lessonName={props.card.lesson.name}
|
||||
/>
|
||||
<Question question={card.question} />
|
||||
<OptionContainer cardId={card.id} options={card.options} />
|
||||
<Answer answer={card.answer} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Card.propTypes = {
|
||||
card: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Card;
|
||||
20
src/components/check-card/container.js
Normal file
20
src/components/check-card/container.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Card from './card';
|
||||
import Fetcher from '../shared/fetches/fetcher';
|
||||
import { fetchCard } from '../../redux/actions/card';
|
||||
import Footer from './footer';
|
||||
|
||||
function CardsContainer() {
|
||||
const { cardId } = useParams();
|
||||
return (
|
||||
<Fragment>
|
||||
<Fetcher action={fetchCard.bind(null, cardId)} stateSelector={state => state.card}>
|
||||
{card => <Card card={card} />}
|
||||
</Fetcher>
|
||||
<Footer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardsContainer;
|
||||
48
src/components/check-card/footer.js
Normal file
48
src/components/check-card/footer.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ArrowSmLeftIcon, ArrowSmRightIcon } from '@heroicons/react/outline';
|
||||
import { fetchCard } from '../../redux/actions/card';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
|
||||
function Footer() {
|
||||
const { cardId } = useParams();
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={
|
||||
'align-center fixed bottom-0 left-0 flex w-full flex-row justify-between bg-black bg-opacity-60'
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
replace
|
||||
to={`/cards/${parseInt(cardId) + 1}/preview`}
|
||||
color='green'
|
||||
textColor='white'
|
||||
className='m-2'
|
||||
>
|
||||
<ArrowSmRightIcon width={20} height={20} className='stroke-current text-white' />
|
||||
بعدی
|
||||
</NavLink>
|
||||
{cardId > 1 && (
|
||||
<NavLink
|
||||
replace
|
||||
to={`/cards/${parseInt(cardId) - 1}/preview`}
|
||||
color='red'
|
||||
className='m-2'
|
||||
>
|
||||
قبلی
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
fetchCard: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { fetchCard })(Footer);
|
||||
26
src/components/check-card/header.js
Normal file
26
src/components/check-card/header.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Segment from '../shared/segment';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Header(props) {
|
||||
return (
|
||||
<Segment textAlign='center' color='violet' backgroundColor='violet'>
|
||||
<p>
|
||||
<span className='font-medium'>پیش نمایش </span>
|
||||
<span>فلش کارت </span>
|
||||
<span className='font-medium'>{props.cardId}</span>
|
||||
<span> درس </span>
|
||||
<span className='font-medium'>{props.lessonName}</span>
|
||||
<span> بخش </span>
|
||||
<span className='font-medium'>{props.chapterName}</span>
|
||||
</p>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
lessonName: PropTypes.string.isRequired,
|
||||
chapterName: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Header;
|
||||
10
src/components/check-card/index.js
Normal file
10
src/components/check-card/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import MainLayout from '../layouts/main';
|
||||
import CardsContainer from './container';
|
||||
|
||||
function CardPage() {
|
||||
const { isSuperuser } = useSelector(({ user }) => user.data);
|
||||
return <MainLayout withFooter>{isSuperuser ? <CardsContainer /> : null}</MainLayout>;
|
||||
}
|
||||
|
||||
export default CardPage;
|
||||
36
src/components/check-card/option-container.js
Normal file
36
src/components/check-card/option-container.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Option from './option';
|
||||
|
||||
class OptionContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
options: [
|
||||
...props.options,
|
||||
{
|
||||
id: 0,
|
||||
text: 'نمی دانم',
|
||||
isCorrect: false
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id='options' className='space-y-3 px-4'>
|
||||
{this.state.options.map((option, index) => (
|
||||
<Option key={option.id} option={option} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionContainer.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default OptionContainer;
|
||||
41
src/components/check-card/option.js
Normal file
41
src/components/check-card/option.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { CORRECT_COLOR, DONT_KNOW_COLOR, WRONG_COLOR } from './utils';
|
||||
|
||||
function Option(props) {
|
||||
const { option } = props;
|
||||
|
||||
const color = option.isCorrect ? CORRECT_COLOR : option.id ? WRONG_COLOR : DONT_KNOW_COLOR;
|
||||
return (
|
||||
<Button
|
||||
id={`option ${option.id}`}
|
||||
disabled
|
||||
outline={false}
|
||||
color={color}
|
||||
colorWeight={300}
|
||||
borderWeight={2}
|
||||
textColor='gray'
|
||||
textColorWeight='600'
|
||||
size='xl'
|
||||
justifyContent={option.id ? 'start' : 'between'}
|
||||
className={`w-full border-2 border-${color}-300`}
|
||||
>
|
||||
<span className={classNames('flex', { 'mx-auto': !option.id })}>
|
||||
{option.id !== 0 && (
|
||||
<span className='shrink-0 font-semibold'>{`گزینه ${props.index + 1}:`}</span>
|
||||
)}
|
||||
<span className={classNames('mx-4 text-justify', { 'font-semibold': !option.id })}>
|
||||
{option.text}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default Option;
|
||||
20
src/components/check-card/question.js
Normal file
20
src/components/check-card/question.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import { DEFAULT_COLOR } from './utils';
|
||||
|
||||
function Question(props) {
|
||||
return (
|
||||
<Segment id='question' color={DEFAULT_COLOR} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>سوال:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.question }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Question.defaultProps = { color: DEFAULT_COLOR };
|
||||
|
||||
Question.propTypes = {
|
||||
question: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired
|
||||
};
|
||||
|
||||
export default Question;
|
||||
4
src/components/check-card/utils.js
Normal file
4
src/components/check-card/utils.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const DEFAULT_COLOR = 'gray';
|
||||
export const CORRECT_COLOR = 'green';
|
||||
export const WRONG_COLOR = 'red';
|
||||
export const DONT_KNOW_COLOR = 'yellow';
|
||||
37
src/components/decks/badge.js
Normal file
37
src/components/decks/badge.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
function Badge(props) {
|
||||
const { color } = props;
|
||||
if (props.number === 0) return null;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={classNames(
|
||||
`bg-${color}-500 flex items-center rounded-xl px-1 py-0.5 leading-none text-white`,
|
||||
`border border-opacity-50 border-${color}-600`,
|
||||
{ 'cursor-not-allowed': !props.to }
|
||||
)}
|
||||
>
|
||||
<span className='m-1 text-xs font-medium'>{props.text}</span>
|
||||
<span className={`bg-white text-xs text-${color}-700 m-1 rounded-xl py-0.5 px-1`}>
|
||||
{props.number}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!props.to) return content;
|
||||
return <Link to={props.to}>{content}</Link>;
|
||||
}
|
||||
|
||||
Badge.defaultProps = { to: '', color: 'blue' };
|
||||
|
||||
Badge.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
number: PropTypes.number.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
90
src/components/decks/chapter-modal.js
Normal file
90
src/components/decks/chapter-modal.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../shared/modal';
|
||||
import Button from '../shared/buttons/button';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
|
||||
function ChapterModal(props) {
|
||||
const { chapter, priority, rateType } = props;
|
||||
if (!chapter) return null;
|
||||
|
||||
const noCards =
|
||||
chapter.reviewCardCount === 0 && chapter.newCardCount === 0 && chapter.archivedCardCount === 0;
|
||||
|
||||
function getURL(cardType) {
|
||||
const url = `/cards/ch-${chapter.id}/${cardType}`;
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
if (priority) urlSearchParams.set('priority', priority);
|
||||
if (cardType !== 'new' && rateType) urlSearchParams.set('rate-type', rateType);
|
||||
const urlSearchParamsSTR = urlSearchParams.toString();
|
||||
if (!urlSearchParamsSTR) return url;
|
||||
return url + `?${urlSearchParamsSTR}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal
|
||||
onClose={props.onClose}
|
||||
header='دسترسی به فلش کارتها'
|
||||
body={
|
||||
<div className='text-base'>
|
||||
<p>
|
||||
<span>شما فصل </span>
|
||||
<span className='font-medium'>{chapter.name}</span>
|
||||
<span> از درس </span>
|
||||
<span className='font-medium'>{props.lessonName}</span>
|
||||
<span> را انتخاب کرده اید.</span>
|
||||
</p>
|
||||
{noCards ? (
|
||||
<div>
|
||||
<p>تمام فلش کارت های جدید و مروری این درس را مطالعه کرده اید.</p>
|
||||
<p>مرورهای بعدی این درس در روزهای آینده فعال خواهد شد.</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
با استفاده از دکمههای زیر میتوانید به فلش کارتهای مورد نظر خود دسترسی داشته باشید.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className='flex flex-wrap justify-end gap-2'>
|
||||
{chapter.reviewCardCount > 0 && (
|
||||
<NavLink to={getURL('review')} size='sm' color='red'>
|
||||
<span className='m-1 text-xs font-medium'>مروری</span>
|
||||
<span className='m-1 rounded-xl bg-white py-0.5 px-1 text-xs text-red-800'>
|
||||
{chapter.reviewCardCount}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
{chapter.newCardCount > 0 && (
|
||||
<NavLink to={getURL('new')} size='sm' color='yellow'>
|
||||
<span className='align-middle text-xs font-medium'>جدید</span>
|
||||
<span className='m-1 rounded-xl bg-white py-0.5 px-1 text-xs text-yellow-800'>
|
||||
{chapter.newCardCount}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
{chapter.archivedCardCount > 0 && (
|
||||
<NavLink to={getURL('archived')} size='sm' color='blue'>
|
||||
<span className='text-xs font-medium'>آرشیو شده</span>
|
||||
<span className='m-1 rounded-xl bg-white py-0.5 px-1 text-xs text-blue-800'>
|
||||
{chapter.archivedCardCount}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
{noCards && <Button color='red' content='بازگشت' onClick={props.onClose} />}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ChapterModal.propTypes = {
|
||||
chapter: PropTypes.object,
|
||||
lessonName: PropTypes.string,
|
||||
priority: PropTypes.string,
|
||||
rateType: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ChapterModal;
|
||||
27
src/components/decks/container.js
Normal file
27
src/components/decks/container.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import StudentDecks from './student-decks';
|
||||
import Fetcher from '../shared/fetches/fetcher';
|
||||
import NoDeck from './no-deck';
|
||||
import { fetchStudentDecks } from '../../redux/actions/student-deck';
|
||||
|
||||
function DecksContainer() {
|
||||
const location = useLocation();
|
||||
const searchPrams = new URLSearchParams(location.search);
|
||||
const priority = searchPrams.get('priority');
|
||||
const rateType = searchPrams.get('rate-type');
|
||||
const minRate = rateType ? { starred: 1, active: 0 }[rateType] : -1;
|
||||
|
||||
return (
|
||||
<Fetcher
|
||||
action={() => fetchStudentDecks(priority, minRate)}
|
||||
stateSelector={state => state.studentDecks}
|
||||
renderEmpty={() => <NoDeck />}
|
||||
>
|
||||
{studentDecks => (
|
||||
<StudentDecks priority={priority} rateType={rateType} studentDecks={studentDecks} />
|
||||
)}
|
||||
</Fetcher>
|
||||
);
|
||||
}
|
||||
|
||||
export default DecksContainer;
|
||||
14
src/components/decks/index.js
Normal file
14
src/components/decks/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
import DecksContainer from './container';
|
||||
import DecksTab from './tab';
|
||||
|
||||
function DecksPage() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<DecksTab />
|
||||
<DecksContainer />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default DecksPage;
|
||||
27
src/components/decks/no-deck.js
Normal file
27
src/components/decks/no-deck.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import Segment from '../shared/segment';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
|
||||
function NoDeck() {
|
||||
return (
|
||||
<Segment className='leading-8'>
|
||||
<p className='text-center'>داوطلب گرامی، شما هیچ بسته فلش کارت فعالی ندارید.</p>
|
||||
<p className='text-center'>
|
||||
لطفا برای خرید بسته فلش کارت مورد نظر خود به
|
||||
<Link to='/store' className='text-blue-600'>
|
||||
{' '}
|
||||
فروشگاه{' '}
|
||||
</Link>
|
||||
مراجعه کنید.
|
||||
</p>
|
||||
<p className='text-center'>
|
||||
<span>همچنین با مراجعه به فروشگاه میتوانید از </span>
|
||||
<span className='font-medium text-pink-500'>بستههای تستی رایگان</span>
|
||||
<span> مدمشاور نیز استفاده کنید.</span>
|
||||
</p>
|
||||
<NavLink to='/store' color='green' content='فروشگاه' className='mx-auto my-4' />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoDeck;
|
||||
78
src/components/decks/reset-modal/index.js
Normal file
78
src/components/decks/reset-modal/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ResetModalStepOne from './step-one';
|
||||
import ResetModalStepTwo from './step-two';
|
||||
import ResetModalStepSuccess from './step-success';
|
||||
import { connect } from 'react-redux';
|
||||
import { createResetRequest } from '../../../redux/actions/reset-request';
|
||||
import { fetchStudentDecks } from '../../../redux/actions/student-deck';
|
||||
import ResetModalStepError from './step-error';
|
||||
|
||||
function ResetModal(props) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [error, setError] = useState(null);
|
||||
const { itemType, itemId, itemName, onClose } = props;
|
||||
|
||||
const itemLabel = `${_[itemType]} ${itemName}`;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ResetModalStepError
|
||||
itemLabel={itemLabel}
|
||||
error={error}
|
||||
onClose={() => {
|
||||
setStep(1);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 1) {
|
||||
return <ResetModalStepOne itemLabel={itemLabel} onClose={onClose} onNext={() => setStep(2)} />;
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
return (
|
||||
<ResetModalStepTwo
|
||||
onClose={onClose}
|
||||
onNext={async () => {
|
||||
try {
|
||||
await props.createResetRequest(itemType, itemId);
|
||||
setStep(0);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 0) {
|
||||
return (
|
||||
<ResetModalStepSuccess
|
||||
itemLabel={itemLabel}
|
||||
onClose={() => {
|
||||
setStep(1);
|
||||
props.fetchStudentDecks();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const _ = { chapter: 'فصل', lesson: 'درس', deck: 'بسته' };
|
||||
|
||||
ResetModal.propTypes = {
|
||||
itemType: PropTypes.oneOf(['chapter', 'lesson', 'deck']).isRequired,
|
||||
itemId: PropTypes.number.isRequired,
|
||||
itemName: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
createResetRequest: PropTypes.func.isRequired,
|
||||
fetchStudentDecks: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { createResetRequest, fetchStudentDecks })(ResetModal);
|
||||
48
src/components/decks/reset-modal/step-error.js
Normal file
48
src/components/decks/reset-modal/step-error.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../shared/modal';
|
||||
import Button from '../../shared/buttons/button';
|
||||
|
||||
function ResetModalStepError(props) {
|
||||
return (
|
||||
<Modal
|
||||
showModal
|
||||
onClose={props.onClose}
|
||||
header={<span className='text-red-400'>بازنشانی فلش کارتها</span>}
|
||||
body={
|
||||
<div className='text-base'>
|
||||
<p>
|
||||
<span>بازنشانی فلش کارت های </span>
|
||||
<span className='font-semibold'>{props.itemLabel}</span>
|
||||
<span> با </span>
|
||||
<span className='font-medium text-red-500'>خطا</span>
|
||||
<span> مواجه شد.</span>
|
||||
</p>
|
||||
<p>در صورت تکرار لطفا ادمین سایت را در جریان قرار دهید.</p>
|
||||
<div className='my-2'>
|
||||
<p>
|
||||
<span className='font-semibold'>کد خطا: </span>
|
||||
<span>{props.error.status}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className='font-semibold'>اطلاعات خطا: </span>
|
||||
<span>{JSON.stringify(props.error.data)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className='flex justify-end'>
|
||||
<Button color='red' content='بازگشت' onClick={props.onClose} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ResetModalStepError.propTypes = {
|
||||
itemLabel: PropTypes.string.isRequired,
|
||||
error: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ResetModalStepError;
|
||||
66
src/components/decks/reset-modal/step-one.js
Normal file
66
src/components/decks/reset-modal/step-one.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../shared/modal';
|
||||
import Button from '../../shared/buttons/button';
|
||||
import { InformationCircleIcon } from '@heroicons/react/outline';
|
||||
import Audio from '../../shared/audio';
|
||||
|
||||
function ResetModalStepOne(props) {
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
|
||||
function toggleShowGuide() {
|
||||
setShowGuide(showGuide => !showGuide);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal
|
||||
onClose={props.onClose}
|
||||
header={<span className='text-red-400'>بازنشانی فلش کارتها</span>}
|
||||
body={
|
||||
<div className='text-base'>
|
||||
<p>
|
||||
<span>شما در حال </span>
|
||||
<span className='font-semibold text-red-500'>بازنشانی </span>
|
||||
<span>فلش کارت های </span>
|
||||
<span className='font-semibold'>{props.itemLabel}</span>
|
||||
<span> هستید.</span>
|
||||
</p>
|
||||
<p>در صورت تایید تمام سوابق مرور فلش کارت های این مبحث حذف خواهد شد.</p>
|
||||
<p>
|
||||
همچنین امتیازاتی که از مطالعه فلش کارت های این بخش کسب شده از بین خواهد رفت و ممکن از
|
||||
رتبه شما در رتبه بندی هفتگی و ماهانه تغییر کند.
|
||||
</p>
|
||||
<p className='font-medium'>آیا مطمئن هستید؟</p>
|
||||
{showGuide && (
|
||||
<>
|
||||
<hr className='my-4' />
|
||||
<div id='guide'>
|
||||
<Audio src='/voices/reset-guide.mp3' caption='فایل صوتی توضیحات بازنشانی (ریست):' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className='flex justify-between'>
|
||||
<Button circular color='gray' colorWeight={400} onClick={toggleShowGuide}>
|
||||
<InformationCircleIcon width={20} height={20} />
|
||||
</Button>
|
||||
<div className='flex gap-1'>
|
||||
<Button color='green' content='بلی' onClick={props.onNext} />
|
||||
<Button color='red' content='خیر' onClick={props.onClose} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ResetModalStepOne.propTypes = {
|
||||
itemLabel: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onNext: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ResetModalStepOne;
|
||||
36
src/components/decks/reset-modal/step-success.js
Normal file
36
src/components/decks/reset-modal/step-success.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../shared/modal';
|
||||
import Button from '../../shared/buttons/button';
|
||||
|
||||
function ResetModalStepSuccess(props) {
|
||||
return (
|
||||
<Modal
|
||||
showModal
|
||||
onClose={props.onClose}
|
||||
header={<span className='text-red-400'>بازنشانی فلش کارتها</span>}
|
||||
body={
|
||||
<div className='text-base'>
|
||||
<p>
|
||||
<span>بازنشانی فلش کارت های </span>
|
||||
<span className='font-semibold'>{props.itemLabel}</span>
|
||||
<span> با </span>
|
||||
<span className='font-medium text-green-500'>موفقیت</span>
|
||||
<span> انجام شد.</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className='flex justify-end'>
|
||||
<Button color='red' content='بازگشت' onClick={props.onClose} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ResetModalStepSuccess.propTypes = {
|
||||
itemLabel: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ResetModalStepSuccess;
|
||||
71
src/components/decks/reset-modal/step-two.js
Normal file
71
src/components/decks/reset-modal/step-two.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../shared/modal';
|
||||
import Button from '../../shared/buttons/button';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormInput from '../../shared/forms/input';
|
||||
|
||||
function ResetModalStepTwo(props) {
|
||||
const [isCorrectCode, setIsCorrectCode] = useState(false);
|
||||
const formRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal
|
||||
onClose={props.onClose}
|
||||
header={<span className='text-red-400'>بازنشانی فلش کارتها</span>}
|
||||
body={
|
||||
<div className='text-base'>
|
||||
<p>برای تایید 4 رقم آخر شماره تلفن همراه خود را وارد نمایید:</p>
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
initialValues={{ code: '' }}
|
||||
validate={({ code }) => {
|
||||
const isCorrect = code === props.correctCode;
|
||||
setIsCorrectCode(isCorrect);
|
||||
if (code.length !== 4) {
|
||||
return { code: '4 رقم آخر شماره تلفن همراه خود را وارد نمایید.' };
|
||||
}
|
||||
if (!isCorrect) return { code: 'کد وارد شده اشتباه است.' };
|
||||
}}
|
||||
onSubmit={props.onNext}
|
||||
>
|
||||
<Form className='grid grid-cols-1 gap-5 p-3'>
|
||||
<Field
|
||||
required
|
||||
name='code'
|
||||
label='4 رقم آخر شماره تلفن همراه'
|
||||
component={FormInput}
|
||||
maxLength={4}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className='flex justify-end gap-1'>
|
||||
<Button
|
||||
disabled={!isCorrectCode}
|
||||
color='green'
|
||||
content='تایید'
|
||||
onClick={formRef.current?.submitForm}
|
||||
/>
|
||||
<Button color='red' content='بازگشت' onClick={props.onClose} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ResetModalStepTwo.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
correctCode: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ user }) {
|
||||
return { correctCode: user.data.mobile.slice(-4) };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ResetModalStepTwo);
|
||||
50
src/components/decks/student-deck/container.js
Normal file
50
src/components/decks/student-deck/container.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Fetcher from '../../shared/fetches/fetcher';
|
||||
import PropTypes from 'prop-types';
|
||||
import StudentDeck from './student-deck';
|
||||
import Loader from '../../shared/loader';
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { fetchStudentDeck } from '../../../redux/actions/student-deck';
|
||||
|
||||
function StudentDeckContainer({ studentDeckId, onSelectChapter, onRefreshSelected }) {
|
||||
const location = useLocation();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const rateType = urlSearchParams.get('rate-type');
|
||||
const priority = urlSearchParams.get('priority');
|
||||
|
||||
const action = useCallback(() => {
|
||||
const minRate = rateType ? { starred: 1, active: 0 }[rateType] : -1;
|
||||
return fetchStudentDeck(studentDeckId, priority, minRate);
|
||||
}, [studentDeckId, priority, rateType]);
|
||||
|
||||
const renderLoader = useCallback(
|
||||
() => (
|
||||
<div className='border border-t-0 border-gray-500 text-center'>
|
||||
<Loader full={false} />
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fetcher action={action} stateSelector={state => state.studentDeck} renderLoader={renderLoader}>
|
||||
{studentDeck => (
|
||||
<StudentDeck
|
||||
priority={priority}
|
||||
rateType={rateType}
|
||||
studentDeck={studentDeck}
|
||||
onSelectChapter={onSelectChapter}
|
||||
onRefreshSelected={onRefreshSelected}
|
||||
/>
|
||||
)}
|
||||
</Fetcher>
|
||||
);
|
||||
}
|
||||
|
||||
StudentDeckContainer.propTypes = {
|
||||
studentDeckId: PropTypes.number.isRequired,
|
||||
onSelectChapter: PropTypes.func.isRequired,
|
||||
onRefreshSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StudentDeckContainer;
|
||||
77
src/components/decks/student-deck/student-deck.js
Normal file
77
src/components/decks/student-deck/student-deck.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Badge from '../badge';
|
||||
import Button from '../../shared/buttons/button';
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import { compareChapter } from '../utils';
|
||||
|
||||
function StudentDeck({ priority, rateType, studentDeck, onSelectChapter, onRefreshSelected }) {
|
||||
function getURL(chapterId, cardType) {
|
||||
const url = `/cards/ch-${chapterId}/${cardType}`;
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
if (priority) urlSearchParams.set('priority', priority);
|
||||
if (cardType !== 'new' && rateType) urlSearchParams.set('rate-type', rateType);
|
||||
const urlSearchParamsSTR = urlSearchParams.toString();
|
||||
if (!urlSearchParamsSTR) return url;
|
||||
return url + `?${urlSearchParamsSTR}`;
|
||||
}
|
||||
|
||||
return studentDeck.deck.chapters.sort(compareChapter).map((chapter, index, array) => (
|
||||
<div
|
||||
key={chapter.id}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSelectChapter(chapter);
|
||||
}}
|
||||
className={classNames('border border-t-0 border-gray-500 text-center', {
|
||||
'rounded-b-lg': index === array.length - 1
|
||||
})}
|
||||
>
|
||||
<div className='relative grid grid-flow-col grid-cols-9 p-4 hover:bg-gray-100 sm:grid-flow-row'>
|
||||
<div className='row-span-3 m-auto'>({index + 1})</div>
|
||||
<div className='col-span-8 m-auto sm:col-span-3'>{chapter.name}</div>
|
||||
<div className='col-span-8 my-2 flex justify-center gap-2 sm:col-span-4 sm:my-auto'>
|
||||
<Badge
|
||||
text='مروری'
|
||||
number={chapter.reviewCardCount}
|
||||
to={getURL(chapter.id, 'review')}
|
||||
color='red'
|
||||
/>
|
||||
<Badge
|
||||
text='جدید'
|
||||
number={chapter.newCardCount}
|
||||
to={getURL(chapter.id, 'new')}
|
||||
color='yellow'
|
||||
/>
|
||||
</div>
|
||||
{!rateType && (
|
||||
<div className='row-span-3 m-auto font-bold'>
|
||||
<Button
|
||||
outline
|
||||
circular
|
||||
size='xs'
|
||||
color='orange'
|
||||
disabled={chapter.cardCount === chapter.newCardCount}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onRefreshSelected({ id: chapter.id, type: 'chapter', name: chapter.name });
|
||||
}}
|
||||
>
|
||||
<RefreshIcon width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
StudentDeck.propTypes = {
|
||||
priority: PropTypes.string,
|
||||
rateType: PropTypes.string,
|
||||
studentDeck: PropTypes.object.isRequired,
|
||||
onSelectChapter: PropTypes.func.isRequired,
|
||||
onRefreshSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StudentDeck;
|
||||
167
src/components/decks/student-decks.js
Normal file
167
src/components/decks/student-decks.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Component, Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
MinusCircleIcon,
|
||||
PlusCircleIcon,
|
||||
RefreshIcon
|
||||
} from '@heroicons/react/outline';
|
||||
import { getStudentDeckComparator } from './utils';
|
||||
import Badge from './badge';
|
||||
import Button from '../shared/buttons/button';
|
||||
import ChapterModal from './chapter-modal';
|
||||
import ResetModal from './reset-modal';
|
||||
import StudentDeckContainer from './student-deck/container';
|
||||
|
||||
class StudentDecks extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedStudentDeck: props.studentDecks.length === 1 ? props.studentDecks[0] : null,
|
||||
selectedChapter: null,
|
||||
selectedRefreshItem: null
|
||||
};
|
||||
}
|
||||
|
||||
isSelectedStudentDeck = studentDeckId => this.state.selectedStudentDeck?.id === studentDeckId;
|
||||
|
||||
handleSelectStudentDeck = studentDeck =>
|
||||
this.setState(state => ({
|
||||
selectedStudentDeck: state.selectedStudentDeck?.id === studentDeck.id ? null : studentDeck
|
||||
}));
|
||||
|
||||
handleSelectChapter = chapter =>
|
||||
this.setState(state => ({
|
||||
selectedChapter: state.selectedChapter?.id === chapter?.id ? null : chapter
|
||||
}));
|
||||
|
||||
handleRefreshSelected = item =>
|
||||
this.setState(state => ({
|
||||
selectedRefreshItem: state.selectedRefreshItem?.id === item?.id ? null : item
|
||||
}));
|
||||
|
||||
getStudentDeckColor = studentDeck => {
|
||||
if (studentDeck.reviewCardCount !== 0) return 'red';
|
||||
return studentDeck.newCardCount !== 0 ? 'blue' : 'green';
|
||||
};
|
||||
|
||||
render() {
|
||||
const comparator = getStudentDeckComparator(this.props.priority);
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='mx-4 rounded-lg'>
|
||||
{this.props.studentDecks.sort(comparator).map(studentDeck => {
|
||||
if (this.props.priority && !studentDeck.deck.prioritized) {
|
||||
return (
|
||||
<div key={studentDeck.id} className='mb-1'>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-flow-col grid-cols-9 sm:grid-flow-row ',
|
||||
`bg-gray-100`,
|
||||
'rounded-lg border border-black p-4 text-center'
|
||||
)}
|
||||
>
|
||||
<div className='row-span-3 m-auto'>
|
||||
<InformationCircleIcon width={25} height={25} />
|
||||
</div>
|
||||
<div className='col-span-8 m-auto font-bold sm:col-span-3'>
|
||||
{studentDeck.deck.name}
|
||||
</div>
|
||||
<div className='col-span-8 my-2 flex justify-center gap-2 sm:col-span-4 sm:my-auto'>
|
||||
به زودی
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={studentDeck.id} className='mb-1'>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-flow-col grid-cols-9 sm:grid-flow-row ',
|
||||
`bg-${this.getStudentDeckColor(studentDeck)}-100`,
|
||||
{ 'rounded-b-lg': !this.isSelectedStudentDeck(studentDeck.id) },
|
||||
'rounded-t-lg border border-black p-4 text-center'
|
||||
)}
|
||||
onClick={() => this.handleSelectStudentDeck(studentDeck)}
|
||||
>
|
||||
<div className='row-span-3 m-auto'>
|
||||
{this.isSelectedStudentDeck(studentDeck.id) ? (
|
||||
<PlusCircleIcon width={25} height={25} />
|
||||
) : (
|
||||
<MinusCircleIcon width={25} height={25} />
|
||||
)}
|
||||
</div>
|
||||
<div className='col-span-8 m-auto font-bold sm:col-span-3'>
|
||||
{studentDeck.deck.name}
|
||||
</div>
|
||||
<div className='col-span-8 my-2 flex justify-center gap-2 sm:col-span-4 sm:my-auto'>
|
||||
<Badge text='مروری' number={studentDeck.reviewCardCount} color='red' />
|
||||
<Badge text='جدید' number={studentDeck.newCardCount} color='yellow' />
|
||||
</div>
|
||||
{!this.props.rateType && (
|
||||
<div className='row-span-3 m-auto font-bold'>
|
||||
<Button
|
||||
outline
|
||||
circular
|
||||
size='xs'
|
||||
color='orange'
|
||||
disabled={studentDeck.deck.cardCount === studentDeck.newCardCount}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
this.handleRefreshSelected({
|
||||
id: studentDeck.deck.id,
|
||||
type: 'deck',
|
||||
name: studentDeck.deck.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
<RefreshIcon width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.isSelectedStudentDeck(studentDeck.id) && (
|
||||
<StudentDeckContainer
|
||||
studentDeckId={studentDeck.id}
|
||||
onSelectChapter={this.handleSelectChapter}
|
||||
onRefreshSelected={this.handleRefreshSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{this.state.selectedChapter && (
|
||||
<ChapterModal
|
||||
chapter={this.state.selectedChapter}
|
||||
lessonName={this.state.selectedStudentDeck?.deck.lesson.name}
|
||||
priority={this.props.priority}
|
||||
rateType={this.props.rateType}
|
||||
onClose={() => this.handleSelectChapter(null)}
|
||||
/>
|
||||
)}
|
||||
{this.state.selectedRefreshItem && (
|
||||
<ResetModal
|
||||
itemType={this.state.selectedRefreshItem.type}
|
||||
itemId={this.state.selectedRefreshItem.id}
|
||||
itemName={this.state.selectedRefreshItem.name}
|
||||
onClose={() => this.handleRefreshSelected(null)}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StudentDecks.propTypes = {
|
||||
priority: PropTypes.string,
|
||||
rateType: PropTypes.string,
|
||||
studentDecks: PropTypes.array.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withRouter(StudentDecks);
|
||||
13
src/components/decks/tab/index.js
Normal file
13
src/components/decks/tab/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import RateTypeTab from './rate-type-tab';
|
||||
import PeriorityTab from './priority-tab';
|
||||
|
||||
function DecksTab() {
|
||||
return (
|
||||
<div className='mb-8'>
|
||||
<PeriorityTab />
|
||||
<RateTypeTab />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DecksTab;
|
||||
38
src/components/decks/tab/priority-tab.js
Normal file
38
src/components/decks/tab/priority-tab.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ButtonGroup from '../../shared/buttons/button-group';
|
||||
import NavLink from '../../shared/buttons/nav-link';
|
||||
|
||||
function PeriorityTab() {
|
||||
const location = useLocation();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const currentPriority = urlSearchParams.get('priority');
|
||||
|
||||
function getLinkColor(priority) {
|
||||
return currentPriority === priority ? 'rose' : 'gray';
|
||||
}
|
||||
|
||||
function getNextURL(priority) {
|
||||
if (priority) {
|
||||
urlSearchParams.set('priority', priority);
|
||||
} else {
|
||||
urlSearchParams.delete('priority');
|
||||
}
|
||||
let url = '/decks';
|
||||
const urlSearchParamsSTR = urlSearchParams.toString();
|
||||
if (urlSearchParamsSTR) url += `?${urlSearchParamsSTR}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup className='mx-auto my-3 w-min'>
|
||||
<NavLink to={getNextURL('high')} size='xl' color={getLinkColor('high')}>
|
||||
ضروری
|
||||
</NavLink>
|
||||
<NavLink to={getNextURL(null)} size='xl' color={getLinkColor(null)}>
|
||||
همه
|
||||
</NavLink>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default PeriorityTab;
|
||||
41
src/components/decks/tab/rate-type-tab.js
Normal file
41
src/components/decks/tab/rate-type-tab.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ButtonGroup from '../../shared/buttons/button-group';
|
||||
import NavLink from '../../shared/buttons/nav-link';
|
||||
|
||||
function RateTypeTab() {
|
||||
const location = useLocation();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const currentRateType = urlSearchParams.get('rate-type');
|
||||
|
||||
function getLinkColor(rateType) {
|
||||
return currentRateType === rateType ? 'blue' : 'gray';
|
||||
}
|
||||
|
||||
function getNextURL(rateType) {
|
||||
if (rateType) {
|
||||
urlSearchParams.set('rate-type', rateType);
|
||||
} else {
|
||||
urlSearchParams.delete('rate-type');
|
||||
}
|
||||
let url = '/decks';
|
||||
const urlSearchParamsSTR = urlSearchParams.toString();
|
||||
if (urlSearchParamsSTR) url += `?${urlSearchParamsSTR}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup className='mx-auto my-3 w-min'>
|
||||
<NavLink to={getNextURL('starred')} size='xl' color={getLinkColor('starred')}>
|
||||
ستاره دار
|
||||
</NavLink>
|
||||
<NavLink to={getNextURL('active')} size='xl' color={getLinkColor('active')}>
|
||||
فعال
|
||||
</NavLink>
|
||||
<NavLink to={getNextURL(null)} size='xl' color={getLinkColor(null)}>
|
||||
همه
|
||||
</NavLink>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export default RateTypeTab;
|
||||
18
src/components/decks/utils.js
Normal file
18
src/components/decks/utils.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function getStudentDeckComparator(priority = null) {
|
||||
return function compareStudentDeck(sd1, sd2) {
|
||||
return (
|
||||
(priority && sd2.deck.prioritized - sd1.deck.prioritized) ||
|
||||
sd2.reviewCardCount - sd1.reviewCardCount ||
|
||||
sd2.newCardCount - sd1.newCardCount ||
|
||||
sd2.deck.cardCount - sd1.deck.cardCount
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function compareChapter(ch1, ch2) {
|
||||
return (
|
||||
ch2.reviewCardCount - ch1.reviewCardCount ||
|
||||
ch2.newCardCount - ch1.newCardCount ||
|
||||
ch2.cardCount - ch1.cardCount
|
||||
);
|
||||
}
|
||||
7
src/components/default/index.js
Normal file
7
src/components/default/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
|
||||
function DefaultPage() {
|
||||
return <MainLayout loginRequired={false} />;
|
||||
}
|
||||
|
||||
export default DefaultPage;
|
||||
118
src/components/forget-password/index.js
Normal file
118
src/components/forget-password/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useState } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import PropTypes from 'prop-types';
|
||||
import MainLayout from '../layouts/main';
|
||||
import MobileForm from './mobile-form';
|
||||
import SMSKeyForm from '../shared/sms-key-form';
|
||||
import PasswordForm from './password-form';
|
||||
import Alert from '../shared/alert';
|
||||
import {
|
||||
checkSMSKeyRequest,
|
||||
forgetPasswordRequest,
|
||||
resetPasswordRequest
|
||||
} from '../../redux/actions/user';
|
||||
import { loginUser } from '../../redux/actions/auth';
|
||||
import { selectIsAuthenticated } from '../../redux/selectors/auth';
|
||||
import { DEFAULT_DECKS_ROUTE } from '../utils/index';
|
||||
|
||||
function ForgetPasswordPage(props) {
|
||||
const [step, setStep] = useState(0);
|
||||
const [mobile, setMobile] = useState(null);
|
||||
const [uid, setUID] = useState(null);
|
||||
const [token, setToken] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const { addToast } = useToasts();
|
||||
if (props.isAuthenticated) return <Redirect to={DEFAULT_DECKS_ROUTE} />;
|
||||
|
||||
function renderForm(step) {
|
||||
if (step === MOBILE) {
|
||||
return (
|
||||
<MobileForm
|
||||
onSubmit={async function (mobile) {
|
||||
const {
|
||||
payload: { uid }
|
||||
} = await props.forgetPasswordRequest(mobile);
|
||||
setMobile(mobile);
|
||||
setUID(uid);
|
||||
setStep(SMS_KEY);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === SMS_KEY) {
|
||||
return (
|
||||
<SMSKeyForm
|
||||
back
|
||||
mobile={mobile}
|
||||
onSubmit={async function (smsKey) {
|
||||
const {
|
||||
payload: { token }
|
||||
} = await props.checkSMSKeyRequest(uid, smsKey);
|
||||
setToken(token);
|
||||
setStep(PASSWORD);
|
||||
}}
|
||||
onResend={async function () {
|
||||
const {
|
||||
payload: { uid }
|
||||
} = await props.forgetPasswordRequest(mobile);
|
||||
setUID(uid);
|
||||
}}
|
||||
onBack={setStep.bind(null, MOBILE)}
|
||||
onError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === PASSWORD) {
|
||||
return (
|
||||
<PasswordForm
|
||||
onSubmit={async function (password) {
|
||||
const { error } = await props.resetPasswordRequest(uid, token, password);
|
||||
if (!error) {
|
||||
addToast('رمز عبور شما با موفقیت تغییر کرد.', { appearance: 'success' });
|
||||
await props.loginUser(mobile, password);
|
||||
setStep(MOBILE);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MainLayout loginRequired={false} consultPackageRequired={false} packageRequired={false}>
|
||||
{error ? (
|
||||
<Alert content={error} className='mt-3 mb-5' />
|
||||
) : (
|
||||
<div className='mx-auto w-full sm:w-4/5 lg:w-3/5'>{renderForm(step)}</div>
|
||||
)}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const [MOBILE, SMS_KEY, PASSWORD] = [0, 1, 2];
|
||||
|
||||
ForgetPasswordPage.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
checkSMSKeyRequest: PropTypes.func.isRequired,
|
||||
forgetPasswordRequest: PropTypes.func.isRequired,
|
||||
resetPasswordRequest: PropTypes.func.isRequired,
|
||||
loginUser: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { isAuthenticated: selectIsAuthenticated(state) };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
checkSMSKeyRequest,
|
||||
forgetPasswordRequest,
|
||||
resetPasswordRequest,
|
||||
loginUser
|
||||
})(ForgetPasswordPage);
|
||||
58
src/components/forget-password/mobile-form.js
Normal file
58
src/components/forget-password/mobile-form.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Segment from '../shared/segment';
|
||||
import Divider from '../shared/divider';
|
||||
import Alert from '../shared/alert';
|
||||
import Button from '../shared/buttons/button';
|
||||
import FormInput from '../shared/forms/input';
|
||||
import { mobile, required } from '../shared/forms/validations';
|
||||
|
||||
function MobileForm(props) {
|
||||
async function submit({ mobile }, { setErrors }) {
|
||||
try {
|
||||
await props.onSubmit(mobile);
|
||||
} catch (error) {
|
||||
setErrors({ _error: error.data.detail || error.data });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Segment>
|
||||
<Divider horizontal content='فرم فراموشی رمز عبور' />
|
||||
<Formik initialValues={{ mobile: '' }} onSubmit={submit}>
|
||||
{({ errors, dirty, isSubmitting }) => (
|
||||
<Fragment>
|
||||
{dirty && errors._error && <Alert content={errors._error} className='mt-3 mb-5' />}
|
||||
<Form>
|
||||
<Field
|
||||
required
|
||||
name='mobile'
|
||||
label='شماره تلفن همراه'
|
||||
type='tel'
|
||||
placeholder='09121234567'
|
||||
component={FormInput}
|
||||
validate={[required, mobile]}
|
||||
/>
|
||||
<div className='text-medium my-4'>
|
||||
شماره تلفن همراه وارد شده هنگام ثبت نام را وارد کنید.
|
||||
</div>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!dirty || isSubmitting}
|
||||
content='ادامه'
|
||||
className='my-2 mr-auto'
|
||||
/>
|
||||
</Form>
|
||||
</Fragment>
|
||||
)}
|
||||
</Formik>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
MobileForm.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MobileForm;
|
||||
79
src/components/forget-password/password-form.js
Normal file
79
src/components/forget-password/password-form.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Component, Fragment } from 'react';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import Divider from '../shared/divider';
|
||||
import Alert from '../shared/alert';
|
||||
import Button from '../shared/buttons/button';
|
||||
import FormInput from '../shared/forms/input';
|
||||
import { password, required } from '../shared/forms/validations';
|
||||
|
||||
class PasswordForm extends Component {
|
||||
submit = async ({ password }, { setErrors }) => {
|
||||
try {
|
||||
await this.props.onSubmit(password);
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || 'تغییر رمز عبور انجام نشد.';
|
||||
setErrors({ _error: errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
validate = values => {
|
||||
const errors = {};
|
||||
if (values.password !== values.password2) errors.password2 = 'رمزهای عبور غیر یکسان هستند.';
|
||||
return errors;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Segment>
|
||||
<Divider horizontal content='فرم تغییر رمز عبور' className='mb-5' />
|
||||
|
||||
<Formik
|
||||
initialValues={{ password: '', password2: '' }}
|
||||
onSubmit={this.submit}
|
||||
validate={this.validate}
|
||||
>
|
||||
{({ errors, dirty, isSubmitting }) => (
|
||||
<Fragment>
|
||||
{dirty && errors._error && <Alert content={errors._error} className='mt-3 mb-5' />}
|
||||
|
||||
<Form>
|
||||
<Field
|
||||
required
|
||||
name='password'
|
||||
type='password'
|
||||
label='رمز عبور جدید'
|
||||
component={FormInput}
|
||||
validate={[required, password]}
|
||||
className='my-4'
|
||||
/>
|
||||
<Field
|
||||
required
|
||||
name='password2'
|
||||
type='password'
|
||||
label='تکرار رمز عبور جدید'
|
||||
placeholder='رمز عبور جدید خود را مجدد وارد نمایید'
|
||||
component={FormInput}
|
||||
validate={[required, password]}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!dirty || isSubmitting}
|
||||
content='تغییر رمز عبور'
|
||||
className='my-2 mr-auto'
|
||||
/>
|
||||
</Form>
|
||||
</Fragment>
|
||||
)}
|
||||
</Formik>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordForm.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default PasswordForm;
|
||||
12
src/components/home/home.js
Normal file
12
src/components/home/home.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<Segment>
|
||||
<p className='text-large'>پنل فلش کارت گروه مد مشاور</p>
|
||||
<p>به پنل فلش کارت گروه مدمشاور خوش آمدید.</p>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
12
src/components/home/index.js
Normal file
12
src/components/home/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
import Home from './home';
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<MainLayout loginRequired={false} consultPackageRequired={false} packageRequired={false}>
|
||||
<Home />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
18
src/components/last-review/answer.js
Normal file
18
src/components/last-review/answer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
|
||||
function Answer(props) {
|
||||
return (
|
||||
<Segment id='answer' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>جواب:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.answer }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Answer.propTypes = {
|
||||
answer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Answer;
|
||||
37
src/components/last-review/card.js
Normal file
37
src/components/last-review/card.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import OptionContainer from './option-container';
|
||||
import Question from './question';
|
||||
import Answer from './answer';
|
||||
import Reviews from './reviews';
|
||||
import Footer from './footer';
|
||||
import { CORRECT_COLOR, DEFAULT_COLOR } from './utils';
|
||||
|
||||
function Card(props) {
|
||||
const { card, color } = props;
|
||||
|
||||
return (
|
||||
<div className='mb-20'>
|
||||
<Question question={card.question} color={color} />
|
||||
<OptionContainer
|
||||
cardId={card.id}
|
||||
options={card.options}
|
||||
selectedOptionId={props.selectedOptionId}
|
||||
/>
|
||||
{color !== DEFAULT_COLOR && <Reviews color={color} />}
|
||||
{color !== DEFAULT_COLOR && <Answer answer={card.answer} color={color} />}
|
||||
<Footer disabledNext={color === DEFAULT_COLOR} showArchive={color === CORRECT_COLOR} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Card.defaultProps = {
|
||||
color: DEFAULT_COLOR
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
card: PropTypes.object.isRequired,
|
||||
selectedOptionId: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Card;
|
||||
32
src/components/last-review/container.js
Normal file
32
src/components/last-review/container.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Card from './card';
|
||||
import Fetcher from '../shared/fetches/fetcher';
|
||||
import { fetchLastReview } from '../../redux/actions/review';
|
||||
import { CORRECT_COLOR, DONT_KNOW_COLOR, WRONG_COLOR } from './utils';
|
||||
|
||||
function LastReviewContainer() {
|
||||
const { chapterId } = useParams();
|
||||
return (
|
||||
<Fetcher
|
||||
action={fetchLastReview.bind(null, chapterId)}
|
||||
stateSelector={state => state.lastReview}
|
||||
>
|
||||
{review => {
|
||||
const color = review.option
|
||||
? review.status === 1
|
||||
? CORRECT_COLOR
|
||||
: WRONG_COLOR
|
||||
: DONT_KNOW_COLOR;
|
||||
return (
|
||||
<Card
|
||||
card={review.studentCard.card}
|
||||
selectedOptionId={review.option || 0}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Fetcher>
|
||||
);
|
||||
}
|
||||
|
||||
export default LastReviewContainer;
|
||||
58
src/components/last-review/footer.js
Normal file
58
src/components/last-review/footer.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ArrowSmLeftIcon, ArrowSmRightIcon } from '@heroicons/react/outline';
|
||||
import Button from '../shared/buttons/button';
|
||||
import { archiveStudentCard } from '../../redux/actions/student-card';
|
||||
import NavLink from '../shared/buttons/nav-link';
|
||||
import { DEFAULT_DECKS_ROUTE } from '../utils/index';
|
||||
|
||||
function Footer(props) {
|
||||
const history = useHistory();
|
||||
const { chapterId, type } = useParams();
|
||||
let nextURL = `/cards/ch-${chapterId}/${type}`;
|
||||
const urlSearchParamsSTR = new URLSearchParams(location.search).toString();
|
||||
if (urlSearchParamsSTR) {
|
||||
nextURL += `?${urlSearchParamsSTR}`;
|
||||
}
|
||||
const { disabledNext } = props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={
|
||||
'align-center fixed bottom-0 left-0 flex w-full flex-row-reverse justify-between bg-black bg-opacity-60'
|
||||
}
|
||||
>
|
||||
<Button color='red' onClick={() => history.push(DEFAULT_DECKS_ROUTE)} className='m-2'>
|
||||
بازگشت
|
||||
<ArrowSmLeftIcon width={20} height={20} className='stroke-current text-white' />
|
||||
</Button>
|
||||
<NavLink
|
||||
to={nextURL}
|
||||
color='green'
|
||||
textColor='white'
|
||||
disabled={disabledNext}
|
||||
className='m-2'
|
||||
>
|
||||
<ArrowSmRightIcon width={20} height={20} className='stroke-current text-white' />
|
||||
بعدی
|
||||
</NavLink>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
disabledNext: PropTypes.bool.isRequired,
|
||||
showArchive: PropTypes.bool.isRequired,
|
||||
archiveStudentCard: PropTypes.func.isRequired,
|
||||
isPermanentArchive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps({ review }) {
|
||||
return { isPermanentArchive: !!review.data.studentCard && !!review.data.studentCard.status };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { archiveStudentCard })(Footer);
|
||||
12
src/components/last-review/index.js
Normal file
12
src/components/last-review/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import MainLayout from '../layouts/main';
|
||||
import LastReviewContainer from './container';
|
||||
|
||||
function LastReviewPage() {
|
||||
return (
|
||||
<MainLayout withFooter>
|
||||
<LastReviewContainer />
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default LastReviewPage;
|
||||
29
src/components/last-review/option-container.js
Normal file
29
src/components/last-review/option-container.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Option from './option';
|
||||
|
||||
class OptionContainer extends Component {
|
||||
render() {
|
||||
const options = [...this.props.options, { id: 0, text: 'نمی دانم', isCorrect: false }];
|
||||
return (
|
||||
<div id='options' className='space-y-3 px-4'>
|
||||
{options.map((option, index) => (
|
||||
<Option
|
||||
key={option.id}
|
||||
option={option}
|
||||
index={index}
|
||||
isSelectedOption={this.props.selectedOptionId === option.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionContainer.propTypes = {
|
||||
cardId: PropTypes.number.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedOptionId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default OptionContainer;
|
||||
75
src/components/last-review/option.js
Normal file
75
src/components/last-review/option.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { ExclamationIcon } from '@heroicons/react/outline';
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/solid';
|
||||
import Button from '../shared/buttons/button';
|
||||
|
||||
function Option(props) {
|
||||
const { option, isSelectedOption } = props;
|
||||
|
||||
const color = option.isCorrect
|
||||
? 'green'
|
||||
: isSelectedOption && option.id
|
||||
? 'red'
|
||||
: option.id
|
||||
? 'gray'
|
||||
: 'yellow';
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled
|
||||
id={`option ${option.id}`}
|
||||
color={color}
|
||||
colorWeight={300}
|
||||
borderWeight={2}
|
||||
textColor='gray'
|
||||
textColorWeight='600'
|
||||
size='xl'
|
||||
justifyContent={option.id ? 'start' : 'between'}
|
||||
className={`w-full border-2 border-${color}-300}`}
|
||||
>
|
||||
<span className={classNames('flex', { 'mx-auto': !option.id })}>
|
||||
{option.id !== 0 && (
|
||||
<span className='shrink-0 font-semibold'>{`گزینه ${props.index + 1}:`}</span>
|
||||
)}
|
||||
<span className={classNames('mx-4 text-justify', { 'font-semibold': !option.id })}>
|
||||
{option.text}
|
||||
</span>
|
||||
</span>
|
||||
{isSelectedOption &&
|
||||
(!option.id ? (
|
||||
<ExclamationIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 stroke-current text-yellow-600`}
|
||||
/>
|
||||
) : option.isCorrect ? (
|
||||
<CheckIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={`shrink-0 rounded-lg
|
||||
border-2 border-green-500
|
||||
stroke-current text-green-500 ms-auto`}
|
||||
/>
|
||||
) : (
|
||||
<XIcon
|
||||
width={25}
|
||||
height={25}
|
||||
className={classNames(
|
||||
`shrink-0 rounded-lg
|
||||
border-2 border-red-500
|
||||
stroke-current text-red-500 ms-auto`
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Option.propTypes = {
|
||||
option: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isSelectedOption: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Option;
|
||||
21
src/components/last-review/question.js
Normal file
21
src/components/last-review/question.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Segment from '../shared/segment';
|
||||
import { DEFAULT_COLOR } from './utils';
|
||||
|
||||
function Question(props) {
|
||||
return (
|
||||
<Segment id='question' color={props.color} colorWeight={300} borderWeight={2}>
|
||||
<span className='font-semibold'>سوال:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.question }} />
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
Question.defaultProps = { color: DEFAULT_COLOR };
|
||||
|
||||
Question.propTypes = {
|
||||
question: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Question;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user