mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
user settings mechanism
This commit is contained in:
parent
5fe0deca17
commit
a90285a4ac
10 changed files with 162 additions and 4 deletions
|
@ -30,7 +30,6 @@ type IProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
|
|
||||||
const Textarea = memo<IProps>(
|
const Textarea = memo<IProps>(
|
||||||
({
|
({
|
||||||
value,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
minRows = 3,
|
minRows = 3,
|
||||||
|
@ -40,6 +39,7 @@ const Textarea = memo<IProps>(
|
||||||
title = '',
|
title = '',
|
||||||
status = '',
|
status = '',
|
||||||
seamless,
|
seamless,
|
||||||
|
value,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [rows, setRows] = useState(minRows || 1);
|
const [rows, setRows] = useState(minRows || 1);
|
||||||
|
@ -97,7 +97,7 @@ const Textarea = memo<IProps>(
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={rows}
|
rows={rows}
|
||||||
value={value}
|
value={value || ''}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={classNames(styles.textarea, className)}
|
className={classNames(styles.textarea, className)}
|
||||||
onChange={onInput}
|
onChange={onInput}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { IUser } from '~/redux/auth/types';
|
|
||||||
import { formatText } from '~/utils/dom';
|
import { formatText } from '~/utils/dom';
|
||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
121
src/components/profile/ProfileSettings/index.tsx
Normal file
121
src/components/profile/ProfileSettings/index.tsx
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import React, { FC, useState, useEffect, useCallback } from 'react';
|
||||||
|
import styles from './styles.scss';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { selectAuthUser, selectAuthProfile } 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';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
user: selectAuthUser(state),
|
||||||
|
profile: selectAuthProfile(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
authPatchUser: AUTH_ACTIONS.authPatchUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||||
|
|
||||||
|
const ProfileSettingsUnconnected: FC<IProps> = ({
|
||||||
|
user,
|
||||||
|
authPatchUser,
|
||||||
|
profile: { patch_errors },
|
||||||
|
}) => {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [new_password, setNewPassword] = useState('');
|
||||||
|
|
||||||
|
const [data, setData] = useState(user);
|
||||||
|
|
||||||
|
const setDescription = useCallback(description => setData({ ...data, description }), [
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const setEmail = useCallback(email => setData({ ...data, email }), [data, setData]);
|
||||||
|
const setUsername = useCallback(username => setData({ ...data, username }), [data, setData]);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const fields = reject(el => !el)({
|
||||||
|
email: data.email !== user.email && data.email,
|
||||||
|
username: data.username !== user.username && data.username,
|
||||||
|
password: password.length > 0 && password,
|
||||||
|
new_password: new_password.length > 0 && new_password,
|
||||||
|
description: data.description !== user.description && data.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.values(fields).length === 0) return;
|
||||||
|
|
||||||
|
authPatchUser(fields);
|
||||||
|
},
|
||||||
|
[data, password, new_password, authPatchUser]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.wrap} onSubmit={onSubmit}>
|
||||||
|
<Group>
|
||||||
|
<Textarea value={data.description} handler={setDescription} title="Описание" />
|
||||||
|
|
||||||
|
<div className={styles.small}>
|
||||||
|
Описание будет видно на странице профиля. Здесь работают те же правила оформления, что и в
|
||||||
|
комментариях.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Group className={styles.pad}>
|
||||||
|
<InputText
|
||||||
|
value={data.username}
|
||||||
|
handler={setUsername}
|
||||||
|
title="Логин"
|
||||||
|
error={patch_errors.username && ERROR_LITERAL[patch_errors.username]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputText value={data.email} handler={setEmail} title="E-mail" />
|
||||||
|
|
||||||
|
<InputText
|
||||||
|
value={new_password}
|
||||||
|
handler={setNewPassword}
|
||||||
|
title="Новый пароль"
|
||||||
|
type="password"
|
||||||
|
error={patch_errors.new_password && ERROR_LITERAL[patch_errors.new_password]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div />
|
||||||
|
|
||||||
|
<InputText
|
||||||
|
value={password}
|
||||||
|
handler={setPassword}
|
||||||
|
title="Старый пароль"
|
||||||
|
type="password"
|
||||||
|
error={patch_errors.password && ERROR_LITERAL[patch_errors.password]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.small}>
|
||||||
|
Чтобы изменить любое из этих полей, нужно ввести старый пароль.
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group horizontal>
|
||||||
|
<Filler />
|
||||||
|
<Button title="Сохранить" type="submit" />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileSettings = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ProfileSettingsUnconnected);
|
||||||
|
|
||||||
|
export { ProfileSettings };
|
14
src/components/profile/ProfileSettings/styles.scss
Normal file
14
src/components/profile/ProfileSettings/styles.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.wrap {
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad {
|
||||||
|
padding: $gap;
|
||||||
|
box-shadow: transparentize($color: $red, $amount: 0.5) 0 0 0 2px;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font: $font_12_regular;
|
||||||
|
padding: 0 $gap $gap;
|
||||||
|
}
|
|
@ -10,10 +10,12 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
import pick from 'ramda/es/pick';
|
import pick from 'ramda/es/pick';
|
||||||
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
import { CoverBackdrop } from '~/components/containers/CoverBackdrop';
|
||||||
|
import { ProfileSettings } from '~/components/profile/ProfileSettings';
|
||||||
|
|
||||||
const TAB_CONTENT = {
|
const TAB_CONTENT = {
|
||||||
profile: <ProfileDescription />,
|
profile: <ProfileDescription />,
|
||||||
messages: <ProfileMessages />,
|
messages: <ProfileMessages />,
|
||||||
|
settings: <ProfileSettings />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
|
@ -71,3 +71,8 @@ export const authSetLastSeenMessages = (
|
||||||
type: AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES,
|
type: AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES,
|
||||||
last_seen_messages,
|
last_seen_messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const authPatchUser = (user: Partial<IUser>) => ({
|
||||||
|
type: AUTH_USER_ACTIONS.PATCH_USER,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const AUTH_USER_ACTIONS = {
|
||||||
|
|
||||||
SET_UPDATES: 'SET_UPDATES',
|
SET_UPDATES: 'SET_UPDATES',
|
||||||
SET_LAST_SEEN_MESSAGES: 'SET_LAST_SEEN_MESSAGES',
|
SET_LAST_SEEN_MESSAGES: 'SET_LAST_SEEN_MESSAGES',
|
||||||
|
PATCH_USER: 'PATCH_USER',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const USER_ERRORS = {
|
export const USER_ERRORS = {
|
||||||
|
|
|
@ -29,6 +29,7 @@ const INITIAL_STATE: IAuthState = {
|
||||||
user: null,
|
user: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
messages_error: null,
|
messages_error: null,
|
||||||
|
patch_errors: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
authSetUpdates,
|
authSetUpdates,
|
||||||
authLoggedIn,
|
authLoggedIn,
|
||||||
authSetLastSeenMessages,
|
authSetLastSeenMessages,
|
||||||
|
authPatchUser,
|
||||||
} from '~/redux/auth/actions';
|
} from '~/redux/auth/actions';
|
||||||
import {
|
import {
|
||||||
apiUserLogin,
|
apiUserLogin,
|
||||||
|
@ -90,7 +91,7 @@ function* refreshUser() {
|
||||||
function* checkUserSaga({ key }: RehydrateAction) {
|
function* checkUserSaga({ key }: RehydrateAction) {
|
||||||
if (key !== 'auth') return;
|
if (key !== 'auth') return;
|
||||||
yield call(refreshUser);
|
yield call(refreshUser);
|
||||||
// yield put(authOpenProfile('gvorcek'));
|
yield put(authOpenProfile('gvorcek', 'settings'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function* gotPostMessageSaga({ token }: ReturnType<typeof gotAuthPostMessage>) {
|
function* gotPostMessageSaga({ token }: ReturnType<typeof gotAuthPostMessage>) {
|
||||||
|
@ -256,6 +257,17 @@ function* setLastSeenMessages({ last_seen_messages }: ReturnType<typeof authSetL
|
||||||
yield call(reqWrapper, apiUpdateUser, { user: { last_seen_messages } });
|
yield call(reqWrapper, apiUpdateUser, { user: { last_seen_messages } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* patchUser({ user }: ReturnType<typeof authPatchUser>) {
|
||||||
|
const { error, data } = yield call(reqWrapper, apiUpdateUser, { user });
|
||||||
|
|
||||||
|
if (error || !data.user || data.errors) {
|
||||||
|
return yield put(authSetProfile({ patch_errors: data.errors }));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield put(authSetUser(data.user));
|
||||||
|
yield put(authSetProfile({ user: { ...data.user }, tab: 'profile' }));
|
||||||
|
}
|
||||||
|
|
||||||
function* authSaga() {
|
function* authSaga() {
|
||||||
yield takeLatest(REHYDRATE, checkUserSaga);
|
yield takeLatest(REHYDRATE, checkUserSaga);
|
||||||
yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga);
|
yield takeLatest([REHYDRATE, AUTH_USER_ACTIONS.LOGGED_IN], startPollingSaga);
|
||||||
|
@ -267,6 +279,7 @@ function* authSaga() {
|
||||||
yield takeLatest(AUTH_USER_ACTIONS.GET_MESSAGES, getMessages);
|
yield takeLatest(AUTH_USER_ACTIONS.GET_MESSAGES, getMessages);
|
||||||
yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default authSaga;
|
export default authSaga;
|
||||||
|
|
|
@ -46,5 +46,7 @@ export type IAuthState = Readonly<{
|
||||||
user: IUser;
|
user: IUser;
|
||||||
messages: IMessage[];
|
messages: IMessage[];
|
||||||
messages_error: string;
|
messages_error: string;
|
||||||
|
|
||||||
|
patch_errors: Record<string, string>;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue