├── .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 
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 |
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 |
--------------------------------------------------------------------------------