├── .gitignore ├── README.md ├── _config.yml ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── actions │ ├── auth.actions.js │ └── settings.actions.js ├── app.js ├── app.test.js ├── components │ ├── customized-editor.js │ ├── index.js │ ├── main-header.js │ ├── main-sidebar.js │ ├── navigation │ │ ├── navigation-link.js │ │ ├── navigation.js │ │ └── navlink-with-submenu.js │ └── private-route-handler.js ├── constants │ ├── app.constants.js │ ├── index.js │ └── redux.constants.js ├── hooks │ ├── debounce-hook.js │ └── test.hook.js ├── index.js ├── pages │ ├── private │ │ └── dashboard │ │ │ └── dashboard.js │ └── public │ │ ├── login │ │ └── login.js │ │ └── register │ │ └── register.js ├── reducers │ ├── auth.reducer.js │ ├── index.js │ └── settings.reducer.js ├── serviceWorker.js ├── services │ └── auth.service.js ├── styles │ ├── __nprogress.scss │ ├── __react-toastify.scss │ ├── __reset.scss │ ├── _animations.scss │ ├── _global.scss │ ├── _grid.scss │ ├── _mixins.scss │ ├── _typography.scss │ ├── _variables.scss │ └── index.scss └── utils │ ├── copy-to-clipboard.js │ ├── generate-url-key.js │ ├── history.js │ ├── index.js │ ├── json-validator.js │ ├── price-formater.js │ ├── routes.js │ ├── store.js │ ├── toasts.js │ └── validator.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to GitHub Pages 2 | 3 | You can use the [editor on GitHub](https://github.com/behnamazimi/reactjs-basic-project-structure/edit/master/README.md) to maintain and preview the content for your website in Markdown files. 4 | 5 | Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. 6 | 7 | ### Markdown 8 | 9 | Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for 10 | 11 | ```markdown 12 | Syntax highlighted code block 13 | 14 | # Header 1 15 | ## Header 2 16 | ### Header 3 17 | 18 | - Bulleted 19 | - List 20 | 21 | 1. Numbered 22 | 2. List 23 | 24 | **Bold** and _Italic_ and `Code` text 25 | 26 | [Link](url) and ![Image](src) 27 | ``` 28 | 29 | For more details see [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). 30 | 31 | ### Jekyll Themes 32 | 33 | Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/behnamazimi/reactjs-basic-project-structure/settings). The name of this theme is saved in the Jekyll `_config.yml` configuration file. 34 | 35 | ### Support or Contact 36 | 37 | Having trouble with Pages? Check out our [documentation](https://help.github.com/categories/github-pages-basics/) or [contact support](https://github.com/contact) and we’ll help you sort it out. 38 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-basic-project-structure", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.2.6", 7 | "draft-js": "^0.11.0", 8 | "draftjs-to-html": "^0.8.4", 9 | "history": "^4.9.0", 10 | "immutability-helper": "^3.0.1", 11 | "js-cookie": "^2.2.1", 12 | "moment-jalaali": "^0.8.3", 13 | "node-sass": "^7.0.0", 14 | "nprogress": "^0.2.0", 15 | "prop-types": "^15.7.2", 16 | "react": "^16.9.0", 17 | "react-custom-scrollbars": "^4.2.1", 18 | "react-dom": "^16.9.0", 19 | "react-google-recaptcha": "^1.1.0", 20 | "react-icons": "^3.7.0", 21 | "react-scripts": "3.1.1", 22 | "react-toastify": "^5.3.2", 23 | "sass-flex-mixin": "^1.0.3", 24 | "simple-crypto-js": "^2.2.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ], 41 | "devDependencies": { 42 | "axios": "^0.21.1", 43 | "react-redux": "^7.1.1", 44 | "react-router-dom": "^5.0.1", 45 | "redux": "^4.0.4", 46 | "redux-thunk": "^2.3.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowMan128/reactjs-basic-project-structure/c3e29f9767b03bc13dcbf33f4c0f2bb2581bcb4b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Project Main Title 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/auth.actions.js: -------------------------------------------------------------------------------- 1 | import {authConstants, appConstants} from "../constants"; 2 | import {authService} from "../services/auth.service"; 3 | import {history} from "../utils"; 4 | import {toasts} from "../utils/toasts"; 5 | import * as axios from "axios"; 6 | 7 | export const authActions = { 8 | loginAttempt, 9 | logout, 10 | setLoggedIn 11 | }; 12 | 13 | function loginAttempt(email, password, submitBtnRef = null) { 14 | return dispatch => { 15 | 16 | authService.login(email, password) 17 | .then(function (response) { 18 | 19 | if (response.data.success && response.data.token) { 20 | let mustSave = { 21 | admin: response.data.admin, 22 | token: response.data.token.token, 23 | refreshToken: response.data.token.refreshToken, 24 | }; 25 | 26 | authService.setLocalCryptoItem(appConstants.AUTH_TOKEN_KEY, mustSave.token) 27 | authService.setLocalCryptoItem(appConstants.AUTH_REFRESH_TOKEN_KEY, mustSave.refreshToken) 28 | authService.setLocalCryptoItem(appConstants.AUTH_USER_KEY, mustSave.admin) 29 | 30 | dispatch({ 31 | type: authConstants.LOGIN_SUCCESS, 32 | payload: { 33 | admin: mustSave.admin, 34 | role: parseInt(mustSave.admin.access) || 10 35 | } 36 | }); 37 | 38 | axios.defaults.headers.common['Authorization'] = authService.getToken(); 39 | axios.defaults.headers.common['Content-Type'] = 'application/json'; 40 | 41 | 42 | toasts.success('Successful login!'); 43 | history.push('/'); 44 | } else { 45 | if (response.status === 200) 46 | toasts.error('Not Activated!'); 47 | else 48 | toasts.error('Not Exist!'); 49 | submitBtnRef.classList.remove('loading') 50 | 51 | } 52 | 53 | 54 | }) 55 | .catch(err => { 56 | submitBtnRef.classList.remove('loading') 57 | console.log(err); 58 | if (err.response && err.response.status === 401) 59 | toasts.error('Not Exist!'); 60 | else 61 | toasts.error('Login Error.'); 62 | 63 | }) 64 | } 65 | } 66 | 67 | function logout() { 68 | return dispatch => { 69 | 70 | authService.logout(); 71 | 72 | dispatch({ 73 | type: authConstants.SET_LOGGED_IN_STATUS, 74 | }); 75 | 76 | history.push('/login'); 77 | } 78 | } 79 | 80 | function setLoggedIn(isAuthenticated) { 81 | return dispatch => { 82 | dispatch({ 83 | type: authConstants.SET_LOGGED_IN_STATUS, 84 | payload: isAuthenticated 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/actions/settings.actions.js: -------------------------------------------------------------------------------- 1 | import {settingsConstants} from "../constants/redux.constants"; 2 | 3 | export const settingsActions = { 4 | toggleMainMenu, 5 | toggleSideMenu, 6 | setCurrentPageTitle 7 | }; 8 | 9 | function toggleMainMenu(status = true) { 10 | return dispatch => { 11 | dispatch({ 12 | type: settingsConstants.MAIN_SIDEBAR_CLOSE, 13 | payload: status 14 | }) 15 | } 16 | } 17 | 18 | function toggleSideMenu(status = true) { 19 | return dispatch => { 20 | dispatch({ 21 | type: settingsConstants.ACTIONS_MENU_OPEN, 22 | payload: status 23 | }) 24 | } 25 | } 26 | 27 | function setCurrentPageTitle(title = true) { 28 | return dispatch => { 29 | dispatch({ 30 | type: settingsConstants.SET_CURRENT_PAGE_TITLE, 31 | payload: title 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {connect} from "react-redux"; 3 | import {authService} from "./services/auth.service" 4 | import {authActions} from "./actions/auth.actions"; 5 | import {Route, Switch, withRouter} from "react-router-dom"; 6 | import {ToastContainer} from 'react-toastify'; 7 | import {history, routes} from "./utils"; 8 | import MainHeader from "./components/main-header"; 9 | import MainSidebar from "./components/main-sidebar"; 10 | import {settingsActions} from "./actions/settings.actions"; 11 | import PrivateRouteHandler from "./components/private-route-handler"; 12 | import cx from "classnames"; 13 | 14 | function App(props) { 15 | 16 | const {main_sidebar_close, isAuthenticated, location, page_title} = props 17 | 18 | useEffect(() => { 19 | 20 | const {dispatch} = props; 21 | // dispatch(authActions.setLoggedIn(authService.isAuthenticated())) 22 | dispatch(authActions.setLoggedIn(true)) 23 | 24 | // set document title on route enter 25 | routes.map(route => { 26 | if (route.path === location.pathname) 27 | dispatch(settingsActions.setCurrentPageTitle(route.title)) 28 | 29 | return route 30 | }) 31 | 32 | // change document title on route change 33 | history.listen((location, action) => { 34 | 35 | routes.map(route => { 36 | if (route.path === location.pathname) 37 | dispatch(settingsActions.setCurrentPageTitle(route.title)) 38 | 39 | return route 40 | }) 41 | 42 | }) 43 | }, []) 44 | 45 | const renderSwitch = () => ( 46 | 47 | {routes.map((route, key) => { 48 | return ; 50 | })} 51 | 52 | ) 53 | 54 | return ( 55 |
56 | {isAuthenticated && 57 | 58 | 59 | 60 | } 61 |
62 | {renderSwitch()} 63 |
64 | 65 | 66 |
67 | ); 68 | } 69 | 70 | 71 | function mapStateToProps(state) { 72 | 73 | return { 74 | isAuthenticated: state.auth.isAuthenticated, 75 | page_title: state.settings.page_title, 76 | main_sidebar_close: state.settings.main_sidebar_close 77 | } 78 | 79 | } 80 | 81 | export default connect(mapStateToProps)(withRouter(App)); 82 | -------------------------------------------------------------------------------- /src/app.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/customized-editor.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {Editor} from 'react-draft-wysiwyg'; 3 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; 4 | import draftToHtml from 'draftjs-to-html'; 5 | import {EditorState, convertToRaw, RichUtils, ContentState} from 'draft-js'; 6 | import cx from "classnames" 7 | import htmlToDraft from "html-to-draftjs"; 8 | 9 | function CustomizedEditor({ 10 | content = '', 11 | onContentChange, 12 | placeholder = 'محتوا را اینجا بنویسید...', 13 | wrapperClassName, 14 | editorClassName, 15 | }) { 16 | 17 | const [editorState, setEditorState] = useState(EditorState.createEmpty()) 18 | const [mainContent, setMainContent] = useState('') 19 | 20 | // this will help to render default content only on mount 21 | const [isLockDefaultContentRender, lockDefaultContentRender] = useState(false) 22 | 23 | useEffect(() => { 24 | 25 | if (content && !isLockDefaultContentRender) { 26 | const contentBlock = htmlToDraft(content); 27 | if (contentBlock) { 28 | const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks); 29 | const eState = EditorState.createWithContent(contentState); 30 | 31 | setEditorState(eState) 32 | } 33 | 34 | lockDefaultContentRender(true) 35 | } 36 | 37 | }, [content]) 38 | 39 | /** 40 | * Editor state change handler 41 | * @param eState 42 | */ 43 | const onEditorStateChange = (eState) => { 44 | const html = draftToHtml(convertToRaw(eState.getCurrentContent())); 45 | 46 | setMainContent(html); 47 | setEditorState(eState) 48 | 49 | if (onContentChange) // invoke content change 50 | onContentChange(html) 51 | 52 | }; 53 | 54 | // handling key commands for editor 55 | function handleKeyCommand(command, editorState) { 56 | const newState = RichUtils.handleKeyCommand(editorState, command); 57 | if (newState) { 58 | this.onChange(newState); 59 | return 'handled'; 60 | } 61 | return 'not-handled'; 62 | } 63 | 64 | return ( 65 |
66 | 76 |
77 | ) 78 | } 79 | 80 | export default CustomizedEditor -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./main-header" 2 | export * from "./main-sidebar" 3 | export * from "./private-route-handler" 4 | -------------------------------------------------------------------------------- /src/components/main-header.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from "react-redux"; 3 | 4 | function MainHeader(props) { 5 | 6 | return ( 7 |

Main Header

8 | ) 9 | } 10 | 11 | function mapStateToProps(state) { 12 | 13 | return { 14 | sidebar_open: state.settings.main_sidebar_close 15 | } 16 | } 17 | 18 | export default connect(mapStateToProps)(MainHeader); -------------------------------------------------------------------------------- /src/components/main-sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from "react-redux"; 3 | import {authActions} from "../actions/auth.actions"; 4 | import {Scrollbars} from "react-custom-scrollbars"; 5 | import {settingsActions} from "../actions/settings.actions"; 6 | import {appConstants} from "../constants/app.constants"; 7 | import Navigation from "./navigation/navigation"; 8 | import cx from "classnames" 9 | 10 | function MainSidebar({main_sidebar_close, dispatch}) { 11 | 12 | const menuItems = [ 13 | { 14 | title: 'Item 1', 15 | icon: 'th', 16 | path: '/', 17 | }, { 18 | title: 'S1', 19 | divider: true 20 | }, { 21 | title: 'Item 2', 22 | icon: 'users', 23 | path: '/i2', 24 | }, { 25 | title: 'S2', 26 | divider: true 27 | }, { 28 | title: 'S3', 29 | divider: true 30 | }, { 31 | title: 'Item 3', 32 | icon: 'lightbulb outline', 33 | path: '/i3', 34 | }, { 35 | title: 'Item 4', 36 | icon: 'lightbulb outline', 37 | corner_icon: 'plus', 38 | path: '/i4', 39 | }, { 40 | title: 'S4', 41 | divider: true 42 | }, { 43 | title: 'Item 4-1', 44 | icon: 'users', 45 | path: '/i4', 46 | }, { 47 | title: 'Item 5', 48 | icon: 'users', 49 | corner_icon: 'plus', 50 | path: '/i5', 51 | }, { 52 | title: 'S5', 53 | divider: true 54 | }, 55 | { 56 | title: 'Item 6', 57 | icon: 'file image', 58 | path: '/i6', 59 | }, { 60 | title: 'Item 7', 61 | icon: 'copy outline', 62 | child: [ 63 | { 64 | title: 'Item 7.1', 65 | path: '/i71', 66 | }, { 67 | title: 'Item 7.2', 68 | path: '/i72', 69 | }, 70 | ] 71 | }, 72 | ] 73 | 74 | const toggleMainMenu = () => { 75 | dispatch(settingsActions.toggleMainMenu(false)) 76 | } 77 | 78 | const doLogout = () => { 79 | dispatch(authActions.logout()); 80 | } 81 | 82 | return ( 83 | 84 | 85 | 99 |
100 | 101 | 102 | 103 | ); 104 | 105 | } 106 | 107 | function mapStateToProps(state) { 108 | return { 109 | main_sidebar_close: state.settings.main_sidebar_close 110 | } 111 | 112 | } 113 | 114 | export default connect(mapStateToProps)(MainSidebar); 115 | -------------------------------------------------------------------------------- /src/components/navigation/navigation-link.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {NavLink} from "react-router-dom"; 3 | import {NavLinkWithSubmenu} from "./navlink-with-submenu"; 4 | 5 | export class NavigationLink extends Component { 6 | 7 | render() { 8 | const {item} = this.props; 9 | if (!item.child) 10 | return ( 11 |
  • 12 | {item.icon} 13 | {item.title}
  • 14 | ); 15 | else 16 | return ( 17 | 18 | ) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/components/navigation/navigation.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {NavigationLink} from "./navigation-link"; 3 | import PropTypes from 'prop-types'; 4 | 5 | export class Navigation extends Component { 6 | 7 | render() { 8 | 9 | const items = this.props.items.map((item, key) => { 10 | if (item.divider) 11 | return (
  • 12 |
    13 | {item.title} 14 |
    15 |
  • ); 16 | else 17 | return ( 18 | 19 | ) 20 | }); 21 | return (items && 22 | 27 | ) 28 | } 29 | } 30 | 31 | Navigation.propTypes = { 32 | items: PropTypes.array.isRequired 33 | }; 34 | 35 | export default Navigation -------------------------------------------------------------------------------- /src/components/navigation/navlink-with-submenu.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {NavLink} from "react-router-dom"; 3 | 4 | export class NavLinkWithSubmenu extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.toggleSubmenu = this.toggleSubmenu.bind(this); 9 | this.state = { 10 | visible: false 11 | } 12 | } 13 | 14 | toggleSubmenu() { 15 | const visibility = this.state.visible; 16 | this.setState({ 17 | visible: !visibility 18 | }) 19 | } 20 | 21 | render() { 22 | const {navTitle, icon} = this.props; 23 | const {visible} = this.state; 24 | const items = this.props.child.map((item, key) => ( 25 |
  • 26 | 27 | {item.icon} 28 | {item.title}
  • 29 | )); 30 | return ( 31 |
  • 32 | {/**/} 33 | {/*eslint-disable-next-line */} 34 | 35 | {icon} 36 | {navTitle} 37 |
      38 | {items} 39 |
    40 |
  • 41 | ); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/components/private-route-handler.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Route, Redirect} from 'react-router-dom'; 3 | import {authService} from "../services/auth.service"; 4 | import NProgress from 'nprogress' 5 | 6 | class PrivateRouteHandler extends Component { 7 | componentWillMount() { 8 | NProgress.start() 9 | } 10 | 11 | componentDidMount() { 12 | NProgress.done() 13 | } 14 | 15 | renderRoutes = (props) => { 16 | const {component: Component, private: isPrivate, redirectOnAuth, ...rest} = this.props 17 | const isAuthenticated =true// authService.isAuthenticated(); 18 | 19 | if (isAuthenticated) { 20 | if (isPrivate || (!isPrivate && !redirectOnAuth)) 21 | return 22 | else if (!isPrivate && redirectOnAuth) 23 | return 24 | // return "Redirected to public component" 25 | 26 | } else if (!isAuthenticated) { 27 | if (isPrivate) { 28 | return 29 | 30 | } else if (!isPrivate) { 31 | return 32 | } 33 | } 34 | 35 | } 36 | 37 | render() { 38 | const {component: Component, ...rest} = this.props 39 | 40 | return ( 41 | 42 | ) 43 | } 44 | 45 | } 46 | 47 | export default PrivateRouteHandler 48 | -------------------------------------------------------------------------------- /src/constants/app.constants.js: -------------------------------------------------------------------------------- 1 | export const appConstants = { 2 | PUBLIC_URL: 'http://localhost:3333/api/v1', 3 | MANAGE_URL: 'http://localhost:3333/api/v1/manage', 4 | FILES_URL: 'http://localhost:3333/', 5 | 6 | // PUBLIC_URL: 'http://core.armanmandegar.com/api/v1', 7 | // MANAGE_URL: 'http://core.armanmandegar.com/api/v1/manage', 8 | // FILES_URL: 'http://core.armanmandegar.com/', 9 | 10 | WEB_URL: 'http://armanmandegar.com', 11 | 12 | APP_NAME: 'App Name', 13 | SERVICE_NAME: 'Service Name', 14 | 15 | COPYRIGHT_TEXT: 'Copyright Message.', 16 | 17 | // colors 18 | MAIN_COLOR_NAME: 'main', 19 | MAIN_COLOR_CODE: '#be0000', 20 | LIGHT_COLOR_NAME: 'light', 21 | LIGHT_COLOR_CODE: '#fff', 22 | 23 | // auth keys 24 | AUTH_TOKEN_KEY: '_kjd__we', 25 | AUTH_REFRESH_TOKEN_KEY: 'er2_s_q1', 26 | AUTH_USER_KEY: 'i_osd_sse32_', 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export * from './app.constants' 2 | export * from './redux.constants' -------------------------------------------------------------------------------- /src/constants/redux.constants.js: -------------------------------------------------------------------------------- 1 | const settingsConstants = { 2 | ACTIONS_MENU_OPEN: 'SETTINGS_ACTIONS_MENU_OPEN', 3 | MAIN_SIDEBAR_CLOSE: 'SETTINGS_MAIN_SIDEBAR_CLOSE', 4 | SET_CURRENT_PAGE_TITLE: 'SETTINGS_SET_CURRENT_PAGE_TITLE', 5 | }; 6 | 7 | const authConstants = { 8 | LOGIN_REQUEST: 'AUTH_LOGIN_REQUEST', 9 | LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS', 10 | SET_LOGGED_IN_STATUS: 'AUTH_SET_LOGGED_IN_STATUS', 11 | 12 | LOGOUT: 'AUTH_LOGOUT', 13 | }; 14 | 15 | export { 16 | settingsConstants, 17 | authConstants 18 | } -------------------------------------------------------------------------------- /src/hooks/debounce-hook.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | 3 | /** 4 | * Hook to handle debounce 5 | * @param watch - variable to watch 6 | * @param func - function that will call on debounce 7 | * @param delay - delay time for function call 8 | * @private 9 | */ 10 | function useDebounce(watch, func, delay = 300) { 11 | const [debounceFunc, setDebounceFunc] = useState(func) 12 | 13 | useEffect(() => { 14 | 15 | const debounceTimeout = setTimeout(() => { 16 | setDebounceFunc(func) 17 | }, delay) 18 | 19 | return () => { 20 | clearTimeout(debounceTimeout) 21 | } 22 | }, [watch]) 23 | 24 | return debounceFunc 25 | } 26 | 27 | export default useDebounce -------------------------------------------------------------------------------- /src/hooks/test.hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This hook is a test for some hook that fetch data from server and 3 | * pass it as dropdown option object 4 | */ 5 | 6 | import React, {useState, useEffect} from "react" 7 | 8 | /** this method should have separate file 9 | * it's just for test */ 10 | const testServices = { 11 | getAllTests: () => { 12 | } 13 | } 14 | 15 | function useTestsInit(type = null) { 16 | const [tests, setTests] = useState([]) 17 | 18 | useEffect(() => { 19 | 20 | (async function f() { 21 | setTests(await getTestsObj('', type)) 22 | })() 23 | 24 | }, []) 25 | 26 | return [tests, setTests] 27 | } 28 | 29 | /** 30 | * handle tests search by trend and return as semantic-ui dropdown options 31 | * 32 | * @param trend 33 | * @param type 34 | * @returns {Promise<*>} 35 | */ 36 | async function getTestsObj(trend = '', type = null) { 37 | 38 | const params = { 39 | limit: 10, 40 | offset: 0, 41 | trend, 42 | type, 43 | status: 'active' 44 | } 45 | 46 | const testsRes = await testServices.getAllTests(params) 47 | 48 | if (testsRes.data.success) 49 | return testsRes.data.result.map((item, key) => ( 50 | { 51 | key, 52 | text: item.title, 53 | value: item.id 54 | } 55 | )) 56 | 57 | return [] 58 | 59 | } 60 | 61 | 62 | export {useTestsInit, getTestsObj} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import {store} from "./utils"; 6 | import {Provider} from "react-redux"; 7 | import "./styles/index.scss" 8 | import axios from "axios" 9 | import {authService} from "./services/auth.service"; 10 | import {Router, Route} from "react-router-dom"; 11 | import {history} from "./utils/history"; 12 | 13 | // set headers here! 14 | axios.defaults.headers.common['Authorization'] = authService.getToken(); 15 | axios.defaults.headers.common['Content-Type'] = 'application/json'; 16 | 17 | axios.interceptors.response.use(function (response) { 18 | return response; 19 | }, function (error) { 20 | 21 | if (error.response && 401 === error.response.status) { 22 | 23 | // authService.logout() 24 | 25 | } else { 26 | return Promise.reject(error); 27 | } 28 | }); 29 | 30 | 31 | ReactDOM.render( 32 | 33 | 34 | 35 | , document.getElementById('root')); 36 | 37 | // If you want your app to work offline and load faster, you can change 38 | // unregister() to register() below. Note this comes with some pitfalls. 39 | // Learn more about service workers: http://bit.ly/CRA-PWA 40 | serviceWorker.unregister(); 41 | -------------------------------------------------------------------------------- /src/pages/private/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | import React, {useRef, useEffect} from 'react'; 2 | 3 | 4 | function Dashboard(props) { 5 | 6 | return ( 7 |
    8 |
    9 | 10 |
    11 |

    Dashboard

    12 |
    13 | 14 |
    15 |
    16 | ) 17 | } 18 | 19 | 20 | export default Dashboard 21 | -------------------------------------------------------------------------------- /src/pages/public/login/login.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {connect} from "react-redux"; 3 | 4 | function LoginPage(props) { 5 | 6 | 7 | return ( 8 |

    Login Page

    9 | ) 10 | } 11 | 12 | 13 | function mapStateToProps(state) { 14 | return { 15 | isAuthenticated: state.auth.isAuthenticated 16 | } 17 | } 18 | 19 | LoginPage = connect(mapStateToProps)(LoginPage) 20 | 21 | export default LoginPage; -------------------------------------------------------------------------------- /src/pages/public/register/register.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {connect} from "react-redux"; 3 | 4 | function RegisterPage(props) { 5 | 6 | return ( 7 |

    Register Page

    8 | ) 9 | } 10 | 11 | function mapStateToProps(state) { 12 | return { 13 | isAuthenticated: state.auth.isAuthenticated 14 | } 15 | 16 | } 17 | 18 | RegisterPage = connect(mapStateToProps)(RegisterPage) 19 | 20 | export default RegisterPage; -------------------------------------------------------------------------------- /src/reducers/auth.reducer.js: -------------------------------------------------------------------------------- 1 | import {authConstants} from "../constants/redux.constants"; 2 | 3 | const initialState = { 4 | role: undefined, 5 | isAuthenticated: false, 6 | admin: null 7 | }; 8 | 9 | export function auth(state = initialState, action) { 10 | 11 | switch (action.type) { 12 | case authConstants.SET_LOGGED_IN_STATUS: 13 | return { 14 | ...state, 15 | isAuthenticated: action.payload, 16 | }; 17 | case authConstants.LOGIN_REQUEST: 18 | return { 19 | ...state, 20 | isAuthenticated: false, 21 | }; 22 | case authConstants.LOGIN_SUCCESS: 23 | return { 24 | ...state, 25 | isAuthenticated: true, 26 | admin: action.payload.admin, 27 | role: action.payload.role 28 | }; 29 | case authConstants.LOGOUT: 30 | return { 31 | ...state, 32 | isAuthenticated: false, 33 | admin: null, 34 | role: null 35 | }; 36 | default: 37 | return state; 38 | } 39 | } -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from "redux"; 2 | import {settings} from "./settings.reducer"; 3 | import {auth} from "./auth.reducer"; 4 | 5 | const rootReducer = combineReducers({ 6 | settings, 7 | auth 8 | }); 9 | 10 | export default rootReducer; -------------------------------------------------------------------------------- /src/reducers/settings.reducer.js: -------------------------------------------------------------------------------- 1 | import {settingsConstants} from "../constants/redux.constants"; 2 | 3 | const initialState = { 4 | actions_menu_open: false, 5 | main_sidebar_close: false, 6 | page_title: "پنل مدیریت سیدبو" 7 | }; 8 | 9 | export function settings(state = initialState, action) { 10 | 11 | switch (action.type) { 12 | case settingsConstants.ACTIONS_MENU_OPEN: 13 | return { 14 | ...state, 15 | actions_menu_open: action.payload 16 | }; 17 | case settingsConstants.MAIN_SIDEBAR_CLOSE: 18 | return { 19 | ...state, 20 | main_sidebar_close: action.payload 21 | }; 22 | case settingsConstants.SET_CURRENT_PAGE_TITLE: 23 | return { 24 | ...state, 25 | page_title: action.payload 26 | }; 27 | default: 28 | return state; 29 | } 30 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's checkif a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/services/auth.service.js: -------------------------------------------------------------------------------- 1 | import * as axios from "axios"; 2 | import {appConstants} from "../constants/app.constants"; 3 | import SimpleCrypto from "simple-crypto-js"; 4 | 5 | let simpleCrypto = new SimpleCrypto('jsdWEnmDFdl4sd02ds34SDF2Dsd4fmk34sdf5jhs5d5sdfsdfbx5zsdSDSsdfsdSdf5d7fcsDFSF'); 6 | 7 | // Axios defualt headers setup 8 | axios.defaults.headers.common['Authorization'] = getToken(); 9 | axios.defaults.headers.common['Content-Type'] = 'application/json'; 10 | 11 | function login(username, password) { 12 | return axios({ 13 | method: 'post', 14 | url: appConstants.PUBLIC_URL + '/auth/admins/login', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | data: { 19 | username, 20 | password 21 | } 22 | }); 23 | } 24 | 25 | 26 | function isAuthenticated() { 27 | let user = getLocalCryptoItem(appConstants.AUTH_USER_KEY); 28 | let token = getLocalCryptoItem(appConstants.AUTH_TOKEN_KEY); 29 | 30 | return !!(user && token); 31 | } 32 | 33 | function getToken() { 34 | let token = getLocalCryptoItem(appConstants.AUTH_TOKEN_KEY) 35 | 36 | if (!token) 37 | return null; 38 | 39 | return 'Bearer ' + token 40 | } 41 | 42 | function getUser() { 43 | let user = getLocalCryptoItem(appConstants.AUTH_USER_KEY) 44 | let token = getLocalCryptoItem(appConstants.AUTH_TOKEN_KEY) 45 | 46 | if (user && token) 47 | return JSON.parse(user); 48 | 49 | return ''; 50 | } 51 | 52 | function logout() { 53 | localStorage.removeItem(appConstants.AUTH_USER_KEY); 54 | localStorage.removeItem(appConstants.AUTH_TOKEN_KEY); 55 | localStorage.removeItem(appConstants.AUTH_REFRESH_TOKEN_KEY); 56 | return true 57 | } 58 | 59 | function register(data) { 60 | return axios({ 61 | method: 'post', 62 | url: appConstants.BASE_URL + '/auth/admins/register', 63 | headers: { 64 | 'Content-Type': 'application/json' 65 | }, 66 | data: data 67 | }); 68 | } 69 | 70 | function setLocalCryptoItem(key, value) { 71 | 72 | try { 73 | localStorage.setItem(key, simpleCrypto.encrypt(value)) 74 | 75 | return true 76 | } catch (e) { 77 | return false; 78 | } 79 | } 80 | 81 | function getLocalCryptoItem(key) { 82 | try { 83 | let data = localStorage.getItem(key) 84 | 85 | return data ? simpleCrypto.decrypt(data) : null; 86 | } catch (e) { 87 | return null; 88 | } 89 | 90 | } 91 | 92 | export const authService = { 93 | login, 94 | logout, 95 | isAuthenticated, 96 | getUser, 97 | register, 98 | getToken, 99 | setLocalCryptoItem, 100 | getLocalCryptoItem, 101 | }; -------------------------------------------------------------------------------- /src/styles/__nprogress.scss: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: auto; 4 | position: fixed; 5 | z-index: 1031; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | transition: .2s; 11 | 12 | &:before { 13 | content: ''; 14 | display: block; 15 | width: 100%; 16 | height: 100%; 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | background-image: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.8)); 21 | filter: blur(8px); 22 | -webkit-filter: blur(8px); 23 | } 24 | } 25 | 26 | #nprogress .bar { 27 | background: $main-color; 28 | z-index: 1032; 29 | position: absolute; 30 | top: calc(50% + 90px); 31 | left: 0; 32 | width: 50%; 33 | height: 6px; 34 | display: none; 35 | } 36 | 37 | /* Fancy blur effect */ 38 | #nprogress .peg { 39 | display: none; 40 | position: absolute; 41 | right: 0; 42 | width: 100px; 43 | height: 100%; 44 | box-shadow: 0 0 10px $main-color, 0 0 5px $main-color; 45 | opacity: 1.0; 46 | 47 | -webkit-transform: rotate(3deg) translate(0px, -4px); 48 | -ms-transform: rotate(3deg) translate(0px, -4px); 49 | transform: rotate(3deg) translate(0px, -4px); 50 | } 51 | 52 | /* Remove these to get rid of the spinner */ 53 | #nprogress .spinner { 54 | display: block; 55 | position: fixed; 56 | z-index: 1033; 57 | top: 50%; 58 | left: 50%; 59 | transform: translate(-50%, -50%); 60 | 61 | &:before{ 62 | content: 'Loading...'; 63 | display: block; 64 | position: absolute; 65 | top: 120%; 66 | width: 500%; 67 | text-align: center; 68 | right: 50%; 69 | transform: translateX(50%); 70 | font-size: 16px; 71 | } 72 | } 73 | 74 | #nprogress .spinner-icon { 75 | width: 80px; 76 | height: 80px; 77 | box-sizing: border-box; 78 | display: block; 79 | position: relative; 80 | animation: nprogress-spinner 1200ms linear infinite; 81 | 82 | &:before { 83 | box-sizing: border-box; 84 | content: ''; 85 | width: 100%; 86 | height: 100%; 87 | display: block; 88 | position: absolute; 89 | border: solid 6px transparent; 90 | border-top-color: $main-color; 91 | border-bottom-color: $main-color; 92 | border-radius: 50%; 93 | animation: nprogress-spinner 1900ms linear infinite; 94 | 95 | } 96 | 97 | &:after { 98 | box-sizing: border-box; 99 | content: ''; 100 | width: 100%; 101 | height: 100%; 102 | display: block; 103 | position: absolute; 104 | border: solid 6px transparent; 105 | border-left-color: $main-color; 106 | border-right-color: $main-color; 107 | border-radius: 50%; 108 | animation: nprogress-spinner 4000ms linear infinite; 109 | //animation-delay: 200ms; 110 | 111 | } 112 | } 113 | 114 | .nprogress-custom-parent { 115 | overflow: hidden; 116 | position: relative; 117 | } 118 | 119 | .nprogress-custom-parent #nprogress .spinner, 120 | .nprogress-custom-parent #nprogress .bar { 121 | position: absolute; 122 | } 123 | 124 | @keyframes nprogress-spinner { 125 | 0% { 126 | transform: rotate(360deg); 127 | } 128 | 100% { 129 | transform: rotate(0deg); 130 | } 131 | } 132 | 133 | @keyframes nprogress-spinner-r { 134 | 0% { 135 | transform: rotate(0deg); 136 | } 137 | 100% { 138 | transform: rotate(360deg); 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/styles/__react-toastify.scss: -------------------------------------------------------------------------------- 1 | .Toastify__toast-container { 2 | z-index: 9999; 3 | position: fixed; 4 | padding: 4px; 5 | width: 320px; 6 | box-sizing: border-box; 7 | color: #fff; 8 | } 9 | 10 | .Toastify__toast-container--top-left { 11 | top: 1em; 12 | left: 1em; 13 | } 14 | 15 | .Toastify__toast-container--top-center { 16 | top: 1em; 17 | left: 50%; 18 | margin-left: -160px; 19 | } 20 | 21 | .Toastify__toast-container--top-right { 22 | top: 1em; 23 | right: 1em; 24 | } 25 | 26 | .Toastify__toast-container--bottom-left { 27 | bottom: 1em; 28 | left: 1em; 29 | } 30 | 31 | .Toastify__toast-container--bottom-center { 32 | bottom: 1em; 33 | left: 50%; 34 | margin-left: -160px; 35 | } 36 | 37 | .Toastify__toast-container--bottom-right { 38 | bottom: 1em; 39 | right: 1em; 40 | } 41 | 42 | @media only screen and (max-width: 480px) { 43 | .Toastify__toast-container { 44 | width: 100vw; 45 | padding: 0; 46 | left: 0; 47 | margin: 0; 48 | } 49 | .Toastify__toast-container--top-left, .Toastify__toast-container--top-center, .Toastify__toast-container--top-right { 50 | top: 0; 51 | } 52 | .Toastify__toast-container--bottom-left, .Toastify__toast-container--bottom-center, .Toastify__toast-container--bottom-right { 53 | bottom: 0; 54 | } 55 | .Toastify__toast-container--rtl { 56 | right: 0; 57 | left: initial; 58 | } 59 | } 60 | 61 | .Toastify__toast { 62 | position: relative; 63 | min-height: 64px; 64 | box-sizing: border-box; 65 | margin-bottom: 1rem; 66 | padding: 8px; 67 | border-radius: 1px; 68 | box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 15px 0 rgba(0, 0, 0, 0.05); 69 | display: -ms-flexbox; 70 | display: flex; 71 | -ms-flex-pack: justify; 72 | justify-content: space-between; 73 | max-height: 800px; 74 | overflow: hidden; 75 | font-family: sans-serif; 76 | cursor: pointer; 77 | direction: ltr; 78 | } 79 | 80 | .Toastify__toast--rtl { 81 | direction: rtl; 82 | } 83 | 84 | .Toastify__toast--default { 85 | background: #fff; 86 | color: #aaa; 87 | } 88 | 89 | .Toastify__toast--info { 90 | background: #3498db; 91 | } 92 | 93 | .Toastify__toast--success { 94 | background: #07bc0c; 95 | } 96 | 97 | .Toastify__toast--warning { 98 | background: #f1c40f; 99 | } 100 | 101 | .Toastify__toast--error { 102 | background: #e74c3c; 103 | } 104 | 105 | .Toastify__toast-body { 106 | margin: auto 0; 107 | -ms-flex: 1; 108 | flex: 1; 109 | } 110 | 111 | @media only screen and (max-width: 480px) { 112 | .Toastify__toast { 113 | margin-bottom: 0; 114 | } 115 | } 116 | 117 | .Toastify__close-button { 118 | color: #fff; 119 | font-weight: bold; 120 | font-size: 14px; 121 | background: transparent; 122 | outline: none; 123 | border: none; 124 | padding: 0; 125 | cursor: pointer; 126 | opacity: 0.7; 127 | transition: 0.3s ease; 128 | -ms-flex-item-align: start; 129 | align-self: flex-start; 130 | } 131 | 132 | .Toastify__close-button--default { 133 | color: #000; 134 | opacity: 0.3; 135 | } 136 | 137 | .Toastify__close-button:hover, .Toastify__close-button:focus { 138 | opacity: 1; 139 | } 140 | 141 | @keyframes Toastify__trackProgress { 142 | 0% { 143 | width: 100%; 144 | } 145 | 100% { 146 | width: 0; 147 | } 148 | } 149 | 150 | .Toastify__progress-bar { 151 | position: absolute; 152 | bottom: 0; 153 | left: 0; 154 | width: 0; 155 | height: 5px; 156 | z-index: 9999; 157 | opacity: 0.7; 158 | animation: Toastify__trackProgress linear 1; 159 | background-color: rgba(255, 255, 255, 0.7); 160 | } 161 | 162 | .Toastify__progress-bar--rtl { 163 | right: 0; 164 | left: initial; 165 | } 166 | 167 | .Toastify__progress-bar--default { 168 | background: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55); 169 | } 170 | 171 | @keyframes Toastify__bounceInRight { 172 | from, 173 | 60%, 174 | 75%, 175 | 90%, 176 | to { 177 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 178 | } 179 | from { 180 | opacity: 0; 181 | transform: translate3d(3000px, 0, 0); 182 | } 183 | 60% { 184 | opacity: 1; 185 | transform: translate3d(-25px, 0, 0); 186 | } 187 | 75% { 188 | transform: translate3d(10px, 0, 0); 189 | } 190 | 90% { 191 | transform: translate3d(-5px, 0, 0); 192 | } 193 | to { 194 | transform: none; 195 | } 196 | } 197 | 198 | @keyframes Toastify__bounceOutRight { 199 | 20% { 200 | opacity: 1; 201 | transform: translate3d(-20px, 0, 0); 202 | } 203 | to { 204 | opacity: 0; 205 | transform: translate3d(2000px, 0, 0); 206 | } 207 | } 208 | 209 | @keyframes Toastify__bounceInLeft { 210 | from, 211 | 60%, 212 | 75%, 213 | 90%, 214 | to { 215 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 216 | } 217 | 0% { 218 | opacity: 0; 219 | transform: translate3d(-3000px, 0, 0); 220 | } 221 | 60% { 222 | opacity: 1; 223 | transform: translate3d(25px, 0, 0); 224 | } 225 | 75% { 226 | transform: translate3d(-10px, 0, 0); 227 | } 228 | 90% { 229 | transform: translate3d(5px, 0, 0); 230 | } 231 | to { 232 | transform: none; 233 | } 234 | } 235 | 236 | @keyframes Toastify__bounceOutLeft { 237 | 20% { 238 | opacity: 1; 239 | transform: translate3d(20px, 0, 0); 240 | } 241 | to { 242 | opacity: 0; 243 | transform: translate3d(-2000px, 0, 0); 244 | } 245 | } 246 | 247 | @keyframes Toastify__bounceInUp { 248 | from, 249 | 60%, 250 | 75%, 251 | 90%, 252 | to { 253 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 254 | } 255 | from { 256 | opacity: 0; 257 | transform: translate3d(0, 3000px, 0); 258 | } 259 | 60% { 260 | opacity: 1; 261 | transform: translate3d(0, -20px, 0); 262 | } 263 | 75% { 264 | transform: translate3d(0, 10px, 0); 265 | } 266 | 90% { 267 | transform: translate3d(0, -5px, 0); 268 | } 269 | to { 270 | transform: translate3d(0, 0, 0); 271 | } 272 | } 273 | 274 | @keyframes Toastify__bounceOutUp { 275 | 20% { 276 | transform: translate3d(0, -10px, 0); 277 | } 278 | 40%, 279 | 45% { 280 | opacity: 1; 281 | transform: translate3d(0, 20px, 0); 282 | } 283 | to { 284 | opacity: 0; 285 | transform: translate3d(0, -2000px, 0); 286 | } 287 | } 288 | 289 | @keyframes Toastify__bounceInDown { 290 | from, 291 | 60%, 292 | 75%, 293 | 90%, 294 | to { 295 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 296 | } 297 | 0% { 298 | opacity: 0; 299 | transform: translate3d(0, -3000px, 0); 300 | } 301 | 60% { 302 | opacity: 1; 303 | transform: translate3d(0, 25px, 0); 304 | } 305 | 75% { 306 | transform: translate3d(0, -10px, 0); 307 | } 308 | 90% { 309 | transform: translate3d(0, 5px, 0); 310 | } 311 | to { 312 | transform: none; 313 | } 314 | } 315 | 316 | @keyframes Toastify__bounceOutDown { 317 | 20% { 318 | transform: translate3d(0, 10px, 0); 319 | } 320 | 40%, 321 | 45% { 322 | opacity: 1; 323 | transform: translate3d(0, -20px, 0); 324 | } 325 | to { 326 | opacity: 0; 327 | transform: translate3d(0, 2000px, 0); 328 | } 329 | } 330 | 331 | .Toastify__bounce-enter--top-left, .Toastify__bounce-enter--bottom-left { 332 | animation-name: Toastify__bounceInLeft; 333 | } 334 | 335 | .Toastify__bounce-enter--top-right, .Toastify__bounce-enter--bottom-right { 336 | animation-name: Toastify__bounceInRight; 337 | } 338 | 339 | .Toastify__bounce-enter--top-center { 340 | animation-name: Toastify__bounceInDown; 341 | } 342 | 343 | .Toastify__bounce-enter--bottom-center { 344 | animation-name: Toastify__bounceInUp; 345 | } 346 | 347 | .Toastify__bounce-exit--top-left, .Toastify__bounce-exit--bottom-left { 348 | animation-name: Toastify__bounceOutLeft; 349 | } 350 | 351 | .Toastify__bounce-exit--top-right, .Toastify__bounce-exit--bottom-right { 352 | animation-name: Toastify__bounceOutRight; 353 | } 354 | 355 | .Toastify__bounce-exit--top-center { 356 | animation-name: Toastify__bounceOutUp; 357 | } 358 | 359 | .Toastify__bounce-exit--bottom-center { 360 | animation-name: Toastify__bounceOutDown; 361 | } 362 | 363 | @keyframes Toastify__zoomIn { 364 | from { 365 | opacity: 0; 366 | transform: scale3d(0.3, 0.3, 0.3); 367 | } 368 | 50% { 369 | opacity: 1; 370 | } 371 | } 372 | 373 | @keyframes Toastify__zoomOut { 374 | from { 375 | opacity: 1; 376 | } 377 | 50% { 378 | opacity: 0; 379 | transform: scale3d(0.3, 0.3, 0.3); 380 | } 381 | to { 382 | opacity: 0; 383 | } 384 | } 385 | 386 | .Toastify__zoom-enter { 387 | animation-name: Toastify__zoomIn; 388 | } 389 | 390 | .Toastify__zoom-exit { 391 | animation-name: Toastify__zoomOut; 392 | } 393 | 394 | @keyframes Toastify__flipIn { 395 | from { 396 | transform: perspective(400px) rotate3d(1, 0, 0, 90deg); 397 | animation-timing-function: ease-in; 398 | opacity: 0; 399 | } 400 | 40% { 401 | transform: perspective(400px) rotate3d(1, 0, 0, -20deg); 402 | animation-timing-function: ease-in; 403 | } 404 | 60% { 405 | transform: perspective(400px) rotate3d(1, 0, 0, 10deg); 406 | opacity: 1; 407 | } 408 | 80% { 409 | transform: perspective(400px) rotate3d(1, 0, 0, -5deg); 410 | } 411 | to { 412 | transform: perspective(400px); 413 | } 414 | } 415 | 416 | @keyframes Toastify__flipOut { 417 | from { 418 | transform: perspective(400px); 419 | } 420 | 30% { 421 | transform: perspective(400px) rotate3d(1, 0, 0, -20deg); 422 | opacity: 1; 423 | } 424 | to { 425 | transform: perspective(400px) rotate3d(1, 0, 0, 90deg); 426 | opacity: 0; 427 | } 428 | } 429 | 430 | .Toastify__flip-enter { 431 | animation-name: Toastify__flipIn; 432 | } 433 | 434 | .Toastify__flip-exit { 435 | animation-name: Toastify__flipOut; 436 | } 437 | 438 | @keyframes Toastify__slideInRight { 439 | from { 440 | transform: translate3d(110%, 0, 0); 441 | visibility: visible; 442 | } 443 | to { 444 | transform: translate3d(0, 0, 0); 445 | } 446 | } 447 | 448 | @keyframes Toastify__slideInLeft { 449 | from { 450 | transform: translate3d(-110%, 0, 0); 451 | visibility: visible; 452 | } 453 | to { 454 | transform: translate3d(0, 0, 0); 455 | } 456 | } 457 | 458 | @keyframes Toastify__slideInUp { 459 | from { 460 | transform: translate3d(0, 110%, 0); 461 | visibility: visible; 462 | } 463 | to { 464 | transform: translate3d(0, 0, 0); 465 | } 466 | } 467 | 468 | @keyframes Toastify__slideInDown { 469 | from { 470 | transform: translate3d(0, -110%, 0); 471 | visibility: visible; 472 | } 473 | to { 474 | transform: translate3d(0, 0, 0); 475 | } 476 | } 477 | 478 | @keyframes Toastify__slideOutRight { 479 | from { 480 | transform: translate3d(0, 0, 0); 481 | } 482 | to { 483 | visibility: hidden; 484 | transform: translate3d(110%, 0, 0); 485 | } 486 | } 487 | 488 | @keyframes Toastify__slideOutLeft { 489 | from { 490 | transform: translate3d(0, 0, 0); 491 | } 492 | to { 493 | visibility: hidden; 494 | transform: translate3d(-110%, 0, 0); 495 | } 496 | } 497 | 498 | @keyframes Toastify__slideOutUp { 499 | from { 500 | transform: translate3d(0, 0, 0); 501 | } 502 | to { 503 | visibility: hidden; 504 | transform: translate3d(0, 110%, 0); 505 | } 506 | } 507 | 508 | @keyframes Toastify__slideOutDown { 509 | from { 510 | transform: translate3d(0, 0, 0); 511 | } 512 | to { 513 | visibility: hidden; 514 | transform: translate3d(0, -110%, 0); 515 | } 516 | } 517 | 518 | .Toastify__slide-enter--top-left, .Toastify__slide-enter--bottom-left { 519 | animation-name: Toastify__slideInLeft; 520 | } 521 | 522 | .Toastify__slide-enter--top-right, .Toastify__slide-enter--bottom-right { 523 | animation-name: Toastify__slideInRight; 524 | } 525 | 526 | .Toastify__slide-enter--top-center { 527 | animation-name: Toastify__slideInDown; 528 | } 529 | 530 | .Toastify__slide-enter--bottom-center { 531 | animation-name: Toastify__slideInUp; 532 | } 533 | 534 | .Toastify__slide-exit--top-left, .Toastify__slide-exit--bottom-left { 535 | animation-name: Toastify__slideOutLeft; 536 | } 537 | 538 | .Toastify__slide-exit--top-right, .Toastify__slide-exit--bottom-right { 539 | animation-name: Toastify__slideOutRight; 540 | } 541 | 542 | .Toastify__slide-exit--top-center { 543 | animation-name: Toastify__slideOutUp; 544 | } 545 | 546 | .Toastify__slide-exit--bottom-center { 547 | animation-name: Toastify__slideOutDown; 548 | } 549 | 550 | .Toastify__toast { 551 | direction: rtl; 552 | min-height: 44px; 553 | font-family: "IRANSans", serif; 554 | -webkit-box-shadow: 0 2px 26px 0 rgba(0, 0, 0, 0.15); 555 | -moz-box-shadow: 0 2px 26px 0 rgba(0, 0, 0, 0.15); 556 | box-shadow: 0 2px 26px 0 rgba(0, 0, 0, 0.15); 557 | } 558 | 559 | .Toastify__toast-container { 560 | width: 350px; 561 | max-width: 400px; 562 | } 563 | 564 | .Toastify__close-button { 565 | margin-top: 2px; 566 | } 567 | 568 | .Toastify__toast-body { 569 | font-size: 16px; 570 | } 571 | 572 | /*# sourceMappingURL=ReactToastify.css.map */ 573 | 574 | @media screen and (max-width: 575px){ 575 | 576 | .Toastify__toast-container { 577 | max-width: 100%; 578 | } 579 | } -------------------------------------------------------------------------------- /src/styles/__reset.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | direction: ltr; 39 | text-align: left; 40 | overflow-x: hidden; 41 | background-color: #fff; 42 | } 43 | 44 | [tabindex="-1"]:focus { 45 | outline: 0 !important; 46 | } 47 | 48 | hr { 49 | box-sizing: content-box; 50 | height: 0; 51 | overflow: visible; 52 | } 53 | 54 | h1, h2, h3, h4, h5, h6 { 55 | margin-top: 0; 56 | margin-bottom: 0.5rem; 57 | } 58 | 59 | p { 60 | margin-top: 0; 61 | margin-bottom: 1rem; 62 | } 63 | 64 | abbr[title], 65 | abbr[data-original-title] { 66 | text-decoration: underline; 67 | -webkit-text-decoration: underline dotted; 68 | text-decoration: underline dotted; 69 | cursor: help; 70 | border-bottom: 0; 71 | } 72 | 73 | address { 74 | margin-bottom: 1rem; 75 | font-style: normal; 76 | line-height: inherit; 77 | } 78 | 79 | ol, 80 | ul, 81 | dl { 82 | margin-top: 0; 83 | margin-bottom: 1rem; 84 | } 85 | 86 | ol ol, 87 | ul ul, 88 | ol ul, 89 | ul ol { 90 | margin-bottom: 0; 91 | } 92 | 93 | dt { 94 | font-weight: 700; 95 | } 96 | 97 | dd { 98 | margin-bottom: .5rem; 99 | margin-left: 0; 100 | } 101 | 102 | blockquote { 103 | margin: 0 0 1rem; 104 | } 105 | 106 | dfn { 107 | font-style: italic; 108 | } 109 | 110 | b, 111 | strong { 112 | font-weight: bolder; 113 | } 114 | 115 | small { 116 | font-size: 80%; 117 | } 118 | 119 | sub, 120 | sup { 121 | position: relative; 122 | font-size: 75%; 123 | line-height: 0; 124 | vertical-align: baseline; 125 | } 126 | 127 | sub { 128 | bottom: -.25em; 129 | } 130 | 131 | sup { 132 | top: -.5em; 133 | } 134 | 135 | a { 136 | color: #007bff; 137 | text-decoration: none; 138 | background-color: transparent; 139 | -webkit-text-decoration-skip: objects; 140 | } 141 | 142 | a:hover { 143 | color: #0056b3; 144 | text-decoration: underline; 145 | } 146 | 147 | a:not([href]):not([tabindex]) { 148 | color: inherit; 149 | text-decoration: none; 150 | } 151 | 152 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 153 | color: inherit; 154 | text-decoration: none; 155 | } 156 | 157 | a:not([href]):not([tabindex]):focus { 158 | outline: 0; 159 | } 160 | 161 | pre, 162 | code, 163 | kbd, 164 | samp { 165 | font-family: monospace, monospace; 166 | font-size: 1em; 167 | } 168 | 169 | pre { 170 | margin-top: 0; 171 | margin-bottom: 1rem; 172 | overflow: auto; 173 | -ms-overflow-style: scrollbar; 174 | } 175 | 176 | figure { 177 | margin: 0 0 1rem; 178 | } 179 | 180 | img { 181 | vertical-align: middle; 182 | border-style: none; 183 | } 184 | 185 | svg:not(:root) { 186 | overflow: hidden; 187 | } 188 | 189 | table { 190 | border-collapse: collapse; 191 | } 192 | 193 | caption { 194 | padding-top: 0.75rem; 195 | padding-bottom: 0.75rem; 196 | color: #6c757d; 197 | text-align: left; 198 | caption-side: bottom; 199 | } 200 | 201 | th { 202 | text-align: inherit; 203 | } 204 | 205 | label { 206 | display: inline-block; 207 | margin-bottom: .5rem; 208 | } 209 | 210 | button { 211 | border-radius: 0; 212 | } 213 | 214 | button:focus { 215 | outline: 1px dotted; 216 | outline: 5px auto -webkit-focus-ring-color; 217 | } 218 | 219 | input, 220 | button, 221 | select, 222 | optgroup, 223 | textarea { 224 | margin: 0; 225 | font-family: inherit; 226 | font-size: inherit; 227 | line-height: inherit; 228 | } 229 | 230 | button, 231 | input { 232 | overflow: visible; 233 | } 234 | 235 | button, 236 | select { 237 | text-transform: none; 238 | } 239 | 240 | button, 241 | html [type="button"], 242 | [type="reset"], 243 | [type="submit"] { 244 | -webkit-appearance: button; 245 | } 246 | 247 | button::-moz-focus-inner, 248 | [type="button"]::-moz-focus-inner, 249 | [type="reset"]::-moz-focus-inner, 250 | [type="submit"]::-moz-focus-inner { 251 | padding: 0; 252 | border-style: none; 253 | } 254 | 255 | input[type="radio"], 256 | input[type="checkbox"] { 257 | box-sizing: border-box; 258 | padding: 0; 259 | } 260 | 261 | input[type="date"], 262 | input[type="time"], 263 | input[type="datetime-local"], 264 | input[type="month"] { 265 | -webkit-appearance: listbox; 266 | } 267 | 268 | textarea { 269 | overflow: auto; 270 | resize: vertical; 271 | } 272 | 273 | fieldset { 274 | min-width: 0; 275 | padding: 0; 276 | margin: 0; 277 | border: 0; 278 | } 279 | 280 | legend { 281 | display: block; 282 | width: 100%; 283 | max-width: 100%; 284 | padding: 0; 285 | margin-bottom: .5rem; 286 | font-size: 1.5rem; 287 | line-height: inherit; 288 | color: inherit; 289 | white-space: normal; 290 | } 291 | 292 | progress { 293 | vertical-align: baseline; 294 | } 295 | 296 | [type="number"]::-webkit-inner-spin-button, 297 | [type="number"]::-webkit-outer-spin-button { 298 | height: auto; 299 | } 300 | 301 | [type="search"] { 302 | outline-offset: -2px; 303 | -webkit-appearance: none; 304 | } 305 | 306 | [type="search"]::-webkit-search-cancel-button, 307 | [type="search"]::-webkit-search-decoration { 308 | -webkit-appearance: none; 309 | } 310 | 311 | ::-webkit-file-upload-button { 312 | font: inherit; 313 | -webkit-appearance: button; 314 | } 315 | 316 | output { 317 | display: inline-block; 318 | } 319 | 320 | summary { 321 | display: list-item; 322 | cursor: pointer; 323 | } 324 | 325 | template { 326 | display: none; 327 | } 328 | 329 | [hidden] { 330 | display: none !important; 331 | } 332 | 333 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/styles/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes slide { 2 | from { 3 | transform: translateY(0); 4 | } 5 | to { 6 | transform: translateY(-100%); 7 | } 8 | } -------------------------------------------------------------------------------- /src/styles/_global.scss: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5, h6, button, input, div, select, textarea, a, label, *:not(i) { 2 | font-family: "CUSTOM_FONT_FAMILY", Tahoma, serif; 3 | } 4 | 5 | .text-left { 6 | text-align: left !important; 7 | } 8 | 9 | .text-center { 10 | text-align: center !important; 11 | } 12 | 13 | .text-right { 14 | text-align: right !important; 15 | } 16 | 17 | .d-ltr { 18 | direction: ltr; 19 | } 20 | 21 | .d-rtl { 22 | direction: rtl; 23 | } 24 | 25 | .scrollable-x { 26 | overflow-x: scroll; 27 | } 28 | 29 | .overflow-hidden { 30 | overflow: hidden; 31 | } 32 | 33 | .d-inline-block { 34 | display: inline-block; 35 | } 36 | 37 | .d-block { 38 | display: block; 39 | } 40 | 41 | .d-none { 42 | display: none; 43 | } 44 | 45 | .p-0 { 46 | padding: 0 !important; 47 | } 48 | 49 | .m-0 { 50 | margin: 0 !important; 51 | } 52 | 53 | .cursor-p { 54 | cursor: pointer; 55 | } -------------------------------------------------------------------------------- /src/styles/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // -- Start editing -- // 3 | // 4 | @import "~sass-flex-mixin/_flex"; 5 | 6 | // Set the number of columns you want to use on your layout. 7 | $flexboxgrid-grid-columns: 12 !default; 8 | // Set the gutter between columns. 9 | $flexboxgrid-gutter-width: 1rem !default; 10 | // Set a margin for the container sides. 11 | $flexboxgrid-outer-margin: 2rem !default; 12 | // Create or remove breakpoints for your project 13 | // Syntax: 14 | // name SIZErem, 15 | $flexboxgrid-breakpoints: 16 | xs 35.938em 33.938em, 17 | sm 48em 46rem, 18 | md 61.938em 59.938em, 19 | lg 74.938em 72.938em, 20 | xl 91.25em 89.25em !default; 21 | $flexboxgrid-max-width: 1200px !default; 22 | 23 | // 24 | // -- Stop editing -- // 25 | // 26 | 27 | $gutter-compensation: $flexboxgrid-gutter-width * .5 * -1; 28 | $half-gutter-width: $flexboxgrid-gutter-width * .5; 29 | 30 | .wrapper { 31 | box-sizing: border-box; 32 | max-width: $flexboxgrid-max-width; 33 | margin: 0 auto; 34 | } 35 | 36 | .container-fluid { 37 | margin-right: auto; 38 | margin-left: auto; 39 | padding-right: $flexboxgrid-outer-margin; 40 | padding-left: $flexboxgrid-outer-margin; 41 | } 42 | 43 | .container { 44 | margin-right: auto; 45 | margin-left: auto; 46 | padding-right: $flexboxgrid-gutter-width; 47 | padding-left: $flexboxgrid-gutter-width; 48 | } 49 | 50 | .row { 51 | box-sizing: border-box; 52 | @include flexbox(); 53 | @include flex(0, 1, auto); 54 | @include flex-direction(row); 55 | @include flex-wrap(wrap); 56 | margin-right: $gutter-compensation; 57 | margin-left: $gutter-compensation; 58 | } 59 | 60 | .row.reverse { 61 | @include flex-direction(row-reverse); 62 | } 63 | 64 | .col.reverse { 65 | @include flex-direction(column-reverse); 66 | } 67 | 68 | @mixin flexboxgrid-sass-col-common { 69 | box-sizing: border-box; 70 | 71 | // split @include flex(0, 0, auto) into individual props 72 | @include flex-grow(0); 73 | @include flex-shrink(0); 74 | 75 | // we leave @include flex-basis(auto) out of common because 76 | // in some spots we need it and some we dont 77 | // more why here: https://github.com/kristoferjoseph/flexboxgrid/issues/126 78 | 79 | padding-right: $half-gutter-width; 80 | padding-left: $half-gutter-width; 81 | } 82 | 83 | $name: xs; 84 | .col-#{$name} { 85 | @include flexboxgrid-sass-col-common; 86 | @include flex-basis(auto); 87 | } 88 | 89 | @for $i from 1 through $flexboxgrid-grid-columns { 90 | .col-#{$name}-#{$i} { 91 | @include flexboxgrid-sass-col-common; 92 | @include flex-basis(100% / $flexboxgrid-grid-columns * $i); 93 | max-width: 100% / $flexboxgrid-grid-columns * $i; 94 | } 95 | } 96 | 97 | @for $i from 0 through $flexboxgrid-grid-columns { 98 | .col-#{$name}-offset-#{$i} { 99 | @include flexboxgrid-sass-col-common; 100 | @if $i == 0 { 101 | margin-left: 0; 102 | } @else { 103 | margin-left: 100% / $flexboxgrid-grid-columns * $i; 104 | } 105 | } 106 | } 107 | 108 | .col-#{$name} { 109 | @include flex-grow(1); 110 | @include flex-basis(0); 111 | max-width: 100%; 112 | } 113 | 114 | .start-#{$name} { 115 | @include justify-content(flex-start); 116 | text-align: left; 117 | } 118 | 119 | .center-#{$name} { 120 | @include justify-content(center); 121 | text-align: center; 122 | } 123 | 124 | .end-#{$name} { 125 | @include justify-content(flex-end); 126 | text-align: right; 127 | } 128 | 129 | .top-#{$name} { 130 | @include align-items(flex-start); 131 | } 132 | 133 | .middle-#{$name} { 134 | @include align-items(center); 135 | } 136 | 137 | .bottom-#{$name} { 138 | @include align-items(flex-end); 139 | } 140 | 141 | .around-#{$name} { 142 | @include justify-content(space-around); 143 | } 144 | 145 | .between-#{$name} { 146 | @include justify-content(space-between); 147 | } 148 | 149 | .first-#{$name} { 150 | order: -1; 151 | } 152 | 153 | .last-#{$name} { 154 | order: 1; 155 | } 156 | 157 | @each $breakpoint in $flexboxgrid-breakpoints { 158 | $name: nth($breakpoint, 1); 159 | $size: nth($breakpoint, 2); 160 | $container: nth($breakpoint, 3); 161 | @media only screen and (min-width: $size) { 162 | .container { 163 | width: $container; 164 | } 165 | 166 | .col-#{$name} { 167 | @include flexboxgrid-sass-col-common; 168 | @include flex-basis(auto); 169 | } 170 | @for $i from 1 through $flexboxgrid-grid-columns { 171 | .col-#{$name}-#{$i} { 172 | @include flexboxgrid-sass-col-common; 173 | @include flex-basis(100% / $flexboxgrid-grid-columns * $i); 174 | max-width: 100% / $flexboxgrid-grid-columns * $i; 175 | } 176 | } 177 | @for $i from 0 through $flexboxgrid-grid-columns { 178 | .col-#{$name}-offset-#{$i} { 179 | @include flexboxgrid-sass-col-common; 180 | @if $i == 0 { 181 | margin-left: 0; 182 | } @else { 183 | margin-left: 100% / $flexboxgrid-grid-columns * $i; 184 | } 185 | } 186 | } 187 | .col-#{$name} { 188 | @include flex-grow(1); 189 | @include flex-basis(0); 190 | max-width: 100%; 191 | } 192 | .start-#{$name} { 193 | @include justify-content(flex-start); 194 | text-align: left; 195 | } 196 | 197 | .center-#{$name} { 198 | @include justify-content(center); 199 | text-align: center; 200 | } 201 | 202 | .end-#{$name} { 203 | @include justify-content(flex-end); 204 | text-align: right; 205 | } 206 | 207 | .top-#{$name} { 208 | @include align-items(flex-start); 209 | } 210 | 211 | .middle-#{$name} { 212 | @include align-items(center); 213 | } 214 | 215 | .bottom-#{$name} { 216 | @include align-items(flex-end); 217 | } 218 | 219 | .around-#{$name} { 220 | @include justify-content(space-around); 221 | } 222 | 223 | .between-#{$name} { 224 | @include justify-content(space-between); 225 | } 226 | 227 | .first-#{$name} { 228 | order: -1; 229 | } 230 | 231 | .last-#{$name} { 232 | order: 1; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @function calculateRem($size) { 2 | $remSize: $size / 16px; 3 | @return $remSize * 1rem; 4 | } 5 | 6 | @mixin font-size($size) { 7 | font-size: $size; 8 | font-size: calculateRem($size); 9 | } 10 | 11 | @mixin bp-large { 12 | @media only screen and (max-width: 60em) { 13 | @content; 14 | } 15 | } 16 | 17 | @mixin bp-medium { 18 | @media only screen and (max-width: 40em) { 19 | @content; 20 | } 21 | } 22 | 23 | @mixin bp-small { 24 | @media only screen and (max-width: 30em) { 25 | @content; 26 | } 27 | } 28 | 29 | @mixin keyframes($animation-name) { 30 | @-webkit-keyframes #{$animation-name} { 31 | @content; 32 | } 33 | @-moz-keyframes #{$animation-name} { 34 | @content; 35 | } 36 | @-ms-keyframes #{$animation-name} { 37 | @content; 38 | } 39 | @-o-keyframes #{$animation-name} { 40 | @content; 41 | } 42 | @keyframes #{$animation-name} { 43 | @content; 44 | } 45 | } 46 | 47 | @mixin animation($str) { 48 | -webkit-animation: #{$str}; 49 | -moz-animation: #{$str}; 50 | -ms-animation: #{$str}; 51 | -o-animation: #{$str}; 52 | animation: #{$str}; 53 | } 54 | 55 | @mixin transition($args...) { 56 | -webkit-transition: $args; 57 | -moz-transition: $args; 58 | -ms-transition: $args; 59 | -o-transition: $args; 60 | transition: $args; 61 | } 62 | 63 | %clearfix { 64 | *zoom: 1; 65 | 66 | &:before, &:after { 67 | content: " "; 68 | display: table; 69 | } 70 | 71 | &:after { 72 | clear: both; 73 | } 74 | } 75 | 76 | @mixin input-placeholder { 77 | &.placeholder { 78 | @content; 79 | } 80 | &:-moz-placeholder { 81 | @content; 82 | } 83 | &::-moz-placeholder { 84 | @content; 85 | } 86 | &:-ms-input-placeholder { 87 | @content; 88 | } 89 | &::-webkit-input-placeholder { 90 | @content; 91 | } 92 | } 93 | 94 | %default-card { 95 | max-width: 100%; 96 | position: relative; 97 | display: -webkit-flex; 98 | display: flex; 99 | -webkit-flex-direction: column; 100 | flex-direction: column; 101 | width: 100%; 102 | min-height: 0; 103 | background: #FFFFFF; 104 | padding: 1.5em; 105 | border: none; 106 | border-radius: 0.28571429rem; 107 | box-shadow: 0 1px 3px 0 #D4D4D5, 0 0 0 1px #D4D4D5; 108 | transition: box-shadow 0.1s ease, transform 0.1s ease, -webkit-transform 0.1s ease; 109 | } -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | //@font-face { 2 | // font-family: 'IRANSans'; 3 | // font-style: normal; 4 | // font-weight: bold; 5 | // src: url('../_fonts/eot/IRANSansWeb_Bold.eot'); 6 | // src: url('../_fonts/eot/IRANSansWeb_Bold.eot?#iefix') format('embedded-opentype'), /* IE6-8 */ 7 | // url('../_fonts/woff2/IRANSansWeb_Bold.woff2') format('woff2'), /* FF39+,Chrome36+, Opera24+*/ 8 | // url('../_fonts/woff/IRANSansWeb_Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ 9 | // url('../_fonts/ttf/IRANSansWeb_Bold.ttf') format('truetype'); 10 | //} 11 | 12 | h1, h2, h3, h4, h5, h6, button, input, div, select, textarea, a, small, strong, span, section, article { 13 | font-family: "CUSTOM_FONT_FAMILY", Tahoma, serif; 14 | } 15 | 16 | button, input, optgroup, select, textarea { 17 | font-family: "CUSTOM_FONT_FAMILY", Tahoma, serif !important; 18 | 19 | } -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $main-color: #be0000; 2 | $dark-main-color: darken(#be0000, 20); 3 | $light-main-color: lighten(#be0000, 20); 4 | $ultra-light-main-color: lighten(#be0000, 50); 5 | 6 | $secondary-color: #05d3bb; 7 | $dark-color: #30322f; 8 | $light-color: #fff; 9 | 10 | $error-color: #e74c3c; 11 | 12 | $ultra-light-grey: #eeeeee; 13 | $light-grey: #cdcdcd; 14 | $grey-color: #909090; 15 | $dark-grey: #818181; 16 | 17 | $circle-radius: 50%; 18 | $main-radius: 30px; 19 | $half-radius: 15px; 20 | $tiny-radius: 5px; 21 | $mini-radius: 3px; 22 | 23 | $text-dark-color: #3b3b3b; 24 | $text-light-color: #a3a3a3; 25 | 26 | $main-font-size: 16px; 27 | $btn-font-size: 12px; 28 | $mini-font-size: 12px; -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Public styles here 3 | */ 4 | @import "_reset"; 5 | @import "variables"; 6 | @import "mixins"; 7 | @import "animations"; 8 | @import "_react-toastify"; 9 | @import "_nprogress"; 10 | @import "grid"; 11 | @import "typography"; 12 | 13 | // global styles 14 | @import "global"; 15 | 16 | /** 17 | * Components 18 | */ 19 | // 20 | -------------------------------------------------------------------------------- /src/utils/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | function copyToClipboard(str) { 2 | try { 3 | 4 | if (!str) 5 | return false 6 | 7 | const el = document.createElement('textarea'); 8 | el.value = str; 9 | el.setAttribute('readonly', ''); 10 | el.style.position = 'absolute'; 11 | el.style.left = '-9999px'; 12 | document.body.appendChild(el); 13 | const selected = 14 | document.getSelection().rangeCount > 0 15 | ? document.getSelection().getRangeAt(0) 16 | : false; 17 | el.select(); 18 | document.execCommand('copy'); 19 | document.body.removeChild(el); 20 | 21 | return true 22 | } catch (e) { 23 | return false 24 | } 25 | } 26 | 27 | export default copyToClipboard -------------------------------------------------------------------------------- /src/utils/generate-url-key.js: -------------------------------------------------------------------------------- 1 | export default function generateUrlKey(string = '', removeShortWords = false) { 2 | let string_array = string 3 | .toString() 4 | .trim() 5 | .replace(/\s\s+/g, ' ') 6 | .split(" "); 7 | 8 | if (removeShortWords) 9 | string_array.map((item, index) => { 10 | if (item.length < 2) 11 | string_array.splice(index, 1); 12 | 13 | return item; 14 | }); 15 | 16 | return string_array.join('-'); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/history.js: -------------------------------------------------------------------------------- 1 | import * as browserHistory from 'history' 2 | import {store} from "./store"; 3 | import {settingsActions} from "../actions/settings.actions"; 4 | 5 | const createHistory = browserHistory.createBrowserHistory 6 | 7 | const history = createHistory(); 8 | history.listen(function (ev) { 9 | store.dispatch(settingsActions.toggleMainMenu(false)) 10 | }); 11 | 12 | export {history}; -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from "./store"; 2 | export * from "./history"; 3 | export * from "./validator"; 4 | export * from "./toasts"; 5 | export * from "./routes"; 6 | export * from "./price-formater"; -------------------------------------------------------------------------------- /src/utils/json-validator.js: -------------------------------------------------------------------------------- 1 | const isEmptyObject = (obj) => { 2 | for (let key in obj) { 3 | if (obj.hasOwnProperty(key)) 4 | return false; 5 | } 6 | return true; 7 | } 8 | 9 | 10 | const isJsonString = (string) => { 11 | try { 12 | 13 | let json = JSON.parse(string) 14 | return true 15 | 16 | } catch (err) { 17 | return false 18 | } 19 | } 20 | 21 | const isValidJSON = (input) => { 22 | let str = input.toString(); 23 | 24 | try { 25 | JSON.parse(str); 26 | } catch (e) { 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | export { 34 | isEmptyObject, 35 | isValidJSON, 36 | isJsonString 37 | } -------------------------------------------------------------------------------- /src/utils/price-formater.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function get a number and separate triple with separator char 3 | * 4 | * @param {number} price 5 | * @param {string} separator 6 | * @return {string} 7 | */ 8 | export default function priceFormatter(price, separator = ',') { 9 | if (price == null) 10 | return '' 11 | 12 | let tmp_price = [], 13 | tmp_c = 1; 14 | price = price.toString(); // convert int or any other types to string 15 | 16 | // this loop will return a reversed array of chars 17 | for (let i = price.length - 1; i >= 0; i--) { 18 | tmp_price.push(price[i]); 19 | if (tmp_c === 3 && i > 0) { 20 | tmp_price.push(separator); 21 | tmp_c = 0; 22 | } 23 | tmp_c++; 24 | 25 | } 26 | 27 | // re reverse and join the chars array of formatting 28 | return tmp_price.reverse().join(''); 29 | } -------------------------------------------------------------------------------- /src/utils/routes.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import RegisterPage from "../pages/public/register/register"; 3 | import LoginPage from "../pages/public/login/login"; 4 | import Dashboard from "../pages/private/dashboard/dashboard"; 5 | 6 | 7 | export const routes = [ 8 | { 9 | path: '/login', 10 | component: LoginPage, 11 | exact: true, 12 | private: false, 13 | redirectOnAuth: true, 14 | title: 'Login', 15 | }, 16 | { 17 | path: '/register', 18 | component: RegisterPage, 19 | exact: true, 20 | private: false, 21 | redirectOnAuth: true, 22 | title: 'Register' 23 | }, 24 | { 25 | path: '/', 26 | component: Dashboard, 27 | exact: true, 28 | private: true, 29 | title: 'Dashboard' 30 | }, 31 | 32 | ] 33 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from "redux"; 2 | 3 | import thunk from "redux-thunk"; 4 | import thunkMiddleware from "redux-thunk"; 5 | import rootReducer from "../reducers"; 6 | 7 | const initialState = {}; 8 | const middleware = [thunk, thunkMiddleware]; 9 | 10 | export const store = createStore( 11 | rootReducer, 12 | initialState, 13 | compose( 14 | applyMiddleware(...middleware), 15 | // window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 16 | ) 17 | ); 18 | -------------------------------------------------------------------------------- /src/utils/toasts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {toast, Zoom} from 'react-toastify'; 3 | 4 | let options = { 5 | position: "top-center", 6 | autoClose: 3000, 7 | hideProgressBar: true, 8 | closeOnClick: true, 9 | pauseOnHover: true, 10 | transition: Zoom, 11 | rtl: true 12 | }; 13 | 14 | export const toasts = { 15 | error: function (message, icon = '') { 16 | message = {message}; 17 | toast.error(message, options); 18 | }, 19 | info: function (message, icon = '') { 20 | message = {message}; 21 | toast.info(message, options); 22 | }, 23 | success: function (message, icon = '', opts) { 24 | message = {message}; 25 | options = Object.assign(options, opts); 26 | toast.success(message, options); 27 | }, 28 | warning: function (message, icon = '') { 29 | message = {message}; 30 | toast.warning(message, options); 31 | }, 32 | }; -------------------------------------------------------------------------------- /src/utils/validator.js: -------------------------------------------------------------------------------- 1 | const isExisty = function isExisty(value) { 2 | return value !== null && value !== undefined; 3 | }; 4 | 5 | const _isEmpty = function _isEmpty(value) { 6 | return value === '' || value === undefined || value == null || (Array.isArray(value) && !value.length); 7 | }; 8 | 9 | const isEmptyTrimed = function isEmptyTrimed(value) { 10 | if (typeof value === 'string') { 11 | return value.trim() === ''; 12 | } 13 | return true; 14 | }; 15 | 16 | const isValidIranianNationalCode = (value) => { 17 | 18 | if (!/^\d{10}$/.test(value)) 19 | return false; 20 | 21 | let check = parseInt(value[9]); 22 | let sum = 0; 23 | let i; 24 | for (i = 0; i < 9; ++i) { 25 | sum += parseInt(value[i]) * (10 - i); 26 | } 27 | sum %= 11; 28 | 29 | // eslint-disable-next-line 30 | return (sum < 2 && check == sum) || (sum >= 2 && check + sum == 11); 31 | } 32 | 33 | export const validations = { 34 | matchRegexp: function matchRegexp(value, regexp) { 35 | const validationRegexp = regexp instanceof RegExp ? regexp : new RegExp(regexp); 36 | return !isExisty(value) || _isEmpty(value) || validationRegexp.test(value); 37 | }, 38 | 39 | // eslint-disable-next-line 40 | isEmail: function isEmail(value) { 41 | // eslint-disable-next-line 42 | return !_isEmpty(value) && validations.matchRegexp(value, /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i); 43 | }, 44 | 45 | isPhone: function isPhone(value) { 46 | return !_isEmpty(value) && validations.matchRegexp(value, /^0([ ]|-|[()]){0,2}9[0|1|2|3|4|9]([ ]|-|[()]){0,2}(?:[0-9]([ ]|-|[()]){0,2}){8}$/i); 47 | }, 48 | 49 | isTelephone: function isPhone(value) { 50 | return !_isEmpty(value) && validations.matchRegexp(value, /^((0)([1-9])[0-9]{9})$/i); 51 | }, 52 | 53 | isURL: function isPhone(value) { 54 | // eslint-disable-next-line 55 | return !_isEmpty(value) && validations.matchRegexp(value, /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/i); 56 | }, 57 | 58 | isValidIranianNationalCode: (value) => { 59 | return isValidIranianNationalCode(value) 60 | }, 61 | 62 | isEmpty: function isEmpty(value) { 63 | return _isEmpty(value); 64 | }, 65 | 66 | required: function required(value) { 67 | return !_isEmpty(value); 68 | }, 69 | 70 | trim: function trim(value) { 71 | return !isEmptyTrimed(value); 72 | }, 73 | 74 | isNumber: function isNumber(value) { 75 | return validations.matchRegexp(value, /^-?[0-9]\d*(\d+)?$/i); 76 | }, 77 | 78 | isFloat: function isFloat(value) { 79 | return validations.matchRegexp(value, /^(?:[1-9]\d*|0)?(?:\.\d+)?$/i); 80 | }, 81 | 82 | isPositive: function isPositive(value) { 83 | if (isExisty(value)) { 84 | return (validations.isNumber(value) || validations.isFloat(value)) && value >= 0; 85 | } 86 | return true; 87 | }, 88 | 89 | maxNumber: function maxNumber(value, max) { 90 | return !isExisty(value) || _isEmpty(value) || parseInt(value, 10) <= parseInt(max, 10); 91 | }, 92 | 93 | minNumber: function minNumber(value, min) { 94 | return !isExisty(value) || _isEmpty(value) || parseInt(value, 10) >= parseInt(min, 10); 95 | }, 96 | 97 | isString: function isString(value) { 98 | return !_isEmpty(value) || typeof value === 'string' || value instanceof String; 99 | }, 100 | minStringLength: function minStringLength(value, length) { 101 | return validations.isString(value) && value.length >= length; 102 | }, 103 | maxStringLength: function maxStringLength(value, length) { 104 | return validations.isString(value) && value.length <= length; 105 | } 106 | }; 107 | 108 | --------------------------------------------------------------------------------