This commit is contained in:
2026-08-08 18:53:36 +03:30
commit 92f21ca1cf
134 changed files with 22909 additions and 0 deletions

10
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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"
}
}

File diff suppressed because one or more lines are too long

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
public/browserconfig.xml Normal file
View 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
View 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>

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -0,0 +1,71 @@
CKEditor 5 classic editor build
========================================
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-build-classic.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic)
[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5?branch=master)
[![Build Status](https://travis-ci.com/ckeditor/ckeditor5.svg?branch=master)](https://app.travis-ci.com/github/ckeditor/ckeditor5)
![Dependency Status](https://img.shields.io/librariesio/release/npm/@ckeditor/ckeditor5-build-classic)
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).
![CKEditor 5 classic editor build screenshot](https://c.cksource.com/a/1/img/npm/ckeditor5-build-classic.png)
## 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).

File diff suppressed because it is too large Load Diff

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

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

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

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

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

View File

@@ -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&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;5</a>.</p>
</div>
<h2>In German</h2>
<div id="editor-de">
<h2>About CKEditor&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;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>

View File

@@ -0,0 +1,27 @@
<div id="editor">
<h2>About CKEditor&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;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">&ldquo;CKEditor 5: The future of rich text editing&ldquo;</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&nbsp;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">&ldquo;dom.select_events.enabled&rdquo;</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 &mdash; and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>
</div>

View File

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

View File

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

View File

@@ -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&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;5</a>.</p>
</div>
<h2>In German</h2>
<div id="editor-de">
<h2>About CKEditor&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;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>

View File

@@ -0,0 +1,27 @@
<div id="editor">
<h2>About CKEditor&nbsp;5</h2>
<p>This is <a href="https://ckeditor.com">CKEditor&nbsp;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">&ldquo;CKEditor 5: The future of rich text editing&ldquo;</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&nbsp;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">&ldquo;dom.select_events.enabled&rdquo;</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 &mdash; and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>
</div>

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

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

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
src/App.css Normal file
View 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
View 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;

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

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

View File

@@ -0,0 +1,12 @@
import MainLayout from '../layouts/main';
import CardsContainer from './container';
function CardsPage() {
return (
<MainLayout>
<CardsContainer />
</MainLayout>
);
}
export default CardsPage;

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

View 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('&nbsp;', '').replaceAll(' ', '') ===
answer.replaceAll('&nbsp;', '').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);

View File

@@ -0,0 +1,12 @@
import MainLayout from '../layouts/main';
import CreateCardContainer from './container';
function CreateCardPage() {
return (
<MainLayout>
<CreateCardContainer />
</MainLayout>
);
}
export default CreateCardPage;

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

View File

@@ -0,0 +1,7 @@
import MainLayout from '../layouts/main';
function DefaultPage() {
return <MainLayout loginRequired={false} />;
}
export default DefaultPage;

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

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

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

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

View File

@@ -0,0 +1,5 @@
function Footer() {
return <footer />;
}
export default Footer;

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

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

View File

@@ -0,0 +1,13 @@
.page-container {
min-height: 100vh;
display: flex;
flex-direction: column;
main {
flex-grow: 1;
}
footer {
min-height: 25px;
}
}

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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