start
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 100
|
||||||
37
.eslintrc.json
Normal file
37
.eslintrc.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"standard",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "17.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"react/prop-types": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
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/
|
||||||
13
.prettierrc.json
Normal file
13
.prettierrc.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
69
package.json
Normal file
69
package.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "uploader-panel",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"my-ckeditor5-build-classic": "file:public/lib/ckeditor5-build-classic",
|
||||||
|
"@ckeditor/ckeditor5-react": "^3.0.3",
|
||||||
|
"@testing-library/jest-dom": "^5.16.1",
|
||||||
|
"@testing-library/react": "^12.1.2",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"moment-jalaali": "^0.9.4",
|
||||||
|
"prop-types": "^15.8.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-bootstrap": "^1.4.3",
|
||||||
|
"react-bootstrap-button-loader": "^2.0.0",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-icons": "^4.2.0",
|
||||||
|
"react-redux": "^7.2.2",
|
||||||
|
"react-router-bootstrap": "^0.25.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-toast-notifications": "^2.5.1",
|
||||||
|
"react-toastify": "^8.1.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-form": "^8.3.8",
|
||||||
|
"redux-thunk": "^2.4.1",
|
||||||
|
"sass": "^1.45.1",
|
||||||
|
"web-vitals": "^2.1.2"
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"eslint": "^7.12.1",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jest": "^26.1.4",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"eslint-plugin-react": "^7.29.4",
|
||||||
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"redux-devtools-extension": "^2.13.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
public/assets/css/bootstrap/v4.5.3/css/bootstrap.min.css
vendored
Normal file
7
public/assets/css/bootstrap/v4.5.3/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
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');
|
||||||
|
}
|
||||||
270
public/assets/css/style.css
Normal file
270
public/assets/css/style.css
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
@import url(./fontiran.css); /* لینک فایلی که وظیفه بارگذاری فونت ها را برعهده دارد */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: IRANSans !important;
|
||||||
|
font-weight: 300;
|
||||||
|
direction: rtl;
|
||||||
|
background-color: #E2E2E2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, text, strong, input, textarea {
|
||||||
|
font-family: IRANSans !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div, a, label, button {
|
||||||
|
font-family: IRANSans !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ltr {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xsmall {
|
||||||
|
font-size: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-small {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-normal {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-large {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xlarge {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-ultralight {
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-regular {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-black {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-fit-content {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainbox {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #EFEFEF;
|
||||||
|
display: table;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-right: 8px solid #FFFF33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainboxnegativ {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #000000;
|
||||||
|
display: table;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-right: 8px solid #FFFF33;
|
||||||
|
color: #F9F9F9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainbox2 {
|
||||||
|
font-size: 1em;
|
||||||
|
width: 90%;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainbox3 {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #b8b8b8;
|
||||||
|
display: table;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-right: 8px solid #bd70ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainbox2negativ {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #F9F9F9;
|
||||||
|
background-color: #000000;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.farsiparagraph {
|
||||||
|
font-size: 1em;
|
||||||
|
width: 47%;
|
||||||
|
float: right;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.englishparagraph {
|
||||||
|
font-size: 1em;
|
||||||
|
width: 47%;
|
||||||
|
float: left;
|
||||||
|
direction: ltr;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightbox {
|
||||||
|
width: 60%;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: 5px;
|
||||||
|
float: right;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
min-width: 0px;
|
||||||
|
background-color: #F7F7F7;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.titelbox {
|
||||||
|
width: 60%;
|
||||||
|
padding-right: 25px;
|
||||||
|
padding-left: 0px;
|
||||||
|
float: right;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
min-width: 0px;
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
color: #4B4B4B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lefttbox {
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: 4px;
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet {
|
||||||
|
width: 35%;
|
||||||
|
float: left;
|
||||||
|
font-size: 21em;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alphabet2 {
|
||||||
|
width: 35%;
|
||||||
|
float: left;
|
||||||
|
direction: ltr;
|
||||||
|
font-size: 1.6em;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333333;
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.7em;
|
||||||
|
text-align: center;
|
||||||
|
direction: ltr;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
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;background-color:#e2e2e2;margin:0}a,body,button,div,h1,h2,h3,h4,h5,h6,input,label,strong,text,textarea{font-family:IRANSans!important}h1{font-weight:700}.wrapper{max-width:900px;margin:0 auto}.ltr{direction:ltr}.text-right{text-align:right}.text-center{text-align:center}.text-left{text-align:left}.text-xsmall{font-size:.6em}.text-normal,.text-small{font-size:.8em}.text-large{font-size:1.2em}.text-xlarge{font-size:1.4em}.text-underline{text-decoration:underline}.text-ultralight{font-weight:200}.text-light{font-weight:300}.text-regular{font-weight:400}.text-medium,blockquote{font-weight:500}.text-bold{font-weight:700}.text-black{font-weight:900}.width-fit-content{width:fit-content}blockquote{padding:10px;border:1px dashed #666}.mainbox,.mainboxnegativ{width:100%;background-color:#efefef;display:table;margin-bottom:30px;border-right:8px solid #ff3}.mainboxnegativ{background-color:#000;color:#f9f9f9}.mainbox2{font-size:1em;width:90%;padding-right:20px;padding-top:10px;padding-bottom:10px}.mainbox3{width:100%;background-color:#b8b8b8;display:table;margin-bottom:30px;border-right:8px solid #bd70ff}.mainbox2negativ{font-size:1em;color:#f9f9f9;background-color:#000;padding-right:20px}.englishparagraph,.farsiparagraph{font-size:1em;width:47%;padding-top:10px;padding-bottom:10px}.farsiparagraph{float:right;padding-right:20px}.englishparagraph{float:left;direction:ltr;padding-left:20px}.rightbox{width:60%;padding-right:20px;padding-left:5px;margin-left:10px;margin-bottom:0;background-color:#f7f7f7}.lefttbox,.rightbox,.titelbox{float:right;min-width:0}.titelbox{width:60%;padding-right:25px;padding-left:0;margin-left:10px;margin-bottom:0;background-color:#f2f2f2;color:#4b4b4b}.lefttbox{padding-right:20px;padding-left:4px;margin-bottom:10px}.alphabet,.alphabet2{width:35%;float:left;font-size:21em;text-align:center;font-weight:500;color:#999}.alphabet2{direction:ltr;font-size:1.6em;text-align:left;color:#333;margin-top:100px}.footer{font-weight:300;font-size:.7em;text-align:center;direction:ltr;margin-bottom:0;padding-bottom:0}audio{max-width:100%}.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}
|
||||||
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>
|
||||||
161
public/index.html
Normal file
161
public/index.html
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<!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/uploader' 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/bootstrap/v4.5.3/css/bootstrap.min.css'
|
||||||
|
rel='stylesheet'
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
1058
public/lib/ckeditor5-build-classic/CHANGELOG.md
Normal file
1058
public/lib/ckeditor5-build-classic/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
4
public/lib/ckeditor5-build-classic/CONTRIBUTING.md
Normal file
4
public/lib/ckeditor5-build-classic/CONTRIBUTING.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Contributing
|
||||||
|
========================================
|
||||||
|
|
||||||
|
See the [official contributors' guide to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html) to learn more.
|
||||||
21
public/lib/ckeditor5-build-classic/LICENSE.md
Normal file
21
public/lib/ckeditor5-build-classic/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Software License Agreement
|
||||||
|
==========================
|
||||||
|
|
||||||
|
**CKEditor 5 classic editor build** – https://github.com/ckeditor/ckeditor5-build-classic <br>
|
||||||
|
Copyright (c) 2003-2022, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
|
||||||
|
|
||||||
|
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
|
||||||
|
|
||||||
|
Sources of Intellectual Property Included in CKEditor
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
|
||||||
|
|
||||||
|
The following libraries are included in CKEditor under the [MIT license](https://opensource.org/licenses/MIT):
|
||||||
|
|
||||||
|
* lodash - Copyright (c) JS Foundation and other contributors https://js.foundation/. Based on Underscore.js, copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors http://underscorejs.org/.
|
||||||
|
|
||||||
|
Trademarks
|
||||||
|
----------
|
||||||
|
|
||||||
|
**CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.
|
||||||
71
public/lib/ckeditor5-build-classic/README.md
Normal file
71
public/lib/ckeditor5-build-classic/README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
CKEditor 5 classic editor build
|
||||||
|
========================================
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic)
|
||||||
|
[](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
|
||||||
|
[](https://app.travis-ci.com/github/ckeditor/ckeditor5)
|
||||||
|

|
||||||
|
|
||||||
|
The classic editor build for CKEditor 5. Read more about the [classic editor build](https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/predefined-builds.html#classic-editor) and see the [demo](https://ckeditor.com/docs/ckeditor5/latest/examples/builds/classic-editor.html).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See:
|
||||||
|
|
||||||
|
* [Installation](https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/quick-start.html) for how to install this package and what it contains.
|
||||||
|
* [Basic API](https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/basic-api.html) for how to create an editor and interact with it.
|
||||||
|
* [Configuration](https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/configuration.html) for how to configure the editor.
|
||||||
|
* [Creating custom builds](https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/quick-start.html#building-the-editor-from-source) for how to customize the build (configure and rebuild the editor bundle).
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
First, install the build from npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save @ckeditor/ckeditor5-build-classic
|
||||||
|
```
|
||||||
|
|
||||||
|
And use it in your website:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="editor">
|
||||||
|
<p>This is the editor content.</p>
|
||||||
|
</div>
|
||||||
|
<script src="./node_modules/@ckeditor/ckeditor5-build-classic/build/ckeditor.js"></script>
|
||||||
|
<script>
|
||||||
|
ClassicEditor
|
||||||
|
.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in your JavaScript application:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||||
|
|
||||||
|
// Or using the CommonJS version:
|
||||||
|
// const ClassicEditor = require( '@ckeditor/ckeditor5-build-classic' );
|
||||||
|
|
||||||
|
ClassicEditor
|
||||||
|
.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If you are planning to integrate CKEditor 5 deep into your application, it is actually more convenient and recommended to install and import the source modules directly (like it happens in `ckeditor.js`). Read more in the [Advanced setup guide](https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/advanced-setup.html).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).
|
||||||
8217
public/lib/ckeditor5-build-classic/package-lock.json
generated
Normal file
8217
public/lib/ckeditor5-build-classic/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
public/lib/ckeditor5-build-classic/package.json
Normal file
82
public/lib/ckeditor5-build-classic/package.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "@ckeditor/ckeditor5-build-classic",
|
||||||
|
"version": "35.0.1",
|
||||||
|
"description": "The classic editor build of CKEditor 5 – the best browser-based rich text editor.",
|
||||||
|
"keywords": [
|
||||||
|
"ckeditor5-build",
|
||||||
|
"ckeditor",
|
||||||
|
"ckeditor5",
|
||||||
|
"ckeditor 5",
|
||||||
|
"wysiwyg",
|
||||||
|
"rich text",
|
||||||
|
"editor",
|
||||||
|
"html",
|
||||||
|
"contentEditable",
|
||||||
|
"editing",
|
||||||
|
"operational transformation",
|
||||||
|
"ot",
|
||||||
|
"collaboration",
|
||||||
|
"collaborative",
|
||||||
|
"real-time",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
|
"main": "./build/ckeditor.js",
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
"ckeditor5-metadata.json",
|
||||||
|
"CHANGELOG.md"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@ckeditor/ckeditor5-alignment": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-autoformat": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-basic-styles": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-editor-classic": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-essentials": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-font": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-heading": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-horizontal-line": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-image": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-indent": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-link": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-list": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-media-embed": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-paragraph": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-paste-from-office": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-remove-format": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-source-editing": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-special-characters": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-table": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-typing": "^35.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@ckeditor/ckeditor5-core": "^35.0.1",
|
||||||
|
"@ckeditor/ckeditor5-dev-utils": "^30.0.0",
|
||||||
|
"@ckeditor/ckeditor5-dev-webpack-plugin": "^30.0.0",
|
||||||
|
"@ckeditor/ckeditor5-theme-lark": "^35.0.1",
|
||||||
|
"css-loader": "^5.2.7",
|
||||||
|
"postcss-loader": "^4.3.0",
|
||||||
|
"raw-loader": "^4.0.1",
|
||||||
|
"style-loader": "^2.0.0",
|
||||||
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
|
"ts-loader": "^9.3.0",
|
||||||
|
"webpack": "^5.58.1",
|
||||||
|
"webpack-cli": "^4.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0",
|
||||||
|
"npm": ">=5.7.1"
|
||||||
|
},
|
||||||
|
"author": "CKSource (http://cksource.com/)",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"homepage": "https://ckeditor.com/ckeditor-5",
|
||||||
|
"bugs": "https://github.com/ckeditor/ckeditor5/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ckeditor/ckeditor5.git",
|
||||||
|
"directory": "packages/ckeditor5-build-classic"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"preversion": "npm run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
public/lib/ckeditor5-build-classic/sample/index.html
Normal file
41
public/lib/ckeditor5-build-classic/sample/index.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>CKEditor 5 – classic editor build – development sample</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>CKEditor 5 – classic editor build – development sample</h1>
|
||||||
|
|
||||||
|
<div id="editor">
|
||||||
|
<h2>Sample</h2>
|
||||||
|
|
||||||
|
<p>This is an instance of the <a href="https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/predefined-builds.html#classic-editor">classic editor build</a>.</p>
|
||||||
|
|
||||||
|
<figure class="image">
|
||||||
|
<img src="../tests/manual/sample.jpg" alt="Autumn fields" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>You can use this sample to validate whether your <a href="https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/alternative-setups/custom-builds.html">custom build</a> works fine.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../build/ckeditor.js"></script>
|
||||||
|
<script>
|
||||||
|
ClassicEditor.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
161
public/lib/ckeditor5-build-classic/src/ckeditor.js
vendored
Normal file
161
public/lib/ckeditor5-build-classic/src/ckeditor.js
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
// The editor creator to use.
|
||||||
|
import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
|
||||||
|
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
|
||||||
|
import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
|
||||||
|
import AutoFormat from '@ckeditor/ckeditor5-autoformat/src/autoformat';
|
||||||
|
import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage';
|
||||||
|
import AutoLink from '@ckeditor/ckeditor5-link/src/autolink';
|
||||||
|
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
|
||||||
|
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
|
||||||
|
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
|
||||||
|
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript';
|
||||||
|
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript';
|
||||||
|
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
|
||||||
|
import Font from '@ckeditor/ckeditor5-font/src/font';
|
||||||
|
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily';
|
||||||
|
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
|
||||||
|
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
|
||||||
|
import Image from '@ckeditor/ckeditor5-image/src/image';
|
||||||
|
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
|
||||||
|
import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert';
|
||||||
|
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
|
||||||
|
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
|
||||||
|
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
|
||||||
|
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
|
||||||
|
import Indent from '@ckeditor/ckeditor5-indent/src/indent';
|
||||||
|
import Link from '@ckeditor/ckeditor5-link/src/link';
|
||||||
|
import List from '@ckeditor/ckeditor5-list/src/list';
|
||||||
|
import ListStyle from '@ckeditor/ckeditor5-list/src/liststyle';
|
||||||
|
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
|
||||||
|
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
|
||||||
|
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice';
|
||||||
|
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat';
|
||||||
|
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters';
|
||||||
|
import SpecialCharactersArrows
|
||||||
|
from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows';
|
||||||
|
import SpecialCharactersMathematical
|
||||||
|
from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical';
|
||||||
|
import SpecialCharactersText
|
||||||
|
from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext';
|
||||||
|
import Table from '@ckeditor/ckeditor5-table/src/table';
|
||||||
|
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
|
||||||
|
import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation';
|
||||||
|
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting';
|
||||||
|
import MyUploadAdapter from './my-upload-adapter';
|
||||||
|
|
||||||
|
export default class ClassicEditor extends ClassicEditorBase {}
|
||||||
|
|
||||||
|
// Plugins to include in the build.
|
||||||
|
ClassicEditor.builtinPlugins = [
|
||||||
|
Essentials,
|
||||||
|
Alignment,
|
||||||
|
AutoFormat,
|
||||||
|
AutoImage,
|
||||||
|
AutoLink,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Strikethrough,
|
||||||
|
Subscript,
|
||||||
|
Superscript,
|
||||||
|
Underline,
|
||||||
|
Font,
|
||||||
|
FontFamily,
|
||||||
|
Heading,
|
||||||
|
HorizontalLine,
|
||||||
|
Image,
|
||||||
|
ImageCaption,
|
||||||
|
ImageInsert,
|
||||||
|
ImageResize,
|
||||||
|
ImageStyle,
|
||||||
|
ImageToolbar,
|
||||||
|
ImageUpload,
|
||||||
|
Indent,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
ListStyle,
|
||||||
|
MediaEmbed,
|
||||||
|
Paragraph,
|
||||||
|
PasteFromOffice,
|
||||||
|
RemoveFormat,
|
||||||
|
SourceEditing,
|
||||||
|
SpecialCharacters,
|
||||||
|
SpecialCharactersArrows,
|
||||||
|
SpecialCharactersMathematical,
|
||||||
|
SpecialCharactersText,
|
||||||
|
Table,
|
||||||
|
TableToolbar,
|
||||||
|
TextTransformation,
|
||||||
|
MyUploadAdapter
|
||||||
|
];
|
||||||
|
|
||||||
|
// Editor configuration.
|
||||||
|
ClassicEditor.defaultConfig = {
|
||||||
|
toolbar: {
|
||||||
|
items: [
|
||||||
|
'heading',
|
||||||
|
'|',
|
||||||
|
'fontfamily',
|
||||||
|
'fontsize',
|
||||||
|
'fontColor',
|
||||||
|
'fontBackgroundColor',
|
||||||
|
'|',
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'underline',
|
||||||
|
'strikethrough',
|
||||||
|
'removeFormat',
|
||||||
|
'|',
|
||||||
|
'superscript',
|
||||||
|
'subscript',
|
||||||
|
'|',
|
||||||
|
'alignment',
|
||||||
|
'bulletedList',
|
||||||
|
'numberedList',
|
||||||
|
'-',
|
||||||
|
'link',
|
||||||
|
'imageUpload',
|
||||||
|
'insertTable',
|
||||||
|
'mediaEmbed',
|
||||||
|
'|',
|
||||||
|
'horizontalLine',
|
||||||
|
'specialCharacters',
|
||||||
|
'|',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'|',
|
||||||
|
'sourceEditing'
|
||||||
|
],
|
||||||
|
viewportTopOffset: 30,
|
||||||
|
shouldNotGroupWhenFull: true
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
styles: ['alignLeft', 'alignCenter', 'alignRight'],
|
||||||
|
resizeOptions: [
|
||||||
|
{ name: 'imageResize:25', label: '25%', value: '25' },
|
||||||
|
{ name: 'imageResize:50', label: '50%', value: '50' },
|
||||||
|
{ name: 'imageResize:75', label: '75%', value: '75' },
|
||||||
|
{ name: 'imageResize:original', label: 'Original', value: null }
|
||||||
|
],
|
||||||
|
toolbar: [
|
||||||
|
'imageStyle:alignRight',
|
||||||
|
'imageStyle:alignCenter',
|
||||||
|
'imageStyle:alignLeft',
|
||||||
|
'|',
|
||||||
|
'imageResize',
|
||||||
|
'|',
|
||||||
|
'imageTextAlternative'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells']
|
||||||
|
},
|
||||||
|
extraPlugin: [MyUploadAdapter],
|
||||||
|
// This value must be kept in sync with the language defined in webpack.config.js.
|
||||||
|
basicEntities: false,
|
||||||
|
language: 'en'
|
||||||
|
};
|
||||||
46
public/lib/ckeditor5-build-classic/src/my-upload-adapter.js
Normal file
46
public/lib/ckeditor5-build-classic/src/my-upload-adapter.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||||
|
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
|
||||||
|
import {logWarning} from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
||||||
|
|
||||||
|
export default class MyUploadAdapter extends Plugin {
|
||||||
|
static get requires() {
|
||||||
|
return [FileRepository];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pluginName() {
|
||||||
|
return 'MyUploadAdapter';
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const options = this.editor.config.get('upload');
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
if (!options['uploadAction']) {
|
||||||
|
logWarning('my-upload-adapter missing uploadAction');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.plugins.get(FileRepository).createUploadAdapter = loader => {
|
||||||
|
return new Adapter(loader, options);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Adapter {
|
||||||
|
constructor(loader, options) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.imageBaseURL = options['imageBaseURL'];
|
||||||
|
this.uploadAction = options['uploadAction'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload() {
|
||||||
|
const file = await this.loader.file;
|
||||||
|
const {error, payload} = await this.uploadAction(file);
|
||||||
|
if (error) return Promise.reject(payload);
|
||||||
|
return {default: `${this.imageBaseURL}${payload.url}`}
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
console.log('abort');
|
||||||
|
}
|
||||||
|
}
|
||||||
237
public/lib/ckeditor5-build-classic/tests/ckeditor.js
vendored
Normal file
237
public/lib/ckeditor5-build-classic/tests/ckeditor.js
vendored
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* globals document */
|
||||||
|
|
||||||
|
import ClassicEditor from '../src/ckeditor';
|
||||||
|
import BaseClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
|
||||||
|
import { describeMemoryUsage, testMemoryUsage } from '@ckeditor/ckeditor5-core/tests/_utils/memory';
|
||||||
|
|
||||||
|
describe( 'ClassicEditor build', () => {
|
||||||
|
let editor, editorElement;
|
||||||
|
|
||||||
|
beforeEach( () => {
|
||||||
|
editorElement = document.createElement( 'div' );
|
||||||
|
editorElement.innerHTML = '<p><strong>foo</strong> bar</p>';
|
||||||
|
|
||||||
|
document.body.appendChild( editorElement );
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
editorElement.remove();
|
||||||
|
editor = null;
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'build', () => {
|
||||||
|
it( 'contains plugins', () => {
|
||||||
|
expect( ClassicEditor.builtinPlugins ).to.not.be.empty;
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'contains config', () => {
|
||||||
|
expect( ClassicEditor.defaultConfig.toolbar ).to.not.be.empty;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'create()', () => {
|
||||||
|
beforeEach( () => {
|
||||||
|
return ClassicEditor.create( editorElement )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
return editor.destroy();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'creates an instance which inherits from the ClassicEditor', () => {
|
||||||
|
expect( editor ).to.be.instanceof( ClassicEditor );
|
||||||
|
expect( editor ).to.be.instanceof( BaseClassicEditor );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'loads data from the editor element', () => {
|
||||||
|
expect( editor.getData() ).to.equal( '<p><strong>foo</strong> bar</p>' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'destroy()', () => {
|
||||||
|
beforeEach( () => {
|
||||||
|
return ClassicEditor.create( editorElement )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'clears editor element if config.updateSourceElementOnDestroy flag is not set', () => {
|
||||||
|
editor.setData( '<p>foo</p>' );
|
||||||
|
|
||||||
|
return editor.destroy()
|
||||||
|
.then( () => {
|
||||||
|
expect( editorElement.innerHTML ).to.equal( '' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'sets the data back to the editor element if config.updateSourceElementOnDestroy flag is set', () => {
|
||||||
|
editor.setData( '<p>foo</p>' );
|
||||||
|
editor.config.set( 'updateSourceElementOnDestroy', true );
|
||||||
|
|
||||||
|
return editor.destroy()
|
||||||
|
.then( () => {
|
||||||
|
expect( editorElement.innerHTML ).to.equal( '<p>foo</p>' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'restores the editor element', () => {
|
||||||
|
expect( editor.sourceElement.style.display ).to.equal( 'none' );
|
||||||
|
|
||||||
|
return editor.destroy()
|
||||||
|
.then( () => {
|
||||||
|
expect( editor.sourceElement.style.display ).to.equal( '' );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'plugins', () => {
|
||||||
|
beforeEach( () => {
|
||||||
|
return ClassicEditor.create( editorElement )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterEach( () => {
|
||||||
|
return editor.destroy();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'paragraph works', () => {
|
||||||
|
const data = '<p>Some text inside a paragraph.</p>';
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'basic-styles work', () => {
|
||||||
|
const data = [
|
||||||
|
'<p>',
|
||||||
|
'<strong>Test:strong</strong>',
|
||||||
|
'<i>Test:i</i>',
|
||||||
|
'</p>'
|
||||||
|
].join( '' );
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'block-quote works', () => {
|
||||||
|
const data = '<blockquote><p>Quote</p></blockquote>';
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'heading works', () => {
|
||||||
|
const data = [
|
||||||
|
'<h2>Heading 1.</h2>',
|
||||||
|
'<h3>Heading 1.1</h3>',
|
||||||
|
'<h4>Heading 1.1.1</h4>',
|
||||||
|
'<h4>Heading 1.1.2</h4>',
|
||||||
|
'<h3>Heading 1.2</h3>',
|
||||||
|
'<h4>Heading 1.2.1</h4>',
|
||||||
|
'<h2>Heading 2</h2>'
|
||||||
|
].join( '' );
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'image works', () => {
|
||||||
|
const data = '<figure class="image"><img src="/assets/sample.png"></figure>';
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'list works', () => {
|
||||||
|
const data = [
|
||||||
|
'<ul>',
|
||||||
|
'<li>Item 1.</li>',
|
||||||
|
'<li>Item 2.</li>',
|
||||||
|
'</ul>',
|
||||||
|
'<ol>',
|
||||||
|
'<li>Item 1.</li>',
|
||||||
|
'<li>Item 2.</li>',
|
||||||
|
'</ol>'
|
||||||
|
].join( '' );
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'link works', () => {
|
||||||
|
const data = '<p><a href="//ckeditor.com">CKEditor.com</a></p>';
|
||||||
|
|
||||||
|
editor.setData( data );
|
||||||
|
expect( editor.getData() ).to.equal( data );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'config', () => {
|
||||||
|
afterEach( () => {
|
||||||
|
return editor.destroy();
|
||||||
|
} );
|
||||||
|
|
||||||
|
// https://github.com/ckeditor/ckeditor5/issues/572
|
||||||
|
it( 'allows configuring toolbar items through config.toolbar', () => {
|
||||||
|
return ClassicEditor
|
||||||
|
.create( editorElement, {
|
||||||
|
toolbar: [ 'bold' ]
|
||||||
|
} )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
|
||||||
|
expect( editor.ui.view.toolbar.items.length ).to.equal( 1 );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// https://github.com/ckeditor/ckeditor5/issues/572
|
||||||
|
it( 'allows configuring toolbar offset without overriding toolbar items', () => {
|
||||||
|
return ClassicEditor
|
||||||
|
.create( editorElement, {
|
||||||
|
ui: {
|
||||||
|
viewportOffset: {
|
||||||
|
top: 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
|
||||||
|
expect( editor.ui.view.toolbar.items.length ).to.equal( 17 );
|
||||||
|
expect( editor.ui.view.stickyPanel.viewportTopOffset ).to.equal( 42 );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'allows removing built-in toolbar items', () => {
|
||||||
|
return ClassicEditor
|
||||||
|
.create( editorElement, {
|
||||||
|
toolbar: {
|
||||||
|
removeItems: [ 'italic' ]
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
.then( newEditor => {
|
||||||
|
editor = newEditor;
|
||||||
|
|
||||||
|
expect( editor.ui.view.toolbar.items.length ).to.equal( 16 );
|
||||||
|
expect( editor.ui.view.toolbar.items.find( item => item.label === 'Italic' ) ).to.be.undefined;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describeMemoryUsage( () => {
|
||||||
|
testMemoryUsage(
|
||||||
|
'should not grow on multiple create/destroy',
|
||||||
|
() => ClassicEditor.create( document.querySelector( '#mem-editor' ) ) );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>CKEditor 5 – classic editor build – RequireJS integration test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>CKEditor 5 – classic editor build – RequireJS integration test (see <a href="https://github.com/ckeditor/ckeditor5/issues/914">#914</a>)</h1>
|
||||||
|
|
||||||
|
<h2>In the default language</h2>
|
||||||
|
|
||||||
|
<div id="editor">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>In German</h2>
|
||||||
|
|
||||||
|
<div id="editor-de">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
|
||||||
|
<script>
|
||||||
|
require( [ '../../build/ckeditor.js' ], ( ClassicEditor ) => {
|
||||||
|
ClassicEditor.create( document.getElementById( 'editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
require( [ '../../build/ckeditor.js', '../../build/translations/de.js' ], ( ClassicEditor ) => {
|
||||||
|
ClassicEditor
|
||||||
|
.create( document.getElementById( 'editor-de' ), {
|
||||||
|
language: 'de'
|
||||||
|
} )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="editor">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
|
||||||
|
<figure class="image">
|
||||||
|
<img src="./sample.jpg" alt="Autumn fields" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>After more than 2 years of building the next generation editor from scratch and closing over 980 tickets, we created a highly <strong>extensible and flexible architecture</strong> which consists of an <strong>amazing editing framework</strong> and <strong>editing solutions</strong> that will be built on top of it.</p>
|
||||||
|
|
||||||
|
<p>We explained this design choice in <a href="https://medium.com/content-uneditable/ckeditor-5-the-future-of-rich-text-editing-2b9300f9df2c">“CKEditor 5: The future of rich text editing“</a>:</p>
|
||||||
|
|
||||||
|
<blockquote><p>(…) we are changing our approach with CKEditor 5. We will no longer have only two solutions available, instead CKEditor will be seen as a framework for editing solutions. At the same time, we will be developing several out-of-the-box solutions based on it, which will be available to use in many different contexts. It will be a real “one size fits all” approach, from little requirements, to super advanced full featured applications.</p></blockquote>
|
||||||
|
|
||||||
|
<h3>Notes</h3>
|
||||||
|
|
||||||
|
<p><a href="https://ckeditor.com">CKEditor 5</a> is <i>under heavy development</i> and this demo is not production-ready software. For example:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Only Chrome, Opera and Safari are supported</strong>.</li>
|
||||||
|
<li>Firefox requires enabling the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/onselectionchange">“dom.select_events.enabled”</a> option.</li>
|
||||||
|
<li><a href="https://github.com/ckeditor/ckeditor5/issues/342">Support for pasting</a> is under development (content filtering is unstable).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>It has <em>bugs</em> that we are aware of — and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-env commonjs */
|
||||||
|
/* globals window, document, console */
|
||||||
|
|
||||||
|
const ClassicEditor = require( '../../build/ckeditor' );
|
||||||
|
|
||||||
|
ClassicEditor.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# CKEditor 5 classic build – standard version (CommonJS `require()`)
|
||||||
|
|
||||||
|
Just play with it.
|
||||||
|
|
||||||
|
**Note:** Remember to rebuild the bundles by running `npm run build` in build's package directory. You can also run Webpack in the watch mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
./node_modules/.bin/webpack -w
|
||||||
|
```
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>CKEditor 5 – classic editor build – global variable test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>CKEditor 5 – classic editor build – global variable test</h1>
|
||||||
|
|
||||||
|
<h2>In the default language</h2>
|
||||||
|
|
||||||
|
<div id="editor">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>In German</h2>
|
||||||
|
|
||||||
|
<div id="editor-de">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../build/ckeditor.js"></script>
|
||||||
|
<script src="../../build/translations/de.js"></script>
|
||||||
|
<script>
|
||||||
|
ClassicEditor.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
|
||||||
|
ClassicEditor
|
||||||
|
.create( document.querySelector( '#editor-de' ), {
|
||||||
|
language: 'de'
|
||||||
|
} )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="editor">
|
||||||
|
<h2>About CKEditor 5</h2>
|
||||||
|
|
||||||
|
<p>This is <a href="https://ckeditor.com">CKEditor 5</a>.</p>
|
||||||
|
|
||||||
|
<figure class="image">
|
||||||
|
<img src="./sample.jpg" alt="Autumn fields" />
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>After more than 2 years of building the next generation editor from scratch and closing over 980 tickets, we created a highly <strong>extensible and flexible architecture</strong> which consists of an <strong>amazing editing framework</strong> and <strong>editing solutions</strong> that will be built on top of it.</p>
|
||||||
|
|
||||||
|
<p>We explained this design choice in <a href="https://medium.com/content-uneditable/ckeditor-5-the-future-of-rich-text-editing-2b9300f9df2c">“CKEditor 5: The future of rich text editing“</a>:</p>
|
||||||
|
|
||||||
|
<blockquote><p>(…) we are changing our approach with CKEditor 5. We will no longer have only two solutions available, instead CKEditor will be seen as a framework for editing solutions. At the same time, we will be developing several out-of-the-box solutions based on it, which will be available to use in many different contexts. It will be a real “one size fits all” approach, from little requirements, to super advanced full featured applications.</p></blockquote>
|
||||||
|
|
||||||
|
<h3>Notes</h3>
|
||||||
|
|
||||||
|
<p><a href="https://ckeditor.com">CKEditor 5</a> is <i>under heavy development</i> and this demo is not production-ready software. For example:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Only Chrome, Opera and Safari are supported</strong>.</li>
|
||||||
|
<li>Firefox requires enabling the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/onselectionchange">“dom.select_events.enabled”</a> option.</li>
|
||||||
|
<li><a href="https://github.com/ckeditor/ckeditor5/issues/342">Support for pasting</a> is under development (content filtering is unstable).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>It has <em>bugs</em> that we are aware of — and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>
|
||||||
|
</div>
|
||||||
16
public/lib/ckeditor5-build-classic/tests/manual/ckeditor.js
vendored
Normal file
16
public/lib/ckeditor5-build-classic/tests/manual/ckeditor.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* globals window, document, console */
|
||||||
|
|
||||||
|
import ClassicEditor from '../../build/ckeditor';
|
||||||
|
|
||||||
|
ClassicEditor.create( document.querySelector( '#editor' ) )
|
||||||
|
.then( editor => {
|
||||||
|
window.editor = editor;
|
||||||
|
} )
|
||||||
|
.catch( error => {
|
||||||
|
console.error( 'There was a problem initializing the editor.', error );
|
||||||
|
} );
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# CKEditor 5 classic build – standard version
|
||||||
|
|
||||||
|
Just play with it.
|
||||||
|
|
||||||
|
**Note:** Remember to rebuild the bundles by running `npm run build` in build's package directory. You can also run Webpack in the watch mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
./node_modules/.bin/webpack -w
|
||||||
|
```
|
||||||
BIN
public/lib/ckeditor5-build-classic/tests/manual/sample.jpg
Normal file
BIN
public/lib/ckeditor5-build-classic/tests/manual/sample.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
103
public/lib/ckeditor5-build-classic/webpack.config.js
Normal file
103
public/lib/ckeditor5-build-classic/webpack.config.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
|
||||||
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
const path = require( 'path' );
|
||||||
|
const webpack = require( 'webpack' );
|
||||||
|
const { bundler, styles } = require( '@ckeditor/ckeditor5-dev-utils' );
|
||||||
|
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
|
||||||
|
const TerserPlugin = require( 'terser-webpack-plugin' );
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
performance: { hints: false },
|
||||||
|
|
||||||
|
entry: path.resolve( __dirname, 'src', 'ckeditor.js' ),
|
||||||
|
|
||||||
|
output: {
|
||||||
|
// The name under which the editor will be exported.
|
||||||
|
library: 'ClassicEditor',
|
||||||
|
|
||||||
|
path: path.resolve( __dirname, 'build' ),
|
||||||
|
filename: 'ckeditor.js',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default'
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin( {
|
||||||
|
sourceMap: true,
|
||||||
|
terserOptions: {
|
||||||
|
output: {
|
||||||
|
// Preserve CKEditor 5 license comments.
|
||||||
|
comments: /^!/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractComments: false
|
||||||
|
} )
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new CKEditorWebpackPlugin( {
|
||||||
|
// UI language. Language codes follow the https://en.wikipedia.org/wiki/ISO_639-1 format.
|
||||||
|
// When changing the built-in language, remember to also change it in the editor's configuration (src/ckeditor.js).
|
||||||
|
language: 'en',
|
||||||
|
additionalLanguages: 'all'
|
||||||
|
} ),
|
||||||
|
new webpack.BannerPlugin( {
|
||||||
|
banner: bundler.getLicenseBanner(),
|
||||||
|
raw: true
|
||||||
|
} )
|
||||||
|
],
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [ 'raw-loader' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'style-loader',
|
||||||
|
options: {
|
||||||
|
injectType: 'singletonStyleTag',
|
||||||
|
attributes: {
|
||||||
|
'data-cke': true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'css-loader',
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: styles.getPostCssConfig( {
|
||||||
|
themeImporter: {
|
||||||
|
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
|
||||||
|
},
|
||||||
|
minify: true
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: [ 'ts-loader' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: [ '.ts', '.js', '.json' ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
83
public/manifest.json
Normal file
83
public/manifest.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Medmoshaver FlashCard Uploader Panel",
|
||||||
|
"name": "Medmoshaver FlashCard Uploader 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:
|
||||||
38
src/App.css
Normal file
38
src/App.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/App.js
Normal file
25
src/App.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
|
import moment from 'moment-jalaali';
|
||||||
|
import HomePage from './components/home';
|
||||||
|
import LoginPage from './components/login';
|
||||||
|
import CreateCardPage from './components/create-card';
|
||||||
|
import CardsPage from './components/cards';
|
||||||
|
import DefaultPage from './components/default';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router basename={process.env.PUBLIC_URL}>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path='/' component={HomePage} />
|
||||||
|
<Route exact path='/login' component={LoginPage} />
|
||||||
|
<Route path='/cards/create' component={CreateCardPage} />
|
||||||
|
<Route path='/cards' component={CardsPage} />
|
||||||
|
<Route path='*' component={DefaultPage} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
moment.loadPersian({ usePersianDigits: false, dialect: 'persian-modern' });
|
||||||
|
|
||||||
|
export default App;
|
||||||
68
src/components/cards/cards.js
Normal file
68
src/components/cards/cards.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import Table from 'react-bootstrap/Table';
|
||||||
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||||
|
import Tooltip from 'react-bootstrap/Tooltip';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function Cards(props) {
|
||||||
|
return (
|
||||||
|
<Table striped bordered hover className='text-center'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='align-middle'>شناسه</th>
|
||||||
|
<th className='align-middle'>
|
||||||
|
<p>درس</p>
|
||||||
|
<p>فصل</p>
|
||||||
|
</th>
|
||||||
|
<th className='align-middle'>سوال</th>
|
||||||
|
<th className='align-middle'>پیش نمایش</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{props.cards.map(card => (
|
||||||
|
<tr key={card.id}>
|
||||||
|
<td className='align-middle'>{card.id}</td>
|
||||||
|
<td className='align-middle'>
|
||||||
|
<p>{card.lesson.name}</p>
|
||||||
|
<p>{card.chapter.name}</p>
|
||||||
|
</td>
|
||||||
|
<td className='align-middle'>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: card.question }}
|
||||||
|
style={{ maxWidth: '62vw', overflowX: 'auto' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className='align-middle'>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='right'
|
||||||
|
overlay={
|
||||||
|
<Tooltip id='tooltip-top'>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: card.question }} />
|
||||||
|
<div>
|
||||||
|
{card.options.map((option, index) => (
|
||||||
|
<p key={option.id}>
|
||||||
|
<span>{`گزینه ${index + 1}: `}</span>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: option.text }} />
|
||||||
|
<span>{option.isCorrect ? ' ✓ ' : ' ✗ '}</span>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: card.answer }} />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button>پیش نمایش</Button>
|
||||||
|
</OverlayTrigger>{' '}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cards.propTypes = {
|
||||||
|
cards: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cards;
|
||||||
16
src/components/cards/container.js
Normal file
16
src/components/cards/container.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import Cards from './cards';
|
||||||
|
import PaginatedFetcher from '../shared/fetches/paginated-fetcher';
|
||||||
|
import { fetchCards } from '../../redux/actions/card';
|
||||||
|
|
||||||
|
function CardsContainer() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<PaginatedFetcher action={fetchCards} stateSelector={state => state.cards}>
|
||||||
|
{cards => <Cards cards={cards} />}
|
||||||
|
</PaginatedFetcher>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardsContainer;
|
||||||
12
src/components/cards/index.js
Normal file
12
src/components/cards/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from '../layouts/main';
|
||||||
|
import CardsContainer from './container';
|
||||||
|
|
||||||
|
function CardsPage() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<CardsContainer />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardsPage;
|
||||||
13
src/components/create-card/container.js
Normal file
13
src/components/create-card/container.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import CardForm from './form';
|
||||||
|
import Fetcher from '../shared/fetches/fetcher';
|
||||||
|
import { fetchLessons } from '../../redux/actions/lesson';
|
||||||
|
|
||||||
|
function CreateCardContainer() {
|
||||||
|
return (
|
||||||
|
<Fetcher action={fetchLessons} stateSelector={state => state.lessons}>
|
||||||
|
{lessons => <CardForm lessons={lessons} />}
|
||||||
|
</Fetcher>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateCardContainer;
|
||||||
166
src/components/create-card/form.js
Normal file
166
src/components/create-card/form.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
change,
|
||||||
|
Field,
|
||||||
|
FieldArray,
|
||||||
|
formValueSelector,
|
||||||
|
reduxForm,
|
||||||
|
reset,
|
||||||
|
SubmissionError
|
||||||
|
} from 'redux-form';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
import Alert from 'react-bootstrap/Alert';
|
||||||
|
import Col from 'react-bootstrap/Col';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import FormEditor from '../shared/forms/editor';
|
||||||
|
import FormSelect from '../shared/forms/select';
|
||||||
|
import FormOptions from './options';
|
||||||
|
import Segment from '../shared/segment';
|
||||||
|
import { createCard } from '../../redux/actions/card';
|
||||||
|
import FormCheckbox from '../shared/forms/checkbox';
|
||||||
|
|
||||||
|
function CardForm(props) {
|
||||||
|
const { handleSubmit, pristine, submitting, error } = props;
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment>
|
||||||
|
{/* form error */}
|
||||||
|
{error && !pristine && (
|
||||||
|
<Alert variant='danger' className='text-center'>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* form */}
|
||||||
|
<Form noValidate onSubmit={handleSubmit(submit.bind(null, props, addToast))}>
|
||||||
|
<Form.Row>
|
||||||
|
<Col sm={6} className='my-1'>
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
horizontal
|
||||||
|
name='lesson'
|
||||||
|
label='درس'
|
||||||
|
component={FormSelect}
|
||||||
|
options={props.lessons.map(({ id, name }) => ({ key: id, text: name, value: id }))}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col sm={6} className='my-1'>
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
horizontal
|
||||||
|
name='chapter'
|
||||||
|
label='فصل'
|
||||||
|
component={FormSelect}
|
||||||
|
options={props.chapters.map(({ id, name }) => ({ key: id, text: name, value: id }))}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Row>
|
||||||
|
|
||||||
|
<Field name='starred' label='ستاره دار' component={FormCheckbox} />
|
||||||
|
<Field required name='question' label='متن سوال' component={FormEditor} />
|
||||||
|
<Field required name='answer' label='جواب' component={FormEditor} />
|
||||||
|
<FieldArray name='options' component={FormOptions} />
|
||||||
|
<Button type='submit' disabled={pristine || submitting} className='float-right'>
|
||||||
|
ایجاد فلش کارت
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit(props, addToast, values) {
|
||||||
|
try {
|
||||||
|
await props.createCard(values);
|
||||||
|
addToast('فلش کارت شما با موفقیت ساخته شد.', { appearance: 'success' });
|
||||||
|
await props.reset('create-card');
|
||||||
|
await props.change('lesson', values.lesson);
|
||||||
|
await props.change('chapter', values.chapter);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.message;
|
||||||
|
throw new SubmissionError({ _error: errorMessage });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(values) {
|
||||||
|
const { lesson, chapter, question, answer, options } = values;
|
||||||
|
const errors = {};
|
||||||
|
if (!lesson) errors.lesson = 'وارد کردن درس لازم است.';
|
||||||
|
if (!chapter) errors.chapter = 'وارد کردن فصل لازم است.';
|
||||||
|
if (!question || !question.trim()) errors.question = 'وارد کردن متن سوال لازم است.';
|
||||||
|
if (!answer || !answer.trim()) errors.answer = 'وارد کردن جواب سوال لازم است.';
|
||||||
|
|
||||||
|
function addOptionError(errorMessage) {
|
||||||
|
if (!('options' in errors)) errors.options = { _error: [] };
|
||||||
|
errors.options._error.push(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options || options.length < 2) {
|
||||||
|
addOptionError('حداقل دو گزینه لازم است.');
|
||||||
|
addOptionError('یک گزینه درست لازم است.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
const correctOptionCount = options.reduce((count, { isCorrect }) => count + +isCorrect, 0);
|
||||||
|
if (correctOptionCount === 0) addOptionError('یک گزینه درست لازم است.');
|
||||||
|
if (correctOptionCount > 1) addOptionError('تعداد گزینه درست بیشتر از یک مورد است.');
|
||||||
|
if (checkDuplicateOption(options)) addOptionError('گزینه تکراری مجاز نیست.');
|
||||||
|
|
||||||
|
if (
|
||||||
|
question &&
|
||||||
|
answer &&
|
||||||
|
(question === answer ||
|
||||||
|
question.replaceAll(' ', '').replaceAll(' ', '') ===
|
||||||
|
answer.replaceAll(' ', '').replaceAll(' ', ''))
|
||||||
|
) {
|
||||||
|
if (!options.some(({ text }) => ['صحیح', 'درست', 'بلی'].includes(text))) {
|
||||||
|
errors._error =
|
||||||
|
'سوال و جواب (بدون یکی از گزینه های "بلی"، "درست" یا "صحیح") نمیتوانند یکسان باشند.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const option = options[i];
|
||||||
|
if (!option.text || !option.text.trim()) {
|
||||||
|
if (!('options' in errors)) errors.options = {};
|
||||||
|
errors.options[i] = { text: 'متن گزینه نمینواند خالی باشد.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDuplicateOption(options) {
|
||||||
|
const optionTextSet = new Set();
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const optionText = options[i].text.trim();
|
||||||
|
if (!optionText) continue;
|
||||||
|
if (optionTextSet.has(optionText)) return true;
|
||||||
|
optionTextSet.add(optionText);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state, { lessons }) {
|
||||||
|
let chapters = [];
|
||||||
|
const selector = formValueSelector('create-card');
|
||||||
|
const selectedLessonId = selector(state, 'lesson');
|
||||||
|
if (selectedLessonId) {
|
||||||
|
const selectedLesson = lessons.find(lesson => lesson.id.toString() === selectedLessonId);
|
||||||
|
chapters = selectedLesson.chapters;
|
||||||
|
}
|
||||||
|
const initialValues = {
|
||||||
|
options: [
|
||||||
|
{ text: '', isCorrect: false, weight: 100 },
|
||||||
|
{ text: '', isCorrect: false, weight: 100 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return { chapters, initialValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps, { change, reset, createCard }),
|
||||||
|
reduxForm({ form: 'create-card', validate })
|
||||||
|
)(CardForm);
|
||||||
12
src/components/create-card/index.js
Normal file
12
src/components/create-card/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import MainLayout from '../layouts/main';
|
||||||
|
import CreateCardContainer from './container';
|
||||||
|
|
||||||
|
function CreateCardPage() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<CreateCardContainer />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateCardPage;
|
||||||
167
src/components/create-card/options.js
Normal file
167
src/components/create-card/options.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { change, Field, formValueSelector } from 'redux-form';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
import Alert from 'react-bootstrap/Alert';
|
||||||
|
import { FaPlus, FaTrashAlt, FaUndo } from 'react-icons/fa';
|
||||||
|
import FormTextArea from '../shared/forms/textarea';
|
||||||
|
import FormCheckbox from '../shared/forms/checkbox';
|
||||||
|
import Segment from '../shared/segment';
|
||||||
|
|
||||||
|
function FormOptions(props) {
|
||||||
|
const {
|
||||||
|
fields,
|
||||||
|
meta: { error },
|
||||||
|
change,
|
||||||
|
isIncorrectOption,
|
||||||
|
showOptionHints,
|
||||||
|
isHintOption
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div>
|
||||||
|
<label className='text-large text-bold'>
|
||||||
|
{'گزینهها: '}
|
||||||
|
<span className='text-danger'>*</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{error && error.length && (
|
||||||
|
<Alert variant='danger' className='text-center'>
|
||||||
|
{error.map((e, index) => (
|
||||||
|
<p key={index} className='m-0'>
|
||||||
|
{index + 1}) {e}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fields.map((member, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className='my-3'>
|
||||||
|
<Segment>
|
||||||
|
<div className='d-flex justify-content-between align-items-baseline mb-0'>
|
||||||
|
<p className='text-bold'>{`گزینه ${index + 1}`}</p>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant='outline-warning'
|
||||||
|
size='sm'
|
||||||
|
onClick={resetOption.bind(null, change, member)}
|
||||||
|
>
|
||||||
|
<FaUndo />
|
||||||
|
</Button>{' '}
|
||||||
|
<Button
|
||||||
|
variant='outline-danger'
|
||||||
|
size='sm'
|
||||||
|
onClick={fields.remove.bind(null, index)}
|
||||||
|
>
|
||||||
|
<FaTrashAlt />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
horizontal
|
||||||
|
required
|
||||||
|
name={`${member}.text`}
|
||||||
|
component={FormTextArea}
|
||||||
|
label='متن'
|
||||||
|
placeholder=''
|
||||||
|
disabled={isHintOption(index)}
|
||||||
|
/>
|
||||||
|
<Form.Row className='justify-content-between align-items-baseline'>
|
||||||
|
{!isIncorrectOption(index) && (
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
name={`${member}.isCorrect`}
|
||||||
|
component={FormCheckbox}
|
||||||
|
label='گزینه درست'
|
||||||
|
disabled={isIncorrectOption(index)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='ml-auto'>
|
||||||
|
{showOptionHints(index) &&
|
||||||
|
optionHints.map((optionHint, index) => (
|
||||||
|
<Fragment key={optionHint.id}>
|
||||||
|
{index > 0 && ' '}
|
||||||
|
<Button
|
||||||
|
variant='outline-secondary'
|
||||||
|
onClick={handleOptionHintClick.bind(null, change, member, optionHint)}
|
||||||
|
>
|
||||||
|
{optionHint.text}
|
||||||
|
</Button>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Row>
|
||||||
|
</Segment>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className='mt-3 mb-5 text-center'>
|
||||||
|
<Button variant='outline-success' onClick={createNewOption.bind(null, fields)}>
|
||||||
|
{'افزودن گزینه جدید '}
|
||||||
|
<FaPlus />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_OPTIONS_COUNT = 2;
|
||||||
|
|
||||||
|
const optionHints = [
|
||||||
|
{ id: 1, text: 'هیچ', value: 'هیچکدام', weight: 0 },
|
||||||
|
{ id: 2, text: '1,2', value: 'گزینههای 1 و 2', weight: 20 },
|
||||||
|
{ id: 3, text: '1,3', value: 'گزینههای 1 و 3', weight: 20 },
|
||||||
|
{ id: 4, text: '2,3', value: 'گزینههای 2 و 3', weight: 20 },
|
||||||
|
{ id: 5, text: 'همه', value: 'همه موارد', weight: 10 }
|
||||||
|
];
|
||||||
|
const optionHintValues = optionHints.map(({ value }) => value);
|
||||||
|
|
||||||
|
function isRealOptionText(text) {
|
||||||
|
return !!text && !optionHintValues.includes(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOptionHintClick(change, member, optionHint) {
|
||||||
|
change('create-card', `${member}.text`, optionHint.value);
|
||||||
|
change('create-card', `${member}.weight`, optionHint.weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOption(change, member) {
|
||||||
|
change('create-card', `${member}.text`, '');
|
||||||
|
change('create-card', `${member}.weight`, 100);
|
||||||
|
change('create-card', `${member}.isCorrect`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewOption(fields) {
|
||||||
|
fields.push({ text: '', isCorrect: false, weight: 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIncorrectOption(options, index) {
|
||||||
|
if (!options || options.length === 0) return false;
|
||||||
|
const firstCorrectOptionIndex = options.findIndex(option => option.isCorrect);
|
||||||
|
return firstCorrectOptionIndex >= 0 && !options[index].isCorrect;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOptionHints(options, index) {
|
||||||
|
if (!options || options.length === 0) return false;
|
||||||
|
const realOptionCount = options.filter(({ text }) => isRealOptionText(text)).length;
|
||||||
|
return realOptionCount >= MIN_OPTIONS_COUNT && !isRealOptionText(options[index].text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHintOption(options, index) {
|
||||||
|
return optionHintValues.includes(options[index].text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const selector = formValueSelector('create-card');
|
||||||
|
const options = selector(state, 'options');
|
||||||
|
return {
|
||||||
|
showOptionHints: showOptionHints.bind(null, options),
|
||||||
|
isIncorrectOption: isIncorrectOption.bind(null, options),
|
||||||
|
isHintOption: isHintOption.bind(null, options)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { change })(FormOptions);
|
||||||
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;
|
||||||
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;
|
||||||
68
src/components/layouts/main/auth-header.js
Normal file
68
src/components/layouts/main/auth-header.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Button from 'react-bootstrap-button-loader';
|
||||||
|
import ButtonGroup from 'react-bootstrap/ButtonGroup';
|
||||||
|
import { logoutUser } from '../../../redux/actions/auth';
|
||||||
|
import { ANONYMOUS, AUTHENTICATED } from '../../../redux/constants';
|
||||||
|
import NavButton from '../../shared/nav-button';
|
||||||
|
import { fetchUserData } from '../../../redux/actions/user';
|
||||||
|
import { fetchData } from '../../utils/fetch';
|
||||||
|
|
||||||
|
function AuthHeader(props) {
|
||||||
|
useEffect(() => fetchData(props.fetchUserData), [props.fetchUserData]);
|
||||||
|
const history = useHistory();
|
||||||
|
const nextURL = `${history.location.pathname}${history.location.search.replace('&', ';')}`;
|
||||||
|
|
||||||
|
if (props.authStatus === ANONYMOUS) {
|
||||||
|
return (
|
||||||
|
<NavButton
|
||||||
|
to={`/login?next=${nextURL}`}
|
||||||
|
variant='outline-light'
|
||||||
|
content='ورود'
|
||||||
|
className={props.className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAuthenticated = props.authStatus === AUTHENTICATED;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup className={props.className}>
|
||||||
|
<NavButton
|
||||||
|
to='/register'
|
||||||
|
variant='light'
|
||||||
|
loading={!isAuthenticated}
|
||||||
|
disabled={!isAuthenticated}
|
||||||
|
content={props.name || props.mobile}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='danger'
|
||||||
|
loading={!isAuthenticated}
|
||||||
|
disabled={!isAuthenticated}
|
||||||
|
onClick={props.logoutUser.bind(null, history)}
|
||||||
|
>
|
||||||
|
خروج
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthHeader.propTypes = {
|
||||||
|
authStatus: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string,
|
||||||
|
mobile: PropTypes.string,
|
||||||
|
fetchUserData: PropTypes.func.isRequired,
|
||||||
|
logoutUser: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
authStatus: state.auth.status,
|
||||||
|
name: state.user.data.name,
|
||||||
|
mobile: state.user.data.mobile
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { fetchUserData, logoutUser })(AuthHeader);
|
||||||
40
src/components/layouts/main/error-boundary.js
Normal file
40
src/components/layouts/main/error-boundary.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Fragment, useEffect } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { toast, ToastContainer, Zoom } from 'react-toastify';
|
||||||
|
import Toast from '../../shared/toast';
|
||||||
|
import { removeError } from '../../../redux/actions/errors';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
function ErrorBoundary(props) {
|
||||||
|
const { errors, removeError } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (let i = 0; i < errors.length; i++) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
error: { status, statusText, data }
|
||||||
|
} = errors[i];
|
||||||
|
toast.error(<Toast errorCode={status} errorText={statusText} errorData={data} />);
|
||||||
|
removeError(id);
|
||||||
|
}
|
||||||
|
}, [errors, removeError]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{props.children}
|
||||||
|
<ToastContainer
|
||||||
|
position='bottom-left'
|
||||||
|
autoClose={10000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
rtl={true}
|
||||||
|
transition={Zoom}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps({ errors }) {
|
||||||
|
return { errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { removeError })(ErrorBoundary);
|
||||||
5
src/components/layouts/main/footer.js
Normal file
5
src/components/layouts/main/footer.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function Footer() {
|
||||||
|
return <footer />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
28
src/components/layouts/main/header.js
Normal file
28
src/components/layouts/main/header.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Navbar from 'react-bootstrap/Navbar';
|
||||||
|
import Nav from 'react-bootstrap/Nav';
|
||||||
|
import AuthHeader from './auth-header';
|
||||||
|
import NavLink from '../../shared/nav-link';
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<header className='mb-4'>
|
||||||
|
<Navbar bg='secondary' variant='dark' expand='lg'>
|
||||||
|
<Navbar.Brand href='/' className='d-none d-sm-block'>
|
||||||
|
MED FLASH
|
||||||
|
</Navbar.Brand>
|
||||||
|
<div className='flex-grow-1 mx-4'>
|
||||||
|
<AuthHeader />
|
||||||
|
</div>
|
||||||
|
<Navbar.Toggle aria-controls='basic-navbar-nav' />
|
||||||
|
<Navbar.Collapse id='basic-navbar-nav' className='flex-shrink-1 flex-grow-0'>
|
||||||
|
<Nav className='mr-auto'>
|
||||||
|
<NavLink to='/cards/create' content='آپلود فلش کارت' />
|
||||||
|
<NavLink to='/cards' content='فلش کارتها' />
|
||||||
|
</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Navbar>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
73
src/components/layouts/main/index.js
Normal file
73
src/components/layouts/main/index.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Redirect, useLocation } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Container } from 'react-bootstrap';
|
||||||
|
import ErrorBoundary from './error-boundary';
|
||||||
|
import Header from './header';
|
||||||
|
import Footer from './footer';
|
||||||
|
import Loader from '../../shared/loader';
|
||||||
|
import ScrollToTop from '../../shared/scroll-to-top';
|
||||||
|
import { ANONYMOUS, AUTHENTICATED, INIT, LOADING } from '../../../redux/constants';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
function MainLayout(props) {
|
||||||
|
const { title, description, loginRequired } = props;
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const currentURL = `${location.pathname}${location.search.replace('&', ';')}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (title) document.title = title;
|
||||||
|
if (description) document.description = description;
|
||||||
|
}, [title, description]);
|
||||||
|
|
||||||
|
if (loginRequired && props.authStatus === ANONYMOUS)
|
||||||
|
return <Redirect to={`/login?next=${currentURL}`} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className='rtl page-container'>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<Container>{renderContent(props)}</Container>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
<ScrollToTop />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent(props) {
|
||||||
|
const { authStatus, loginRequired } = props;
|
||||||
|
|
||||||
|
if (loginRequired && [INIT, LOADING].includes(authStatus)) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!loginRequired || authStatus === AUTHENTICATED) return props.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainLayout.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
loginRequired: PropTypes.bool.isRequired,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
|
||||||
|
};
|
||||||
|
|
||||||
|
MainLayout.defaultProps = {
|
||||||
|
loginRequired: true
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
authStatus: state.auth.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(MainLayout);
|
||||||
13
src/components/layouts/main/index.scss
Normal file
13
src/components/layouts/main/index.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
min-height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/components/login/form.js
Normal file
64
src/components/login/form.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Field, reduxForm, reset, SubmissionError } from 'redux-form';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
import Alert from 'react-bootstrap/Alert';
|
||||||
|
import FormInput from '../shared/forms/input';
|
||||||
|
import { mobile, required } from '../shared/forms';
|
||||||
|
import { loginUser } from '../../redux/actions/auth';
|
||||||
|
import Segment from '../shared/segment';
|
||||||
|
|
||||||
|
function LoginForm(props) {
|
||||||
|
const { handleSubmit, pristine, submitting, error } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment>
|
||||||
|
{/* form error */}
|
||||||
|
{error && !pristine && (
|
||||||
|
<Alert variant='danger' className='text-center'>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* form */}
|
||||||
|
<Form onSubmit={handleSubmit(submit.bind(null, props))}>
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
name='mobile'
|
||||||
|
label='شماره تلفن همراه'
|
||||||
|
type='tel'
|
||||||
|
placeholder='09121234567'
|
||||||
|
component={FormInput}
|
||||||
|
validate={[required, mobile]}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
required
|
||||||
|
name='password'
|
||||||
|
label='رمز عبور'
|
||||||
|
type='password'
|
||||||
|
component={FormInput}
|
||||||
|
validate={required}
|
||||||
|
/>
|
||||||
|
<Button type='submit' disabled={pristine || submitting}>
|
||||||
|
ورود
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit(props, { mobile, password }) {
|
||||||
|
try {
|
||||||
|
await props.loginUser(mobile, password);
|
||||||
|
await props.reset();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.message || 'شماره تلفن یا رمز عبور نادرست است.';
|
||||||
|
throw new SubmissionError({ _error: errorMessage });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(null, { loginUser, reset }),
|
||||||
|
reduxForm({ form: 'login' })
|
||||||
|
)(LoginForm);
|
||||||
27
src/components/login/index.js
Normal file
27
src/components/login/index.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import Col from 'react-bootstrap/Col';
|
||||||
|
import MainLayout from '../layouts/main';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import LoginForm from './form';
|
||||||
|
import useQueryParams from '../../hooks/query-params';
|
||||||
|
import { AUTHENTICATED } from '../../redux/constants';
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
const isAuthenticated = useSelector(state => state.auth.status === AUTHENTICATED);
|
||||||
|
const { next } = useQueryParams();
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const nextURL = next ? next.replace(';', '&') : '/cards/create';
|
||||||
|
return <Redirect to={nextURL} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout loginRequired={false}>
|
||||||
|
<Col sm={12} md={10} lg={8} className='mx-auto'>
|
||||||
|
<LoginForm />
|
||||||
|
</Col>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
41
src/components/shared/editor.js
Normal file
41
src/components/shared/editor.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CKEditor } from '@ckeditor/ckeditor5-react';
|
||||||
|
import ClassicEditor from 'my-ckeditor5-build-classic/build/ckeditor';
|
||||||
|
import { uploadImage } from '../../redux/actions/image';
|
||||||
|
import 'my-ckeditor5-build-classic/build/translations/fa';
|
||||||
|
|
||||||
|
function Editor(props) {
|
||||||
|
return (
|
||||||
|
<CKEditor
|
||||||
|
editor={ClassicEditor}
|
||||||
|
data={props.data}
|
||||||
|
onChange={props.onChange}
|
||||||
|
onBlur={props.onBlur}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
config={{
|
||||||
|
upload: {
|
||||||
|
imageBaseURL: process.env.REACT_APP_IMAGE_BASE_URL,
|
||||||
|
uploadAction: props.uploadImage
|
||||||
|
},
|
||||||
|
language: 'fa'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Editor.defaultProps = {
|
||||||
|
value: '',
|
||||||
|
onBlur: function () {},
|
||||||
|
onFocus: function () {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onBlur: PropTypes.func.isRequired,
|
||||||
|
onFocus: PropTypes.func.isRequired,
|
||||||
|
uploadImage: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, { uploadImage })(Editor);
|
||||||
15
src/components/shared/error.js
Normal file
15
src/components/shared/error.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function Error(props) {
|
||||||
|
return <p className='text-center'>{props.content}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error.defaultProps = {
|
||||||
|
content: 'خطایی رخ داده است.'
|
||||||
|
};
|
||||||
|
|
||||||
|
Error.propTypes = {
|
||||||
|
content: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error;
|
||||||
57
src/components/shared/fetches/fetcher.js
Normal file
57
src/components/shared/fetches/fetcher.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Loader from '../loader';
|
||||||
|
import Error from '../error';
|
||||||
|
import { fetchData } from '../../utils/fetch';
|
||||||
|
import { ERROR, INIT, LOADING } from '../../../redux/constants';
|
||||||
|
|
||||||
|
function Fetcher(props) {
|
||||||
|
useEffect(() => fetchData(props.action), [props.action]);
|
||||||
|
// Loading
|
||||||
|
if ([INIT, LOADING].includes(props.status)) return <Loader />;
|
||||||
|
// Error
|
||||||
|
if (props.status === ERROR) {
|
||||||
|
const result = props.renderError(props.error);
|
||||||
|
if (result !== null) return result;
|
||||||
|
}
|
||||||
|
// Empty Data
|
||||||
|
if (
|
||||||
|
(Array.isArray(props.data) && props.data.length === 0) ||
|
||||||
|
(Object.entries(props.data).length === 0 && props.data.constructor === Object)
|
||||||
|
) {
|
||||||
|
const result = props.renderEmpty();
|
||||||
|
if (result !== null) return result;
|
||||||
|
}
|
||||||
|
// Good Condition
|
||||||
|
return props.children(props.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Fetcher.propTypes = {
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
stateSelector: PropTypes.func.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]).isRequired,
|
||||||
|
renderEmpty: PropTypes.func.isRequired,
|
||||||
|
renderError: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
Fetcher.defaultProps = {
|
||||||
|
renderEmpty: () => null,
|
||||||
|
renderError: () => <Error />
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, { stateSelector }) {
|
||||||
|
return {
|
||||||
|
status: stateSelector(state).status,
|
||||||
|
error: stateSelector(state).error,
|
||||||
|
data: stateSelector(state).data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch, { action }) {
|
||||||
|
return bindActionCreators({ action }, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Fetcher);
|
||||||
73
src/components/shared/fetches/multi-fetcher.js
Normal file
73
src/components/shared/fetches/multi-fetcher.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Loader from '../loader';
|
||||||
|
import { fetchData, mergeStatuses } from '../../utils/fetch';
|
||||||
|
import { ERROR, INIT, LOADING } from '../../../redux/constants';
|
||||||
|
import Error from '../error';
|
||||||
|
|
||||||
|
function MultiFetcher(props) {
|
||||||
|
useEffect(() => fetchData(props.action), [props.action]);
|
||||||
|
// Loading
|
||||||
|
if ([INIT, LOADING].includes(props.status)) return <Loader />;
|
||||||
|
// Error
|
||||||
|
if (props.status === ERROR) return props.renderError(props.errorKeys);
|
||||||
|
// Empty Data
|
||||||
|
const emptyDataKeys = [];
|
||||||
|
for (const key in props.data) {
|
||||||
|
const data = props.data[key];
|
||||||
|
if (
|
||||||
|
(Array.isArray(data) && data.length === 0) ||
|
||||||
|
(Object.entries(data).length === 0 && data.constructor === Object)
|
||||||
|
)
|
||||||
|
emptyDataKeys.push(key);
|
||||||
|
}
|
||||||
|
if (emptyDataKeys.length !== 0) {
|
||||||
|
const result = props.renderEmpty(emptyDataKeys);
|
||||||
|
if (result !== null) return result;
|
||||||
|
}
|
||||||
|
// Good Condition
|
||||||
|
return props.children(props.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiFetcher.propTypes = {
|
||||||
|
actions: PropTypes.arrayOf(PropTypes.func).isRequired,
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
stateSelectors: PropTypes.objectOf(PropTypes.func).isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
renderEmpty: PropTypes.func.isRequired,
|
||||||
|
renderError: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiFetcher.defaultProps = {
|
||||||
|
renderEmpty: () => null,
|
||||||
|
renderError: () => <Error />
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, { stateSelectors }) {
|
||||||
|
const statuses = [];
|
||||||
|
const data = {};
|
||||||
|
const errorKeys = [];
|
||||||
|
for (const key in stateSelectors) {
|
||||||
|
if (!(key in stateSelectors)) continue;
|
||||||
|
const selectedState = stateSelectors[key](state);
|
||||||
|
statuses.push(selectedState.status);
|
||||||
|
data[key] = selectedState.data;
|
||||||
|
if (selectedState.status === ERROR) errorKeys.push(key);
|
||||||
|
}
|
||||||
|
return { status: mergeStatuses(statuses), data, errorKeys };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch, { actions }) {
|
||||||
|
function action() {
|
||||||
|
return function (dispatch) {
|
||||||
|
for (let i = 0; i < actions.length; i++) dispatch(actions[i]());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindActionCreators({ action }, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MultiFetcher);
|
||||||
40
src/components/shared/fetches/paginated-fetcher.js
Normal file
40
src/components/shared/fetches/paginated-fetcher.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Fetcher from './fetcher';
|
||||||
|
import Pagination from '../pagination';
|
||||||
|
import Error from '../error';
|
||||||
|
import useQueryParams from '../../../hooks/query-params';
|
||||||
|
|
||||||
|
function PaginatedFetcher(props) {
|
||||||
|
const page = parseInt(useQueryParams().get('page')) || 1;
|
||||||
|
return (
|
||||||
|
<Fetcher action={props.action.bind(null, page)} stateSelector={props.stateSelector}>
|
||||||
|
{data => (
|
||||||
|
<Fragment>
|
||||||
|
{props.children(data)}
|
||||||
|
<Pagination page={page} pageCount={props.pageCount} />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fetcher>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaginatedFetcher.propTypes = {
|
||||||
|
action: PropTypes.func.isRequired,
|
||||||
|
stateSelector: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
renderEmpty: PropTypes.func.isRequired,
|
||||||
|
renderError: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
PaginatedFetcher.defaultProps = {
|
||||||
|
renderEmpty: () => null,
|
||||||
|
renderError: () => <Error />
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, { stateSelector }) {
|
||||||
|
return { pageCount: stateSelector(state).pageCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PaginatedFetcher);
|
||||||
28
src/components/shared/forms/checkbox.js
Normal file
28
src/components/shared/forms/checkbox.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
|
||||||
|
function FormCheckbox(props) {
|
||||||
|
const {
|
||||||
|
input,
|
||||||
|
required,
|
||||||
|
label,
|
||||||
|
disabled,
|
||||||
|
meta: { touched, error }
|
||||||
|
} = props;
|
||||||
|
const id = props.id || input.name;
|
||||||
|
const checked = typeof input.value === 'boolean' ? input.value : input.value === 'true';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id}>
|
||||||
|
<Form.Check type='checkbox'>
|
||||||
|
<Form.Check.Input type='checkbox' disabled={disabled} checked={checked} {...input} />
|
||||||
|
<Form.Check.Label>
|
||||||
|
{label}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Check.Label>
|
||||||
|
</Form.Check>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormCheckbox;
|
||||||
30
src/components/shared/forms/editor.js
Normal file
30
src/components/shared/forms/editor.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
import Editor from '../editor';
|
||||||
|
|
||||||
|
function FromEditor(props) {
|
||||||
|
const { input, meta, label, required } = props;
|
||||||
|
const { touched, error } = meta;
|
||||||
|
const id = props.id || input.name;
|
||||||
|
const exactLabel = ['.', '?', '!', '؟'].includes(label.charAt(label.length - 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id}>
|
||||||
|
<Form.Label>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<Editor
|
||||||
|
data={input.value}
|
||||||
|
onChange={(event, editor) => {
|
||||||
|
const data = editor.getData();
|
||||||
|
input.onChange(data);
|
||||||
|
}}
|
||||||
|
onBlur={() => input.onBlur()}
|
||||||
|
onFocus={() => input.onFocus()}
|
||||||
|
/>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FromEditor;
|
||||||
0
src/components/shared/forms/form.css
Normal file
0
src/components/shared/forms/form.css
Normal file
5
src/components/shared/forms/index.js
Normal file
5
src/components/shared/forms/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// export {default as FormCheckbox} from './checkbox';
|
||||||
|
export { default as FormInput } from './input';
|
||||||
|
// export {default as FormSelect} from './select';
|
||||||
|
// export {default as FormTextArea} from './textarea';
|
||||||
|
export * from './validations';
|
||||||
43
src/components/shared/forms/input.js
Normal file
43
src/components/shared/forms/input.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
|
||||||
|
function FormInput(props) {
|
||||||
|
const {
|
||||||
|
input,
|
||||||
|
required,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
horizontal,
|
||||||
|
meta: { touched, error }
|
||||||
|
} = props;
|
||||||
|
const id = props.id || input.name;
|
||||||
|
const placeholder = props.placeholder || label;
|
||||||
|
const exactLabel = ['.', '?', '!', '؟'].includes(label.charAt(label.length - 1));
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id} className='d-flex align-items-baseline'>
|
||||||
|
<Form.Label className='mr-3 flex-shrink-0'>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<div className='flex-grow-1'>
|
||||||
|
<Form.Control {...input} required={required} type={type} placeholder={placeholder} />
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id}>
|
||||||
|
<Form.Label>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control {...input} required={required} type={type} placeholder={placeholder} />
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInput;
|
||||||
82
src/components/shared/forms/select.js
Normal file
82
src/components/shared/forms/select.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
|
||||||
|
function FormSelect(props) {
|
||||||
|
const {
|
||||||
|
input,
|
||||||
|
required,
|
||||||
|
horizontal,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
multiple,
|
||||||
|
size,
|
||||||
|
meta: { touched, error }
|
||||||
|
} = props;
|
||||||
|
const id = props.id || input.name;
|
||||||
|
const value = multiple && !input.value ? [] : input.value;
|
||||||
|
const exactLabel = ['.', '?', '!', '؟'].includes(label.charAt(label.length - 1));
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id} className='d-flex align-items-baseline'>
|
||||||
|
<Form.Label className='mr-3 flex-shrink-0'>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<div className='flex-grow-1'>
|
||||||
|
<Form.Control
|
||||||
|
{...input}
|
||||||
|
custom
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={options}
|
||||||
|
size={size}
|
||||||
|
as='select'
|
||||||
|
isValid={!!value}
|
||||||
|
>
|
||||||
|
{options.map(({ value, text, key }) => (
|
||||||
|
<option key={key} value={value}>
|
||||||
|
{text}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id}>
|
||||||
|
<Form.Label>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
{...input}
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={options}
|
||||||
|
size={size}
|
||||||
|
as='select'
|
||||||
|
>
|
||||||
|
{options.map(({ value, text, key }) => (
|
||||||
|
<option key={key} value={value}>
|
||||||
|
{text}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormSelect.defaultProps = {
|
||||||
|
exactLabel: false,
|
||||||
|
placeholder: 'انتخاب کنید...',
|
||||||
|
options: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormSelect;
|
||||||
61
src/components/shared/forms/textarea.js
Normal file
61
src/components/shared/forms/textarea.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import Form from 'react-bootstrap/Form';
|
||||||
|
|
||||||
|
function FormTextArea(props) {
|
||||||
|
const {
|
||||||
|
input,
|
||||||
|
required,
|
||||||
|
disabled,
|
||||||
|
horizontal,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
size,
|
||||||
|
meta: { touched, error }
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const id = props.id || input.name;
|
||||||
|
const placeholder = props.placeholder || label;
|
||||||
|
const exactLabel = ['.', '?', '!', '؟'].includes(label.charAt(label.length - 1));
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id} className='d-flex align-items-baseline'>
|
||||||
|
<Form.Label className='mr-3 flex-shrink-0'>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<div className='flex-grow-1'>
|
||||||
|
<Form.Control
|
||||||
|
{...input}
|
||||||
|
required={required}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
as='textarea'
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={id}>
|
||||||
|
<Form.Label>
|
||||||
|
{exactLabel ? label : `${label} :`}
|
||||||
|
{required && <span className='text-danger'> * </span>}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
{...input}
|
||||||
|
required={required}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
size={size}
|
||||||
|
as='textarea'
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{touched && error != null && <Form.Text className='text-danger'>{error}</Form.Text>}
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormTextArea;
|
||||||
32
src/components/shared/forms/validations.js
Normal file
32
src/components/shared/forms/validations.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// validation functions
|
||||||
|
export function required(value) {
|
||||||
|
return value != null ? undefined : 'وارد کردن این فیلد الزامی است.';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function number(value) {
|
||||||
|
return value && isNaN(Number(value)) ? 'لطفا یک عدد وارد نمایید.' : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function email(value) {
|
||||||
|
return value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)
|
||||||
|
? 'لطفا یک ایمیل معتبر وارد نمایید.'
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mobile(value) {
|
||||||
|
return value && !/^[0][9][0-9]{9}$/i.test(value)
|
||||||
|
? 'لطفا یک شماره تلفن همراه معتبر با حروف انگلیسی وارد نمایید.'
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function password(value) {
|
||||||
|
return value && !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/i.test(value)
|
||||||
|
? 'لطفا یک رمزعبور دارای حداقل 8 کاراکتر و ترکیبی از حروف انگلیسی و اعداد وارد نمایید.'
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function smsKey(value) {
|
||||||
|
return value && !/^[0-9]{6}$/i.test(value)
|
||||||
|
? 'لطفا کد 6 رقمی پیامک شده به شماره خود را وارد نمایید.'
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
25
src/components/shared/loader.js
Normal file
25
src/components/shared/loader.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Spinner from 'react-bootstrap/Spinner';
|
||||||
|
|
||||||
|
function Loader(props) {
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
<div className='my-2'>
|
||||||
|
<Spinner animation='border' role='status' />
|
||||||
|
</div>
|
||||||
|
{props.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader.defaultProps = {
|
||||||
|
inverted: true,
|
||||||
|
content: 'لطفا منتظر بمانید.'
|
||||||
|
};
|
||||||
|
|
||||||
|
Loader.propTypes = {
|
||||||
|
inverted: PropTypes.bool.isRequired,
|
||||||
|
content: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loader;
|
||||||
19
src/components/shared/nav-button.js
Normal file
19
src/components/shared/nav-button.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import Button from 'react-bootstrap-button-loader';
|
||||||
|
|
||||||
|
function NavButton(props) {
|
||||||
|
const { to, content, ...otherProps } = props;
|
||||||
|
return (
|
||||||
|
<LinkContainer to={to}>
|
||||||
|
<Button {...otherProps}>{content}</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavButton.propTypes = {
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavButton;
|
||||||
19
src/components/shared/nav-link.js
Normal file
19
src/components/shared/nav-link.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import Nav from 'react-bootstrap/Nav';
|
||||||
|
|
||||||
|
function NavLink(props) {
|
||||||
|
const { to, content, ...otherProps } = props;
|
||||||
|
return (
|
||||||
|
<LinkContainer to={to}>
|
||||||
|
<Nav.Link {...otherProps}>{content}</Nav.Link>
|
||||||
|
</LinkContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NavLink.propTypes = {
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavLink;
|
||||||
36
src/components/shared/pagination.js
Normal file
36
src/components/shared/pagination.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import Pagination from 'react-bootstrap/Pagination';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
function PaginationComponent(props) {
|
||||||
|
const { page, pageCount } = props;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const paginationClicked = event =>
|
||||||
|
history.push(`${history.location.pathname}?page=${event.target.text}`);
|
||||||
|
|
||||||
|
if (pageCount < 2) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination className='justify-content-center'>
|
||||||
|
{page > 3 && <Pagination.Item onClick={paginationClicked}>{1}</Pagination.Item>}
|
||||||
|
{page > 4 && <Pagination.Ellipsis />}
|
||||||
|
{page > 2 && <Pagination.Item onClick={paginationClicked}>{page - 2}</Pagination.Item>}
|
||||||
|
{page > 1 && <Pagination.Item onClick={paginationClicked}>{page - 1}</Pagination.Item>}
|
||||||
|
<Pagination.Item active onClick={paginationClicked}>
|
||||||
|
{page}
|
||||||
|
</Pagination.Item>
|
||||||
|
{page < pageCount && (
|
||||||
|
<Pagination.Item onClick={paginationClicked}>{page + 1}</Pagination.Item>
|
||||||
|
)}
|
||||||
|
{page < pageCount - 1 && (
|
||||||
|
<Pagination.Item onClick={paginationClicked}>{page + 2}</Pagination.Item>
|
||||||
|
)}
|
||||||
|
{page < pageCount - 3 && <Pagination.Ellipsis />}
|
||||||
|
{page < pageCount - 2 && (
|
||||||
|
<Pagination.Item onClick={paginationClicked}>{pageCount}</Pagination.Item>
|
||||||
|
)}
|
||||||
|
</Pagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaginationComponent;
|
||||||
31
src/components/shared/scroll-to-top.js
Normal file
31
src/components/shared/scroll-to-top.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import { FaAngleUp } from 'react-icons/fa';
|
||||||
|
import styles from './scroll-to-top.module.scss';
|
||||||
|
|
||||||
|
function ScrollToTop() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('scroll', handleScrollEvent.bind(null, setIsVisible));
|
||||||
|
return document.removeEventListener('scroll', handleScrollEvent.bind(null, setIsVisible));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={scrollToTop} className={styles['scroll-button']}>
|
||||||
|
<FaAngleUp />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScrollEvent(setIsVisible) {
|
||||||
|
setIsVisible(window.pageYOffset > 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScrollToTop;
|
||||||
14
src/components/shared/scroll-to-top.module.scss
Normal file
14
src/components/shared/scroll-to-top.module.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.scroll-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 15px;
|
||||||
|
left: 15px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: gray;
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/components/shared/segment.js
Normal file
17
src/components/shared/segment.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Card from 'react-bootstrap/Card';
|
||||||
|
|
||||||
|
function Segment(props) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Card.Body>{props.content || props.children}</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Segment.propTypes = {
|
||||||
|
content: PropTypes.string,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Segment;
|
||||||
27
src/components/shared/toast.js
Normal file
27
src/components/shared/toast.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function Toast(props) {
|
||||||
|
const { errorCode, errorText, errorData } = props;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div>خطا</div>
|
||||||
|
<div dir='auto' className='text-start'>
|
||||||
|
{errorCode ? `${errorCode}: ${errorText}` : errorText}
|
||||||
|
</div>
|
||||||
|
{errorData && (
|
||||||
|
<div dir='auto' className='text-start'>
|
||||||
|
{`error data: ${JSON.stringify(errorData, null, 2)}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.propTypes = {
|
||||||
|
errorCode: PropTypes.number.isRequired,
|
||||||
|
errorText: PropTypes.string.isRequired,
|
||||||
|
errorData: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.array]).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
9
src/components/utils/datetime.js
Normal file
9
src/components/utils/datetime.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import moment from 'moment-jalaali';
|
||||||
|
|
||||||
|
export function convertUnixToString(timestamp, format = 'dddd jDD-jMM-jYYYY') {
|
||||||
|
return moment.unix(timestamp).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDateTimeToString(timestamp, format = 'dddd jDD-jMM-jYYYY') {
|
||||||
|
return moment(timestamp).format(format);
|
||||||
|
}
|
||||||
15
src/components/utils/fetch.js
Normal file
15
src/components/utils/fetch.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ERROR, INIT, LOADING, OK } from '../../redux/constants';
|
||||||
|
|
||||||
|
export function fetchData(fetchFunction) {
|
||||||
|
fetchFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeStatuses(statuses) {
|
||||||
|
let isOK = true;
|
||||||
|
for (let i = 0; i < statuses.length; i++) {
|
||||||
|
if (statuses[i] === INIT) return INIT;
|
||||||
|
if (statuses[i] === ERROR) return ERROR;
|
||||||
|
isOK = isOK && statuses[i] === OK;
|
||||||
|
}
|
||||||
|
return isOK ? OK : LOADING;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user