refactor(users/settings): refactoring forms according to specifications

This commit is contained in:
haitao(lj) 2022-12-08 19:07:31 +08:00
parent 50133f5fe3
commit 32f7a0a89c
5 changed files with 317 additions and 320 deletions

View File

@ -640,7 +640,8 @@ ui:
account: Account account: Account
interface: Interface interface: Interface
profile: profile:
btn_name: Update profile heading: Profile
btn_name: Save
display_name: display_name:
label: Display Name label: Display Name
msg: Display name cannot be empty. msg: Display name cannot be empty.
@ -658,7 +659,7 @@ ui:
custom: Custom custom: Custom
btn_refresh: Refresh btn_refresh: Refresh
custom_text: You can upload your image. custom_text: You can upload your image.
default: Default default: System
msg: Please upload an avatar msg: Please upload an avatar
bio: bio:
label: About Me (optional) label: About Me (optional)
@ -670,10 +671,12 @@ ui:
label: Location (optional) label: Location (optional)
placeholder: "City, Country" placeholder: "City, Country"
notification: notification:
heading: Notifications
email: email:
label: Email Notifications label: Email Notifications
radio: "Answers to your questions, comments, and more" radio: "Answers to your questions, comments, and more"
account: account:
heading: Account
change_email_btn: Change email change_email_btn: Change email
change_pass_btn: Change password change_pass_btn: Change password
change_email_info: >- change_email_info: >-
@ -694,6 +697,7 @@ ui:
pass_confirm: pass_confirm:
label: Confirm New Password label: Confirm New Password
interface: interface:
heading: Interface
lang: lang:
label: Interface Language label: Interface Language
text: User interface language. It will change when you refresh the page. text: User interface language. It will change when you refresh the page.

View File

@ -1,11 +1,16 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import ModifyEmail from './components/ModifyEmail'; import ModifyEmail from './components/ModifyEmail';
import ModifyPassword from './components/ModifyPass'; import ModifyPassword from './components/ModifyPass';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'settings.account',
});
return ( return (
<> <>
<h3 className="mb-4">{t('heading')}</h3>
<ModifyEmail /> <ModifyEmail />
<ModifyPassword /> <ModifyPassword />
</> </>

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState, FormEvent } from 'react'; import React, { useEffect, useState, FormEvent } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { LangsType, FormDataType } from '@/common/interface'; import type { LangsType, FormDataType } from '@/common/interface';
@ -7,6 +6,7 @@ import { useToast } from '@/hooks';
import { updateUserInterface } from '@/services'; import { updateUserInterface } from '@/services';
import { localize } from '@/utils'; import { localize } from '@/utils';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
const Index = () => { const Index = () => {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
@ -15,19 +15,34 @@ const Index = () => {
const loggedUserInfo = loggedUserInfoStore.getState().user; const loggedUserInfo = loggedUserInfoStore.getState().user;
const toast = useToast(); const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>(); const [langs, setLangs] = useState<LangsType[]>();
const [formData, setFormData] = useState<FormDataType>({ const schema: JSONSchema = {
lang: { title: t('heading'),
value: loggedUserInfo.language, properties: {
isInvalid: false, lang: {
errorMsg: '', type: 'string',
title: t('lang.label'),
description: t('lang.text'),
enum: langs?.map((_) => _.value),
enumNames: langs?.map((_) => _.label),
default: loggedUserInfo.language,
},
}, },
}); };
const uiSchema: UISchema = {
lang: {
'ui:widget': 'select',
},
};
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
const getLangs = async () => { const getLangs = async () => {
const res: LangsType[] = await localize.loadLanguageOptions(); const res: LangsType[] = await localize.loadLanguageOptions();
setLangs(res); setLangs(res);
}; };
const handleOnChange = (d) => {
setFormData(d);
};
const handleSubmit = (event: FormEvent) => { const handleSubmit = (event: FormEvent) => {
event.preventDefault(); event.preventDefault();
const lang = formData.lang.value; const lang = formData.lang.value;
@ -48,39 +63,16 @@ const Index = () => {
getLangs(); getLangs();
}, []); }, []);
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="emailSend" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('lang.label')}</Form.Label> <SchemaForm
<Form.Select schema={schema}
value={formData.lang.value} uiSchema={uiSchema}
isInvalid={formData.lang.isInvalid} formData={formData}
onChange={(e) => { onChange={handleOnChange}
setFormData({ onSubmit={handleSubmit}
lang: { />
value: e.target.value, </>
isInvalid: false,
errorMsg: '',
},
});
}}>
{langs?.map((item) => {
return (
<option value={item.value} key={item.label}>
{item.label}
</option>
);
})}
</Form.Select>
<Form.Text as="div">{t('lang.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.lang.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
); );
}; };

View File

@ -1,23 +1,33 @@
import React, { useState, FormEvent, useEffect } from 'react'; import React, { useState, FormEvent, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
import { setNotice, getLoggedUserInfo } from '@/services'; import { setNotice, getLoggedUserInfo } from '@/services';
import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
const Index = () => { const Index = () => {
const toast = useToast(); const toast = useToast();
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'settings.notification', keyPrefix: 'settings.notification',
}); });
const [formData, setFormData] = useState<FormDataType>({ const schema: JSONSchema = {
notice_switch: { title: t('heading'),
value: false, properties: {
isInvalid: false, notice_switch: {
errorMsg: '', type: 'boolean',
title: t('email.label'),
label: t('email.radio'),
default: false,
},
}, },
}); };
const uiSchema: UISchema = {
notice_switch: {
'ui:widget': 'switch',
},
};
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
const getProfile = () => { const getProfile = () => {
getLoggedUserInfo().then((res) => { getLoggedUserInfo().then((res) => {
@ -47,34 +57,20 @@ const Index = () => {
useEffect(() => { useEffect(() => {
getProfile(); getProfile();
}, []); }, []);
const handleChange = (ud) => {
setFormData(ud);
};
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="emailSend" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('email.label')}</Form.Label> <SchemaForm
<Form.Check schema={schema}
required uiSchema={uiSchema}
type="checkbox" formData={formData}
label={t('email.radio')} onChange={handleChange}
checked={formData.notice_switch.value} onSubmit={handleSubmit}
onChange={(e) => { />
setFormData({ </>
notice_switch: {
value: e.target.checked,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.notice_switch.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
); );
}; };

View File

@ -1,12 +1,12 @@
import React, { FormEvent, useState, useEffect } from 'react'; import React, { FormEvent, useState, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap'; import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { marked } from 'marked'; import { marked } from 'marked';
import MD5 from 'md5'; import MD5 from 'md5';
import type { FormDataType } from '@/common/interface'; import type { FormDataType } from '@/common/interface';
import { UploadImg, Avatar } from '@/components'; import { UploadImg, Avatar, Icon } from '@/components';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
import { modifyUserInfo, getLoggedUserInfo } from '@/services'; import { modifyUserInfo, getLoggedUserInfo } from '@/services';
@ -19,7 +19,7 @@ const Index: React.FC = () => {
const toast = useToast(); const toast = useToast();
const { user, update } = loggedUserInfoStore(); const { user, update } = loggedUserInfoStore();
const [mailHash, setMailHash] = useState(''); const [mailHash, setMailHash] = useState('');
const [count, setCount] = useState(0); const [count] = useState(0);
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
display_name: { display_name: {
@ -60,6 +60,40 @@ const Index: React.FC = () => {
const handleChange = (params: FormDataType) => { const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params }); setFormData({ ...formData, ...params });
}; };
const handleAvatarChange = (evt) => {
const { value: v } = evt.currentTarget;
if (v === 'gravatar') {
handleChange({
avatar: {
...formData.avatar,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
isInvalid: false,
errorMsg: '',
},
});
}
if (v === 'custom') {
handleChange({
avatar: {
...formData.avatar,
type: 'custom',
isInvalid: false,
errorMsg: '',
},
});
}
if (v === 'default') {
handleChange({
avatar: {
...formData.avatar,
type: 'default',
isInvalid: false,
errorMsg: '',
},
});
}
};
const avatarUpload = (path: string) => { const avatarUpload = (path: string) => {
setFormData({ setFormData({
@ -73,6 +107,17 @@ const Index: React.FC = () => {
}, },
}); });
}; };
const removeCustomAvatar = () => {
setFormData({
...formData,
avatar: {
...formData.avatar,
custom: '',
isInvalid: false,
errorMsg: '',
},
});
};
const checkValidated = (): boolean => { const checkValidated = (): boolean => {
let bol = true; let bol = true;
@ -201,265 +246,220 @@ const Index: React.FC = () => {
}); });
}; };
const refreshGravatar = () => { // const refreshGravatar = () => {
setCount((pre) => pre + 1); // setCount((pre) => pre + 1);
}; // };
useEffect(() => { useEffect(() => {
getProfile(); getProfile();
}, []); }, []);
return ( return (
<Form noValidate onSubmit={handleSubmit}> <>
<Form.Group controlId="displayName" className="mb-3"> <h3 className="mb-4">{t('heading')}</h3>
<Form.Label>{t('display_name.label')}</Form.Label> <Form noValidate onSubmit={handleSubmit}>
<Form.Control <Form.Group controlId="displayName" className="mb-3">
required <Form.Label>{t('display_name.label')}</Form.Label>
type="text" <Form.Control
value={formData.display_name.value} required
isInvalid={formData.display_name.isInvalid} type="text"
onChange={(e) => value={formData.display_name.value}
handleChange({ isInvalid={formData.display_name.isInvalid}
display_name: { onChange={(e) =>
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.display_name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="userName" className="mb-3">
<Form.Label>{t('username.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.username.value}
isInvalid={formData.username.isInvalid}
onChange={(e) =>
handleChange({
username: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Text as="div">{t('username.caption')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.username.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-2">
<Form.Check
inline
type="radio"
id="gravatar"
label={t('avatar.gravatar')}
className="mb-0"
checked={formData.avatar.type === 'gravatar'}
onChange={() =>
handleChange({ handleChange({
avatar: { display_name: {
...formData.avatar, value: e.target.value,
type: 'gravatar',
gravatar: `https://www.gravatar.com/avatar/${mailHash}`,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
}) })
} }
/> />
<Form.Check <Form.Control.Feedback type="invalid">
inline {formData.display_name.errorMsg}
type="radio" </Form.Control.Feedback>
label={t('avatar.custom')} </Form.Group>
id="custom"
className="mb-0"
checked={formData.avatar.type === 'custom'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'custom',
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Check
inline
type="radio"
id="default"
label={t('avatar.default')}
className="mb-0"
checked={formData.avatar.type === 'default'}
onChange={() =>
handleChange({
avatar: {
...formData.avatar,
type: 'default',
isInvalid: false,
errorMsg: '',
},
})
}
/>
</div>
<div className="d-flex align-items-center">
{formData.avatar.type === 'gravatar' && (
<>
<Avatar
size="128px"
avatar={formData.avatar.gravatar}
searchStr={`s=256&d=identicon${
count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/>
<div>
<Button
variant="outline-secondary"
className="mb-2"
onClick={refreshGravatar}>
{t('avatar.btn_refresh')}
</Button>
<div>
<Form.Text className="text-muted mt-0">
<Trans i18nKey="settings.profile.gravatar_text">
You can change your image on{' '}
<a
href="https://gravatar.com"
target="_blank"
rel="noreferrer">
gravatar.com
</a>
</Trans>
</Form.Text>
</div>
</div>
</>
)}
{formData.avatar.type === 'custom' && ( <Form.Group controlId="userName" className="mb-3">
<> <Form.Label>{t('username.label')}</Form.Label>
<Avatar <Form.Control
size="128px" required
searchStr="s=256" type="text"
avatar={formData.avatar.custom} value={formData.username.value}
className="me-3 rounded" isInvalid={formData.username.isInvalid}
/> onChange={(e) =>
<div> handleChange({
<UploadImg username: {
type="avatar" value: e.target.value,
uploadCallback={avatarUpload} isInvalid: false,
className="mb-2" errorMsg: '',
},
})
}
/>
<Form.Text as="div">{t('username.caption')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.username.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-3">
<Form.Select
name="avatar.type"
value={formData.avatar.type}
onChange={handleAvatarChange}>
<option value="gravatar" key="gravatar">
{t('avatar.gravatar')}
</option>
<option value="default" key="default">
{t('avatar.default')}
</option>
<option value="custom" key="custom">
{t('avatar.custom')}
</option>
</Form.Select>
</div>
<div className="d-flex">
{formData.avatar.type === 'gravatar' && (
<Stack>
<Avatar
size="160px"
avatar={formData.avatar.gravatar}
searchStr={`s=256&d=identicon${
count > 0 ? `&t=${new Date().valueOf()}` : ''
}`}
className="me-3 rounded"
/> />
<div> <Form.Text className="text-muted mt-1">
<Form.Text className="text-muted mt-0"> <Trans i18nKey="settings.profile.avatar.gravatar_text">
<Trans i18nKey="settings.profile.avatar.text"> You can change image on
You can upload your image. <a
</Trans> href="https://gravatar.com"
</Form.Text> target="_blank"
</div> rel="noreferrer">
</div> gravatar.com
</> </a>
)} </Trans>
{formData.avatar.type === 'default' && ( </Form.Text>
<Avatar size="128px" avatar="" className="me-3 rounded" /> </Stack>
)} )}
</div>
<Form.Control
isInvalid={formData.avatar.isInvalid}
className="d-none"
/>
<Form.Control.Feedback type="invalid">
{formData.avatar.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="bio" className="mb-3"> {formData.avatar.type === 'custom' && (
<Form.Label>{t('bio.label')}</Form.Label> <Stack>
<Form.Control <Stack direction="horizontal" className="align-items-start">
className="font-monospace" <Avatar
required size="160px"
as="textarea" searchStr="s=256"
rows={5} avatar={formData.avatar.custom}
value={formData.bio.value} className="me-2 bg-gray-300 "
isInvalid={formData.bio.isInvalid} />
onChange={(e) => <ButtonGroup vertical className="fit-content">
handleChange({ <UploadImg type="avatar" uploadCallback={avatarUpload}>
bio: { <Icon name="cloud-upload" />
value: e.target.value, </UploadImg>
isInvalid: false, <Button
errorMsg: '', variant="outline-secondary"
}, onClick={removeCustomAvatar}>
}) <Icon name="trash" />
} </Button>
/> </ButtonGroup>
<Form.Control.Feedback type="invalid"> </Stack>
{formData.bio.errorMsg} <Form.Text className="text-muted mt-1">
</Form.Control.Feedback> <Trans i18nKey="settings.profile.avatar.text">
</Form.Group> You can upload your image.
</Trans>
</Form.Text>
</Stack>
)}
{formData.avatar.type === 'default' && (
<Avatar size="160px" avatar="" />
)}
</div>
<Form.Control
isInvalid={formData.avatar.isInvalid}
className="d-none"
/>
<Form.Control.Feedback type="invalid">
{formData.avatar.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="website" className="mb-3"> <Form.Group controlId="bio" className="mb-3">
<Form.Label>{t('website.label')}</Form.Label> <Form.Label>{t('bio.label')}</Form.Label>
<Form.Control <Form.Control
required className="font-monospace"
type="text" required
placeholder={t('website.placeholder')} as="textarea"
value={formData.website.value} rows={5}
isInvalid={formData.website.isInvalid} value={formData.bio.value}
onChange={(e) => isInvalid={formData.bio.isInvalid}
handleChange({ onChange={(e) =>
website: { handleChange({
value: e.target.value, bio: {
isInvalid: false, value: e.target.value,
errorMsg: '', isInvalid: false,
}, errorMsg: '',
}) },
} })
/> }
<Form.Control.Feedback type="invalid"> />
{formData.website.errorMsg} <Form.Control.Feedback type="invalid">
</Form.Control.Feedback> {formData.bio.errorMsg}
</Form.Group> </Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="email" className="mb-3"> <Form.Group controlId="website" className="mb-3">
<Form.Label>{t('location.label')}</Form.Label> <Form.Label>{t('website.label')}</Form.Label>
<Form.Control <Form.Control
required required
type="text" type="text"
placeholder={t('location.placeholder')} placeholder={t('website.placeholder')}
value={formData.location.value} value={formData.website.value}
isInvalid={formData.location.isInvalid} isInvalid={formData.website.isInvalid}
onChange={(e) => onChange={(e) =>
handleChange({ handleChange({
location: { website: {
value: e.target.value, value: e.target.value,
isInvalid: false, isInvalid: false,
errorMsg: '', errorMsg: '',
}, },
}) })
} }
/> />
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{formData.location.errorMsg} {formData.website.errorMsg}
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Button variant="primary" type="submit"> <Form.Group controlId="email" className="mb-3">
{t('btn_name')} <Form.Label>{t('location.label')}</Form.Label>
</Button> <Form.Control
</Form> required
type="text"
placeholder={t('location.placeholder')}
value={formData.location.value}
isInvalid={formData.location.isInvalid}
onChange={(e) =>
handleChange({
location: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
})
}
/>
<Form.Control.Feedback type="invalid">
{formData.location.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</Form>
</>
); );
}; };