From 6dcb21e9e4fe6a6130aee68506d53a13e5fe579b Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Mon, 25 Nov 2019 16:08:22 +0700 Subject: [PATCH] added request code dialog --- src/components/input/Button/index.tsx | 4 +- src/components/input/Button/styles.scss | 28 ++++++ src/constants/api.ts | 31 +++--- .../dialogs/BetterScrollDialog/index.tsx | 17 +++- .../dialogs/BetterScrollDialog/styles.scss | 28 ++++++ src/containers/dialogs/LoginDialog/index.tsx | 16 ++-- .../dialogs/LoginDialog/styles.scss | 4 +- .../dialogs/RestoreRequestDialog/index.tsx | 94 ++++++++++++++++--- .../dialogs/RestoreRequestDialog/styles.scss | 47 ++++++++++ src/index.tsx | 17 ++-- src/redux/auth/actions.ts | 5 + src/redux/auth/api.ts | 45 ++++----- src/redux/auth/constants.ts | 1 + src/redux/auth/reducer.ts | 2 +- src/redux/auth/sagas.ts | 20 +++- src/redux/auth/selectors.ts | 15 +-- src/redux/auth/types.ts | 2 +- src/redux/modal/reducer.ts | 4 +- 18 files changed, 292 insertions(+), 88 deletions(-) create mode 100644 src/containers/dialogs/RestoreRequestDialog/styles.scss diff --git a/src/components/input/Button/index.tsx b/src/components/input/Button/index.tsx index 6020680f..6b40b9b7 100644 --- a/src/components/input/Button/index.tsx +++ b/src/components/input/Button/index.tsx @@ -9,6 +9,7 @@ type IButtonProps = DetailedHTMLProps< HTMLButtonElement > & { size?: 'mini' | 'normal' | 'big' | 'giant' | 'micro' | 'small'; + color?: 'primary' | 'secondary' | 'outline' | 'link'; iconLeft?: IIcon; iconRight?: IIcon; seamless?: boolean; @@ -25,6 +26,7 @@ type IButtonProps = DetailedHTMLProps< const Button: FC = memo( ({ className = '', + color = 'primary', size = 'normal', iconLeft, iconRight, @@ -44,7 +46,7 @@ const Button: FC = memo( createElement( seamless || non_submitting ? 'div' : 'button', { - className: classnames(styles.button, className, styles[size], { + className: classnames(styles.button, className, styles[size], styles[color], { red, grey, seamless, diff --git a/src/components/input/Button/styles.scss b/src/components/input/Button/styles.scss index 91a0af24..fded59e7 100644 --- a/src/components/input/Button/styles.scss +++ b/src/components/input/Button/styles.scss @@ -143,6 +143,34 @@ padding-right: $gap; } + &.primary { + background: $red_gradient; + } + + &.secondary { + background: $green_gradient; + } + + &.outline { + background: transparent; + box-shadow: inset transparentize(white, 0.8) 0 0 0 2px; + color: transparentize(white, 0.8); + + svg { + fill: transparentize(white, 0.8); + } + } + + &.link { + background: transparent; + color: white; + box-shadow: none; + + svg { + fill: white; + } + } + > * { margin: 0 5px; diff --git a/src/constants/api.ts b/src/constants/api.ts index fca54c6b..2fdc7c95 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,29 +1,30 @@ -import { INode } from "~/redux/types"; +import { INode } from '~/redux/types'; export const API = { BASE: process.env.API_HOST, USER: { - LOGIN: "/user/login", + LOGIN: '/user/login', VKONTAKTE_LOGIN: `${process.env.API_HOST}/user/vkontakte`, - ME: "/user/", + ME: '/user/', PROFILE: (username: string) => `/user/${username}/profile`, MESSAGES: (username: string) => `/user/${username}/messages`, MESSAGE_SEND: (username: string) => `/user/${username}/messages`, - GET_UPDATES: "/user/updates", + GET_UPDATES: '/user/updates', + REQUEST_CODE: (code?: string) => `/user/restore/${code || ''}`, - UPLOAD: (target, type) => `/upload/${target}/${type}` + UPLOAD: (target, type) => `/upload/${target}/${type}`, }, NODE: { - SAVE: "/node/", - GET: "/node/", - GET_DIFF: "/node/diff", + SAVE: '/node/', + GET: '/node/', + GET_DIFF: '/node/diff', GET_NODE: (id: number | string) => `/node/${id}`, - COMMENT: (id: INode["id"]) => `/node/${id}/comment`, - RELATED: (id: INode["id"]) => `/node/${id}/related`, - UPDATE_TAGS: (id: INode["id"]) => `/node/${id}/tags`, - POST_LIKE: (id: INode["id"]) => `/node/${id}/like`, - POST_STAR: (id: INode["id"]) => `/node/${id}/heroic`, - SET_CELL_VIEW: (id: INode["id"]) => `/node/${id}/cell-view` - } + COMMENT: (id: INode['id']) => `/node/${id}/comment`, + RELATED: (id: INode['id']) => `/node/${id}/related`, + UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`, + POST_LIKE: (id: INode['id']) => `/node/${id}/like`, + POST_STAR: (id: INode['id']) => `/node/${id}/heroic`, + SET_CELL_VIEW: (id: INode['id']) => `/node/${id}/cell-view`, + }, }; diff --git a/src/containers/dialogs/BetterScrollDialog/index.tsx b/src/containers/dialogs/BetterScrollDialog/index.tsx index e60c00ad..3b5c54b7 100644 --- a/src/containers/dialogs/BetterScrollDialog/index.tsx +++ b/src/containers/dialogs/BetterScrollDialog/index.tsx @@ -1,7 +1,8 @@ -import React, { FC, MouseEventHandler, useEffect, useRef } from 'react'; +import React, { FC, MouseEventHandler, useEffect, useRef, ReactElement } from 'react'; import * as styles from './styles.scss'; import { enableBodyScroll, disableBodyScroll } from 'body-scroll-lock'; import { Icon } from '~/components/input/Icon'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; interface IProps { children: React.ReactChild; @@ -11,6 +12,8 @@ interface IProps { size?: 'medium' | 'big'; width?: number; error?: string; + is_loading?: boolean; + overlay?: ReactElement; onOverlayClick?: MouseEventHandler; onRefCapture?: (ref: any) => void; @@ -25,6 +28,8 @@ const BetterScrollDialog: FC = ({ width = 600, error, onClose, + is_loading, + overlay = null, }) => { const ref = useRef(null); @@ -51,7 +56,15 @@ const BetterScrollDialog: FC = ({ {error &&
{error}
} - {footer &&
{footer}
} + {!!is_loading && ( +
+ +
+ )} + + {overlay} + + {footer &&
{footer}
} ); diff --git a/src/containers/dialogs/BetterScrollDialog/styles.scss b/src/containers/dialogs/BetterScrollDialog/styles.scss index 93d7f093..348dbf6b 100644 --- a/src/containers/dialogs/BetterScrollDialog/styles.scss +++ b/src/containers/dialogs/BetterScrollDialog/styles.scss @@ -109,3 +109,31 @@ width: 100%; height: 100%; } + +@keyframes appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.shade { + position: absolute; + background: transparentize($content_bg, 0.3); + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + border-radius: $radius; + animation: appear 1s forwards; + + svg { + fill: white; + } +} diff --git a/src/containers/dialogs/LoginDialog/index.tsx b/src/containers/dialogs/LoginDialog/index.tsx index 3099e839..fbb8726a 100644 --- a/src/containers/dialogs/LoginDialog/index.tsx +++ b/src/containers/dialogs/LoginDialog/index.tsx @@ -67,12 +67,7 @@ const LoginDialogUnconnected: FC = ({ const buttons = useMemo( () => ( - @@ -88,7 +83,7 @@ const LoginDialogUnconnected: FC = ({ return (
- +
@@ -98,7 +93,12 @@ const LoginDialogUnconnected: FC = ({ - diff --git a/src/containers/dialogs/LoginDialog/styles.scss b/src/containers/dialogs/LoginDialog/styles.scss index 176fa7c9..fe5f8b35 100644 --- a/src/containers/dialogs/LoginDialog/styles.scss +++ b/src/containers/dialogs/LoginDialog/styles.scss @@ -28,9 +28,7 @@ $vk_color: $secondary_color; } .forgot_button { - background: $content_bg; - box-shadow: none; - color: $secondary_color; + opacity: 0.5; } .buttons { diff --git a/src/containers/dialogs/RestoreRequestDialog/index.tsx b/src/containers/dialogs/RestoreRequestDialog/index.tsx index eb6201e7..139decdd 100644 --- a/src/containers/dialogs/RestoreRequestDialog/index.tsx +++ b/src/containers/dialogs/RestoreRequestDialog/index.tsx @@ -1,33 +1,101 @@ -import React, { FC, useState, useMemo, useCallback } from 'react'; +import React, { FC, useState, useMemo, useCallback, useEffect } 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'; +import styles from './styles.scss'; -const mapStateToProps = () => ({}); -const mapDispatchToProps = {}; +import * as AUTH_ACTIONS from '~/redux/auth/actions'; +import pick from 'ramda/es/pick'; +import { selectAuthRestore } from '~/redux/auth/selectors'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; +import { ERROR_LITERAL } from '~/constants/errors'; +import { Icon } from '~/components/input/Icon'; + +const mapStateToProps = state => ({ + restore: selectAuthRestore(state), +}); + +const mapDispatchToProps = pick(['authRequestRestoreCode', 'authSetRestore'], AUTH_ACTIONS); type IProps = IDialogProps & ReturnType & typeof mapDispatchToProps & {}; -const RestoreRequestDialogUnconnected: FC = ({}) => { +const RestoreRequestDialogUnconnected: FC = ({ + restore: { error, is_loading, is_succesfull }, + authSetRestore, + onRequestClose, + authRequestRestoreCode, +}) => { const [field, setField] = useState(); - const onSubmit = useCallback(event => { - event.preventDefault(); - }, []); + const onSubmit = useCallback( + event => { + event.preventDefault(); - const buttons = useMemo(() => , []); + if (!field) return; + + authRequestRestoreCode(field); + }, + [authRequestRestoreCode, field] + ); + + useEffect(() => { + if (error || is_succesfull) { + authSetRestore({ error: null, is_succesfull: false }); + } + }, [field]); + + const buttons = useMemo( + () => ( + + + + ), + [] + ); + + const header = useMemo(() =>
ILLUSTRATE ME
, []); + + const overlay = useMemo( + () => + is_succesfull ? ( + + + +
Проверьте почту, мы отправили на неё код
+ +
+ + + + ) : null, + [is_succesfull] + ); return ( - - - + +
+ + -
Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля.
-
+
+ Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля. +
+ +
); diff --git a/src/containers/dialogs/RestoreRequestDialog/styles.scss b/src/containers/dialogs/RestoreRequestDialog/styles.scss new file mode 100644 index 00000000..246337f9 --- /dev/null +++ b/src/containers/dialogs/RestoreRequestDialog/styles.scss @@ -0,0 +1,47 @@ +.wrap { + padding: $gap $gap $gap * 4; +} + +.buttons { + padding: $gap; +} + +.text { + font: $font_14_regular; + padding: $gap; + color: darken(white, 50%); +} + +.illustration { + min-height: 160px; + display: flex; + align-items: center; + justify-content: center; + font: $font_18_semibold; +} + +.shade { + @include outer_shadow(); + + background: $content_bg; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: $radius; + padding: $gap * 2; + box-sizing: border-box; + text-transform: uppercase; + font: $font_18_semibold; + text-align: center; + color: $wisegreen; + + svg { + fill: $wisegreen; + } +} diff --git a/src/index.tsx b/src/index.tsx index 42974b80..50ef273c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,11 @@ -import * as React from "react"; -import { render } from "react-dom"; -import { Provider } from "react-redux"; -import { PersistGate } from "redux-persist/integration/react"; -import { configureStore } from "~/redux/store"; -import App from "~/containers/App"; +import * as React from 'react'; +import { render } from 'react-dom'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { configureStore } from '~/redux/store'; +import App from '~/containers/App'; -require("./styles/main.scss"); +require('./styles/main.scss'); const { store, persistor } = configureStore(); @@ -15,12 +15,13 @@ render( , - document.getElementById("app") + document.getElementById('app') ); /* [Stage 0]: +- illustrate restoreRequestDialog - check if email is registered at social login - friendship - password restore diff --git a/src/redux/auth/actions.ts b/src/redux/auth/actions.ts index 3b005f42..3de34ed2 100644 --- a/src/redux/auth/actions.ts +++ b/src/redux/auth/actions.ts @@ -77,6 +77,11 @@ export const authPatchUser = (user: Partial) => ({ user, }); +export const authRequestRestoreCode = (field: string) => ({ + type: AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, + field, +}); + export const authSetRestore = (restore: Partial) => ({ type: AUTH_USER_ACTIONS.SET_RESTORE, restore, diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index 2c4cc485..46d6643c 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -1,17 +1,12 @@ -import { - api, - errorMiddleware, - resultMiddleware, - configWithToken -} from "~/utils/api"; -import { API } from "~/constants/api"; -import { IResultWithStatus, IMessage } from "~/redux/types"; -import { userLoginTransform } from "~/redux/auth/transforms"; -import { IUser } from "./types"; +import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api'; +import { API } from '~/constants/api'; +import { IResultWithStatus, IMessage } from '~/redux/types'; +import { userLoginTransform } from '~/redux/auth/transforms'; +import { IUser } from './types'; export const apiUserLogin = ({ username, - password + password, }: { username: string; password: string; @@ -22,9 +17,7 @@ export const apiUserLogin = ({ .catch(errorMiddleware) .then(userLoginTransform); -export const apiAuthGetUser = ({ - access -}): Promise> => +export const apiAuthGetUser = ({ access }): Promise> => api .get(API.USER.ME, configWithToken(access)) .then(resultMiddleware) @@ -32,7 +25,7 @@ export const apiAuthGetUser = ({ export const apiAuthGetUserProfile = ({ access, - username + username, }): Promise> => api .get(API.USER.PROFILE(username), configWithToken(access)) @@ -41,7 +34,7 @@ export const apiAuthGetUserProfile = ({ export const apiAuthGetUserMessages = ({ access, - username + username, }): Promise> => api .get(API.USER.MESSAGES(username), configWithToken(access)) @@ -51,7 +44,7 @@ export const apiAuthGetUserMessages = ({ export const apiAuthSendMessage = ({ access, username, - message + message, }): Promise> => api .post(API.USER.MESSAGE_SEND(username), { message }, configWithToken(access)) @@ -61,21 +54,21 @@ export const apiAuthSendMessage = ({ export const apiAuthGetUpdates = ({ access, exclude_dialogs, - last + last, }): Promise> => api - .get( - API.USER.GET_UPDATES, - configWithToken(access, { params: { exclude_dialogs, last } }) - ) + .get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } })) .then(resultMiddleware) .catch(errorMiddleware); -export const apiUpdateUser = ({ - access, - user -}): Promise> => +export const apiUpdateUser = ({ access, user }): Promise> => api .patch(API.USER.ME, { user }, configWithToken(access)) .then(resultMiddleware) .catch(errorMiddleware); + +export const apiRequestRestoreCode = ({ field }): Promise> => + api + .post(API.USER.REQUEST_CODE(), { field }) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index c22fedc8..d1556146 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -20,6 +20,7 @@ export const AUTH_USER_ACTIONS = { PATCH_USER: 'PATCH_USER', SET_RESTORE: 'SET_RESTORE', + REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE', RESTORE_PASSWORD: 'RESTORE_PASSWORD', }; diff --git a/src/redux/auth/reducer.ts b/src/redux/auth/reducer.ts index 885bb259..679042de 100644 --- a/src/redux/auth/reducer.ts +++ b/src/redux/auth/reducer.ts @@ -37,7 +37,7 @@ const INITIAL_STATE: IAuthState = { user: null, is_loading: false, is_succesfull: false, - errors: {}, + error: null, }, }; diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index 8bcf02ec..d5d58d8e 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -16,6 +16,7 @@ import { authPatchUser, authRestorePassword, authSetRestore, + authRequestRestoreCode, } from '~/redux/auth/actions'; import { apiUserLogin, @@ -25,6 +26,7 @@ import { apiAuthSendMessage, apiAuthGetUpdates, apiUpdateUser, + apiRequestRestoreCode, } from '~/redux/auth/api'; import { modalSetShown, modalShowDialog } from '~/redux/modal/actions'; import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors'; @@ -275,11 +277,26 @@ function* patchUser({ user }: ReturnType) { yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' })); } +function* requestRestoreCode({ field }: ReturnType) { + if (!field) return; + + yield put(authSetRestore({ error: null, is_loading: true })); + const { error, data } = yield call(apiRequestRestoreCode, { field }); + + console.log(data); + + if (data.error || error) { + return yield put(authSetRestore({ is_loading: false, error: data.error || error })); + } + + yield put(authSetRestore({ is_loading: false, is_succesfull: true })); +} + function* restorePassword({ code }: ReturnType) { if (!code && !code.length) { return yield put( authSetRestore({ - errors: { code: ERRORS.CODE_IS_INVALID }, + error: ERRORS.CODE_IS_INVALID, is_loading: false, }) ); @@ -300,6 +317,7 @@ function* authSaga() { 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); + yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode); } export default authSaga; diff --git a/src/redux/auth/selectors.ts b/src/redux/auth/selectors.ts index 9004dc0d..d92ba7f3 100644 --- a/src/redux/auth/selectors.ts +++ b/src/redux/auth/selectors.ts @@ -1,9 +1,10 @@ import { IState } from '~/redux/store'; -export const selectAuth = (state: IState): IState['auth'] => state.auth; -export const selectUser = (state: IState): IState['auth']['user'] => state.auth.user; -export const selectToken = (state: IState): IState['auth']['token'] => state.auth.token; -export const selectAuthLogin = (state: IState): IState['auth']['login'] => state.auth.login; -export const selectAuthProfile = (state: IState): IState['auth']['profile'] => state.auth.profile; -export const selectAuthUser = (state: IState): IState['auth']['user'] => state.auth.user; -export const selectAuthUpdates = (state: IState): IState['auth']['updates'] => state.auth.updates; +export const selectAuth = (state: IState) => state.auth; +export const selectUser = (state: IState) => state.auth.user; +export const selectToken = (state: IState) => state.auth.token; +export const selectAuthLogin = (state: IState) => state.auth.login; +export const selectAuthProfile = (state: IState) => state.auth.profile; +export const selectAuthUser = (state: IState) => state.auth.user; +export const selectAuthUpdates = (state: IState) => state.auth.updates; +export const selectAuthRestore = (state: IState) => state.auth.restore; diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts index 07fd8c5d..c3d049be 100644 --- a/src/redux/auth/types.ts +++ b/src/redux/auth/types.ts @@ -55,6 +55,6 @@ export type IAuthState = Readonly<{ user: Pick; is_loading: boolean; is_succesfull: boolean; - errors: Record; + error: string; }; }>; diff --git a/src/redux/modal/reducer.ts b/src/redux/modal/reducer.ts index be90b850..38b439b3 100644 --- a/src/redux/modal/reducer.ts +++ b/src/redux/modal/reducer.ts @@ -9,8 +9,8 @@ export interface IModalState { } const INITIAL_STATE: IModalState = { - is_shown: false, - dialog: null, + is_shown: true, + dialog: DIALOGS.RESTORE_REQUEST, }; export default createReducer(INITIAL_STATE, MODAL_HANDLERS);