From 5396cf7611b9b66db6c9bf9b34bdea14bf24855b Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Sun, 26 Jul 2020 18:31:15 +0700 Subject: [PATCH] added account list and ability to drop them --- .../profile/ProfileSettings/index.tsx | 28 ++--- .../profile/ProfileSettingsSocials/index.tsx | 63 +++++++++++ .../ProfileSettingsSocials/styles.scss | 36 +++++++ src/constants/api.ts | 2 + src/redux/auth/actions.ts | 22 +++- src/redux/auth/api.ts | 37 ++++++- src/redux/auth/constants.ts | 5 + src/redux/auth/handlers.ts | 12 ++- src/redux/auth/reducer.ts | 6 ++ src/redux/auth/sagas.ts | 102 +++++++++++++----- src/redux/auth/types.ts | 16 ++- 11 files changed, 282 insertions(+), 47 deletions(-) create mode 100644 src/components/profile/ProfileSettingsSocials/index.tsx create mode 100644 src/components/profile/ProfileSettingsSocials/styles.scss diff --git a/src/components/profile/ProfileSettings/index.tsx b/src/components/profile/ProfileSettings/index.tsx index 9339229e..15925a5d 100644 --- a/src/components/profile/ProfileSettings/index.tsx +++ b/src/components/profile/ProfileSettings/index.tsx @@ -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 mapDispatchToProps & {}; const ProfileSettingsUnconnected: FC = ({ 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 = ({ 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 = ({ комментариях. + + = ({ ); }; -const ProfileSettings = connect( - mapStateToProps, - mapDispatchToProps -)(ProfileSettingsUnconnected); +const ProfileSettings = connect(mapStateToProps, mapDispatchToProps)(ProfileSettingsUnconnected); export { ProfileSettings }; diff --git a/src/components/profile/ProfileSettingsSocials/index.tsx b/src/components/profile/ProfileSettingsSocials/index.tsx new file mode 100644 index 00000000..607394d3 --- /dev/null +++ b/src/components/profile/ProfileSettingsSocials/index.tsx @@ -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 = { + vkontakte: 'vk', + google: 'google', +}; + +const ProfileSettingsSocials: FC = ({ + authGetSocials, + authDropSocial, + accounts, + is_loading, +}) => { + useEffect(() => { + authGetSocials(); + }, [authGetSocials]); + + if (!accounts.length) return null; + + return ( +
+ {is_loading && ( +
+ {[...new Array(accounts.length || 1)].map((_, i) => ( + + + + + ))} +
+ )} + + {!is_loading && + accounts.map(it => ( +
+
+ +
+ +
{it.name}
+ +
+ authDropSocial(it.provider, it.id)} /> +
+
+ ))} +
+ ); +}; + +export { ProfileSettingsSocials }; diff --git a/src/components/profile/ProfileSettingsSocials/styles.scss b/src/components/profile/ProfileSettingsSocials/styles.scss new file mode 100644 index 00000000..6415a5d1 --- /dev/null +++ b/src/components/profile/ProfileSettingsSocials/styles.scss @@ -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; + } + } +} diff --git a/src/constants/api.ts b/src/constants/api.ts index b5e31f0f..50f4d006 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -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/', diff --git a/src/redux/auth/actions.ts b/src/redux/auth/actions.ts index c84b7b39..461d7a61 100644 --- a/src/redux/auth/actions.ts +++ b/src/redux/auth/actions.ts @@ -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) => ({ + type: AUTH_USER_ACTIONS.SET_SOCIALS, + socials, +}); diff --git a/src/redux/auth/api.ts b/src/redux/auth/api.ts index f24dd08e..434f1245 100644 --- a/src/redux/auth/api.ts +++ b/src/redux/auth/api.ts @@ -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> => api .get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } })) .then(resultMiddleware) @@ -86,3 +87,31 @@ export const apiRestoreCode = ({ code, password }): Promise> => + 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> => + api + .delete(API.USER.DROP_SOCIAL(provider, id), configWithToken(access)) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/auth/constants.ts b/src/redux/auth/constants.ts index 5f763ede..65b72686 100644 --- a/src/redux/auth/constants.ts +++ b/src/redux/auth/constants.ts @@ -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 = { diff --git a/src/redux/auth/handlers.ts b/src/redux/auth/handlers.ts index 85f12974..699ce945 100644 --- a/src/redux/auth/handlers.ts +++ b/src/redux/auth/handlers.ts @@ -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 { (state: IAuthState, payload: T extends (...args: any[]) => infer R ? R : any): IAuthState; @@ -65,6 +64,16 @@ const setRestore: ActionHandler = (state, ...restore, }, }); +const setSocials: ActionHandler = (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, }; diff --git a/src/redux/auth/reducer.ts b/src/redux/auth/reducer.ts index 9b05e13c..f8ebbb11 100644 --- a/src/redux/auth/reducer.ts +++ b/src/redux/auth/reducer.ts @@ -31,6 +31,12 @@ const INITIAL_STATE: IAuthState = { messages: [], messages_error: null, patch_errors: {}, + + socials: { + accounts: [], + error: '', + is_loading: false, + }, }, restore: { diff --git a/src/redux/auth/sagas.ts b/src/redux/auth/sagas.ts index a368c1da..aab1395c 100644 --- a/src/redux/auth/sagas.ts +++ b/src/redux/auth/sagas.ts @@ -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) yield call(refreshUser); } +function* getSocials() { + yield put(authSetSocials({ is_loading: true, error: '' })); + + try { + const { data, error }: Unwrap> = 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) { + try { + yield put(authSetSocials({ error: '' })); + const { error }: Unwrap> = 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; diff --git a/src/redux/auth/types.ts b/src/redux/auth/types.ts index 98f312f4..40f7f4ba 100644 --- a/src/redux/auth/types.ts +++ b/src/redux/auth/types.ts @@ -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; + + socials: { + accounts: ISocialAccount[]; + error: string; + is_loading: boolean; + }; }; restore: {