From c0c832d15865c342ebced4e78c01296c2d49dcc4 Mon Sep 17 00:00:00 2001
From: Fedor Katurov <gotham48@gmail.com>
Date: Mon, 25 Nov 2019 15:02:32 +0700
Subject: [PATCH] fixed cyclic dependencies for dialogs

---
 package-lock.json                             | 127 ++++++++++---
 package.json                                  |   1 +
 src/components/editors/AudioEditor/index.tsx  |   3 +-
 src/components/editors/AudioGrid/index.tsx    |   3 +-
 src/constants/dialogs.ts                      |  30 +++
 src/constants/errors.ts                       |  62 ++++---
 src/containers/dialogs/LoginDialog/index.tsx  | 117 ++++++------
 .../dialogs/LoginDialog/styles.scss           |  22 ++-
 src/containers/dialogs/Modal/index.tsx        |   2 +-
 .../dialogs/RestoreRequestDialog/index.tsx    |  41 +++++
 src/redux/auth/reducer.ts                     |   1 +
 src/redux/auth/sagas.ts                       | 173 +++++++-----------
 src/redux/auth/types.ts                       |   1 +
 src/redux/modal/constants.ts                  |  28 +--
 src/redux/node/sagas.ts                       |   3 +-
 src/redux/types.ts                            |  32 ++--
 src/styles/colors.scss                        |   3 +
 src/styles/global.scss                        |   5 +
 webpack.config.js                             |   8 +
 19 files changed, 399 insertions(+), 263 deletions(-)
 create mode 100644 src/constants/dialogs.ts
 create mode 100644 src/containers/dialogs/RestoreRequestDialog/index.tsx

diff --git a/package-lock.json b/package-lock.json
index 85c20da7..04c24fa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3052,6 +3052,7 @@
       "version": "2.10.1",
       "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
       "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+      "optional": true,
       "requires": {
         "hoek": "2.x.x"
       }
@@ -3419,6 +3420,12 @@
         "safe-buffer": "^5.0.1"
       }
     },
+    "circular-dependency-plugin": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz",
+      "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==",
+      "dev": true
+    },
     "clap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",
@@ -3493,6 +3500,10 @@
         }
       }
     },
+    "classie": {
+      "version": "github:eiriklv/classie#da1d3019904433872a8656d3cd69fc41d69c477a",
+      "from": "github:eiriklv/classie"
+    },
     "classnames": {
       "version": "2.2.6",
       "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
@@ -4963,6 +4974,10 @@
         "minimalistic-assert": "^1.0.0"
       }
     },
+    "desandro-get-style-property": {
+      "version": "github:eiriklv/get-style-property#a5a74ad48d96c7d5ddcf652e9fa5d4283af37823",
+      "from": "github:eiriklv/get-style-property"
+    },
     "destroy": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -5030,6 +5045,19 @@
         "buffer-indexof": "^1.0.0"
       }
     },
+    "doc-ready": {
+      "version": "github:eiriklv/doc-ready#31c2481101af5dab33311fea4c7fc684b58fa8ad",
+      "from": "github:eiriklv/doc-ready",
+      "requires": {
+        "eventie": "github:eiriklv/eventie"
+      },
+      "dependencies": {
+        "eventie": {
+          "version": "github:eiriklv/eventie#c9d290c6aa57712257dc8c0b6bf21c9374190a3c",
+          "from": "github:eiriklv/eventie"
+        }
+      }
+    },
     "doctrine": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -5706,12 +5734,21 @@
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
       "dev": true
     },
+    "eventemitter": {
+      "version": "github:braznaavtrav/EventEmitter#7169056a2f8b3b55d78ab1b85bad39277e7e88b2",
+      "from": "github:braznaavtrav/EventEmitter"
+    },
     "eventemitter3": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
       "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==",
       "dev": true
     },
+    "eventie": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/eventie/-/eventie-1.0.6.tgz",
+      "integrity": "sha1-1P/IsMK15JPCqhsiy+kY067nRDc="
+    },
     "events": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
@@ -6294,7 +6331,8 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -6709,7 +6747,8 @@
         "safe-buffer": {
           "version": "5.1.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -6765,6 +6804,7 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -6808,12 +6848,14 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         }
       }
     },
@@ -6899,6 +6941,13 @@
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
       "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
     },
+    "get-size": {
+      "version": "github:eiriklv/get-size#c1ebd019815fc6247c094c17a41b61d0e8191b08",
+      "from": "github:eiriklv/get-size",
+      "requires": {
+        "desandro-get-style-property": "github:eiriklv/get-style-property"
+      }
+    },
     "get-stdin": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
@@ -7233,7 +7282,8 @@
     "hoek": {
       "version": "2.16.3",
       "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
-      "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+      "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+      "optional": true
     },
     "hoist-non-react-statics": {
       "version": "3.3.0",
@@ -7541,6 +7591,14 @@
       "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
       "optional": true
     },
+    "imagesloaded": {
+      "version": "github:eiriklv/imagesloaded#04535a148206e58790927e133f24ca199163b995",
+      "from": "github:eiriklv/imagesloaded",
+      "requires": {
+        "eventie": ">=1.0.4 <2",
+        "wolfy87-eventemitter": "4.x"
+      }
+    },
     "immutable": {
       "version": "3.8.2",
       "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
@@ -8644,6 +8702,10 @@
         "object-visit": "^1.0.0"
       }
     },
+    "matches-selector": {
+      "version": "github:desandro/matches-selector#082376f4bbe7ff8c5c6bb258ec43259c9a80a7c3",
+      "from": "github:desandro/matches-selector#v1.0.3"
+    },
     "math-expression-evaluator": {
       "version": "1.2.17",
       "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
@@ -9614,6 +9676,24 @@
         "os-tmpdir": "^1.0.0"
       }
     },
+    "outlayer": {
+      "version": "github:eiriklv/outlayer#aea1c8239d30ccc1d3664ca3fff2f6d4b20fb812",
+      "from": "github:eiriklv/outlayer",
+      "requires": {
+        "desandro-get-style-property": "github:eiriklv/get-style-property",
+        "doc-ready": "github:eiriklv/doc-ready",
+        "eventemitter": "github:braznaavtrav/EventEmitter",
+        "eventie": "github:eiriklv/eventie",
+        "get-size": "github:eiriklv/get-size",
+        "matches-selector": "github:desandro/matches-selector#v1.0.3"
+      },
+      "dependencies": {
+        "eventie": {
+          "version": "github:eiriklv/eventie#c9d290c6aa57712257dc8c0b6bf21c9374190a3c",
+          "from": "github:eiriklv/eventie"
+        }
+      }
+    },
     "output-file-sync": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-2.0.1.tgz",
@@ -9679,6 +9759,16 @@
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
       "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
     },
+    "packery": {
+      "version": "github:eiriklv/packery#8e812a0a16575ef923f5e72efcea85aadc6fea67",
+      "from": "github:eiriklv/packery",
+      "requires": {
+        "classie": "github:eiriklv/classie",
+        "desandro-get-style-property": "github:eiriklv/get-style-property",
+        "get-size": "github:eiriklv/get-size",
+        "outlayer": "github:eiriklv/outlayer"
+      }
+    },
     "pako": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz",
@@ -10993,25 +11083,9 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/react-packery-component/-/react-packery-component-1.0.2.tgz",
       "integrity": "sha1-FSEiHaSRZ8s87fR9W1UsM0iBgKk=",
-      "dependencies": {
-        "imagesloaded": {
-          "version": "github:eiriklv/imagesloaded#04535a148206e58790927e133f24ca199163b995",
-          "from": "github:eiriklv/imagesloaded#04535a148206e58790927e133f24ca199163b995",
-          "requires": {
-            "eventie": ">=1.0.4 <2",
-            "wolfy87-eventemitter": "4.x"
-          }
-        },
-        "packery": {
-          "version": "github:eiriklv/packery#8e812a0a16575ef923f5e72efcea85aadc6fea67",
-          "from": "github:eiriklv/packery#8e812a0a16575ef923f5e72efcea85aadc6fea67",
-          "requires": {
-            "classie": "github:eiriklv/classie#da1d3019904433872a8656d3cd69fc41d69c477a",
-            "desandro-get-style-property": "github:eiriklv/get-style-property#a5a74ad48d96c7d5ddcf652e9fa5d4283af37823",
-            "get-size": "github:eiriklv/get-size#c1ebd019815fc6247c094c17a41b61d0e8191b08",
-            "outlayer": "github:eiriklv/outlayer#aea1c8239d30ccc1d3664ca3fff2f6d4b20fb812"
-          }
-        }
+      "requires": {
+        "imagesloaded": "github:eiriklv/imagesloaded",
+        "packery": "github:eiriklv/packery"
       }
     },
     "react-redux": {
@@ -14433,6 +14507,11 @@
         "string-width": "^1.0.2 || 2"
       }
     },
+    "wolfy87-eventemitter": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-4.3.0.tgz",
+      "integrity": "sha1-ZJc5bJXnQ1nwa241QJM5MY2Nlk8="
+    },
     "wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
diff --git a/package.json b/package.json
index 0e10bc8c..6e1acd2b 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
     "babel-preset-env": "^1.6.1",
     "babel-preset-react": "^6.24.1",
     "babel-preset-stage-2": "^6.24.1",
+    "circular-dependency-plugin": "^5.2.0",
     "css-loader": "^0.28.11",
     "file-loader": "^1.1.11",
     "html-webpack-plugin": "^3.2.0",
diff --git a/src/components/editors/AudioEditor/index.tsx b/src/components/editors/AudioEditor/index.tsx
index 9826c74c..efa12941 100644
--- a/src/components/editors/AudioEditor/index.tsx
+++ b/src/components/editors/AudioEditor/index.tsx
@@ -4,8 +4,9 @@ import { connect } from 'react-redux';
 import { UPLOAD_TYPES } from '~/redux/uploads/constants';
 import { ImageGrid } from '../ImageGrid';
 import { AudioGrid } from '../AudioGrid';
-import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
 import { selectUploads } from '~/redux/uploads/selectors';
+
+import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
 import * as styles from './styles.scss';
 
 const mapStateToProps = selectUploads;
diff --git a/src/components/editors/AudioGrid/index.tsx b/src/components/editors/AudioGrid/index.tsx
index 8dd8556d..c3ca845e 100644
--- a/src/components/editors/AudioGrid/index.tsx
+++ b/src/components/editors/AudioGrid/index.tsx
@@ -1,11 +1,12 @@
 import React, { FC, useCallback } from 'react';
 import { SortEnd } from 'react-sortable-hoc';
-import * as styles from './styles.scss';
 import { IFile } from '~/redux/types';
 import { IUploadStatus } from '~/redux/uploads/reducer';
 import { moveArrItem } from '~/utils/fn';
 import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
 
+import * as styles from './styles.scss';
+
 interface IProps {
   files: IFile[];
   setFiles: (val: IFile[]) => void;
diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts
new file mode 100644
index 00000000..d0c6b5b7
--- /dev/null
+++ b/src/constants/dialogs.ts
@@ -0,0 +1,30 @@
+import { NODE_TYPES } from '~/redux/node/constants';
+import { EditorDialogImage } from '~/containers/editors/EditorDialogImage';
+import { EditorDialogText } from '~/containers/editors/EditorDialogText';
+import { EditorDialogVideo } from '~/containers/editors/EditorDialogVideo';
+import { EditorDialogAudio } from '~/containers/editors/EditorDialogAudio';
+import { LoginDialog } from '~/containers/dialogs/LoginDialog';
+import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
+import { TestDialog } from '~/containers/dialogs/TestDialog';
+import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
+import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
+import { DIALOGS } from '~/redux/modal/constants';
+
+export const DIALOG_CONTENT = {
+  [DIALOGS.EDITOR_IMAGE]: EditorDialogImage,
+  [DIALOGS.EDITOR_TEXT]: EditorDialogText,
+  [DIALOGS.EDITOR_VIDEO]: EditorDialogVideo,
+  [DIALOGS.EDITOR_AUDIO]: EditorDialogAudio,
+  [DIALOGS.LOGIN]: LoginDialog,
+  [DIALOGS.LOADING]: LoadingDialog,
+  [DIALOGS.TEST]: TestDialog,
+  [DIALOGS.PROFILE]: ProfileDialog,
+  [DIALOGS.RESTORE_REQUEST]: RestoreRequestDialog,
+};
+
+export const NODE_EDITOR_DIALOGS = {
+  [NODE_TYPES.IMAGE]: DIALOGS.EDITOR_IMAGE,
+  [NODE_TYPES.TEXT]: DIALOGS.EDITOR_TEXT,
+  [NODE_TYPES.VIDEO]: DIALOGS.EDITOR_VIDEO,
+  [NODE_TYPES.AUDIO]: DIALOGS.EDITOR_AUDIO,
+};
diff --git a/src/constants/errors.ts b/src/constants/errors.ts
index 5e10b468..4efef113 100644
--- a/src/constants/errors.ts
+++ b/src/constants/errors.ts
@@ -1,35 +1,37 @@
 export const ERRORS = {
-  NOT_AN_EMAIL: "Not_An_Email",
-  TOO_SHIRT: "Is_Too_Shirt",
-  EMPTY_RESPONSE: "Empty_Response",
-  NO_COMMENTS: "No_Comments",
-  FILES_REQUIRED: "Files_Required",
-  TEXT_REQUIRED: "Text_Required",
-  UNKNOWN_NODE_TYPE: "Unknown_Node_Type",
-  URL_INVALID: "Url_Invalid",
-  FILES_AUDIO_REQUIRED: "Files_Audio_Required",
-  NOT_ENOUGH_RIGHTS: "Not_Enough_Rights",
-  INCORRECT_DATA: "Incorrect_Data",
-  IMAGE_CONVERSION_FAILED: "Image_Conversion_Failed",
-  USER_NOT_FOUND: "User_Not_found",
-  USER_EXIST: "User_Exist",
-  INCORRECT_PASSWORD: "Incorrect_Password"
+  NOT_AN_EMAIL: 'Not_An_Email',
+  TOO_SHIRT: 'Is_Too_Shirt',
+  EMPTY_RESPONSE: 'Empty_Response',
+  NO_COMMENTS: 'No_Comments',
+  FILES_REQUIRED: 'Files_Required',
+  TEXT_REQUIRED: 'Text_Required',
+  UNKNOWN_NODE_TYPE: 'Unknown_Node_Type',
+  URL_INVALID: 'Url_Invalid',
+  FILES_AUDIO_REQUIRED: 'Files_Audio_Required',
+  NOT_ENOUGH_RIGHTS: 'Not_Enough_Rights',
+  INCORRECT_DATA: 'Incorrect_Data',
+  IMAGE_CONVERSION_FAILED: 'Image_Conversion_Failed',
+  USER_NOT_FOUND: 'User_Not_found',
+  USER_EXIST: 'User_Exist',
+  INCORRECT_PASSWORD: 'Incorrect_Password',
+  CODE_IS_INVALID: 'Code_Is_Invalid',
 };
 
 export const ERROR_LITERAL = {
-  [ERRORS.NOT_AN_EMAIL]: "Введите правильный e-mail",
-  [ERRORS.TOO_SHIRT]: "Слишком короткий",
-  [ERRORS.NO_COMMENTS]: "Комментариев пока нет",
-  [ERRORS.EMPTY_RESPONSE]: "Пустой ответ сервера",
-  [ERRORS.FILES_REQUIRED]: "Добавьте файлы",
-  [ERRORS.TEXT_REQUIRED]: "Нужно немного текста",
-  [ERRORS.UNKNOWN_NODE_TYPE]: "Неизвестный тип поста",
-  [ERRORS.URL_INVALID]: "Неизвестный адрес",
-  [ERRORS.FILES_AUDIO_REQUIRED]: "Нужна хотя бы одна песня",
-  [ERRORS.NOT_ENOUGH_RIGHTS]: "У вас недостаточно прав",
-  [ERRORS.INCORRECT_DATA]: "Недопустимые данные",
-  [ERRORS.IMAGE_CONVERSION_FAILED]: "Не удалось изменить изображение",
-  [ERRORS.USER_NOT_FOUND]: "Пользователь не найден",
-  [ERRORS.USER_EXIST]: "Такой пользователь уже существует",
-  [ERRORS.INCORRECT_PASSWORD]: "Неправильный пароль"
+  [ERRORS.NOT_AN_EMAIL]: 'Введите правильный e-mail',
+  [ERRORS.TOO_SHIRT]: 'Слишком короткий',
+  [ERRORS.NO_COMMENTS]: 'Комментариев пока нет',
+  [ERRORS.EMPTY_RESPONSE]: 'Пустой ответ сервера',
+  [ERRORS.FILES_REQUIRED]: 'Добавьте файлы',
+  [ERRORS.TEXT_REQUIRED]: 'Нужно немного текста',
+  [ERRORS.UNKNOWN_NODE_TYPE]: 'Неизвестный тип поста',
+  [ERRORS.URL_INVALID]: 'Неизвестный адрес',
+  [ERRORS.FILES_AUDIO_REQUIRED]: 'Нужна хотя бы одна песня',
+  [ERRORS.NOT_ENOUGH_RIGHTS]: 'У вас недостаточно прав',
+  [ERRORS.INCORRECT_DATA]: 'Недопустимые данные',
+  [ERRORS.IMAGE_CONVERSION_FAILED]: 'Не удалось изменить изображение',
+  [ERRORS.USER_NOT_FOUND]: 'Пользователь не найден',
+  [ERRORS.USER_EXIST]: 'Такой пользователь уже существует',
+  [ERRORS.INCORRECT_PASSWORD]: 'Неправильный пароль',
+  [ERRORS.CODE_IS_INVALID]: 'Код не существует или устарел',
 };
diff --git a/src/containers/dialogs/LoginDialog/index.tsx b/src/containers/dialogs/LoginDialog/index.tsx
index c30ba655..3099e839 100644
--- a/src/containers/dialogs/LoginDialog/index.tsx
+++ b/src/containers/dialogs/LoginDialog/index.tsx
@@ -1,37 +1,43 @@
-import React, { FC, FormEvent, useCallback, useEffect, useState } from "react";
-import { connect } from "react-redux";
-import { ScrollDialog } from "../ScrollDialog";
-import { IDialogProps } from "~/redux/modal/constants";
-import { useCloseOnEscape } from "~/utils/hooks";
-import { Group } from "~/components/containers/Group";
-import { InputText } from "~/components/input/InputText";
-import { Button } from "~/components/input/Button";
-import { Padder } from "~/components/containers/Padder";
-import * as styles from "./styles.scss";
-import { selectAuthLogin } from "~/redux/auth/selectors";
-import * as ACTIONS from "~/redux/auth/actions";
-import { API } from "~/constants/api";
-import { BetterScrollDialog } from "../BetterScrollDialog";
+import React, { FC, FormEvent, useCallback, useEffect, useState, useMemo } from 'react';
+import { connect } from 'react-redux';
+import { IDialogProps } from '~/redux/modal/constants';
+import { DIALOGS } from '~/redux/modal/constants';
+import { useCloseOnEscape } from '~/utils/hooks';
+import { Group } from '~/components/containers/Group';
+import { InputText } from '~/components/input/InputText';
+import { Button } from '~/components/input/Button';
+import { Padder } from '~/components/containers/Padder';
+import { selectAuthLogin } from '~/redux/auth/selectors';
+import { API } from '~/constants/api';
+import { BetterScrollDialog } from '../BetterScrollDialog';
+
+import * as styles from './styles.scss';
+import * as ACTIONS from '~/redux/auth/actions';
+import * as MODAL_ACTIONS from '~/redux/modal/actions';
 
 const mapStateToProps = selectAuthLogin;
 
 const mapDispatchToProps = {
   userSendLoginRequest: ACTIONS.userSendLoginRequest,
-  userSetLoginError: ACTIONS.userSetLoginError
+  userSetLoginError: ACTIONS.userSetLoginError,
+  modalShowDialog: MODAL_ACTIONS.modalShowDialog,
 };
 
-type IProps = ReturnType<typeof mapStateToProps> &
-  typeof mapDispatchToProps &
-  IDialogProps & {};
+type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & IDialogProps & {};
+
+console.log('initial', MODAL_ACTIONS);
 
 const LoginDialogUnconnected: FC<IProps> = ({
   onRequestClose,
   error,
   userSendLoginRequest,
-  userSetLoginError
+  userSetLoginError,
+  modalShowDialog,
 }) => {
-  const [username, setUserName] = useState("");
-  const [password, setPassword] = useState("");
+  console.log({ modalShowDialog, MODAL_ACTIONS });
+
+  const [username, setUserName] = useState('');
+  const [password, setPassword] = useState('');
 
   const onSubmit = useCallback(
     (event: FormEvent) => {
@@ -41,61 +47,60 @@ const LoginDialogUnconnected: FC<IProps> = ({
     [userSendLoginRequest, username, password]
   );
 
+  const onRestoreRequest = useCallback(
+    event => {
+      console.log('a', { MODAL_ACTIONS, modalShowDialog, userSetLoginError });
+      event.preventDefault();
+      modalShowDialog(DIALOGS.RESTORE_REQUEST);
+    },
+    [modalShowDialog, userSetLoginError]
+  );
+
   const onSocialLogin = useCallback(() => {
-    window.open(API.USER.VKONTAKTE_LOGIN, "", "width=600,height=400");
+    window.open(API.USER.VKONTAKTE_LOGIN, '', 'width=600,height=400');
   }, []);
 
   useEffect(() => {
     if (error) userSetLoginError(null);
   }, [username, password]);
 
-  const buttons = (
-    <Group horizontal className={styles.footer}>
-      <Button stretchy>
-        <span>Войти</span>
-      </Button>
-    </Group>
+  const buttons = useMemo(
+    () => (
+      <Group className={styles.footer}>
+        <Button
+          className={styles.secondary_button}
+          iconLeft="vk"
+          type="button"
+          onClick={onSocialLogin}
+        >
+          <span>Вконтакте</span>
+        </Button>
+
+        <Button>
+          <span>Войти</span>
+        </Button>
+      </Group>
+    ),
+    [onSocialLogin]
   );
 
   useCloseOnEscape(onRequestClose);
 
   return (
     <form onSubmit={onSubmit}>
-      <BetterScrollDialog
-        width={260}
-        error={error}
-        onClose={onRequestClose}
-        footer={buttons}
-      >
+      <BetterScrollDialog width={260} error={error} onClose={onRequestClose} footer={buttons}>
         <Padder>
           <div className={styles.wrap}>
             <Group>
               <h2>РЕШИТЕЛЬНО ВОЙТИ</h2>
 
-              <InputText
-                title="Логин"
-                handler={setUserName}
-                value={username}
-                autoFocus
-              />
+              <InputText title="Логин" handler={setUserName} value={username} autoFocus />
 
-              <InputText
-                title="Пароль"
-                handler={setPassword}
-                value={password}
-                type="password"
-              />
+              <InputText title="Пароль" handler={setPassword} value={password} type="password" />
 
-              <Group className={styles.buttons}>
-                <Button
-                  className={styles.vk}
-                  iconLeft="vk"
-                  type="button"
-                  onClick={onSocialLogin}
-                >
-                  <span>Вконтакте</span>
-                </Button>
-              </Group>
+              <Button className={styles.forgot_button} type="button" onClick={onRestoreRequest}>
+                Вспомнить пароль
+              </Button>
             </Group>
           </div>
         </Padder>
diff --git a/src/containers/dialogs/LoginDialog/styles.scss b/src/containers/dialogs/LoginDialog/styles.scss
index 3e0b6dc0..176fa7c9 100644
--- a/src/containers/dialogs/LoginDialog/styles.scss
+++ b/src/containers/dialogs/LoginDialog/styles.scss
@@ -1,4 +1,5 @@
-$vk_color: darken(desaturate($blue, 100%), 30%);
+$secondary_color: darken(desaturate($blue, 100%), 30%);
+$vk_color: $secondary_color;
 
 .wrap {
   display: flex;
@@ -15,7 +16,7 @@ $vk_color: darken(desaturate($blue, 100%), 30%);
   }
 }
 
-.vk {
+.secondary_button {
   background: $content_bg;
   box-shadow: inset $vk_color 0 0 0 2px;
   color: $vk_color;
@@ -23,11 +24,15 @@ $vk_color: darken(desaturate($blue, 100%), 30%);
   svg {
     fill: $vk_color;
     margin-right: $gap;
-    // width: 24px;
-    // height: 24px;
   }
 }
 
+.forgot_button {
+  background: $content_bg;
+  box-shadow: none;
+  color: $secondary_color;
+}
+
 .buttons {
   margin: $gap * 2 0 0 0 !important;
   padding: $gap * 2 0 0 0;
@@ -41,3 +46,12 @@ $vk_color: darken(desaturate($blue, 100%), 30%);
     // text-align: left;
   }
 }
+
+.links {
+  font: $font_14_regular;
+  text-align: center;
+
+  a {
+    color: lighten($content_bg, 40%);
+  }
+}
diff --git a/src/containers/dialogs/Modal/index.tsx b/src/containers/dialogs/Modal/index.tsx
index 8d37d0f6..cbb1f69d 100644
--- a/src/containers/dialogs/Modal/index.tsx
+++ b/src/containers/dialogs/Modal/index.tsx
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
 import * as styles from './styles.scss';
 import { IState } from '~/redux/store';
 import * as ACTIONS from '~/redux/modal/actions';
-import { DIALOG_CONTENT } from '~/redux/modal/constants';
+import { DIALOG_CONTENT } from '~/constants/dialogs';
 
 const mapStateToProps = ({ modal }: IState) => ({ ...modal });
 const mapDispatchToProps = {
diff --git a/src/containers/dialogs/RestoreRequestDialog/index.tsx b/src/containers/dialogs/RestoreRequestDialog/index.tsx
new file mode 100644
index 00000000..eb6201e7
--- /dev/null
+++ b/src/containers/dialogs/RestoreRequestDialog/index.tsx
@@ -0,0 +1,41 @@
+import React, { FC, useState, useMemo, useCallback } from 'react';
+import { IDialogProps } from '~/redux/types';
+import { connect } from 'react-redux';
+import { BetterScrollDialog } from '../BetterScrollDialog';
+import { Group } from '~/components/containers/Group';
+import { InputText } from '~/components/input/InputText';
+import { Button } from '~/components/input/Button';
+
+const mapStateToProps = () => ({});
+const mapDispatchToProps = {};
+
+type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
+
+const RestoreRequestDialogUnconnected: FC<IProps> = ({}) => {
+  const [field, setField] = useState();
+
+  const onSubmit = useCallback(event => {
+    event.preventDefault();
+  }, []);
+
+  const buttons = useMemo(() => <Button>Восстановить</Button>, []);
+
+  return (
+    <form onSubmit={onSubmit}>
+      <BetterScrollDialog footer={buttons}>
+        <Group>
+          <InputText title="Имя или email" value={field} handler={setField} />
+
+          <div>Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля.</div>
+        </Group>
+      </BetterScrollDialog>
+    </form>
+  );
+};
+
+const RestoreRequestDialog = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(RestoreRequestDialogUnconnected);
+
+export { RestoreRequestDialog };
diff --git a/src/redux/auth/reducer.ts b/src/redux/auth/reducer.ts
index 1c9ba775..885bb259 100644
--- a/src/redux/auth/reducer.ts
+++ b/src/redux/auth/reducer.ts
@@ -34,6 +34,7 @@ const INITIAL_STATE: IAuthState = {
 
   restore: {
     code: '',
+    user: null,
     is_loading: false,
     is_succesfull: false,
     errors: {},
diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts
index e16cd5d2..8bcf02ec 100644
--- a/src/redux/auth/sagas.ts
+++ b/src/redux/auth/sagas.ts
@@ -1,17 +1,5 @@
-import {
-  call,
-  put,
-  takeEvery,
-  takeLatest,
-  select,
-  delay
-} from "redux-saga/effects";
-import {
-  AUTH_USER_ACTIONS,
-  EMPTY_USER,
-  USER_ERRORS,
-  USER_ROLES
-} from "~/redux/auth/constants";
+import { call, put, takeEvery, takeLatest, select, delay } from 'redux-saga/effects';
+import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ERRORS, USER_ROLES } from '~/redux/auth/constants';
 import {
   authSetToken,
   userSetLoginError,
@@ -25,8 +13,10 @@ import {
   authSetUpdates,
   authLoggedIn,
   authSetLastSeenMessages,
-  authPatchUser
-} from "~/redux/auth/actions";
+  authPatchUser,
+  authRestorePassword,
+  authSetRestore,
+} from '~/redux/auth/actions';
 import {
   apiUserLogin,
   apiAuthGetUser,
@@ -34,31 +24,19 @@ import {
   apiAuthGetUserMessages,
   apiAuthSendMessage,
   apiAuthGetUpdates,
-  apiUpdateUser
-} from "~/redux/auth/api";
-import { modalSetShown, modalShowDialog } from "~/redux/modal/actions";
-import {
-  selectToken,
-  selectAuthProfile,
-  selectAuthUser,
-  selectAuthUpdates
-} from "./selectors";
-import {
-  IResultWithStatus,
-  INotification,
-  IMessageNotification
-} from "../types";
-import { IUser, IAuthState } from "./types";
-import { REHYDRATE, RehydrateAction } from "redux-persist";
-import { selectModal } from "../modal/selectors";
-import { IModalState } from "../modal/reducer";
-import { DIALOGS } from "../modal/constants";
-import { ERRORS } from "~/constants/errors";
+  apiUpdateUser,
+} from '~/redux/auth/api';
+import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
+import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors';
+import { IResultWithStatus, INotification, IMessageNotification } from '../types';
+import { IUser, IAuthState } from './types';
+import { REHYDRATE, RehydrateAction } from 'redux-persist';
+import { selectModal } from '~/redux/modal/selectors';
+import { IModalState } from '~/redux/modal/reducer';
+import { DIALOGS } from '~/redux/modal/constants';
+import { ERRORS } from '~/constants/errors';
 
-export function* reqWrapper(
-  requestAction,
-  props = {}
-): ReturnType<typeof requestAction> {
+export function* reqWrapper(requestAction, props = {}): ReturnType<typeof requestAction> {
   const access = yield select(selectToken);
 
   const result = yield call(requestAction, { access, ...props });
@@ -70,22 +48,16 @@ export function* reqWrapper(
   return result;
 }
 
-function* sendLoginRequestSaga({
-  username,
-  password
-}: ReturnType<typeof userSendLoginRequest>) {
+function* sendLoginRequestSaga({ username, password }: ReturnType<typeof userSendLoginRequest>) {
   if (!username || !password) return;
 
   const {
     error,
-    data: { token, user }
-  }: IResultWithStatus<{ token: string; user: IUser }> = yield call(
-    apiUserLogin,
-    {
-      username,
-      password
-    }
-  );
+    data: { token, user },
+  }: IResultWithStatus<{ token: string; user: IUser }> = yield call(apiUserLogin, {
+    username,
+    password,
+  });
 
   if (error) {
     yield put(userSetLoginError(error));
@@ -101,17 +73,14 @@ function* sendLoginRequestSaga({
 function* refreshUser() {
   const {
     error,
-    data: { user }
-  }: IResultWithStatus<{ user: IUser }> = yield call(
-    reqWrapper,
-    apiAuthGetUser
-  );
+    data: { user },
+  }: IResultWithStatus<{ user: IUser }> = yield call(reqWrapper, apiAuthGetUser);
 
   if (error) {
     yield put(
       authSetUser({
         ...EMPTY_USER,
-        is_user: false
+        is_user: false,
       })
     );
 
@@ -122,7 +91,7 @@ function* refreshUser() {
 }
 
 function* checkUserSaga({ key }: RehydrateAction) {
-  if (key !== "auth") return;
+  if (key !== 'auth') return;
   yield call(refreshUser);
   // yield put(authOpenProfile("gvorcek", "settings"));
 }
@@ -142,21 +111,18 @@ function* logoutSaga() {
   yield put(
     authSetUpdates({
       last: null,
-      notifications: []
+      notifications: [],
     })
   );
 }
 
-function* openProfile({
-  username,
-  tab = "profile"
-}: ReturnType<typeof authOpenProfile>) {
+function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenProfile>) {
   yield put(modalShowDialog(DIALOGS.PROFILE));
   yield put(authSetProfile({ is_loading: true, tab }));
 
   const {
     error,
-    data: { user }
+    data: { user },
   } = yield call(reqWrapper, apiAuthGetUserProfile, { username });
 
   if (error || !user) {
@@ -176,16 +142,15 @@ function* getMessages({ username }: ReturnType<typeof authGetMessages>) {
       messages:
         messages &&
         messages.length > 0 &&
-        (messages[0].to.username === username ||
-          messages[0].from.username === username)
+        (messages[0].to.username === username || messages[0].from.username === username)
           ? messages
-          : []
+          : [],
     })
   );
 
   const {
     error,
-    data
+    data,
     // data: { messages },
   } = yield call(reqWrapper, apiAuthGetUserMessages, { username });
 
@@ -193,21 +158,19 @@ function* getMessages({ username }: ReturnType<typeof authGetMessages>) {
     return yield put(
       authSetProfile({
         is_loading_messages: false,
-        messages_error: ERRORS.EMPTY_RESPONSE
+        messages_error: ERRORS.EMPTY_RESPONSE,
       })
     );
   }
 
-  yield put(
-    authSetProfile({ is_loading_messages: false, messages: data.messages })
-  );
+  yield put(authSetProfile({ is_loading_messages: false, messages: data.messages }));
 
   const { notifications } = yield select(selectAuthUpdates);
 
   // clear viewed message from notifcation list
   const filtered = notifications.filter(
     notification =>
-      notification.type !== "message" ||
+      notification.type !== 'message' ||
       (notification as IMessageNotification).content.from.username !== username
   );
 
@@ -216,23 +179,18 @@ function* getMessages({ username }: ReturnType<typeof authGetMessages>) {
   }
 }
 
-function* sendMessage({
-  message,
-  onSuccess
-}: ReturnType<typeof authSendMessage>) {
+function* sendMessage({ message, onSuccess }: ReturnType<typeof authSendMessage>) {
   const {
-    user: { username }
+    user: { username },
   } = yield select(selectAuthProfile);
 
   if (!username) return;
 
-  yield put(
-    authSetProfile({ is_sending_messages: true, messages_error: null })
-  );
+  yield put(authSetProfile({ is_sending_messages: true, messages_error: null }));
 
   const { error, data } = yield call(reqWrapper, apiAuthSendMessage, {
     username,
-    message
+    message,
   });
 
   console.log({ error, data });
@@ -241,7 +199,7 @@ function* sendMessage({
     return yield put(
       authSetProfile({
         is_sending_messages: false,
-        messages_error: error || ERRORS.EMPTY_RESPONSE
+        messages_error: error || ERRORS.EMPTY_RESPONSE,
       })
     );
   }
@@ -255,7 +213,7 @@ function* sendMessage({
   yield put(
     authSetProfile({
       is_sending_messages: false,
-      messages: [data.message, ...messages]
+      messages: [data.message, ...messages],
     })
   );
 
@@ -265,35 +223,28 @@ function* sendMessage({
 function* getUpdates() {
   const user = yield select(selectAuthUser);
 
-  if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id)
-    return;
+  if (!user || !user.is_user || user.role === USER_ROLES.GUEST || !user.id) return;
 
   const modal: IModalState = yield select(selectModal);
-  const profile: IAuthState["profile"] = yield select(selectAuthProfile);
-  const { last }: IAuthState["updates"] = yield select(selectAuthUpdates);
+  const profile: IAuthState['profile'] = yield select(selectAuthProfile);
+  const { last }: IAuthState['updates'] = yield select(selectAuthUpdates);
   const exclude_dialogs =
-    modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id
-      ? profile.user.id
-      : null;
+    modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null;
 
-  const {
-    error,
-    data
-  }: IResultWithStatus<{ notifications: INotification[] }> = yield call(
+  const { error, data }: IResultWithStatus<{ notifications: INotification[] }> = yield call(
     reqWrapper,
     apiAuthGetUpdates,
     { exclude_dialogs, last: last || user.last_seen_messages }
   );
 
-  if (error || !data || !data.notifications || !data.notifications.length)
-    return;
+  if (error || !data || !data.notifications || !data.notifications.length) return;
 
   const { notifications } = data;
 
   yield put(
     authSetUpdates({
       last: notifications[0].created_at,
-      notifications
+      notifications,
     })
   );
 }
@@ -305,9 +256,7 @@ function* startPollingSaga() {
   }
 }
 
-function* setLastSeenMessages({
-  last_seen_messages
-}: ReturnType<typeof authSetLastSeenMessages>) {
+function* setLastSeenMessages({ last_seen_messages }: ReturnType<typeof authSetLastSeenMessages>) {
   if (!Date.parse(last_seen_messages)) return;
 
   yield call(reqWrapper, apiUpdateUser, { user: { last_seen_messages } });
@@ -323,7 +272,19 @@ function* patchUser({ user }: ReturnType<typeof authPatchUser>) {
   }
 
   yield put(authSetUser({ ...me, ...data.user }));
-  yield put(authSetProfile({ user: { ...me, ...data.user }, tab: "profile" }));
+  yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' }));
+}
+
+function* restorePassword({ code }: ReturnType<typeof authRestorePassword>) {
+  if (!code && !code.length) {
+    return yield put(
+      authSetRestore({
+        errors: { code: ERRORS.CODE_IS_INVALID },
+        is_loading: false,
+      })
+    );
+  }
+  console.log({ code });
 }
 
 function* authSaga() {
@@ -336,11 +297,9 @@ function* authSaga() {
   yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile);
   yield takeLatest(AUTH_USER_ACTIONS.GET_MESSAGES, getMessages);
   yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
-  yield takeLatest(
-    AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES,
-    setLastSeenMessages
-  );
+  yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages);
   yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser);
+  yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword);
 }
 
 export default authSaga;
diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts
index 38922761..07fd8c5d 100644
--- a/src/redux/auth/types.ts
+++ b/src/redux/auth/types.ts
@@ -52,6 +52,7 @@ export type IAuthState = Readonly<{
 
   restore: {
     code: string;
+    user: Pick<IUser, 'username' | 'photo'>;
     is_loading: boolean;
     is_succesfull: boolean;
     errors: Record<string, string>;
diff --git a/src/redux/modal/constants.ts b/src/redux/modal/constants.ts
index b611f962..07f723ef 100644
--- a/src/redux/modal/constants.ts
+++ b/src/redux/modal/constants.ts
@@ -8,12 +8,7 @@ import { EditorDialogAudio } from '~/containers/editors/EditorDialogAudio';
 import { NODE_TYPES } from '../node/constants';
 import { TestDialog } from '~/containers/dialogs/TestDialog';
 import { ProfileDialog } from '~/containers/dialogs/ProfileDialog';
-
-export const MODAL_ACTIONS = {
-  SET_SHOWN: 'MODAL.SET_SHOWN',
-  SET_DIALOG: 'SET_DIALOG',
-  SHOW_DIALOG: 'SHOW_DIALOG',
-};
+import { RestoreRequestDialog } from '~/containers/dialogs/RestoreRequestDialog';
 
 export const DIALOGS = {
   EDITOR_IMAGE: 'EDITOR_IMAGE',
@@ -23,25 +18,14 @@ export const DIALOGS = {
   LOGIN: 'LOGIN',
   LOADING: 'LOADING',
   PROFILE: 'PROFILE',
+  RESTORE_REQUEST: 'RESTORE_REQUEST',
   TEST: 'TEST',
 };
 
-export const DIALOG_CONTENT = {
-  [DIALOGS.EDITOR_IMAGE]: EditorDialogImage,
-  [DIALOGS.EDITOR_TEXT]: EditorDialogText,
-  [DIALOGS.EDITOR_VIDEO]: EditorDialogVideo,
-  [DIALOGS.EDITOR_AUDIO]: EditorDialogAudio,
-  [DIALOGS.LOGIN]: LoginDialog,
-  [DIALOGS.LOADING]: LoadingDialog,
-  [DIALOGS.TEST]: TestDialog,
-  [DIALOGS.PROFILE]: ProfileDialog,
-};
-
-export const NODE_EDITOR_DIALOGS = {
-  [NODE_TYPES.IMAGE]: DIALOGS.EDITOR_IMAGE,
-  [NODE_TYPES.TEXT]: DIALOGS.EDITOR_TEXT,
-  [NODE_TYPES.VIDEO]: DIALOGS.EDITOR_VIDEO,
-  [NODE_TYPES.AUDIO]: DIALOGS.EDITOR_AUDIO,
+export const MODAL_ACTIONS = {
+  SET_SHOWN: 'MODAL.SET_SHOWN',
+  SET_DIALOG: 'SET_DIALOG',
+  SHOW_DIALOG: 'SHOW_DIALOG',
 };
 
 export interface IDialogProps {
diff --git a/src/redux/node/sagas.ts b/src/redux/node/sagas.ts
index 1ac51712..bd6e3a61 100644
--- a/src/redux/node/sagas.ts
+++ b/src/redux/node/sagas.ts
@@ -40,7 +40,8 @@ import { selectFlowNodes, selectFlow } from '../flow/selectors';
 import { URLS } from '~/constants/urls';
 import { selectNode } from './selectors';
 import { IResultWithStatus, INode } from '../types';
-import { NODE_EDITOR_DIALOGS, DIALOGS } from '../modal/constants';
+import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
+import { DIALOGS } from '~/redux/modal/constants';
 import { INodeState } from './reducer';
 import { IFlowState } from '../flow/reducer';
 
diff --git a/src/redux/types.ts b/src/redux/types.ts
index 6a9b38c0..4e21edc9 100644
--- a/src/redux/types.ts
+++ b/src/redux/types.ts
@@ -1,7 +1,7 @@
-import { DetailedHTMLProps, InputHTMLAttributes } from "react";
-import { DIALOGS } from "~/redux/modal/constants";
-import { ERRORS } from "~/constants/errors";
-import { IUser } from "./auth/types";
+import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
+import { DIALOGS } from '~/redux/modal/constants';
+import { ERRORS } from '~/constants/errors';
+import { IUser } from './auth/types';
 
 export interface ITag {
   id: number;
@@ -55,7 +55,7 @@ export interface IResultWithStatus<T> {
 
 export type UUID = string;
 
-export type IUploadType = "image" | "text" | "audio" | "video" | "other";
+export type IUploadType = 'image' | 'text' | 'audio' | 'video' | 'other';
 
 export interface IFile {
   id?: number;
@@ -96,12 +96,12 @@ export interface IFileWithUUID {
 }
 
 export interface IBlockText {
-  type: "text";
+  type: 'text';
   text: string;
 }
 
 export interface IBlockEmbed {
-  type: "video";
+  type: 'video';
   url: string;
 }
 
@@ -124,7 +124,7 @@ export interface INode {
   is_heroic?: boolean;
 
   flow: {
-    display: "single" | "vertical" | "horizontal" | "quadro";
+    display: 'single' | 'vertical' | 'horizontal' | 'quadro';
     show_description: boolean;
   };
 
@@ -147,7 +147,7 @@ export interface IComment {
   update_at?: string;
 }
 
-export type IMessage = Omit<IComment, "user" | "node"> & {
+export type IMessage = Omit<IComment, 'user' | 'node'> & {
   from: IUser;
   to: IUser;
 };
@@ -155,7 +155,7 @@ export type IMessage = Omit<IComment, "user" | "node"> & {
 export interface ICommentGroup {
   user: IUser;
   comments: IComment[];
-  ids: IComment["id"][];
+  ids: IComment['id'][];
 }
 
 export type IUploadProgressHandler = (progress: ProgressEvent) => void;
@@ -164,25 +164,25 @@ export type IValidationErrors = Record<string, IError>;
 export type InputHandler<T = string> = (val: T) => void;
 
 export const NOTIFICATION_TYPES = {
-  message: "message",
-  comment: "comment",
-  node: "node"
+  message: 'message',
+  comment: 'comment',
+  node: 'node',
 };
 
 export type IMessageNotification = {
-  type: typeof NOTIFICATION_TYPES["message"];
+  type: typeof NOTIFICATION_TYPES['message'];
   content: Partial<IMessage>;
   created_at: string;
 };
 
 export type ICommentNotification = {
-  type: typeof NOTIFICATION_TYPES["comment"];
+  type: typeof NOTIFICATION_TYPES['comment'];
   content: Partial<IComment>;
   created_at: string;
 };
 
 export type INodeNotification = {
-  type: typeof NOTIFICATION_TYPES["node"];
+  type: typeof NOTIFICATION_TYPES['node'];
   content: Partial<INode>;
   created_at: string;
 };
diff --git a/src/styles/colors.scss b/src/styles/colors.scss
index 1fada816..0319f9ab 100644
--- a/src/styles/colors.scss
+++ b/src/styles/colors.scss
@@ -12,6 +12,9 @@ $grass: #41800d;
 $wisegreen: #007962;
 // $wisegreen: #006868;
 
+$primary: $red;
+$secondary: $wisegreen;
+
 $red_gradient: linear-gradient(165deg, $orange -50%, $red 150%);
 $blue_gradient: linear-gradient(170deg, $green, $dark_blue);
 $green_gradient: linear-gradient(
diff --git a/src/styles/global.scss b/src/styles/global.scss
index fdbcedd5..4b112c1a 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -96,6 +96,11 @@ body {
   font-weight: bold;
 }
 
+a {
+  color: $primary;
+  text-decoration: underline;
+}
+
 ::-webkit-scrollbar {
   width: 18px;
   height: 18px;
diff --git a/webpack.config.js b/webpack.config.js
index 2dc6a614..07314a45 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,6 +5,7 @@ const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const { join } = require('path');
 const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
 const Dotenv = require('dotenv-webpack');
+const CircularDependencyPlugin = require('circular-dependency-plugin');
 
 const htmlPlugin = new HtmlWebPackPlugin({
   template: './src/index.html',
@@ -38,6 +39,13 @@ module.exports = () => {
       filename: '[name].[contenthash].css',
       chunkFilename: '[id].[contenthash].css',
     }),
+    new CircularDependencyPlugin({
+      // exclude: /node_modules/,
+      include: /LoginDialog/,
+      failOnError: true,
+      allowAsyncCycles: false,
+      cwd: process.cwd(),
+    }),
   ];
 
   return {