diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/settings/Settings.tsx')
-rw-r--r-- | Timeline/ClientApp/src/app/settings/Settings.tsx | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/settings/Settings.tsx b/Timeline/ClientApp/src/app/settings/Settings.tsx new file mode 100644 index 00000000..96a3fab4 --- /dev/null +++ b/Timeline/ClientApp/src/app/settings/Settings.tsx @@ -0,0 +1,253 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import axios, { AxiosError } from 'axios'; +import { + Container, + Row, + Col, + Input, + Modal, + ModalHeader, + ModalBody, + ModalFooter, + Button, +} from 'reactstrap'; + +import { apiBaseUrl } from '../config'; + +import { useUser, userLogout, useUserLoggedIn } from '../data/user'; + +import AppBar from '../common/AppBar'; +import OperationDialog, { + OperationInputErrorInfo, +} from '../common/OperationDialog'; +import { CommonErrorResponse } from '../data/common'; + +interface ChangePasswordDialogProps { + open: boolean; + close: () => void; +} + +async function changePassword( + oldPassword: string, + newPassword: string, + token: string +): Promise<void> { + const url = `${apiBaseUrl}/userop/changepassword?token=${token}`; + try { + await axios.post(url, { + oldPassword, + newPassword, + }); + } catch (e) { + const error = e as AxiosError<CommonErrorResponse>; + if ( + error.response && + error.response.status === 400 && + error.response.data && + error.response.data.message + ) { + throw error.response.data.message; + } + throw e; + } +} + +const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => { + const user = useUserLoggedIn(); + const history = useHistory(); + const { t } = useTranslation(); + + const [redirect, setRedirect] = useState<boolean>(false); + + return ( + <OperationDialog + open={props.open} + title={t('settings.dialogChangePassword.title')} + titleColor="dangerous" + inputPrompt={t('settings.dialogChangePassword.prompt')} + inputScheme={[ + { + type: 'text', + label: t('settings.dialogChangePassword.inputOldPassword'), + password: true, + validator: (v) => + v === '' + ? 'settings.dialogChangePassword.errorEmptyOldPassword' + : null, + }, + { + type: 'text', + label: t('settings.dialogChangePassword.inputNewPassword'), + password: true, + validator: (v, values) => { + const error: OperationInputErrorInfo = {}; + error[1] = + v === '' + ? 'settings.dialogChangePassword.errorEmptyNewPassword' + : null; + if (v === values[2]) { + error[2] = null; + } else { + if (values[2] !== '') { + error[2] = 'settings.dialogChangePassword.errorRetypeNotMatch'; + } + } + return error; + }, + }, + { + type: 'text', + label: t('settings.dialogChangePassword.inputRetypeNewPassword'), + password: true, + validator: (v, values) => + v !== values[1] + ? 'settings.dialogChangePassword.errorRetypeNotMatch' + : null, + }, + ]} + onProcess={async ([oldPassword, newPassword]) => { + await changePassword( + oldPassword as string, + newPassword as string, + user.token + ); + userLogout(); + setRedirect(true); + }} + close={() => { + props.close(); + if (redirect) { + history.push('/login'); + } + }} + /> + ); +}; + +const ConfirmLogoutDialog: React.FC<{ + toggle: () => void; + onConfirm: () => void; +}> = ({ toggle, onConfirm }) => { + const { t } = useTranslation(); + + return ( + <Modal isOpen centered> + <ModalHeader className="text-danger"> + {t('settings.dialogConfirmLogout.title')} + </ModalHeader> + <ModalBody>{t('settings.dialogConfirmLogout.prompt')}</ModalBody> + <ModalFooter> + <Button color="secondary" onClick={toggle}> + {t('operationDialog.cancel')} + </Button> + <Button color="danger" onClick={onConfirm}> + {t('operationDialog.confirm')} + </Button> + </ModalFooter> + </Modal> + ); +}; + +const Settings: React.FC = (_) => { + const { i18n, t } = useTranslation(); + const user = useUser(); + const history = useHistory(); + + const [dialog, setDialog] = useState<null | 'changepassword' | 'logout'>( + null + ); + + const language = i18n.language.slice(0, 2); + + return ( + <> + <AppBar /> + <Container fluid className="mt-appbar"> + {user ? ( + <> + <Row className="border-bottom p-3 cursor-pointer"> + <Col xs="12"> + <h5 + onClick={() => { + history.push(`/users/${user.username}`); + }} + > + {t('settings.gotoSelf')} + </h5> + </Col> + </Row> + <Row className="border-bottom p-3 cursor-pointer"> + <Col xs="12"> + <h5 + className="text-danger" + onClick={() => setDialog('changepassword')} + > + {t('settings.changePassword')} + </h5> + </Col> + </Row> + <Row className="border-bottom p-3 cursor-pointer"> + <Col xs="12"> + <h5 + className="text-danger" + onClick={() => { + setDialog('logout'); + }} + > + {t('settings.logout')} + </h5> + </Col> + </Row> + </> + ) : null} + <Row className="align-items-center border-bottom p-3"> + <Col xs="12" sm="auto"> + <h5>{t('settings.languagePrimary')}</h5> + <p>{t('settings.languageSecondary')}</p> + </Col> + <Col xs="auto" className="ml-auto"> + <Input + type="select" + value={language} + onChange={(e) => { + void i18n.changeLanguage(e.target.value); + }} + > + <option value="zh">中文</option> + <option value="en">English</option> + </Input> + </Col> + </Row> + {(() => { + switch (dialog) { + case 'changepassword': + return ( + <ChangePasswordDialog + open + close={() => { + setDialog(null); + }} + /> + ); + case 'logout': + return ( + <ConfirmLogoutDialog + toggle={() => setDialog(null)} + onConfirm={() => { + userLogout(); + history.push('/'); + }} + /> + ); + default: + return null; + } + })()} + </Container> + </> + ); +}; + +export default Settings; |