1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

added request code dialog

This commit is contained in:
Fedor Katurov 2019-11-25 16:08:22 +07:00
parent c0c832d158
commit 6dcb21e9e4
18 changed files with 292 additions and 88 deletions

View file

@ -9,6 +9,7 @@ type IButtonProps = DetailedHTMLProps<
HTMLButtonElement HTMLButtonElement
> & { > & {
size?: 'mini' | 'normal' | 'big' | 'giant' | 'micro' | 'small'; size?: 'mini' | 'normal' | 'big' | 'giant' | 'micro' | 'small';
color?: 'primary' | 'secondary' | 'outline' | 'link';
iconLeft?: IIcon; iconLeft?: IIcon;
iconRight?: IIcon; iconRight?: IIcon;
seamless?: boolean; seamless?: boolean;
@ -25,6 +26,7 @@ type IButtonProps = DetailedHTMLProps<
const Button: FC<IButtonProps> = memo( const Button: FC<IButtonProps> = memo(
({ ({
className = '', className = '',
color = 'primary',
size = 'normal', size = 'normal',
iconLeft, iconLeft,
iconRight, iconRight,
@ -44,7 +46,7 @@ const Button: FC<IButtonProps> = memo(
createElement( createElement(
seamless || non_submitting ? 'div' : 'button', seamless || non_submitting ? 'div' : 'button',
{ {
className: classnames(styles.button, className, styles[size], { className: classnames(styles.button, className, styles[size], styles[color], {
red, red,
grey, grey,
seamless, seamless,

View file

@ -143,6 +143,34 @@
padding-right: $gap; 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; margin: 0 5px;

View file

@ -1,29 +1,30 @@
import { INode } from "~/redux/types"; import { INode } from '~/redux/types';
export const API = { export const API = {
BASE: process.env.API_HOST, BASE: process.env.API_HOST,
USER: { USER: {
LOGIN: "/user/login", LOGIN: '/user/login',
VKONTAKTE_LOGIN: `${process.env.API_HOST}/user/vkontakte`, VKONTAKTE_LOGIN: `${process.env.API_HOST}/user/vkontakte`,
ME: "/user/", ME: '/user/',
PROFILE: (username: string) => `/user/${username}/profile`, PROFILE: (username: string) => `/user/${username}/profile`,
MESSAGES: (username: string) => `/user/${username}/messages`, MESSAGES: (username: string) => `/user/${username}/messages`,
MESSAGE_SEND: (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: { NODE: {
SAVE: "/node/", SAVE: '/node/',
GET: "/node/", GET: '/node/',
GET_DIFF: "/node/diff", GET_DIFF: '/node/diff',
GET_NODE: (id: number | string) => `/node/${id}`, GET_NODE: (id: number | string) => `/node/${id}`,
COMMENT: (id: INode["id"]) => `/node/${id}/comment`, COMMENT: (id: INode['id']) => `/node/${id}/comment`,
RELATED: (id: INode["id"]) => `/node/${id}/related`, RELATED: (id: INode['id']) => `/node/${id}/related`,
UPDATE_TAGS: (id: INode["id"]) => `/node/${id}/tags`, UPDATE_TAGS: (id: INode['id']) => `/node/${id}/tags`,
POST_LIKE: (id: INode["id"]) => `/node/${id}/like`, POST_LIKE: (id: INode['id']) => `/node/${id}/like`,
POST_STAR: (id: INode["id"]) => `/node/${id}/heroic`, POST_STAR: (id: INode['id']) => `/node/${id}/heroic`,
SET_CELL_VIEW: (id: INode["id"]) => `/node/${id}/cell-view` SET_CELL_VIEW: (id: INode['id']) => `/node/${id}/cell-view`,
} },
}; };

View file

@ -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 * as styles from './styles.scss';
import { enableBodyScroll, disableBodyScroll } from 'body-scroll-lock'; import { enableBodyScroll, disableBodyScroll } from 'body-scroll-lock';
import { Icon } from '~/components/input/Icon'; import { Icon } from '~/components/input/Icon';
import { LoaderCircle } from '~/components/input/LoaderCircle';
interface IProps { interface IProps {
children: React.ReactChild; children: React.ReactChild;
@ -11,6 +12,8 @@ interface IProps {
size?: 'medium' | 'big'; size?: 'medium' | 'big';
width?: number; width?: number;
error?: string; error?: string;
is_loading?: boolean;
overlay?: ReactElement;
onOverlayClick?: MouseEventHandler<HTMLDivElement>; onOverlayClick?: MouseEventHandler<HTMLDivElement>;
onRefCapture?: (ref: any) => void; onRefCapture?: (ref: any) => void;
@ -25,6 +28,8 @@ const BetterScrollDialog: FC<IProps> = ({
width = 600, width = 600,
error, error,
onClose, onClose,
is_loading,
overlay = null,
}) => { }) => {
const ref = useRef(null); const ref = useRef(null);
@ -51,7 +56,15 @@ const BetterScrollDialog: FC<IProps> = ({
{error && <div className={styles.error}>{error}</div>} {error && <div className={styles.error}>{error}</div>}
</div> </div>
{footer && <div className={styles.header}>{footer}</div>} {!!is_loading && (
<div className={styles.shade}>
<LoaderCircle size={48} />
</div>
)}
{overlay}
{footer && <div className={styles.footer}>{footer}</div>}
</div> </div>
</div> </div>
); );

View file

@ -109,3 +109,31 @@
width: 100%; width: 100%;
height: 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;
}
}

View file

@ -67,12 +67,7 @@ const LoginDialogUnconnected: FC<IProps> = ({
const buttons = useMemo( const buttons = useMemo(
() => ( () => (
<Group className={styles.footer}> <Group className={styles.footer}>
<Button <Button color="outline" iconLeft="vk" type="button" onClick={onSocialLogin}>
className={styles.secondary_button}
iconLeft="vk"
type="button"
onClick={onSocialLogin}
>
<span>Вконтакте</span> <span>Вконтакте</span>
</Button> </Button>
@ -88,7 +83,7 @@ const LoginDialogUnconnected: FC<IProps> = ({
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<BetterScrollDialog width={260} error={error} onClose={onRequestClose} footer={buttons}> <BetterScrollDialog width={300} error={error} onClose={onRequestClose} footer={buttons}>
<Padder> <Padder>
<div className={styles.wrap}> <div className={styles.wrap}>
<Group> <Group>
@ -98,7 +93,12 @@ const LoginDialogUnconnected: FC<IProps> = ({
<InputText title="Пароль" handler={setPassword} value={password} type="password" /> <InputText title="Пароль" handler={setPassword} value={password} type="password" />
<Button className={styles.forgot_button} type="button" onClick={onRestoreRequest}> <Button
color="link"
type="button"
onClick={onRestoreRequest}
className={styles.forgot_button}
>
Вспомнить пароль Вспомнить пароль
</Button> </Button>
</Group> </Group>

View file

@ -28,9 +28,7 @@ $vk_color: $secondary_color;
} }
.forgot_button { .forgot_button {
background: $content_bg; opacity: 0.5;
box-shadow: none;
color: $secondary_color;
} }
.buttons { .buttons {

View file

@ -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 { IDialogProps } from '~/redux/types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { BetterScrollDialog } from '../BetterScrollDialog'; import { BetterScrollDialog } from '../BetterScrollDialog';
import { Group } from '~/components/containers/Group'; import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText'; import { InputText } from '~/components/input/InputText';
import { Button } from '~/components/input/Button'; import { Button } from '~/components/input/Button';
import styles from './styles.scss';
const mapStateToProps = () => ({}); import * as AUTH_ACTIONS from '~/redux/auth/actions';
const mapDispatchToProps = {}; 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 mapStateToProps> & typeof mapDispatchToProps & {}; type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const RestoreRequestDialogUnconnected: FC<IProps> = ({}) => { const RestoreRequestDialogUnconnected: FC<IProps> = ({
restore: { error, is_loading, is_succesfull },
authSetRestore,
onRequestClose,
authRequestRestoreCode,
}) => {
const [field, setField] = useState(); const [field, setField] = useState();
const onSubmit = useCallback(event => { const onSubmit = useCallback(
event => {
event.preventDefault(); event.preventDefault();
}, []);
const buttons = useMemo(() => <Button>Восстановить</Button>, []); if (!field) return;
authRequestRestoreCode(field);
},
[authRequestRestoreCode, field]
);
useEffect(() => {
if (error || is_succesfull) {
authSetRestore({ error: null, is_succesfull: false });
}
}, [field]);
const buttons = useMemo(
() => (
<Group className={styles.buttons}>
<Button>Восстановить</Button>
</Group>
),
[]
);
const header = useMemo(() => <div className={styles.illustration}>ILLUSTRATE ME</div>, []);
const overlay = useMemo(
() =>
is_succesfull ? (
<Group className={styles.shade}>
<Icon icon="check" size={64} />
<div>Проверьте почту, мы отправили на неё код</div>
<div />
<Button color="secondary" onClick={onRequestClose}>
Отлично!
</Button>
</Group>
) : null,
[is_succesfull]
);
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<BetterScrollDialog footer={buttons}> <BetterScrollDialog
header={header}
footer={buttons}
width={300}
onClose={onRequestClose}
is_loading={is_loading}
error={error && ERROR_LITERAL[error]}
overlay={overlay}
>
<div className={styles.wrap}>
<Group> <Group>
<InputText title="Имя или email" value={field} handler={setField} /> <InputText title="Имя или email" value={field} handler={setField} autoFocus />
<div>Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля.</div> <div className={styles.text}>
Введите имя пользователя или адрес почты. Мы пришлем ссылку для сброса пароля.
</div>
</Group> </Group>
</div>
</BetterScrollDialog> </BetterScrollDialog>
</form> </form>
); );

View file

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

View file

@ -1,11 +1,11 @@
import * as React from "react"; import * as React from 'react';
import { render } from "react-dom"; import { render } from 'react-dom';
import { Provider } from "react-redux"; import { Provider } from 'react-redux';
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from 'redux-persist/integration/react';
import { configureStore } from "~/redux/store"; import { configureStore } from '~/redux/store';
import App from "~/containers/App"; import App from '~/containers/App';
require("./styles/main.scss"); require('./styles/main.scss');
const { store, persistor } = configureStore(); const { store, persistor } = configureStore();
@ -15,12 +15,13 @@ render(
<App /> <App />
</PersistGate> </PersistGate>
</Provider>, </Provider>,
document.getElementById("app") document.getElementById('app')
); );
/* /*
[Stage 0]: [Stage 0]:
- illustrate restoreRequestDialog
- check if email is registered at social login - check if email is registered at social login
- friendship - friendship
- password restore - password restore

View file

@ -77,6 +77,11 @@ export const authPatchUser = (user: Partial<IUser>) => ({
user, user,
}); });
export const authRequestRestoreCode = (field: string) => ({
type: AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE,
field,
});
export const authSetRestore = (restore: Partial<IAuthState['restore']>) => ({ export const authSetRestore = (restore: Partial<IAuthState['restore']>) => ({
type: AUTH_USER_ACTIONS.SET_RESTORE, type: AUTH_USER_ACTIONS.SET_RESTORE,
restore, restore,

View file

@ -1,17 +1,12 @@
import { import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils/api';
api, import { API } from '~/constants/api';
errorMiddleware, import { IResultWithStatus, IMessage } from '~/redux/types';
resultMiddleware, import { userLoginTransform } from '~/redux/auth/transforms';
configWithToken import { IUser } from './types';
} 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 = ({ export const apiUserLogin = ({
username, username,
password password,
}: { }: {
username: string; username: string;
password: string; password: string;
@ -22,9 +17,7 @@ export const apiUserLogin = ({
.catch(errorMiddleware) .catch(errorMiddleware)
.then(userLoginTransform); .then(userLoginTransform);
export const apiAuthGetUser = ({ export const apiAuthGetUser = ({ access }): Promise<IResultWithStatus<{ user: IUser }>> =>
access
}): Promise<IResultWithStatus<{ user: IUser }>> =>
api api
.get(API.USER.ME, configWithToken(access)) .get(API.USER.ME, configWithToken(access))
.then(resultMiddleware) .then(resultMiddleware)
@ -32,7 +25,7 @@ export const apiAuthGetUser = ({
export const apiAuthGetUserProfile = ({ export const apiAuthGetUserProfile = ({
access, access,
username username,
}): Promise<IResultWithStatus<{ user: IUser }>> => }): Promise<IResultWithStatus<{ user: IUser }>> =>
api api
.get(API.USER.PROFILE(username), configWithToken(access)) .get(API.USER.PROFILE(username), configWithToken(access))
@ -41,7 +34,7 @@ export const apiAuthGetUserProfile = ({
export const apiAuthGetUserMessages = ({ export const apiAuthGetUserMessages = ({
access, access,
username username,
}): Promise<IResultWithStatus<{ messages: IMessage[] }>> => }): Promise<IResultWithStatus<{ messages: IMessage[] }>> =>
api api
.get(API.USER.MESSAGES(username), configWithToken(access)) .get(API.USER.MESSAGES(username), configWithToken(access))
@ -51,7 +44,7 @@ export const apiAuthGetUserMessages = ({
export const apiAuthSendMessage = ({ export const apiAuthSendMessage = ({
access, access,
username, username,
message message,
}): Promise<IResultWithStatus<{ message: IMessage }>> => }): Promise<IResultWithStatus<{ message: IMessage }>> =>
api api
.post(API.USER.MESSAGE_SEND(username), { message }, configWithToken(access)) .post(API.USER.MESSAGE_SEND(username), { message }, configWithToken(access))
@ -61,21 +54,21 @@ export const apiAuthSendMessage = ({
export const apiAuthGetUpdates = ({ export const apiAuthGetUpdates = ({
access, access,
exclude_dialogs, exclude_dialogs,
last last,
}): Promise<IResultWithStatus<{ message: IMessage }>> => }): Promise<IResultWithStatus<{ message: IMessage }>> =>
api api
.get( .get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } }))
API.USER.GET_UPDATES,
configWithToken(access, { params: { exclude_dialogs, last } })
)
.then(resultMiddleware) .then(resultMiddleware)
.catch(errorMiddleware); .catch(errorMiddleware);
export const apiUpdateUser = ({ export const apiUpdateUser = ({ access, user }): Promise<IResultWithStatus<{ user: IUser }>> =>
access,
user
}): Promise<IResultWithStatus<{ user: IUser }>> =>
api api
.patch(API.USER.ME, { user }, configWithToken(access)) .patch(API.USER.ME, { user }, configWithToken(access))
.then(resultMiddleware) .then(resultMiddleware)
.catch(errorMiddleware); .catch(errorMiddleware);
export const apiRequestRestoreCode = ({ field }): Promise<IResultWithStatus<{}>> =>
api
.post(API.USER.REQUEST_CODE(), { field })
.then(resultMiddleware)
.catch(errorMiddleware);

View file

@ -20,6 +20,7 @@ export const AUTH_USER_ACTIONS = {
PATCH_USER: 'PATCH_USER', PATCH_USER: 'PATCH_USER',
SET_RESTORE: 'SET_RESTORE', SET_RESTORE: 'SET_RESTORE',
REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE',
RESTORE_PASSWORD: 'RESTORE_PASSWORD', RESTORE_PASSWORD: 'RESTORE_PASSWORD',
}; };

View file

@ -37,7 +37,7 @@ const INITIAL_STATE: IAuthState = {
user: null, user: null,
is_loading: false, is_loading: false,
is_succesfull: false, is_succesfull: false,
errors: {}, error: null,
}, },
}; };

View file

@ -16,6 +16,7 @@ import {
authPatchUser, authPatchUser,
authRestorePassword, authRestorePassword,
authSetRestore, authSetRestore,
authRequestRestoreCode,
} from '~/redux/auth/actions'; } from '~/redux/auth/actions';
import { import {
apiUserLogin, apiUserLogin,
@ -25,6 +26,7 @@ import {
apiAuthSendMessage, apiAuthSendMessage,
apiAuthGetUpdates, apiAuthGetUpdates,
apiUpdateUser, apiUpdateUser,
apiRequestRestoreCode,
} from '~/redux/auth/api'; } from '~/redux/auth/api';
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions'; import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors'; import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors';
@ -275,11 +277,26 @@ function* patchUser({ user }: ReturnType<typeof authPatchUser>) {
yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' })); yield put(authSetProfile({ user: { ...me, ...data.user }, tab: 'profile' }));
} }
function* requestRestoreCode({ field }: ReturnType<typeof authRequestRestoreCode>) {
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<typeof authRestorePassword>) { function* restorePassword({ code }: ReturnType<typeof authRestorePassword>) {
if (!code && !code.length) { if (!code && !code.length) {
return yield put( return yield put(
authSetRestore({ authSetRestore({
errors: { code: ERRORS.CODE_IS_INVALID }, error: ERRORS.CODE_IS_INVALID,
is_loading: false, is_loading: false,
}) })
); );
@ -300,6 +317,7 @@ function* authSaga() {
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.PATCH_USER, patchUser);
yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword); yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword);
yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode);
} }
export default authSaga; export default authSaga;

View file

@ -1,9 +1,10 @@
import { IState } from '~/redux/store'; import { IState } from '~/redux/store';
export const selectAuth = (state: IState): IState['auth'] => state.auth; export const selectAuth = (state: IState) => state.auth;
export const selectUser = (state: IState): IState['auth']['user'] => state.auth.user; export const selectUser = (state: IState) => state.auth.user;
export const selectToken = (state: IState): IState['auth']['token'] => state.auth.token; export const selectToken = (state: IState) => state.auth.token;
export const selectAuthLogin = (state: IState): IState['auth']['login'] => state.auth.login; export const selectAuthLogin = (state: IState) => state.auth.login;
export const selectAuthProfile = (state: IState): IState['auth']['profile'] => state.auth.profile; export const selectAuthProfile = (state: IState) => state.auth.profile;
export const selectAuthUser = (state: IState): IState['auth']['user'] => state.auth.user; export const selectAuthUser = (state: IState) => state.auth.user;
export const selectAuthUpdates = (state: IState): IState['auth']['updates'] => state.auth.updates; export const selectAuthUpdates = (state: IState) => state.auth.updates;
export const selectAuthRestore = (state: IState) => state.auth.restore;

View file

@ -55,6 +55,6 @@ export type IAuthState = Readonly<{
user: Pick<IUser, 'username' | 'photo'>; user: Pick<IUser, 'username' | 'photo'>;
is_loading: boolean; is_loading: boolean;
is_succesfull: boolean; is_succesfull: boolean;
errors: Record<string, string>; error: string;
}; };
}>; }>;

View file

@ -9,8 +9,8 @@ export interface IModalState {
} }
const INITIAL_STATE: IModalState = { const INITIAL_STATE: IModalState = {
is_shown: false, is_shown: true,
dialog: null, dialog: DIALOGS.RESTORE_REQUEST,
}; };
export default createReducer(INITIAL_STATE, MODAL_HANDLERS); export default createReducer(INITIAL_STATE, MODAL_HANDLERS);