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

added account list and ability to drop them

This commit is contained in:
Fedor Katurov 2020-07-26 18:31:15 +07:00
parent 2388a7e20e
commit 5396cf7611
11 changed files with 282 additions and 47 deletions

View file

@ -1,17 +1,16 @@
import React, { FC, useState, useEffect, useCallback } from 'react';
import React, { FC, useCallback, useEffect, useState } from 'react';
import styles from './styles.scss';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { selectAuthUser, selectAuthProfile } from '~/redux/auth/selectors';
import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors';
import { Textarea } from '~/components/input/Textarea';
import { Button } from '~/components/input/Button';
import { Group } from '~/components/containers/Group';
import { Filler } from '~/components/containers/Filler';
import { TextInput } from '~/components/input/TextInput';
import { InputText } from '~/components/input/InputText';
import reject from 'ramda/es/reject';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { ERROR_LITERAL } from '~/constants/errors';
import { ProfileSettingsSocials } from '~/components/profile/ProfileSettingsSocials';
const mapStateToProps = state => ({
user: selectAuthUser(state),
@ -21,15 +20,19 @@ const mapStateToProps = state => ({
const mapDispatchToProps = {
authPatchUser: AUTH_ACTIONS.authPatchUser,
authSetProfile: AUTH_ACTIONS.authSetProfile,
authGetSocials: AUTH_ACTIONS.authGetSocials,
authDropSocial: AUTH_ACTIONS.authDropSocial,
};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const ProfileSettingsUnconnected: FC<IProps> = ({
user,
profile: { patch_errors },
profile: { patch_errors, socials },
authPatchUser,
authSetProfile,
authGetSocials,
authDropSocial,
}) => {
const [password, setPassword] = useState('');
const [new_password, setNewPassword] = useState('');
@ -40,11 +43,8 @@ const ProfileSettingsUnconnected: FC<IProps> = ({
data,
setData,
]);
const setEmail = useCallback(email => setData({ ...data, email }), [data, setData]);
const setUsername = useCallback(username => setData({ ...data, username }), [data, setData]);
const setFullname = useCallback(fullname => setData({ ...data, fullname }), [data, setData]);
const onSubmit = useCallback(
@ -88,6 +88,13 @@ const ProfileSettingsUnconnected: FC<IProps> = ({
комментариях.
</div>
<ProfileSettingsSocials
accounts={socials.accounts}
is_loading={socials.is_loading}
authGetSocials={authGetSocials}
authDropSocial={authDropSocial}
/>
<Group className={styles.pad}>
<InputText
value={data.username}
@ -135,9 +142,6 @@ const ProfileSettingsUnconnected: FC<IProps> = ({
);
};
const ProfileSettings = connect(
mapStateToProps,
mapDispatchToProps
)(ProfileSettingsUnconnected);
const ProfileSettings = connect(mapStateToProps, mapDispatchToProps)(ProfileSettingsUnconnected);
export { ProfileSettings };

View file

@ -0,0 +1,63 @@
import React, { FC, useEffect, Fragment } from 'react';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import { IAuthState, ISocialProvider } from '~/redux/auth/types';
import styles from './styles.scss';
import { Placeholder } from '~/components/placeholders/Placeholder';
import { Icon } from '~/components/input/Icon';
interface IProps {
accounts: IAuthState['profile']['socials']['accounts'];
is_loading: boolean;
authGetSocials: typeof AUTH_ACTIONS.authGetSocials;
authDropSocial: typeof AUTH_ACTIONS.authDropSocial;
}
const SOCIAL_ICONS: Record<ISocialProvider, string> = {
vkontakte: 'vk',
google: 'google',
};
const ProfileSettingsSocials: FC<IProps> = ({
authGetSocials,
authDropSocial,
accounts,
is_loading,
}) => {
useEffect(() => {
authGetSocials();
}, [authGetSocials]);
if (!accounts.length) return null;
return (
<div className={styles.wrap}>
{is_loading && (
<div className={styles.loader}>
{[...new Array(accounts.length || 1)].map((_, i) => (
<Fragment key={i}>
<Placeholder width="50%" />
<Placeholder width="auto" />
</Fragment>
))}
</div>
)}
{!is_loading &&
accounts.map(it => (
<div className={styles.account}>
<div className={styles.account__provider}>
<Icon icon={SOCIAL_ICONS[it.provider]} />
</div>
<div className={styles.account__name}>{it.name}</div>
<div className={styles.account__drop}>
<Icon icon="close" size={22} onClick={() => authDropSocial(it.provider, it.id)} />
</div>
</div>
))}
</div>
);
};
export { ProfileSettingsSocials };

View file

@ -0,0 +1,36 @@
.wrap {
padding: $gap,
}
.loader {
display: grid;
grid-row-gap: $gap;
grid-column-gap: $gap * 4;
grid-template-columns: 1fr 32px;
& > div {
height: 22px;
width: auto;
}
}
.account {
display: grid;
grid-template-columns: 20px auto 20px;
grid-column-gap: $gap;
&__name {
font: $font_16_semibold;
}
&__drop {
cursor: pointer;
opacity: 0.5;
transition: opacity 0.25s;
fill: $red;
&:hover {
opacity: 1;
}
}
}

View file

@ -12,6 +12,8 @@ export const API = {
GET_UPDATES: '/user/updates',
REQUEST_CODE: (code?: string) => `/user/restore/${code || ''}`,
UPLOAD: (target, type) => `/upload/${target}/${type}`,
GET_SOCIALS: '/oauth/',
DROP_SOCIAL: (provider, id) => `/oauth/${provider}/${id}`,
},
NODE: {
SAVE: '/node/',

View file

@ -1,5 +1,5 @@
import { AUTH_USER_ACTIONS } from '~/redux/auth/constants';
import { IAuthState, IUser } from '~/redux/auth/types';
import { IAuthState, ISocialProvider, IUser } from '~/redux/auth/types';
import { IMessage } from '../types';
export const userSendLoginRequest = ({
@ -101,3 +101,23 @@ export const authRestorePassword = (password: string) => ({
type: AUTH_USER_ACTIONS.RESTORE_PASSWORD,
password,
});
export const authGetSocials = () => ({
type: AUTH_USER_ACTIONS.GET_SOCIALS,
});
export const authAddSocial = (provider: ISocialProvider) => ({
type: AUTH_USER_ACTIONS.ADD_SOCIAL,
provider,
});
export const authDropSocial = (provider: string, id: string) => ({
type: AUTH_USER_ACTIONS.DROP_SOCIAL,
provider,
id,
});
export const authSetSocials = (socials: Partial<IAuthState['profile']['socials']>) => ({
type: AUTH_USER_ACTIONS.SET_SOCIALS,
socials,
});

View file

@ -2,7 +2,7 @@ import { api, errorMiddleware, resultMiddleware, configWithToken } from '~/utils
import { API } from '~/constants/api';
import { IResultWithStatus, IMessage, INotification } from '~/redux/types';
import { userLoginTransform } from '~/redux/auth/transforms';
import { IUser } from './types';
import { ISocialAccount, IUser } from './types';
export const apiUserLogin = ({
username,
@ -55,9 +55,10 @@ export const apiAuthGetUpdates = ({
access,
exclude_dialogs,
last,
}): Promise<
IResultWithStatus<{ notifications: INotification[]; boris: { commented_at: string } }>
> =>
}): Promise<IResultWithStatus<{
notifications: INotification[];
boris: { commented_at: string };
}>> =>
api
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } }))
.then(resultMiddleware)
@ -86,3 +87,31 @@ export const apiRestoreCode = ({ code, password }): Promise<IResultWithStatus<{}
.post(API.USER.REQUEST_CODE(code), { password })
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiGetSocials = ({
access,
}: {
access: string;
}): Promise<IResultWithStatus<{
accounts: ISocialAccount[];
}>> =>
api
.get(API.USER.GET_SOCIALS, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const apiDropSocial = ({
access,
id,
provider,
}: {
access: string;
id: string;
provider: string;
}): Promise<IResultWithStatus<{
accounts: ISocialAccount[];
}>> =>
api
.delete(API.USER.DROP_SOCIAL(provider, id), configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);

View file

@ -24,6 +24,11 @@ export const AUTH_USER_ACTIONS = {
REQUEST_RESTORE_CODE: 'REQUEST_RESTORE_CODE',
SHOW_RESTORE_MODAL: 'SHOW_RESTORE_MODAL',
RESTORE_PASSWORD: 'RESTORE_PASSWORD',
GET_SOCIALS: 'GET_SOCIALS',
DROP_SOCIAL: 'DROP_SOCIAL',
ADD_SOCIAL: 'ADD_SOCIAL',
SET_SOCIALS: 'SET_SOCIALS',
};
export const USER_ERRORS = {

View file

@ -1,7 +1,6 @@
import { AUTH_USER_ACTIONS } from '~/redux/auth/constants';
import * as ActionCreators from '~/redux/auth/actions';
import { IAuthState } from '~/redux/auth/types';
import { Action } from 'history';
interface ActionHandler<T> {
(state: IAuthState, payload: T extends (...args: any[]) => infer R ? R : any): IAuthState;
@ -65,6 +64,16 @@ const setRestore: ActionHandler<typeof ActionCreators.authSetRestore> = (state,
...restore,
},
});
const setSocials: ActionHandler<typeof ActionCreators.authSetSocials> = (state, { socials }) => ({
...state,
profile: {
...state.profile,
socials: {
...state.profile.socials,
...socials,
},
},
});
export const AUTH_USER_HANDLERS = {
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
@ -74,4 +83,5 @@ export const AUTH_USER_HANDLERS = {
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
[AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES]: setLastSeenMessages,
[AUTH_USER_ACTIONS.SET_RESTORE]: setRestore,
[AUTH_USER_ACTIONS.SET_SOCIALS]: setSocials,
};

View file

@ -31,6 +31,12 @@ const INITIAL_STATE: IAuthState = {
messages: [],
messages_error: null,
patch_errors: {},
socials: {
accounts: [],
error: '',
is_loading: false,
},
},
restore: {

View file

@ -1,48 +1,53 @@
import { call, put, takeEvery, takeLatest, select, delay } from 'redux-saga/effects';
import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ERRORS, USER_ROLES } from '~/redux/auth/constants';
import {
authSetToken,
userSetLoginError,
authSetUser,
userSendLoginRequest,
gotAuthPostMessage,
authOpenProfile,
authSetProfile,
authDropSocial,
authGetMessages,
authSendMessage,
authSetUpdates,
authGetSocials,
authLoadProfile,
authLoggedIn,
authSetLastSeenMessages,
authOpenProfile,
authPatchUser,
authShowRestoreModal,
authSetRestore,
authRequestRestoreCode,
authRestorePassword,
authLoadProfile,
authSendMessage,
authSetLastSeenMessages,
authSetProfile,
authSetRestore,
authSetSocials,
authSetToken,
authSetUpdates,
authSetUser,
authShowRestoreModal,
gotAuthPostMessage,
userSendLoginRequest,
userSetLoginError,
} from '~/redux/auth/actions';
import {
apiUserLogin,
apiAuthGetUser,
apiAuthGetUserProfile,
apiAuthGetUserMessages,
apiAuthSendMessage,
apiAuthGetUpdates,
apiUpdateUser,
apiRequestRestoreCode,
apiAuthGetUser,
apiAuthGetUserMessages,
apiAuthGetUserProfile,
apiAuthSendMessage,
apiCheckRestoreCode,
apiDropSocial,
apiGetSocials,
apiRequestRestoreCode,
apiRestoreCode,
apiUpdateUser,
apiUserLogin,
} from '~/redux/auth/api';
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
import {
selectToken,
selectAuthProfile,
selectAuthUser,
selectAuthUpdates,
selectAuthRestore,
selectAuth,
selectAuthProfile,
selectAuthRestore,
selectAuthUpdates,
selectAuthUser,
selectToken,
} from './selectors';
import { IResultWithStatus, INotification, IMessageNotification, Unwrap } from '../types';
import { IUser, IAuthState } from './types';
import { IMessageNotification, IResultWithStatus, Unwrap } from '../types';
import { IAuthState, IUser } from './types';
import { REHYDRATE, RehydrateAction } from 'redux-persist';
import { selectModal } from '~/redux/modal/selectors';
import { IModalState } from '~/redux/modal/reducer';
@ -372,6 +377,45 @@ function* restorePassword({ password }: ReturnType<typeof authRestorePassword>)
yield call(refreshUser);
}
function* getSocials() {
yield put(authSetSocials({ is_loading: true, error: '' }));
try {
const { data, error }: Unwrap<ReturnType<typeof apiGetSocials>> = yield call(
reqWrapper,
apiGetSocials,
{}
);
if (error) {
throw new Error(error);
}
yield put(authSetSocials({ is_loading: false, accounts: data.accounts, error: '' }));
} catch (e) {
yield put(authSetSocials({ is_loading: false, error: e.toString() }));
}
}
function* dropSocial({ provider, id }: ReturnType<typeof authDropSocial>) {
try {
yield put(authSetSocials({ error: '' }));
const { error }: Unwrap<ReturnType<typeof apiDropSocial>> = yield call(
reqWrapper,
apiDropSocial,
{ id, provider }
);
if (error) {
throw new Error(error);
}
yield call(getSocials);
} catch (e) {
yield put(authSetSocials({ error: e.toString() }));
}
}
function* authSaga() {
yield takeEvery(REHYDRATE, checkUserSaga);
yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga);
@ -388,6 +432,8 @@ function* authSaga() {
yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode);
yield takeLatest(AUTH_USER_ACTIONS.SHOW_RESTORE_MODAL, showRestoreModal);
yield takeLatest(AUTH_USER_ACTIONS.RESTORE_PASSWORD, restorePassword);
yield takeLatest(AUTH_USER_ACTIONS.GET_SOCIALS, getSocials);
yield takeLatest(AUTH_USER_ACTIONS.DROP_SOCIAL, dropSocial);
}
export default authSaga;

View file

@ -24,6 +24,15 @@ export interface IUser {
is_user: boolean;
}
export type ISocialProvider = 'vkontakte' | 'google';
export interface ISocialAccount {
provider: ISocialProvider;
id: string;
name: string;
photo: string;
}
export type IAuthState = Readonly<{
user: IUser;
token: string;
@ -48,8 +57,13 @@ export type IAuthState = Readonly<{
user: IUser;
messages: IMessage[];
messages_error: string;
patch_errors: Record<string, string>;
socials: {
accounts: ISocialAccount[];
error: string;
is_loading: boolean;
};
};
restore: {