diff --git a/src/actions/fetch.js b/src/actions/fetch.js index 24f7bd4..4af3dbe 100644 --- a/src/actions/fetch.js +++ b/src/actions/fetch.js @@ -220,10 +220,17 @@ export function requestPasswordChange(newPassword, password) { export async function requestResendVerify() { return makeAPIGETRequest( - './api/auth/resend_verify', + 'api/auth/resend_verify', ); } +export async function requestLogOut() { + const ret = makeAPIGETRequest( + 'api/auth/logout', + ); + return !ret.errors; +} + export function requestNameChange(name) { return makeAPIPOSTRequest( 'api/auth/change_name', diff --git a/src/actions/index.js b/src/actions/index.js index 3b7d676..fc43e7f 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -604,6 +604,15 @@ export function showUserAreaModal(): Action { ); } +export function changeWindowType(windowId, windowType, args = null) { + return { + type: 'CHANGE_WINDOW_TYPE', + windowId, + windowType, + args, + }; +} + export function showRegisterModal(): Action { return showModal( 'REGISTER', diff --git a/src/actions/types.js b/src/actions/types.js index 6bd21cc..27c6c53 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -94,6 +94,11 @@ export type Action = | { type: 'RESTORE_WINDOW' } | { type: 'MOVE_WINDOW', windowId: number, xDiff: number, yDiff: number } | { type: 'RESIZE_WINDOW', windowId: number, xDiff: number, yDiff: number } + | { type: 'CHANGE_WINDOW_TYPE', + windowId: number, + windowType: number, + args: Object, + } | { type: 'BLOCK_USER', userId: number, userName: string } | { type: 'UNBLOCK_USER', userId: number, userName: string } | { type: 'SET_BLOCKING_DM', blockDm: boolean } diff --git a/src/components/Admintools.jsx b/src/components/Admintools.jsx index 667c375..c056278 100644 --- a/src/components/Admintools.jsx +++ b/src/components/Admintools.jsx @@ -195,7 +195,7 @@ function Admintools({ }, []); return ( -

+

{resp && (
)} -

+
); } diff --git a/src/components/ChangeMail.jsx b/src/components/ChangeMail.jsx index b448e7d..ad36add 100644 --- a/src/components/ChangeMail.jsx +++ b/src/components/ChangeMail.jsx @@ -3,8 +3,9 @@ * @flow */ -import React from 'react'; +import React, { useState } from 'react'; import { t } from 'ttag'; + import { validateEMail, validatePassword, } from '../utils/validation'; @@ -21,96 +22,78 @@ function validate(email, password) { return errors; } -class ChangeMail extends React.Component { - constructor() { - super(); - this.state = { - password: '', - email: '', - submitting: false, - success: false, +const ChangeMail = ({ done }) => { + const [password, setPassword] = useState(''); + const [email, setEmail] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState(false); + const [errors, setErrors] = useState([]); - errors: [], - }; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - async handleSubmit(e) { - e.preventDefault(); - - const { email, password, submitting } = this.state; - if (submitting) return; - - const errors = validate(email, password); - - this.setState({ errors }); - if (errors.length > 0) return; - this.setState({ submitting: true }); - - const { errors: resperrors } = await requestMailChange(email, password); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); + const handleSubmit = async () => { + if (submitting) { return; } - this.setState({ - success: true, - }); - } - render() { - const { success } = this.state; - const { done } = this.props; - if (success) { - return ( -
-

- {t`Changed Mail successfully. We sent you a verification mail, \ - please verify your new mail address.`} -

- -
- ); + const valErrors = validate(email, password); + if (valErrors.length > 0) { + setErrors(valErrors); + return; } - const { - errors, password, email, submitting, - } = this.state; + + setSubmitting(true); + const { errors: respErrors } = await requestMailChange(email, password); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); + return; + } + setSuccess(true); + }; + + if (success) { return (
-
- {errors.map((error) => ( -

- {t`Error`}:  - {error} -

- ))} - this.setState({ password: evt.target.value })} - type="password" - placeholder={t`Password`} - /> -
- this.setState({ email: evt.target.value })} - type="text" - placeholder={t`New Mail`} - /> -
- - -
+

+ {t`Changed Mail successfully. We sent you a verification mail, \ + please verify your new mail address.`} +

+
); } -} -export default ChangeMail; + return ( +
+
+ {errors.map((error) => ( +

+ {t`Error`}:  + {error} +

+ ))} + setPassword(evt.target.value)} + type="password" + placeholder={t`Password`} + /> +
+ setEmail(evt.target.value)} + type="text" + placeholder={t`New Mail`} + /> +
+ + +
+
+ ); +}; + +export default React.memo(ChangeMail); diff --git a/src/components/ChangeName.jsx b/src/components/ChangeName.jsx index 40d9dfc..45998d6 100644 --- a/src/components/ChangeName.jsx +++ b/src/components/ChangeName.jsx @@ -3,10 +3,13 @@ * @flow */ -import React from 'react'; +import React, { useState } from 'react'; import { t } from 'ttag'; +import { useDispatch } from 'react-redux'; + import { validateName } from '../utils/validation'; import { requestNameChange } from '../actions/fetch'; +import { setName } from '../actions'; function validate(name) { @@ -18,69 +21,56 @@ function validate(name) { return errors; } -class ChangeName extends React.Component { - constructor() { - super(); - this.state = { - name: '', - submitting: false, +const ChangeName = ({ done }) => { + const [name, setStName] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState([]); - errors: [], - }; + const dispatch = useDispatch(); - this.handleSubmit = this.handleSubmit.bind(this); - } - - async handleSubmit(e) { - e.preventDefault(); - - const { name, submitting } = this.state; - if (submitting) return; - - const errors = validate(name); - - this.setState({ errors }); - if (errors.length > 0) return; - this.setState({ submitting: true }); - - const { errors: resperrors } = await requestNameChange(name); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); + const handleSubmit = async () => { + if (submitting) { return; } - const { setName, done } = this.props; - setName(name); + + const valErrors = validate(name); + if (valErrors.length > 0) { + setErrors(valErrors); + return; + } + + setSubmitting(true); + const { errors: respErrors } = await requestNameChange(name); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); + return; + } + dispatch(setName(name)); done(); - } + }; - render() { - const { errors, name, submitting } = this.state; - const { done } = this.props; - return ( -
-
- {errors.map((error) => ( -

- {t`Error`}: {error}

- ))} - this.setState({ name: evt.target.value })} - type="text" - placeholder={t`New Username`} - /> -
- - -
-
- ); - } -} + return ( +
+
+ {errors.map((error) => ( +

+ {t`Error`}: {error}

+ ))} + setStName(evt.target.value)} + type="text" + placeholder={t`New Username`} + /> +
+ + +
+
+ ); +}; -export default ChangeName; +export default React.memo(ChangeName); diff --git a/src/components/ChangePassword.jsx b/src/components/ChangePassword.jsx index 136cfca..207dcd0 100644 --- a/src/components/ChangePassword.jsx +++ b/src/components/ChangePassword.jsx @@ -5,6 +5,9 @@ import React, { useState } from 'react'; import { t } from 'ttag'; +import { useSelector, useDispatch } from 'react-redux'; + +import { setMailreg } from '../actions'; import { validatePassword } from '../utils/validation'; import { requestPasswordChange } from '../actions/fetch'; @@ -25,7 +28,7 @@ function validate(mailreg, password, newPassword, confirmPassword) { return errors; } -const ChangePassword = ({ mailreg, done, cancel }) => { +const ChangePassword = ({ done }) => { const [password, setPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -33,6 +36,9 @@ const ChangePassword = ({ mailreg, done, cancel }) => { const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState([]); + const mailreg = useSelector((state) => state.user.mailreg); + const dispatch = useDispatch(); + if (success) { return (
@@ -66,6 +72,7 @@ const ChangePassword = ({ mailreg, done, cancel }) => { setSubmitting(false); return; } + dispatch(setMailreg(true)); setSuccess(true); }} > @@ -104,7 +111,7 @@ const ChangePassword = ({ mailreg, done, cancel }) => { diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index 3152121..b0607d5 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -270,8 +270,8 @@ function Converter({ ); return ( -

-

{t`Choose Canvas`}:  +

+
{t`Choose Canvas`}:  -

+

{t`Palette Download`}

-

+

{jt`Palette for ${gimpLink}`}: 

{t`Image Converter`}

{t`Convert an image to canvas colors`}

) : null} -

+
); } diff --git a/src/components/DeleteAccount.jsx b/src/components/DeleteAccount.jsx index 4efb5b6..98713cb 100644 --- a/src/components/DeleteAccount.jsx +++ b/src/components/DeleteAccount.jsx @@ -3,8 +3,8 @@ * @flow */ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { t } from 'ttag'; import { validatePassword } from '../utils/validation'; @@ -20,76 +20,55 @@ function validate(password) { return errors; } -class DeleteAccount extends React.Component { - constructor() { - super(); - this.state = { - password: '', - submitting: false, +const DeleteAccount = ({ done }) => { + const [password, setPassword] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState([]); - errors: [], - }; + const dispatch = useDispatch(); - this.handleSubmit = this.handleSubmit.bind(this); - } - - async handleSubmit(e) { - e.preventDefault(); - - const { password, submitting } = this.state; - if (submitting) return; - - const errors = validate(password); - - this.setState({ errors }); - if (errors.length > 0) return; - this.setState({ submitting: true }); - - const { errors: resperrors } = await requestDeleteAccount(password); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); + const handleSubmit = async () => { + if (submitting) { return; } - const { logout } = this.props; - logout(); - } - render() { - const { errors, password, submitting } = this.state; - const { done } = this.props; - return ( -
-
- {errors.map((error) => ( -

{t`Error`} - : {error}

- ))} - this.setState({ password: evt.target.value })} - type="password" - placeholder={t`Password`} - /> -
- - -
-
- ); - } -} + const valErrors = validate(password); + if (valErrors.length > 0) { + setErrors(valErrors); + return; + } -function mapDispatchToProps(dispatch) { - return { - async logout() { - dispatch(logoutUser()); - }, + setSubmitting(true); + const { errors: respErrors } = await requestDeleteAccount(password); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); + return; + } + dispatch(logoutUser()); }; -} -export default connect(null, mapDispatchToProps)(DeleteAccount); + return ( +
+
+ {errors.map((error) => ( +

{t`Error`} + : {error}

+ ))} + setPassword(evt.target.value)} + type="password" + placeholder={t`Password`} + /> +
+ + +
+
+ ); +}; + +export default React.memo(DeleteAccount); diff --git a/src/components/LogInArea.jsx b/src/components/LogInArea.jsx new file mode 100644 index 0000000..b9a5538 --- /dev/null +++ b/src/components/LogInArea.jsx @@ -0,0 +1,85 @@ +/* + * @flow + */ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { t } from 'ttag'; + +import LogInForm from './LogInForm'; +import { changeWindowType } from '../actions'; + +const logoStyle = { + marginRight: 5, +}; + +const LogInArea = ({ windowId }) => { + const dispatch = useDispatch(); + + return ( +
+

+ {t`Login to access more features and stats.`} +


+

{t`Login with Name or Mail:`}

+ +

dispatch(changeWindowType(windowId, 'FORGOT_PASSWORD'))} + role="presentation" + > + {t`I forgot my Password.`}

+

{t`or login with:`}

+ + Discord + + + Google + + + Facebook + + + VK + + + Reddit + +

{t`or register here:`}

+ +
+ ); +}; + +export default React.memo(LogInArea); diff --git a/src/components/LogInForm.jsx b/src/components/LogInForm.jsx index 51f8f18..a8e9f89 100644 --- a/src/components/LogInForm.jsx +++ b/src/components/LogInForm.jsx @@ -2,8 +2,8 @@ * LogIn Form * @flow */ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { t } from 'ttag'; import { @@ -31,86 +31,64 @@ const inputStyles = { maxWidth: '35em', }; -class LogInForm extends React.Component { - constructor() { - super(); - this.state = { - nameoremail: '', - password: '', - submitting: false, +const LogInForm = () => { + const [nameoremail, setNameOrEmail] = useState(''); + const [password, setPassword] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState([]); - errors: [], - }; + const dispatch = useDispatch(); - this.handleSubmit = this.handleSubmit.bind(this); - } + const handleSubmit = async () => { + if (submitting) { + return; + } - async handleSubmit(e) { - e.preventDefault(); + const valErrors = validate(nameoremail, password); + if (valErrors.length > 0) { + setErrors(valErrors); + return; + } - const { nameoremail, password, submitting } = this.state; - const { login } = this.props; - if (submitting) return; - - const errors = validate(nameoremail, password); - - this.setState({ errors }); - if (errors.length > 0) return; - - this.setState({ submitting: true }); - const { errors: resperrors, me } = await requestLogin( + setSubmitting(true); + const { errors: respErrors, me } = await requestLogin( nameoremail, password, ); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); return; } - login(me); - } - - render() { - const { - errors, nameoremail, password, submitting, - } = this.state; - return ( -
- {errors.map((error) => ( -

{t`Error`}: {error}

- ))} - this.setState({ nameoremail: evt.target.value })} - type="text" - placeholder={t`Name or Email`} - />
- this.setState({ password: evt.target.value })} - type="password" - placeholder={t`Password`} - /> -

- -

-
- ); - } -} - -function mapDispatchToProps(dispatch) { - return { - login(me) { - dispatch(loginUser(me)); - }, + dispatch(loginUser(me)); }; -} -export default connect(null, mapDispatchToProps)(LogInForm); + return ( +
+ {errors.map((error) => ( +

{t`Error`}: {error}

+ ))} + setNameOrEmail(evt.target.value)} + type="text" + placeholder={t`Name or Email`} + />
+ setPassword(evt.target.value)} + type="password" + placeholder={t`Password`} + /> +

+ +

+
+ ); +}; + +export default React.memo(LogInForm); diff --git a/src/components/Menu.jsx b/src/components/Menu.jsx index 5b39f70..2d33e3a 100644 --- a/src/components/Menu.jsx +++ b/src/components/Menu.jsx @@ -11,13 +11,6 @@ import HelpButton from './buttons/HelpButton'; import SettingsButton from './buttons/SettingsButton'; import LogInButton from './buttons/LogInButton'; import DownloadButton from './buttons/DownloadButton'; -/* - * removed MinecraftButton cause it didn't get used in over a year - * also CSS rule got removed - * and MinecraftModal from ModalRoot - * and MinecraftTPButton from App - * (support for it will be otherwise still kept) - */ function Menu({ menuOpen, diff --git a/src/components/ModalRoot.jsx b/src/components/ModalRoot.jsx index 9fc1f31..2ea7b93 100644 --- a/src/components/ModalRoot.jsx +++ b/src/components/ModalRoot.jsx @@ -44,7 +44,7 @@ const ModalRoot = () => { return null; } - const Content = COMPONENTS[windowType || 'NONE']; + const Content = COMPONENTS[windowType]; return ( (render || open) diff --git a/src/components/NewPasswordForm.jsx b/src/components/NewPasswordForm.jsx deleted file mode 100644 index 6544c6d..0000000 --- a/src/components/NewPasswordForm.jsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Form for requesting password-reset mail - * @flow - */ -import React from 'react'; -import { t } from 'ttag'; -import { validateEMail } from '../utils/validation'; -import { requestNewPassword } from '../actions/fetch'; - -function validate(email) { - const errors = []; - const mailerror = validateEMail(email); - if (mailerror) errors.push(mailerror); - return errors; -} - -const inputStyles = { - display: 'inline-block', - width: '100%', - maxWidth: '35em', -}; - -class NewPasswordForm extends React.Component { - constructor() { - super(); - this.state = { - email: '', - submitting: false, - success: false, - - errors: [], - }; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - async handleSubmit(e) { - e.preventDefault(); - - const { email, submitting } = this.state; - if (submitting) return; - - const errors = validate(email); - - this.setState({ errors }); - if (errors.length > 0) return; - - this.setState({ submitting: true }); - const { errors: resperrors } = await requestNewPassword(email); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); - return; - } - this.setState({ - success: true, - }); - } - - render() { - const { success } = this.state; - const { back } = this.props; - if (success) { - return ( -
-

- {t`Sent you a mail with instructions to reset your password.`} -

- -
- ); - } - const { errors, email, submitting } = this.state; - return ( -
- {errors.map((error) => ( -

{t`Error`}: {error}

- ))} - this.setState({ email: evt.target.value })} - type="text" - placeholder={t`Email`} - /> -
- - -
- ); - } -} - -export default NewPasswordForm; diff --git a/src/components/Rankings.jsx b/src/components/Rankings.jsx index 94fad9b..fdac5b4 100644 --- a/src/components/Rankings.jsx +++ b/src/components/Rankings.jsx @@ -5,52 +5,42 @@ /* eslint-disable max-len */ -import React from 'react'; +import React, { useState } from 'react'; import { t } from 'ttag'; import TotalRankings from './TotalRankings'; import DailyRankings from './DailyRankings'; -class Rankings extends React.Component { - constructor() { - super(); - this.state = { - orderDaily: false, - }; - } +const Rankings = () => { + const [orderDaily, setOrderDaily] = useState(false); - render() { - const { orderDaily } = this.state; - return ( + return ( +
-

- { - this.setState({ orderDaily: false }); - }} - >{t`Total`} | - { this.setState({ orderDaily: true }); }} - >{t`Daily`} -

- {(orderDaily) ? : } -

- {t`Ranking updates every 5 min. Daily rankings get reset at midnight UTC.`} -

+ setOrderDaily(false)} + >{t`Total`} | + setOrderDaily(true)} + >{t`Daily`}
- ); - } -} + {(orderDaily) ? : } +

+ {t`Ranking updates every 5 min. Daily rankings get reset at midnight UTC.`} +

+
+ ); +}; -export default Rankings; +export default React.memo(Rankings); diff --git a/src/components/SignUpForm.jsx b/src/components/SignUpForm.jsx deleted file mode 100644 index 6a9afc5..0000000 --- a/src/components/SignUpForm.jsx +++ /dev/null @@ -1,163 +0,0 @@ -/* - * SignUp Form to register new user by mail - * @flow - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { t } from 'ttag'; -import { - validateEMail, validateName, validatePassword, -} from '../utils/validation'; -import { requestRegistration } from '../actions/fetch'; - -import { showUserAreaModal, loginUser } from '../actions'; - - -function validate(name, email, password, confirmPassword) { - const errors = []; - const mailerror = validateEMail(email); - if (mailerror) errors.push(mailerror); - const nameerror = validateName(name); - if (nameerror) errors.push(nameerror); - const passworderror = validatePassword(password); - if (passworderror) errors.push(passworderror); - - if (password !== confirmPassword) { - errors.push('Passwords do not match'); - } - return errors; -} - -const inputStyles = { - display: 'inline-block', - width: '100%', - maxWidth: '35em', -}; - -class SignUpForm extends React.Component { - constructor() { - super(); - this.state = { - name: '', - email: '', - password: '', - confirmPassword: '', - submitting: false, - - errors: [], - }; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - async handleSubmit(e) { - e.preventDefault(); - - const { - name, email, password, confirmPassword, submitting, - } = this.state; - if (submitting) return; - - const errors = validate(name, email, password, confirmPassword); - - this.setState({ errors }); - if (errors.length > 0) return; - - this.setState({ submitting: true }); - const { errors: resperrors, me } = await requestRegistration( - name, - email, - password, - ); - if (resperrors) { - this.setState({ - errors: resperrors, - submitting: false, - }); - return; - } - const { login, userarea } = this.props; - login(me); - userarea(); - } - - render() { - const { - errors, - name, - email, - password, - confirmPassword, - submitting, - } = this.state; - const { - back, - } = this.props; - return ( -
- {errors.map((error) => ( -

{t`Error`} - : {error}

- ))} - this.setState({ name: evt.target.value })} - type="text" - placeholder={t`Name`} - />
- this.setState({ email: evt.target.value })} - type="text" - placeholder={t`Email`} - />
- this.setState({ password: evt.target.value })} - type="password" - placeholder={t`Password`} - />
- this.setState({ - confirmPassword: evt.target.value, - })} - type="password" - placeholder={t`Confirm Password`} - />
- - -
- ); - } -} - - -function mapDispatchToProps(dispatch) { - return { - login(me) { - dispatch(loginUser(me)); - }, - userarea() { - dispatch(showUserAreaModal()); - }, - }; -} - -export default connect(null, mapDispatchToProps)(SignUpForm); diff --git a/src/components/SocialSettings.jsx b/src/components/SocialSettings.jsx index e959c62..aecc2f5 100644 --- a/src/components/SocialSettings.jsx +++ b/src/components/SocialSettings.jsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { t } from 'ttag'; import { @@ -13,105 +13,83 @@ import { } from '../actions'; import MdToggleButtonHover from './MdToggleButtonHover'; -const SocialSettings = ({ - blocked, - fetching, - blockDm, - setBlockDm, - unblock, - done, -}) => ( -
-
- { + const blocked = useSelector((state) => state.chat.blocked); + const blockDm = useSelector((state) => state.user.blockDm); + const fetching = useSelector((state) => state.fetching.fetchingApi); + const dispatch = useDispatch(); + + return ( +
+
+ + {t`Block all Private Messages`} + + { + if (!fetching) { + dispatch(setBlockingDm(!blockDm)); + } + }} + /> +
+
+

- {t`Block all Private Messages`} - - { - if (!fetching) { - setBlockDm(!blockDm); + >{t`Unblock Users`}

+ { + (blocked.length) ? ( + + { + blocked.map((bl) => ( +
{ + if (!fetching) { + dispatch(setUserBlock(bl[0], bl[1], false)); + } + }} + > + {`⦸ ${bl[1]}`} +
+ )) } - }} - /> -
-
-

{t`Unblock Users`}

- { - (blocked.length) ? ( - - { - blocked.map((bl) => ( -
{ - if (!fetching) { - unblock(bl[0], bl[1]); - } - }} - > - {`⦸ ${bl[1]}`} -
- )) - } -
- ) - : ( -

{t`You have no users blocked`}

+ ) - } -
- -
-); + : ( +

{t`You have no users blocked`}

+ ) + } +
+ +
+ ); +}; -function mapStateToProps(state: State) { - const { blocked } = state.chat; - const { blockDm } = state.user; - const { fetchingApi: fetching } = state.fetching; - return { - blocked, - blockDm, - fetching, - }; -} - -function mapDispatchToProps(dispatch) { - return { - setBlockDm(block) { - dispatch(setBlockingDm(block)); - }, - unblock(userId, userName) { - dispatch(setUserBlock(userId, userName, false)); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(SocialSettings); +export default React.memo(SocialSettings); diff --git a/src/components/UserArea.jsx b/src/components/UserArea.jsx deleted file mode 100644 index cffd819..0000000 --- a/src/components/UserArea.jsx +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Menu to change user credentials - * @flow - */ - -import React from 'react'; -import { connect } from 'react-redux'; -import { t } from 'ttag'; - -import type { State } from '../reducers'; - -import UserMessages from './UserMessages'; -import ChangePassword from './ChangePassword'; -import ChangeName from './ChangeName'; -import ChangeMail from './ChangeMail'; -import DeleteAccount from './DeleteAccount'; -import SocialSettings from './SocialSettings'; -import { logoutUser } from '../actions'; - -import { numberToString } from '../core/utils'; - -const Stat = ({ text, value, rank }) => ( -

- {(rank) ? `${text}: #` : `${text}: `} -   - {numberToString(value)} -

-); - -class UserArea extends React.Component { - constructor() { - super(); - this.state = { - // that should be an ENUM tbh - changeNameExtended: false, - changeMailExtended: false, - changePasswdExtended: false, - deleteAccountExtended: false, - }; - } - - render() { - const { - stats, name, logout, mailreg, setMailreg, setName, - } = this.props; - const { - changeNameExtended, - changeMailExtended, - changePasswdExtended, - deleteAccountExtended, - socialSettingsExtended, - } = this.state; - return ( -

- - - - - -

-

{t`Your name is: ${name}`}

( - {t`Log out`} | - this.setState({ - changeNameExtended: true, - changeMailExtended: false, - changePasswdExtended: false, - deleteAccountExtended: false, - socialSettingsExtended: false, - })} - > {t`Change Username`} | - {(mailreg) - && ( - - this.setState({ - changeNameExtended: false, - changeMailExtended: true, - changePasswdExtended: false, - deleteAccountExtended: false, - socialSettingsExtended: false, - })} - > {t`Change Mail`} | - - )} - this.setState({ - changeNameExtended: false, - changeMailExtended: false, - changePasswdExtended: true, - deleteAccountExtended: false, - socialSettingsExtended: false, - })} - > {t`Change Password`} | - this.setState({ - changeNameExtended: false, - changeMailExtended: false, - changePasswdExtended: false, - deleteAccountExtended: true, - socialSettingsExtended: false, - })} - > {t`Delete Account`} ) -
( - this.setState({ - changeNameExtended: false, - changeMailExtended: false, - changePasswdExtended: false, - deleteAccountExtended: false, - socialSettingsExtended: true, - })} - > {t`Social Settings`} ) -

-

- {(changePasswdExtended) - && ( - { - setMailreg(true); - this.setState({ changePasswdExtended: false }); - }} - cancel={() => { this.setState({ changePasswdExtended: false }); }} - /> - )} - {(changeNameExtended) - && ( - { this.setState({ changeNameExtended: false }); }} - /> - )} - {(changeMailExtended) - && ( - { this.setState({ changeMailExtended: false }); }} - /> - )} - {(deleteAccountExtended) - && ( - { this.setState({ deleteAccountExtended: false }); }} - /> - )} - {(socialSettingsExtended) - && ( - { this.setState({ socialSettingsExtended: false }); }} - /> - )} -

- ); - } -} - - -function mapStateToProps(state: State) { - const { - name, - mailreg, - } = state.user; - const { - totalPixels, - dailyTotalPixels, - ranking, - dailyRanking, - } = state.ranks; - const stats = { - totalPixels, - dailyTotalPixels, - ranking, - dailyRanking, - }; - - return { name, mailreg, stats }; -} - -function mapDispatchToProps(dispatch) { - return { - async logout() { - const response = await fetch( - './api/auth/logout', - { credentials: 'include' }, - ); - if (response.ok) { - dispatch(logoutUser()); - } - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(UserArea); diff --git a/src/components/UserAreaContent.jsx b/src/components/UserAreaContent.jsx new file mode 100644 index 0000000..072b5f8 --- /dev/null +++ b/src/components/UserAreaContent.jsx @@ -0,0 +1,130 @@ +/* + * Menu to change user credentials + * @flow + */ + +import React, { useState, useCallback } from 'react'; +import { useSelector, shallowEqual, useDispatch } from 'react-redux'; +import { t } from 'ttag'; + +import UserMessages from './UserMessages'; +import ChangePassword from './ChangePassword'; +import ChangeName from './ChangeName'; +import ChangeMail from './ChangeMail'; +import DeleteAccount from './DeleteAccount'; +import SocialSettings from './SocialSettings'; +import { logoutUser } from '../actions'; +import { requestLogOut } from '../actions/fetch'; + +import { numberToString } from '../core/utils'; + +const AREAS = { + CHANGE_NAME: ChangeName, + CHANGE_MAIL: ChangeMail, + CHANGE_PASSWORD: ChangePassword, + DELETE_ACCOUNT: DeleteAccount, + SOCIAL_SETTINGS: SocialSettings, +}; + +const Stat = ({ text, value, rank }) => ( +

+ {(rank) ? `${text}: #` : `${text}: `} +   + {numberToString(value)} +

+); + +const UserAreaContent = () => { + const [area, setArea] = useState('NONE'); + + const dispatch = useDispatch(); + const logout = useCallback(async () => { + const ret = await requestLogOut(); + if (ret) { + dispatch(logoutUser()); + } + }, [dispatch]); + + const mailreg = useSelector((state) => state.user.mailreg); + const name = useSelector((state) => state.user.name); + const stats = useSelector((state) => ({ + totalPixels: state.ranks.totalPixels, + dailyTotalPixels: state.ranks.dailyTotalPixels, + ranking: state.ranks.ranking, + dailyRanking: state.ranks.dailyRanking, + }), shallowEqual); + + const Area = AREAS[area]; + + return ( +
+ + + + + +
+

{t`Your name is: ${name}`}

( + {t`Log out`} | + setArea('CHANGE_NAME')} + > {t`Change Username`} | + {(mailreg) + && ( + + setArea('CHANGE_MAIL')} + > {t`Change Mail`} | + + )} + setArea('CHANGE_PASSWORD')} + > {t`Change Password`} | + setArea('DELETE_ACCOUNT')} + > {t`Delete Account`} ) +
( + setArea('SOCIAL_SETTINGS')} + > {t`Social Settings`} ) +
+ {(Area) && setArea(null)} />} +
+ ); +}; + +export default React.memo(UserAreaContent); diff --git a/src/components/buttons/DownloadButton.jsx b/src/components/buttons/DownloadButton.jsx index 5ed4d08..7a9a732 100644 --- a/src/components/buttons/DownloadButton.jsx +++ b/src/components/buttons/DownloadButton.jsx @@ -4,16 +4,13 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { MdFileDownload } from 'react-icons/md'; import fileDownload from 'js-file-download'; import { t } from 'ttag'; import { getRenderer } from '../../ui/renderer'; -import type { State } from '../../reducers'; - - /** * https://jsfiddle.net/AbdiasSoftware/7PRNN/ */ @@ -29,23 +26,21 @@ function download(view) { } -const DownloadButton = ({ view }) => ( -
download(view)} - > - -
-); +const DownloadButton = () => { + const view = useSelector((state) => state.canvas.view); -// TODO optimize -function mapStateToProps(state: State) { - const { view } = state.canvas; - return { view }; -} + return ( +
download(view)} + > + +
+ ); +}; -export default connect(mapStateToProps)(DownloadButton); +export default React.memo(DownloadButton); diff --git a/src/components/buttons/GlobeButton.jsx b/src/components/buttons/GlobeButton.jsx index f8bd0a5..901599d 100644 --- a/src/components/buttons/GlobeButton.jsx +++ b/src/components/buttons/GlobeButton.jsx @@ -4,12 +4,10 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import { useSelector, shallowEqual } from 'react-redux'; import { Md3DRotation } from 'react-icons/md'; import { t } from 'ttag'; -import type { State } from '../../reducers'; - /** * https://jsfiddle.net/AbdiasSoftware/7PRNN/ @@ -21,28 +19,26 @@ function globe(canvasId, canvasIdent, canvasSize, view) { } -const GlobeButton = ({ - canvasId, canvasIdent, canvasSize, view, -}) => ( -
globe(canvasId, canvasIdent, canvasSize, view)} - > - -
-); +const GlobeButton = () => { + const [canvasId, canvasIdent, canvasSize, view] = useSelector((state) => [ + state.canvas.canvasId, + state.canvas.canvasIdent, + state.canvas.canvasSize, + state.canvas.view, + ], shallowEqual); -function mapStateToProps(state: State) { - const { - canvasId, canvasIdent, canvasSize, view, - } = state.canvas; - return { - canvasId, canvasIdent, canvasSize, view, - }; -} + return ( +
globe(canvasId, canvasIdent, canvasSize, view)} + > + +
+ ); +}; -export default connect(mapStateToProps)(GlobeButton); +export default React.memo(GlobeButton); diff --git a/src/components/buttons/PalselButton.jsx b/src/components/buttons/PalselButton.jsx index 2abccae..9ad1e2f 100644 --- a/src/components/buttons/PalselButton.jsx +++ b/src/components/buttons/PalselButton.jsx @@ -5,44 +5,36 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { MdPalette } from 'react-icons/md'; import { t } from 'ttag'; import { toggleOpenPalette } from '../../actions'; -const PalselButton = ({ - palette, onToggle, selectedColor, paletteOpen, -}) => ( -
- -
-); +const PalselButton = () => { + const paletteOpen = useSelector((state) => state.gui.paletteOpen); + const [palette, selectedColor] = useSelector((state) => [ + state.canvas.palette, + state.canvas.selectedColor, + ], shallowEqual); + const dispatch = useDispatch(); -// TODO simplify... -function mapStateToProps(state: State) { - const { paletteOpen } = state.gui; - const { palette, selectedColor } = state.canvas; - return { palette, selectedColor, paletteOpen }; -} + return ( +
dispatch(toggleOpenPalette())} + > + +
+ ); +}; -function mapDispatchToProps(dispatch) { - return { - onToggle() { - dispatch(toggleOpenPalette()); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PalselButton); +export default React.memo(PalselButton); diff --git a/src/components/contextmenus/ChannelContextMenu.jsx b/src/components/contextmenus/ChannelContextMenu.jsx index a8a9f40..1008417 100644 --- a/src/components/contextmenus/ChannelContextMenu.jsx +++ b/src/components/contextmenus/ChannelContextMenu.jsx @@ -3,49 +3,31 @@ * @flow */ -import React, { - useRef, useEffect, -} from 'react'; -import { connect } from 'react-redux'; +import React, { useRef, useCallback } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { t } from 'ttag'; +import { + useClickOutside, +} from '../hooks/clickOutside'; import { hideContextMenu, setLeaveChannel, muteChatChannel, unmuteChatChannel, } from '../../actions'; -import type { State } from '../../reducers'; -const ChannelContextMenu = ({ - xPos, - yPos, - cid, - channels, - leave, - muteArr, - mute, - unmute, - close, -}) => { +const ChannelContextMenu = () => { const wrapperRef = useRef(null); - useEffect(() => { - const handleClickOutside = (event) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { - event.stopPropagation(); - close(); - } - }; - document.addEventListener('click', handleClickOutside, { - capture: true, - }); - return () => { - document.removeEventListener('click', handleClickOutside, { - capture: true, - }); - }; - }, [wrapperRef]); + const channels = useSelector((state) => state.chat.channels); + const muteArr = useSelector((state) => state.chatRead.mute); + const { xPos, yPos, args } = useSelector((state) => state.contextMenu); + const { cid } = args; + const dispatch = useDispatch(); + const close = useCallback(() => dispatch(hideContextMenu()), [dispatch]); + + useClickOutside([wrapperRef], close); const isMuted = muteArr.includes(cid); @@ -62,9 +44,9 @@ const ChannelContextMenu = ({ role="button" onClick={() => { if (isMuted) { - unmute(cid); + dispatch(unmuteChatChannel(cid)); } else { - mute(cid); + dispatch(muteChatChannel(cid)); } }} tabIndex={0} @@ -77,7 +59,7 @@ const ChannelContextMenu = ({
{ - leave(cid); + dispatch(setLeaveChannel(cid)); close(); }} tabIndex={0} @@ -89,43 +71,4 @@ const ChannelContextMenu = ({ ); }; -function mapStateToProps(state: State) { - const { - xPos, - yPos, - args, - } = state.contextMenu; - const { - channels, - } = state.chat; - const { - cid, - } = args; - const { mute: muteArr } = state.chatRead; - return { - xPos, - yPos, - cid, - channels, - muteArr, - }; -} - -function mapDispatchToProps(dispatch) { - return { - close() { - dispatch(hideContextMenu()); - }, - leave(cid) { - dispatch(setLeaveChannel(cid)); - }, - mute(cid) { - dispatch(muteChatChannel(cid)); - }, - unmute(cid) { - dispatch(unmuteChatChannel(cid)); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(ChannelContextMenu); +export default React.memo(ChannelContextMenu); diff --git a/src/components/contextmenus/ChannelDropDown.jsx b/src/components/contextmenus/ChannelDropDown.jsx index 830016d..bc66899 100644 --- a/src/components/contextmenus/ChannelDropDown.jsx +++ b/src/components/contextmenus/ChannelDropDown.jsx @@ -5,12 +5,14 @@ */ import React, { - useRef, useState, useEffect, useCallback, useLayoutEffect, + useRef, useState, useEffect, useCallback, } from 'react'; import { useSelector } from 'react-redux'; import { MdChat } from 'react-icons/md'; import { FaUserFriends } from 'react-icons/fa'; +import { useConditionalClickOutside } from '../hooks/clickOutside'; + const ChannelDropDown = ({ setChatChannel, chatChannel, @@ -37,35 +39,18 @@ const ChannelDropDown = ({ setOffset(buttonRef.current.clientHeight); }, [buttonRef]); - const handleClickOutside = useCallback((event) => { - if (wrapperRef.current - && !wrapperRef.current.contains(event.target) - && !buttonRef.current.contains(event.target) - ) { - event.stopPropagation(); - setShow(false); - } - }, []); + useConditionalClickOutside( + [wrapperRef], + show, + useCallback(() => setShow(false), []), + ); - const handleWindowResize = useCallback(() => { - setShow(false); - }, []); - - useLayoutEffect(() => { + useEffect(() => { if (show) { if (channels[chatChannel]) { const chType = (channels[chatChannel][1] === 1) ? 1 : 0; setType(chType); } - document.addEventListener('click', handleClickOutside, { - capture: true, - }); - window.addEventListener('resize', handleWindowResize); - } else { - document.removeEventListener('click', handleClickOutside, { - capture: true, - }); - window.removeEventListener('resize', handleWindowResize); } }, [show]); @@ -133,7 +118,7 @@ const ChannelDropDown = ({ ref={buttonRef} role="button" tabIndex={-1} - onClick={() => setShow(!show)} + onClick={() => setShow(true)} className={`channelbtn${(show) ? ' selected' : ''}`} > {(unreadAny && chatNotify && !show) && ( diff --git a/src/components/contextmenus/UserContextMenu.jsx b/src/components/contextmenus/UserContextMenu.jsx index bc81182..197285c 100644 --- a/src/components/contextmenus/UserContextMenu.jsx +++ b/src/components/contextmenus/UserContextMenu.jsx @@ -3,12 +3,13 @@ * @flow */ -import React, { - useRef, useEffect, -} from 'react'; +import React, { useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { t } from 'ttag'; +import { + useClickOutside, +} from '../hooks/clickOutside'; import { hideContextMenu, addToChatInputMessage, @@ -29,22 +30,7 @@ const UserContextMenu = () => { const dispatch = useDispatch(); const close = () => dispatch(hideContextMenu()); - useEffect(() => { - const handleClickOutside = (event) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { - event.stopPropagation(); - close(); - } - }; - document.addEventListener('click', handleClickOutside, { - capture: true, - }); - return () => { - document.removeEventListener('click', handleClickOutside, { - capture: true, - }); - }; - }, [wrapperRef]); + useClickOutside([wrapperRef], close); return (
{ + if (insideRefs.every((ref) => !ref.current + || !ref.current.contains(event.target))) { + event.stopPropagation(); + callback(); + } + }, [callback]); + + const handleWindowResize = useCallback(() => { + callback(); + }, [callback]); + + useLayoutEffect(() => { + if (active) { + document.addEventListener('click', handleClickOutside, { + capture: true, + }); + window.addEventListener('resize', handleWindowResize); + } else { + document.removeEventListener('click', handleClickOutside, { + capture: true, + }); + window.removeEventListener('resize', handleWindowResize); + } + }, [active, callback]); +} + +/* + * listen to click outside or window resize + * @param insideRefs references to elements that are considered inside + * @param callback function that gets fired on click outside + */ +export function useClickOutside(insideRefs, callback) { + useEffect(() => { + const handleClickOutside = (event) => { + if (insideRefs.every((ref) => !ref.current + || !ref.current.contains(event.target))) { + event.stopPropagation(); + callback(); + } + }; + + const handleWindowResize = () => { + callback(); + }; + + document.addEventListener('click', handleClickOutside, { + capture: true, + }); + window.addEventListener('resize', handleWindowResize); + return () => { + document.removeEventListener('click', handleClickOutside, { + capture: true, + }); + window.removeEventListener('resize', handleWindowResize); + }; + }, [callback]); +} diff --git a/src/components/windows/CanvasSelect.jsx b/src/components/windows/CanvasSelect.jsx index 94f3f13..065ed77 100644 --- a/src/components/windows/CanvasSelect.jsx +++ b/src/components/windows/CanvasSelect.jsx @@ -4,62 +4,48 @@ */ import React from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { t } from 'ttag'; import CanvasItem from '../CanvasItem'; -import { showArchiveModal } from '../../actions'; - -import type { State } from '../../reducers'; +import { changeWindowType } from '../../actions'; -const CanvasSelect = ({ - canvases, - showHiddenCanvases, - showArchive, -}) => ( -

-

- {t`Select the canvas you want to use. \ -Every canvas is unique and has different palettes, cooldown and requirements. \ -Archive of closed canvases can be accessed here:`}  - {t`Archive`}) +const CanvasSelect = ({ windowId }) => { + const [canvases, showHiddenCanvases] = useSelector((state) => [ + state.canvas.canvases, + state.canvas.showHiddenCanvases, + ], shallowEqual); + const dispatch = useDispatch(); + + return ( +

+

+ {t`Select the canvas you want to use. \ + Every canvas is unique and has different palettes, cooldown and requirements. \ + Archive of closed canvases can be accessed here:`}  + dispatch(changeWindowType(windowId, 'ARCHIVE'))} + >{t`Archive`}) +

+ { + Object.keys(canvases).map((canvasId) => ( + (canvases[canvasId].hid && !showHiddenCanvases) + ? null + : + )) + }

- { - Object.keys(canvases).map((canvasId) => ( - (canvases[canvasId].hid && !showHiddenCanvases) - ? null - : - )) - } -

-); + ); +}; -function mapDispatchToProps(dispatch) { - return { - showArchive() { - dispatch(showArchiveModal()); - }, - }; -} - -function mapStateToProps(state: State) { - const { - canvases, - showHiddenCanvases, - } = state.canvas; - return { canvases, showHiddenCanvases }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(CanvasSelect); +export default React.memo(CanvasSelect); diff --git a/src/components/windows/Chat.jsx b/src/components/windows/Chat.jsx index dc70093..395a486 100644 --- a/src/components/windows/Chat.jsx +++ b/src/components/windows/Chat.jsx @@ -173,7 +173,7 @@ const Chat = ({ } {(ownName) ? ( -
+
handleSubmit(e)} style={{ display: 'flex', flexDirection: 'row' }} diff --git a/src/components/windows/ForgotPassword.jsx b/src/components/windows/ForgotPassword.jsx index 0e91800..041d7e1 100644 --- a/src/components/windows/ForgotPassword.jsx +++ b/src/components/windows/ForgotPassword.jsx @@ -1,35 +1,94 @@ -/** - * +/* + * Form for requesting password-reset mail * @flow */ - -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { t } from 'ttag'; -import { showUserAreaModal } from '../../actions'; -import NewPasswordForm from '../NewPasswordForm'; +import { changeWindowType } from '../../actions'; +import { validateEMail } from '../../utils/validation'; +import { requestNewPassword } from '../../actions/fetch'; -const ForgotPassword = ({ login }) => ( -

-

- {t`Enter your mail address and we will send you a new password:`} -


-

- -

{t`Consider joining us on Guilded:`}  - pixelplanet.fun/guilded -

-

-

-); - -function mapDispatchToProps(dispatch) { - return { - login() { - dispatch(showUserAreaModal()); - }, - }; +function validate(email) { + const errors = []; + const mailerror = validateEMail(email); + if (mailerror) errors.push(mailerror); + return errors; } -export default connect(null, mapDispatchToProps)(ForgotPassword); +const inputStyles = { + display: 'inline-block', + width: '100%', + maxWidth: '35em', +}; + +const ForgotPassword = ({ windowId }) => { + const [email, setEmail] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [success, setSuccess] = useState(false); + const [errors, setErrors] = useState([]); + + const dispatch = useDispatch(); + const back = () => dispatch(changeWindowType(windowId, 'USERAREA')); + + const handleSubmit = async () => { + if (submitting) { + return; + } + + const valErrors = validate(email); + if (valErrors.length > 0) { + setErrors(valErrors); + return; + } + + setSubmitting(true); + const { errors: respErrors } = await requestNewPassword(email); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); + return; + } + setSuccess(true); + }; + + if (success) { + return ( +
+

+ {t`Sent you a mail with instructions to reset your password.`} +

+ +
+ ); + } + return ( +
+

+ {t`Enter your mail address and we will send you a new password:`} +


+ + {errors.map((error) => ( +

{t`Error`}: {error}

+ ))} + setEmail(evt.target.value)} + type="text" + placeholder={t`Email`} + /> +
+ + + +
+ ); +}; + +export default React.memo(ForgotPassword); diff --git a/src/components/windows/Minecraft.jsx b/src/components/windows/Minecraft.jsx deleted file mode 100644 index 544144c..0000000 --- a/src/components/windows/Minecraft.jsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * - * @flow - */ - -import React from 'react'; -import { connect } from 'react-redux'; - - -const MinecraftModal = () => ( -

-

You can also place pixels from our Minecraft Server at

-

-

Please Note that the Minecraft Server is down from time to time

-

-); - -function mapStateToProps(state: State) { - const { center } = state.user; - return { center }; -} - -const data = { - content: connect(mapStateToProps)(MinecraftModal), - title: 'PixelPlanet Minecraft Server', -}; - -export default data; diff --git a/src/components/windows/Register.jsx b/src/components/windows/Register.jsx index 4a13deb..bad3a7d 100644 --- a/src/components/windows/Register.jsx +++ b/src/components/windows/Register.jsx @@ -1,36 +1,133 @@ -/** - * +/* + * SignUp Form to register new user by mail * @flow */ -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; import { t } from 'ttag'; +import { + validateEMail, validateName, validatePassword, +} from '../../utils/validation'; +import { requestRegistration } from '../../actions/fetch'; -import { showUserAreaModal } from '../../actions'; - -// import { send_registration } from '../ui/register'; -import SignUpForm from '../SignUpForm'; +import { changeWindowType, loginUser } from '../../actions'; -const Register = ({ login }) => ( -

-

{t`Register new account here`}


-

- -

{t`Consider joining us on Guilded:`}  - pixelplanet.fun/guilded -

-

-

-); +function validate(name, email, password, confirmPassword) { + const errors = []; + const mailerror = validateEMail(email); + if (mailerror) errors.push(mailerror); + const nameerror = validateName(name); + if (nameerror) errors.push(nameerror); + const passworderror = validatePassword(password); + if (passworderror) errors.push(passworderror); -function mapDispatchToProps(dispatch) { - return { - login() { - dispatch(showUserAreaModal()); - }, - }; + if (password !== confirmPassword) { + errors.push('Passwords do not match'); + } + return errors; } -export default connect(null, mapDispatchToProps)(Register); +const inputStyles = { + display: 'inline-block', + width: '100%', + maxWidth: '35em', +}; + +const Register = ({ windowId }) => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [submitting, setSubmitting] = useState(''); + const [errors, setErrors] = useState([]); + + const dispatch = useDispatch(); + + const handleSubmit = async () => { + if (submitting) { + return; + } + + const valErrors = validate(name, email, password, confirmPassword); + if (valErrors.length > 0) { + setErrors(valErrors); + return; + } + + setSubmitting(true); + const { errors: respErrors, me } = await requestRegistration( + name, + email, + password, + ); + setSubmitting(false); + if (respErrors) { + setErrors(respErrors); + return; + } + + dispatch(loginUser(me)); + dispatch(changeWindowType(windowId, 'USERAREA')); + }; + + return ( +
+
+

{t`Register new account here`}


+ {errors.map((error) => ( +

{t`Error`} + : {error}

+ ))} + setName(evt.target.value)} + type="text" + placeholder={t`Name`} + />
+ setEmail(evt.target.value)} + type="text" + placeholder={t`Email`} + />
+ setPassword(evt.target.value)} + type="password" + placeholder={t`Password`} + />
+ setConfirmPassword(evt.target.value)} + type="password" + placeholder={t`Confirm Password`} + />
+ + +
+
+ ); +}; + + +export default React.memo(Register); diff --git a/src/components/windows/Settings.jsx b/src/components/windows/Settings.jsx index 59580c4..e3f43db 100644 --- a/src/components/windows/Settings.jsx +++ b/src/components/windows/Settings.jsx @@ -120,7 +120,7 @@ function Settings({ chatNotify, }) { return ( -

+

)} -

+
); } diff --git a/src/components/windows/UserArea.jsx b/src/components/windows/UserArea.jsx index 15c81d1..52e11d6 100644 --- a/src/components/windows/UserArea.jsx +++ b/src/components/windows/UserArea.jsx @@ -4,18 +4,12 @@ */ import React, { Suspense } from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { t } from 'ttag'; -import type { State } from '../../reducers'; - - -import { - showRegisterModal, showForgotPasswordModal, setName, setMailreg, -} from '../../actions'; -import LogInForm from '../LogInForm'; +import LogInArea from '../LogInArea'; import Tabs from '../Tabs'; -import UserAreaContent from '../UserArea'; +import UserAreaContent from '../UserAreaContent'; import Rankings from '../Rankings'; // eslint-disable-next-line max-len @@ -23,136 +17,38 @@ const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ '. // eslint-disable-next-line max-len const Admintools = React.lazy(() => import(/* webpackChunkName: "admintools" */ '../Admintools')); -const logoStyle = { - marginRight: 5, +const UserArea = ({ windowId }) => { + const name = useSelector((state) => state.user.name); + const userlvl = useSelector((state) => state.user.userlvl); + + return ( +
+ +
+ {(name) ? : } +
+
+ +
+
+ Loading...
}> + + +
+ {userlvl && ( +
+ {t`Loading...`}
}> + + +
+ )} + +
+ {t`Consider joining us on Guilded:`}  + pixelplanet.fun/guilded +
+
+ ); }; -const LogInArea = ({ register, forgotPassword, me }) => ( -

-

- {t`Login to access more features and stats.`} -


-

{t`Login with Name or Mail:`}

- -

- {t`I forgot my Password.`}

-

{t`or login with:`}

- - Discord - - - Google - - - Facebook - - - VK - - - Reddit - -

{t`or register here:`}

- -

-); - -const UserArea = ({ - name, - register, - forgotPassword, - setUserName, - setUserMailreg, - userlvl, -}) => ( -

- {(name === null) - ? ( - - ) - : ( - -

- -
-
- -
-
- Loading...
}> - - -
- {userlvl && ( -
- {t`Loading...`}
}> - - -
- )} - - )} -

{t`Consider joining us on Guilded:`}  - pixelplanet.fun/guilded -

-

-); - -function mapDispatchToProps(dispatch) { - return { - register() { - dispatch(showRegisterModal()); - }, - forgotPassword() { - dispatch(showForgotPasswordModal()); - }, - setUserName(name) { - dispatch(setName(name)); - }, - setUserMailreg(mailreg) { - dispatch(setMailreg(mailreg)); - }, - }; -} - -function mapStateToProps(state: State) { - const { name, userlvl } = state.user; - return { name, userlvl }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(UserArea); +export default React.memo(UserArea); diff --git a/src/components/windows/index.jsx b/src/components/windows/index.jsx index f92d96a..fb7b36b 100644 --- a/src/components/windows/index.jsx +++ b/src/components/windows/index.jsx @@ -1,9 +1,6 @@ /* * @flow */ - -import React from 'react'; - import Help from './Help'; import Settings from './Settings'; import UserArea from './UserArea'; @@ -14,7 +11,7 @@ import Chat from './Chat'; import ForgotPassword from './ForgotPassword'; export default { - NONE:
, + NONE: null, HELP: Help, SETTINGS: Settings, USERAREA: UserArea, @@ -23,5 +20,5 @@ export default { CHAT: Chat, CANVAS_SELECTION: CanvasSelect, ARCHIVE: Archive, - /* other modals */ + /* other windows */ }; diff --git a/src/reducers/windows.js b/src/reducers/windows.js index 25d54e6..289b5e5 100644 --- a/src/reducers/windows.js +++ b/src/reducers/windows.js @@ -327,6 +327,43 @@ export default function windows( }; } + case 'CHANGE_WINDOW_TYPE': { + const { + windowId, + windowType, + } = action; + const args = { + ...state.args, + [windowId]: { + ...action.args, + }, + }; + if (windowId === 0) { + return { + ...state, + args, + modal: { + ...state.modal, + windowType, + }, + }; + } + const newWindows = state.windows.map((win) => { + if (win.windowId !== windowId) { + return win; + } + return { + ...win, + windowType, + }; + }); + return { + ...state, + args, + windows: newWindows, + }; + } + case 'FOCUS_WINDOW': { const { windowId, diff --git a/src/styles/default.css b/src/styles/default.css index 41df2b8..ce3b5fd 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -239,7 +239,7 @@ tr:nth-child(even) { border-width: thin; border-radius: 4px; width: 50px; - height: 100%; + height: 22px; white-space: nowrap; font-size: 14px; overflow-x: hidden;