LogInForm, Rankings, ChangeMail, NewPasswordForm, DeleteAccount, UserArea and ChangeName from class to hook
create clickOutside hooks and use those in context menus fix window naviagions
This commit is contained in:
parent
a60242617d
commit
e80e5737ea
|
@ -220,10 +220,17 @@ export function requestPasswordChange(newPassword, password) {
|
||||||
|
|
||||||
export async function requestResendVerify() {
|
export async function requestResendVerify() {
|
||||||
return makeAPIGETRequest(
|
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) {
|
export function requestNameChange(name) {
|
||||||
return makeAPIPOSTRequest(
|
return makeAPIPOSTRequest(
|
||||||
'api/auth/change_name',
|
'api/auth/change_name',
|
||||||
|
|
|
@ -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 {
|
export function showRegisterModal(): Action {
|
||||||
return showModal(
|
return showModal(
|
||||||
'REGISTER',
|
'REGISTER',
|
||||||
|
|
|
@ -94,6 +94,11 @@ export type Action =
|
||||||
| { type: 'RESTORE_WINDOW' }
|
| { type: 'RESTORE_WINDOW' }
|
||||||
| { type: 'MOVE_WINDOW', windowId: number, xDiff: number, yDiff: number }
|
| { type: 'MOVE_WINDOW', windowId: number, xDiff: number, yDiff: number }
|
||||||
| { type: 'RESIZE_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: 'BLOCK_USER', userId: number, userName: string }
|
||||||
| { type: 'UNBLOCK_USER', userId: number, userName: string }
|
| { type: 'UNBLOCK_USER', userId: number, userName: string }
|
||||||
| { type: 'SET_BLOCKING_DM', blockDm: boolean }
|
| { type: 'SET_BLOCKING_DM', blockDm: boolean }
|
||||||
|
|
|
@ -195,7 +195,7 @@ function Admintools({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
|
<div style={{ textAlign: 'center', paddingLeft: '5%', paddingRight: '5%' }}>
|
||||||
{resp && (
|
{resp && (
|
||||||
<div style={{
|
<div style={{
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
|
@ -593,7 +593,7 @@ function Admintools({
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateEMail, validatePassword,
|
validateEMail, validatePassword,
|
||||||
} from '../utils/validation';
|
} from '../utils/validation';
|
||||||
|
@ -21,96 +22,78 @@ function validate(email, password) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChangeMail extends React.Component {
|
const ChangeMail = ({ done }) => {
|
||||||
constructor() {
|
const [password, setPassword] = useState('');
|
||||||
super();
|
const [email, setEmail] = useState('');
|
||||||
this.state = {
|
const [submitting, setSubmitting] = useState(false);
|
||||||
password: '',
|
const [success, setSuccess] = useState(false);
|
||||||
email: '',
|
const [errors, setErrors] = useState([]);
|
||||||
submitting: false,
|
|
||||||
success: false,
|
|
||||||
|
|
||||||
errors: [],
|
const handleSubmit = async () => {
|
||||||
};
|
if (submitting) {
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
success: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const valErrors = validate(email, password);
|
||||||
const { success } = this.state;
|
if (valErrors.length > 0) {
|
||||||
const { done } = this.props;
|
setErrors(valErrors);
|
||||||
if (success) {
|
return;
|
||||||
return (
|
|
||||||
<div className="inarea">
|
|
||||||
<p
|
|
||||||
className="modalmessage"
|
|
||||||
>
|
|
||||||
{t`Changed Mail successfully. We sent you a verification mail, \
|
|
||||||
please verify your new mail address.`}
|
|
||||||
</p>
|
|
||||||
<button type="button" onClick={done}>Close</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const {
|
|
||||||
errors, password, email, submitting,
|
setSubmitting(true);
|
||||||
} = this.state;
|
const { errors: respErrors } = await requestMailChange(email, password);
|
||||||
|
setSubmitting(false);
|
||||||
|
if (respErrors) {
|
||||||
|
setErrors(respErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuccess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="inarea">
|
<div className="inarea">
|
||||||
<form onSubmit={this.handleSubmit}>
|
<p
|
||||||
{errors.map((error) => (
|
className="modalmessage"
|
||||||
<p key={error} className="errormessage">
|
>
|
||||||
<span>{t`Error`}</span>:
|
{t`Changed Mail successfully. We sent you a verification mail, \
|
||||||
{error}
|
please verify your new mail address.`}
|
||||||
</p>
|
</p>
|
||||||
))}
|
<button type="button" onClick={done}>Close</button>
|
||||||
<input
|
|
||||||
value={password}
|
|
||||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
|
||||||
type="password"
|
|
||||||
placeholder={t`Password`}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<input
|
|
||||||
value={email}
|
|
||||||
onChange={(evt) => this.setState({ email: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder={t`New Mail`}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button type="submit">
|
|
||||||
{(submitting) ? '...' : t`Save`}
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={done}>{t`Cancel`}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default ChangeMail;
|
return (
|
||||||
|
<div className="inarea">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error} className="errormessage">
|
||||||
|
<span>{t`Error`}</span>:
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
value={password}
|
||||||
|
onChange={(evt) => setPassword(evt.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder={t`Password`}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
value={email}
|
||||||
|
onChange={(evt) => setEmail(evt.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={t`New Mail`}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">
|
||||||
|
{(submitting) ? '...' : t`Save`}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={done}>{t`Cancel`}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ChangeMail);
|
||||||
|
|
|
@ -3,10 +3,13 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { validateName } from '../utils/validation';
|
import { validateName } from '../utils/validation';
|
||||||
import { requestNameChange } from '../actions/fetch';
|
import { requestNameChange } from '../actions/fetch';
|
||||||
|
import { setName } from '../actions';
|
||||||
|
|
||||||
|
|
||||||
function validate(name) {
|
function validate(name) {
|
||||||
|
@ -18,69 +21,56 @@ function validate(name) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChangeName extends React.Component {
|
const ChangeName = ({ done }) => {
|
||||||
constructor() {
|
const [name, setStName] = useState('');
|
||||||
super();
|
const [submitting, setSubmitting] = useState(false);
|
||||||
this.state = {
|
const [errors, setErrors] = useState([]);
|
||||||
name: '',
|
|
||||||
submitting: false,
|
|
||||||
|
|
||||||
errors: [],
|
const dispatch = useDispatch();
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
const handleSubmit = async () => {
|
||||||
}
|
if (submitting) {
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
return;
|
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();
|
done();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { errors, name, submitting } = this.state;
|
<div className="inarea">
|
||||||
const { done } = this.props;
|
<form onSubmit={handleSubmit}>
|
||||||
return (
|
{errors.map((error) => (
|
||||||
<div className="inarea">
|
<p key={error} className="errormessage">
|
||||||
<form onSubmit={this.handleSubmit}>
|
<span>{t`Error`}</span>: {error}</p>
|
||||||
{errors.map((error) => (
|
))}
|
||||||
<p key={error} className="errormessage">
|
<input
|
||||||
<span>{t`Error`}</span>: {error}</p>
|
value={name}
|
||||||
))}
|
onChange={(evt) => setStName(evt.target.value)}
|
||||||
<input
|
type="text"
|
||||||
value={name}
|
placeholder={t`New Username`}
|
||||||
onChange={(evt) => this.setState({ name: evt.target.value })}
|
/>
|
||||||
type="text"
|
<br />
|
||||||
placeholder={t`New Username`}
|
<button type="submit">
|
||||||
/>
|
{(submitting) ? '...' : t`Save`}
|
||||||
<br />
|
</button>
|
||||||
<button type="submit">
|
<button type="button" onClick={done}>{t`Cancel`}</button>
|
||||||
{(submitting) ? '...' : t`Save`}
|
</form>
|
||||||
</button>
|
</div>
|
||||||
<button type="button" onClick={done}>{t`Cancel`}</button>
|
);
|
||||||
</form>
|
};
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChangeName;
|
export default React.memo(ChangeName);
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { setMailreg } from '../actions';
|
||||||
import { validatePassword } from '../utils/validation';
|
import { validatePassword } from '../utils/validation';
|
||||||
import { requestPasswordChange } from '../actions/fetch';
|
import { requestPasswordChange } from '../actions/fetch';
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ function validate(mailreg, password, newPassword, confirmPassword) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangePassword = ({ mailreg, done, cancel }) => {
|
const ChangePassword = ({ done }) => {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
@ -33,6 +36,9 @@ const ChangePassword = ({ mailreg, done, cancel }) => {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [errors, setErrors] = useState([]);
|
const [errors, setErrors] = useState([]);
|
||||||
|
|
||||||
|
const mailreg = useSelector((state) => state.user.mailreg);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="inarea">
|
<div className="inarea">
|
||||||
|
@ -66,6 +72,7 @@ const ChangePassword = ({ mailreg, done, cancel }) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
dispatch(setMailreg(true));
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -104,7 +111,7 @@ const ChangePassword = ({ mailreg, done, cancel }) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cancel}
|
onClick={done}
|
||||||
>
|
>
|
||||||
{t`Cancel`}
|
{t`Cancel`}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -270,8 +270,8 @@ function Converter({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<p className="modalcotext">{t`Choose Canvas`}:
|
<div className="modalcotext">{t`Choose Canvas`}:
|
||||||
<select
|
<select
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const sel = e.target;
|
const sel = e.target;
|
||||||
|
@ -296,9 +296,9 @@ function Converter({
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</div>
|
||||||
<h3 className="modaltitle">{t`Palette Download`}</h3>
|
<h3 className="modaltitle">{t`Palette Download`}</h3>
|
||||||
<p className="modalcotext">
|
<div className="modalcotext">
|
||||||
{jt`Palette for ${gimpLink}`}:
|
{jt`Palette for ${gimpLink}`}:
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -319,7 +319,7 @@ function Converter({
|
||||||
<p>
|
<p>
|
||||||
{jt`Credit for the Palette of the Moon goes to ${starhouseLink}.`}
|
{jt`Credit for the Palette of the Moon goes to ${starhouseLink}.`}
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
<h3 className="modaltitle">{t`Image Converter`}</h3>
|
<h3 className="modaltitle">{t`Image Converter`}</h3>
|
||||||
<p className="modalcotext">{t`Convert an image to canvas colors`}</p>
|
<p className="modalcotext">{t`Convert an image to canvas colors`}</p>
|
||||||
<input
|
<input
|
||||||
|
@ -611,7 +611,7 @@ function Converter({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import { validatePassword } from '../utils/validation';
|
import { validatePassword } from '../utils/validation';
|
||||||
|
@ -20,76 +20,55 @@ function validate(password) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteAccount extends React.Component {
|
const DeleteAccount = ({ done }) => {
|
||||||
constructor() {
|
const [password, setPassword] = useState('');
|
||||||
super();
|
const [submitting, setSubmitting] = useState(false);
|
||||||
this.state = {
|
const [errors, setErrors] = useState([]);
|
||||||
password: '',
|
|
||||||
submitting: false,
|
|
||||||
|
|
||||||
errors: [],
|
const dispatch = useDispatch();
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
const handleSubmit = async () => {
|
||||||
}
|
if (submitting) {
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { logout } = this.props;
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const valErrors = validate(password);
|
||||||
const { errors, password, submitting } = this.state;
|
if (valErrors.length > 0) {
|
||||||
const { done } = this.props;
|
setErrors(valErrors);
|
||||||
return (
|
return;
|
||||||
<div className="inarea" style={{ backgroundColor: '#ff6666' }}>
|
}
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
{errors.map((error) => (
|
|
||||||
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
|
||||||
: {error}</p>
|
|
||||||
))}
|
|
||||||
<input
|
|
||||||
value={password}
|
|
||||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
|
||||||
type="password"
|
|
||||||
placeholder={t`Password`}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button type="submit">
|
|
||||||
{(submitting) ? '...' : t`Yes, Delete My Account!`}
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={done}>{t`Cancel`}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
setSubmitting(true);
|
||||||
return {
|
const { errors: respErrors } = await requestDeleteAccount(password);
|
||||||
async logout() {
|
setSubmitting(false);
|
||||||
dispatch(logoutUser());
|
if (respErrors) {
|
||||||
},
|
setErrors(respErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(logoutUser());
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(DeleteAccount);
|
return (
|
||||||
|
<div className="inarea" style={{ backgroundColor: '#ff6666' }}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
||||||
|
: {error}</p>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
value={password}
|
||||||
|
onChange={(evt) => setPassword(evt.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder={t`Password`}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">
|
||||||
|
{(submitting) ? '...' : t`Yes, Delete My Account!`}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={done}>{t`Cancel`}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeleteAccount);
|
||||||
|
|
85
src/components/LogInArea.jsx
Normal file
85
src/components/LogInArea.jsx
Normal file
|
@ -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 (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<p className="modaltext">
|
||||||
|
{t`Login to access more features and stats.`}
|
||||||
|
</p><br />
|
||||||
|
<h2>{t`Login with Name or Mail:`}</h2>
|
||||||
|
<LogInForm />
|
||||||
|
<p
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => dispatch(changeWindowType(windowId, 'FORGOT_PASSWORD'))}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
{t`I forgot my Password.`}</p>
|
||||||
|
<h2>{t`or login with:`}</h2>
|
||||||
|
<a href="./api/auth/discord">
|
||||||
|
<img
|
||||||
|
style={logoStyle}
|
||||||
|
width={32}
|
||||||
|
src={`${window.ssv.assetserver}/discordlogo.svg`}
|
||||||
|
alt="Discord"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="./api/auth/google">
|
||||||
|
<img
|
||||||
|
style={logoStyle}
|
||||||
|
width={32}
|
||||||
|
src={`${window.ssv.assetserver}/googlelogo.svg`}
|
||||||
|
alt="Google"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="./api/auth/facebook">
|
||||||
|
<img
|
||||||
|
style={logoStyle}
|
||||||
|
width={32}
|
||||||
|
src={`${window.ssv.assetserver}/facebooklogo.svg`}
|
||||||
|
alt="Facebook"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="./api/auth/vk">
|
||||||
|
<img
|
||||||
|
style={logoStyle}
|
||||||
|
width={32}
|
||||||
|
src={`${window.ssv.assetserver}/vklogo.svg`}
|
||||||
|
alt="VK"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="./api/auth/reddit">
|
||||||
|
<img
|
||||||
|
style={logoStyle}
|
||||||
|
width={32}
|
||||||
|
src={`${window.ssv.assetserver}/redditlogo.svg`}
|
||||||
|
alt="Reddit"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<h2>{t`or register here:`}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={
|
||||||
|
() => dispatch(changeWindowType(windowId, 'REGISTER'))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t`Register`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LogInArea);
|
|
@ -2,8 +2,8 @@
|
||||||
* LogIn Form
|
* LogIn Form
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -31,86 +31,64 @@ const inputStyles = {
|
||||||
maxWidth: '35em',
|
maxWidth: '35em',
|
||||||
};
|
};
|
||||||
|
|
||||||
class LogInForm extends React.Component {
|
const LogInForm = () => {
|
||||||
constructor() {
|
const [nameoremail, setNameOrEmail] = useState('');
|
||||||
super();
|
const [password, setPassword] = useState('');
|
||||||
this.state = {
|
const [submitting, setSubmitting] = useState(false);
|
||||||
nameoremail: '',
|
const [errors, setErrors] = useState([]);
|
||||||
password: '',
|
|
||||||
submitting: false,
|
|
||||||
|
|
||||||
errors: [],
|
const dispatch = useDispatch();
|
||||||
};
|
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
const handleSubmit = async () => {
|
||||||
}
|
if (submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
async handleSubmit(e) {
|
const valErrors = validate(nameoremail, password);
|
||||||
e.preventDefault();
|
if (valErrors.length > 0) {
|
||||||
|
setErrors(valErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { nameoremail, password, submitting } = this.state;
|
setSubmitting(true);
|
||||||
const { login } = this.props;
|
const { errors: respErrors, me } = await requestLogin(
|
||||||
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(
|
|
||||||
nameoremail,
|
nameoremail,
|
||||||
password,
|
password,
|
||||||
);
|
);
|
||||||
if (resperrors) {
|
setSubmitting(false);
|
||||||
this.setState({
|
if (respErrors) {
|
||||||
errors: resperrors,
|
setErrors(respErrors);
|
||||||
submitting: false,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
login(me);
|
dispatch(loginUser(me));
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
errors, nameoremail, password, submitting,
|
|
||||||
} = this.state;
|
|
||||||
return (
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
{errors.map((error) => (
|
|
||||||
<p key={error}><span>{t`Error`}</span>: {error}</p>
|
|
||||||
))}
|
|
||||||
<input
|
|
||||||
value={nameoremail}
|
|
||||||
style={inputStyles}
|
|
||||||
onChange={(evt) => this.setState({ nameoremail: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder={t`Name or Email`}
|
|
||||||
/><br />
|
|
||||||
<input
|
|
||||||
value={password}
|
|
||||||
style={inputStyles}
|
|
||||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
|
||||||
type="password"
|
|
||||||
placeholder={t`Password`}
|
|
||||||
/>
|
|
||||||
<p>
|
|
||||||
<button type="submit">
|
|
||||||
{(submitting) ? '...' : t`LogIn`}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
login(me) {
|
|
||||||
dispatch(loginUser(me));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(LogInForm);
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error}><span>{t`Error`}</span>: {error}</p>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
value={nameoremail}
|
||||||
|
style={inputStyles}
|
||||||
|
onChange={(evt) => setNameOrEmail(evt.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={t`Name or Email`}
|
||||||
|
/><br />
|
||||||
|
<input
|
||||||
|
value={password}
|
||||||
|
style={inputStyles}
|
||||||
|
onChange={(evt) => setPassword(evt.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder={t`Password`}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<button type="submit">
|
||||||
|
{(submitting) ? '...' : t`LogIn`}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(LogInForm);
|
||||||
|
|
|
@ -11,13 +11,6 @@ import HelpButton from './buttons/HelpButton';
|
||||||
import SettingsButton from './buttons/SettingsButton';
|
import SettingsButton from './buttons/SettingsButton';
|
||||||
import LogInButton from './buttons/LogInButton';
|
import LogInButton from './buttons/LogInButton';
|
||||||
import DownloadButton from './buttons/DownloadButton';
|
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({
|
function Menu({
|
||||||
menuOpen,
|
menuOpen,
|
||||||
|
|
|
@ -44,7 +44,7 @@ const ModalRoot = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = COMPONENTS[windowType || 'NONE'];
|
const Content = COMPONENTS[windowType];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(render || open)
|
(render || open)
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<p className="modalmessage">
|
|
||||||
{t`Sent you a mail with instructions to reset your password.`}
|
|
||||||
</p>
|
|
||||||
<button type="button" onClick={back}>Back</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { errors, email, submitting } = this.state;
|
|
||||||
return (
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
{errors.map((error) => (
|
|
||||||
<p key={error}><span>{t`Error`}</span>: {error}</p>
|
|
||||||
))}
|
|
||||||
<input
|
|
||||||
style={inputStyles}
|
|
||||||
value={email}
|
|
||||||
onChange={(evt) => this.setState({ email: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder={t`Email`}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<button type="submit">
|
|
||||||
{(submitting) ? '...' : t`Submit`}
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={back}>{t`Cancel`}</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewPasswordForm;
|
|
|
@ -5,52 +5,42 @@
|
||||||
|
|
||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import TotalRankings from './TotalRankings';
|
import TotalRankings from './TotalRankings';
|
||||||
import DailyRankings from './DailyRankings';
|
import DailyRankings from './DailyRankings';
|
||||||
|
|
||||||
|
|
||||||
class Rankings extends React.Component {
|
const Rankings = () => {
|
||||||
constructor() {
|
const [orderDaily, setOrderDaily] = useState(false);
|
||||||
super();
|
|
||||||
this.state = {
|
|
||||||
orderDaily: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { orderDaily } = this.state;
|
<div>
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<span
|
||||||
<span
|
role="button"
|
||||||
role="button"
|
tabIndex={-1}
|
||||||
tabIndex={-1}
|
className={
|
||||||
className={
|
(!orderDaily) ? 'modallinkselected' : 'modallink'
|
||||||
(!orderDaily) ? 'modallinkselected' : 'modallink'
|
}
|
||||||
}
|
onClick={() => setOrderDaily(false)}
|
||||||
onClick={() => {
|
>{t`Total`}</span> |
|
||||||
this.setState({ orderDaily: false });
|
<span
|
||||||
}}
|
role="button"
|
||||||
>{t`Total`}</span> |
|
tabIndex={-1}
|
||||||
<span
|
className={
|
||||||
role="button"
|
(orderDaily) ? 'modallinkselected' : 'modallink'
|
||||||
tabIndex={-1}
|
}
|
||||||
className={
|
onClick={() => setOrderDaily(true)}
|
||||||
(orderDaily) ? 'modallinkselected' : 'modallink'
|
>{t`Daily`}</span>
|
||||||
}
|
|
||||||
onClick={() => { this.setState({ orderDaily: true }); }}
|
|
||||||
>{t`Daily`}</span>
|
|
||||||
</p>
|
|
||||||
{(orderDaily) ? <DailyRankings /> : <TotalRankings />}
|
|
||||||
<p className="modaltext">
|
|
||||||
{t`Ranking updates every 5 min. Daily rankings get reset at midnight UTC.`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{(orderDaily) ? <DailyRankings /> : <TotalRankings />}
|
||||||
}
|
<p className="modaltext">
|
||||||
}
|
{t`Ranking updates every 5 min. Daily rankings get reset at midnight UTC.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Rankings;
|
export default React.memo(Rankings);
|
||||||
|
|
|
@ -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 (
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
{errors.map((error) => (
|
|
||||||
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
|
||||||
: {error}</p>
|
|
||||||
))}
|
|
||||||
<input
|
|
||||||
style={inputStyles}
|
|
||||||
value={name}
|
|
||||||
autoComplete="username"
|
|
||||||
onChange={(evt) => this.setState({ name: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder={t`Name`}
|
|
||||||
/><br />
|
|
||||||
<input
|
|
||||||
style={inputStyles}
|
|
||||||
value={email}
|
|
||||||
autoComplete="email"
|
|
||||||
onChange={(evt) => this.setState({ email: evt.target.value })}
|
|
||||||
type="text"
|
|
||||||
placeholder={t`Email`}
|
|
||||||
/><br />
|
|
||||||
<input
|
|
||||||
style={inputStyles}
|
|
||||||
value={password}
|
|
||||||
autoComplete="new-password"
|
|
||||||
onChange={(evt) => this.setState({ password: evt.target.value })}
|
|
||||||
type="password"
|
|
||||||
placeholder={t`Password`}
|
|
||||||
/><br />
|
|
||||||
<input
|
|
||||||
style={inputStyles}
|
|
||||||
value={confirmPassword}
|
|
||||||
autoComplete="new-password"
|
|
||||||
onChange={(evt) => this.setState({
|
|
||||||
confirmPassword: evt.target.value,
|
|
||||||
})}
|
|
||||||
type="password"
|
|
||||||
placeholder={t`Confirm Password`}
|
|
||||||
/><br />
|
|
||||||
<button type="submit">
|
|
||||||
{(submitting) ? '...' : t`Submit`}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={back}
|
|
||||||
>
|
|
||||||
{t`Cancel`}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
login(me) {
|
|
||||||
dispatch(loginUser(me));
|
|
||||||
},
|
|
||||||
userarea() {
|
|
||||||
dispatch(showUserAreaModal());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(SignUpForm);
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -13,105 +13,83 @@ import {
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
import MdToggleButtonHover from './MdToggleButtonHover';
|
import MdToggleButtonHover from './MdToggleButtonHover';
|
||||||
|
|
||||||
const SocialSettings = ({
|
const SocialSettings = ({ done }) => {
|
||||||
blocked,
|
const blocked = useSelector((state) => state.chat.blocked);
|
||||||
fetching,
|
const blockDm = useSelector((state) => state.user.blockDm);
|
||||||
blockDm,
|
const fetching = useSelector((state) => state.fetching.fetchingApi);
|
||||||
setBlockDm,
|
const dispatch = useDispatch();
|
||||||
unblock,
|
|
||||||
done,
|
return (
|
||||||
}) => (
|
<div className="inarea">
|
||||||
<div className="inarea">
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: 'flex',
|
||||||
display: 'flex',
|
flexWrap: 'nowrap',
|
||||||
flexWrap: 'nowrap',
|
margin: 10,
|
||||||
margin: 10,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span
|
||||||
<span
|
style={{
|
||||||
|
flex: 'auto',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
className="modaltitle"
|
||||||
|
>
|
||||||
|
{t`Block all Private Messages`}
|
||||||
|
</span>
|
||||||
|
<MdToggleButtonHover
|
||||||
|
value={blockDm}
|
||||||
|
onToggle={() => {
|
||||||
|
if (!fetching) {
|
||||||
|
dispatch(setBlockingDm(!blockDm));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="modaldivider" />
|
||||||
|
<p
|
||||||
style={{
|
style={{
|
||||||
flex: 'auto',
|
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
marginLeft: 10,
|
||||||
}}
|
}}
|
||||||
className="modaltitle"
|
className="modaltitle"
|
||||||
>
|
>{t`Unblock Users`}</p>
|
||||||
{t`Block all Private Messages`}
|
{
|
||||||
</span>
|
(blocked.length) ? (
|
||||||
<MdToggleButtonHover
|
<span
|
||||||
value={blockDm}
|
className="unblocklist"
|
||||||
onToggle={() => {
|
>
|
||||||
if (!fetching) {
|
{
|
||||||
setBlockDm(!blockDm);
|
blocked.map((bl) => (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
if (!fetching) {
|
||||||
|
dispatch(setUserBlock(bl[0], bl[1], false));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`⦸ ${bl[1]}`}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}}
|
</span>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="modaldivider" />
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
textAlign: 'left',
|
|
||||||
marginLeft: 10,
|
|
||||||
}}
|
|
||||||
className="modaltitle"
|
|
||||||
>{t`Unblock Users`}</p>
|
|
||||||
{
|
|
||||||
(blocked.length) ? (
|
|
||||||
<span
|
|
||||||
className="unblocklist"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
blocked.map((bl) => (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
if (!fetching) {
|
|
||||||
unblock(bl[0], bl[1]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`⦸ ${bl[1]}`}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<p className="modaltext">{t`You have no users blocked`}</p>
|
|
||||||
)
|
)
|
||||||
}
|
: (
|
||||||
<div className="modaldivider" />
|
<p className="modaltext">{t`You have no users blocked`}</p>
|
||||||
<button
|
)
|
||||||
type="button"
|
}
|
||||||
onClick={done}
|
<div className="modaldivider" />
|
||||||
style={{ margin: 10 }}
|
<button
|
||||||
>
|
type="button"
|
||||||
Done
|
onClick={done}
|
||||||
</button>
|
style={{ margin: 10 }}
|
||||||
</div>
|
>
|
||||||
);
|
Done
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
export default React.memo(SocialSettings);
|
||||||
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);
|
|
||||||
|
|
|
@ -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 }) => (
|
|
||||||
<p>
|
|
||||||
<span className="stattext">{(rank) ? `${text}: #` : `${text}: `}</span>
|
|
||||||
|
|
||||||
<span className="statvalue">{numberToString(value)}</span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
<UserMessages />
|
|
||||||
<Stat
|
|
||||||
text={t`Todays Placed Pixels`}
|
|
||||||
value={stats.dailyTotalPixels}
|
|
||||||
/>
|
|
||||||
<Stat
|
|
||||||
text={t`Daily Rank`}
|
|
||||||
value={stats.dailyRanking}
|
|
||||||
rank
|
|
||||||
/>
|
|
||||||
<Stat
|
|
||||||
text={t`Placed Pixels`}
|
|
||||||
value={stats.totalPixels}
|
|
||||||
/>
|
|
||||||
<Stat
|
|
||||||
text={t`Total Rank`}
|
|
||||||
value={stats.ranking}
|
|
||||||
rank
|
|
||||||
/>
|
|
||||||
<p className="modaltext">
|
|
||||||
<p>{t`Your name is: ${name}`}</p>(
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={logout}
|
|
||||||
> {t`Log out`}</span> |
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={() => this.setState({
|
|
||||||
changeNameExtended: true,
|
|
||||||
changeMailExtended: false,
|
|
||||||
changePasswdExtended: false,
|
|
||||||
deleteAccountExtended: false,
|
|
||||||
socialSettingsExtended: false,
|
|
||||||
})}
|
|
||||||
> {t`Change Username`}</span> |
|
|
||||||
{(mailreg)
|
|
||||||
&& (
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={() => this.setState({
|
|
||||||
changeNameExtended: false,
|
|
||||||
changeMailExtended: true,
|
|
||||||
changePasswdExtended: false,
|
|
||||||
deleteAccountExtended: false,
|
|
||||||
socialSettingsExtended: false,
|
|
||||||
})}
|
|
||||||
> {t`Change Mail`}</span> |
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={() => this.setState({
|
|
||||||
changeNameExtended: false,
|
|
||||||
changeMailExtended: false,
|
|
||||||
changePasswdExtended: true,
|
|
||||||
deleteAccountExtended: false,
|
|
||||||
socialSettingsExtended: false,
|
|
||||||
})}
|
|
||||||
> {t`Change Password`}</span> |
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={() => this.setState({
|
|
||||||
changeNameExtended: false,
|
|
||||||
changeMailExtended: false,
|
|
||||||
changePasswdExtended: false,
|
|
||||||
deleteAccountExtended: true,
|
|
||||||
socialSettingsExtended: false,
|
|
||||||
})}
|
|
||||||
> {t`Delete Account`}</span> )
|
|
||||||
<br />(
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabIndex={-1}
|
|
||||||
className="modallink"
|
|
||||||
onClick={() => this.setState({
|
|
||||||
changeNameExtended: false,
|
|
||||||
changeMailExtended: false,
|
|
||||||
changePasswdExtended: false,
|
|
||||||
deleteAccountExtended: false,
|
|
||||||
socialSettingsExtended: true,
|
|
||||||
})}
|
|
||||||
> {t`Social Settings`}</span> )
|
|
||||||
</p>
|
|
||||||
<p className="modaltext" />
|
|
||||||
{(changePasswdExtended)
|
|
||||||
&& (
|
|
||||||
<ChangePassword
|
|
||||||
mailreg={mailreg}
|
|
||||||
done={() => {
|
|
||||||
setMailreg(true);
|
|
||||||
this.setState({ changePasswdExtended: false });
|
|
||||||
}}
|
|
||||||
cancel={() => { this.setState({ changePasswdExtended: false }); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(changeNameExtended)
|
|
||||||
&& (
|
|
||||||
<ChangeName
|
|
||||||
setName={setName}
|
|
||||||
done={() => { this.setState({ changeNameExtended: false }); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(changeMailExtended)
|
|
||||||
&& (
|
|
||||||
<ChangeMail
|
|
||||||
done={() => { this.setState({ changeMailExtended: false }); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(deleteAccountExtended)
|
|
||||||
&& (
|
|
||||||
<DeleteAccount
|
|
||||||
done={() => { this.setState({ deleteAccountExtended: false }); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(socialSettingsExtended)
|
|
||||||
&& (
|
|
||||||
<SocialSettings
|
|
||||||
done={() => { this.setState({ socialSettingsExtended: false }); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
130
src/components/UserAreaContent.jsx
Normal file
130
src/components/UserAreaContent.jsx
Normal file
|
@ -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 }) => (
|
||||||
|
<p>
|
||||||
|
<span className="stattext">{(rank) ? `${text}: #` : `${text}: `}</span>
|
||||||
|
|
||||||
|
<span className="statvalue">{numberToString(value)}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<UserMessages />
|
||||||
|
<Stat
|
||||||
|
text={t`Todays Placed Pixels`}
|
||||||
|
value={stats.dailyTotalPixels}
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
text={t`Daily Rank`}
|
||||||
|
value={stats.dailyRanking}
|
||||||
|
rank
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
text={t`Placed Pixels`}
|
||||||
|
value={stats.totalPixels}
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
text={t`Total Rank`}
|
||||||
|
value={stats.ranking}
|
||||||
|
rank
|
||||||
|
/>
|
||||||
|
<div className="modaltext">
|
||||||
|
<p>{t`Your name is: ${name}`}</p>(
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={logout}
|
||||||
|
> {t`Log out`}</span> |
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setArea('CHANGE_NAME')}
|
||||||
|
> {t`Change Username`}</span> |
|
||||||
|
{(mailreg)
|
||||||
|
&& (
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setArea('CHANGE_MAIL')}
|
||||||
|
> {t`Change Mail`}</span> |
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setArea('CHANGE_PASSWORD')}
|
||||||
|
> {t`Change Password`}</span> |
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setArea('DELETE_ACCOUNT')}
|
||||||
|
> {t`Delete Account`}</span> )
|
||||||
|
<br />(
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => setArea('SOCIAL_SETTINGS')}
|
||||||
|
> {t`Social Settings`}</span> )
|
||||||
|
</div>
|
||||||
|
{(Area) && <Area done={() => setArea(null)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(UserAreaContent);
|
|
@ -4,16 +4,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { MdFileDownload } from 'react-icons/md';
|
import { MdFileDownload } from 'react-icons/md';
|
||||||
import fileDownload from 'js-file-download';
|
import fileDownload from 'js-file-download';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import { getRenderer } from '../../ui/renderer';
|
import { getRenderer } from '../../ui/renderer';
|
||||||
|
|
||||||
import type { State } from '../../reducers';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://jsfiddle.net/AbdiasSoftware/7PRNN/
|
* https://jsfiddle.net/AbdiasSoftware/7PRNN/
|
||||||
*/
|
*/
|
||||||
|
@ -29,23 +26,21 @@ function download(view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const DownloadButton = ({ view }) => (
|
const DownloadButton = () => {
|
||||||
<div
|
const view = useSelector((state) => state.canvas.view);
|
||||||
id="downloadbutton"
|
|
||||||
className="actionbuttons"
|
|
||||||
role="button"
|
|
||||||
title={t`Make Screenshot`}
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => download(view)}
|
|
||||||
>
|
|
||||||
<MdFileDownload />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO optimize
|
return (
|
||||||
function mapStateToProps(state: State) {
|
<div
|
||||||
const { view } = state.canvas;
|
id="downloadbutton"
|
||||||
return { view };
|
className="actionbuttons"
|
||||||
}
|
role="button"
|
||||||
|
title={t`Make Screenshot`}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => download(view)}
|
||||||
|
>
|
||||||
|
<MdFileDownload />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(DownloadButton);
|
export default React.memo(DownloadButton);
|
||||||
|
|
|
@ -4,12 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector, shallowEqual } from 'react-redux';
|
||||||
import { Md3DRotation } from 'react-icons/md';
|
import { Md3DRotation } from 'react-icons/md';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import type { State } from '../../reducers';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://jsfiddle.net/AbdiasSoftware/7PRNN/
|
* https://jsfiddle.net/AbdiasSoftware/7PRNN/
|
||||||
|
@ -21,28 +19,26 @@ function globe(canvasId, canvasIdent, canvasSize, view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const GlobeButton = ({
|
const GlobeButton = () => {
|
||||||
canvasId, canvasIdent, canvasSize, view,
|
const [canvasId, canvasIdent, canvasSize, view] = useSelector((state) => [
|
||||||
}) => (
|
state.canvas.canvasId,
|
||||||
<div
|
state.canvas.canvasIdent,
|
||||||
role="button"
|
state.canvas.canvasSize,
|
||||||
tabIndex={-1}
|
state.canvas.view,
|
||||||
id="globebutton"
|
], shallowEqual);
|
||||||
title={t`Globe View`}
|
|
||||||
className="actionbuttons"
|
|
||||||
onClick={() => globe(canvasId, canvasIdent, canvasSize, view)}
|
|
||||||
>
|
|
||||||
<Md3DRotation />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
return (
|
||||||
const {
|
<div
|
||||||
canvasId, canvasIdent, canvasSize, view,
|
role="button"
|
||||||
} = state.canvas;
|
tabIndex={-1}
|
||||||
return {
|
id="globebutton"
|
||||||
canvasId, canvasIdent, canvasSize, view,
|
title={t`Globe View`}
|
||||||
};
|
className="actionbuttons"
|
||||||
}
|
onClick={() => globe(canvasId, canvasIdent, canvasSize, view)}
|
||||||
|
>
|
||||||
|
<Md3DRotation />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(GlobeButton);
|
export default React.memo(GlobeButton);
|
||||||
|
|
|
@ -5,44 +5,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
|
||||||
import { MdPalette } from 'react-icons/md';
|
import { MdPalette } from 'react-icons/md';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import { toggleOpenPalette } from '../../actions';
|
import { toggleOpenPalette } from '../../actions';
|
||||||
|
|
||||||
const PalselButton = ({
|
const PalselButton = () => {
|
||||||
palette, onToggle, selectedColor, paletteOpen,
|
const paletteOpen = useSelector((state) => state.gui.paletteOpen);
|
||||||
}) => (
|
const [palette, selectedColor] = useSelector((state) => [
|
||||||
<div
|
state.canvas.palette,
|
||||||
id="palselbutton"
|
state.canvas.selectedColor,
|
||||||
className={`actionbuttons ${(paletteOpen) ? '' : 'pressed'}`}
|
], shallowEqual);
|
||||||
style={{
|
const dispatch = useDispatch();
|
||||||
color: palette.isDark(selectedColor) ? 'white' : 'black',
|
|
||||||
backgroundColor: palette.colors[selectedColor],
|
|
||||||
}}
|
|
||||||
role="button"
|
|
||||||
title={(paletteOpen) ? t`Close Palette` : t`Open Palette`}
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onToggle}
|
|
||||||
>
|
|
||||||
<MdPalette />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO simplify...
|
return (
|
||||||
function mapStateToProps(state: State) {
|
<div
|
||||||
const { paletteOpen } = state.gui;
|
id="palselbutton"
|
||||||
const { palette, selectedColor } = state.canvas;
|
className={`actionbuttons ${(paletteOpen) ? '' : 'pressed'}`}
|
||||||
return { palette, selectedColor, paletteOpen };
|
style={{
|
||||||
}
|
color: palette.isDark(selectedColor) ? 'white' : 'black',
|
||||||
|
backgroundColor: palette.colors[selectedColor],
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
title={(paletteOpen) ? t`Close Palette` : t`Open Palette`}
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => dispatch(toggleOpenPalette())}
|
||||||
|
>
|
||||||
|
<MdPalette />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
export default React.memo(PalselButton);
|
||||||
return {
|
|
||||||
onToggle() {
|
|
||||||
dispatch(toggleOpenPalette());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(PalselButton);
|
|
||||||
|
|
|
@ -3,49 +3,31 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, { useRef, useCallback } from 'react';
|
||||||
useRef, useEffect,
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
} from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClickOutside,
|
||||||
|
} from '../hooks/clickOutside';
|
||||||
import {
|
import {
|
||||||
hideContextMenu,
|
hideContextMenu,
|
||||||
setLeaveChannel,
|
setLeaveChannel,
|
||||||
muteChatChannel,
|
muteChatChannel,
|
||||||
unmuteChatChannel,
|
unmuteChatChannel,
|
||||||
} from '../../actions';
|
} from '../../actions';
|
||||||
import type { State } from '../../reducers';
|
|
||||||
|
|
||||||
const ChannelContextMenu = ({
|
const ChannelContextMenu = () => {
|
||||||
xPos,
|
|
||||||
yPos,
|
|
||||||
cid,
|
|
||||||
channels,
|
|
||||||
leave,
|
|
||||||
muteArr,
|
|
||||||
mute,
|
|
||||||
unmute,
|
|
||||||
close,
|
|
||||||
}) => {
|
|
||||||
const wrapperRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const channels = useSelector((state) => state.chat.channels);
|
||||||
const handleClickOutside = (event) => {
|
const muteArr = useSelector((state) => state.chatRead.mute);
|
||||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
const { xPos, yPos, args } = useSelector((state) => state.contextMenu);
|
||||||
event.stopPropagation();
|
const { cid } = args;
|
||||||
close();
|
const dispatch = useDispatch();
|
||||||
}
|
const close = useCallback(() => dispatch(hideContextMenu()), [dispatch]);
|
||||||
};
|
|
||||||
document.addEventListener('click', handleClickOutside, {
|
useClickOutside([wrapperRef], close);
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [wrapperRef]);
|
|
||||||
|
|
||||||
const isMuted = muteArr.includes(cid);
|
const isMuted = muteArr.includes(cid);
|
||||||
|
|
||||||
|
@ -62,9 +44,9 @@ const ChannelContextMenu = ({
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMuted) {
|
if (isMuted) {
|
||||||
unmute(cid);
|
dispatch(unmuteChatChannel(cid));
|
||||||
} else {
|
} else {
|
||||||
mute(cid);
|
dispatch(muteChatChannel(cid));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -77,7 +59,7 @@ const ChannelContextMenu = ({
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
leave(cid);
|
dispatch(setLeaveChannel(cid));
|
||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -89,43 +71,4 @@ const ChannelContextMenu = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
export default React.memo(ChannelContextMenu);
|
||||||
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);
|
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
useRef, useState, useEffect, useCallback, useLayoutEffect,
|
useRef, useState, useEffect, useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { MdChat } from 'react-icons/md';
|
import { MdChat } from 'react-icons/md';
|
||||||
import { FaUserFriends } from 'react-icons/fa';
|
import { FaUserFriends } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { useConditionalClickOutside } from '../hooks/clickOutside';
|
||||||
|
|
||||||
const ChannelDropDown = ({
|
const ChannelDropDown = ({
|
||||||
setChatChannel,
|
setChatChannel,
|
||||||
chatChannel,
|
chatChannel,
|
||||||
|
@ -37,35 +39,18 @@ const ChannelDropDown = ({
|
||||||
setOffset(buttonRef.current.clientHeight);
|
setOffset(buttonRef.current.clientHeight);
|
||||||
}, [buttonRef]);
|
}, [buttonRef]);
|
||||||
|
|
||||||
const handleClickOutside = useCallback((event) => {
|
useConditionalClickOutside(
|
||||||
if (wrapperRef.current
|
[wrapperRef],
|
||||||
&& !wrapperRef.current.contains(event.target)
|
show,
|
||||||
&& !buttonRef.current.contains(event.target)
|
useCallback(() => setShow(false), []),
|
||||||
) {
|
);
|
||||||
event.stopPropagation();
|
|
||||||
setShow(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleWindowResize = useCallback(() => {
|
useEffect(() => {
|
||||||
setShow(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (show) {
|
if (show) {
|
||||||
if (channels[chatChannel]) {
|
if (channels[chatChannel]) {
|
||||||
const chType = (channels[chatChannel][1] === 1) ? 1 : 0;
|
const chType = (channels[chatChannel][1] === 1) ? 1 : 0;
|
||||||
setType(chType);
|
setType(chType);
|
||||||
}
|
}
|
||||||
document.addEventListener('click', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('click', handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
window.removeEventListener('resize', handleWindowResize);
|
|
||||||
}
|
}
|
||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
|
@ -133,7 +118,7 @@ const ChannelDropDown = ({
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={() => setShow(!show)}
|
onClick={() => setShow(true)}
|
||||||
className={`channelbtn${(show) ? ' selected' : ''}`}
|
className={`channelbtn${(show) ? ' selected' : ''}`}
|
||||||
>
|
>
|
||||||
{(unreadAny && chatNotify && !show) && (
|
{(unreadAny && chatNotify && !show) && (
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, { useRef } from 'react';
|
||||||
useRef, useEffect,
|
|
||||||
} from 'react';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClickOutside,
|
||||||
|
} from '../hooks/clickOutside';
|
||||||
import {
|
import {
|
||||||
hideContextMenu,
|
hideContextMenu,
|
||||||
addToChatInputMessage,
|
addToChatInputMessage,
|
||||||
|
@ -29,22 +30,7 @@ const UserContextMenu = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const close = () => dispatch(hideContextMenu());
|
const close = () => dispatch(hideContextMenu());
|
||||||
|
|
||||||
useEffect(() => {
|
useClickOutside([wrapperRef], close);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
74
src/components/hooks/clickOutside.js
Normal file
74
src/components/hooks/clickOutside.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* @flex
|
||||||
|
*
|
||||||
|
* detect click outside
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useLayoutEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keeps listening to outside clicks or window resize
|
||||||
|
* as long as active is true
|
||||||
|
* @param insideRefs references to elements that are considered inside
|
||||||
|
* @param calback function that gets fired on click outside
|
||||||
|
* @param active boolean if we should listen or not
|
||||||
|
*/
|
||||||
|
export function useConditionalClickOutside(insideRefs, active, callback) {
|
||||||
|
const handleClickOutside = useCallback((event) => {
|
||||||
|
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]);
|
||||||
|
}
|
|
@ -4,62 +4,48 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import CanvasItem from '../CanvasItem';
|
import CanvasItem from '../CanvasItem';
|
||||||
import { showArchiveModal } from '../../actions';
|
import { changeWindowType } from '../../actions';
|
||||||
|
|
||||||
import type { State } from '../../reducers';
|
|
||||||
|
|
||||||
|
|
||||||
const CanvasSelect = ({
|
const CanvasSelect = ({ windowId }) => {
|
||||||
canvases,
|
const [canvases, showHiddenCanvases] = useSelector((state) => [
|
||||||
showHiddenCanvases,
|
state.canvas.canvases,
|
||||||
showArchive,
|
state.canvas.showHiddenCanvases,
|
||||||
}) => (
|
], shallowEqual);
|
||||||
<p style={{
|
const dispatch = useDispatch();
|
||||||
textAlign: 'center',
|
|
||||||
paddingLeft: '5%',
|
return (
|
||||||
paddingRight: '5%',
|
<p style={{
|
||||||
paddingTop: 20,
|
textAlign: 'center',
|
||||||
}}
|
paddingLeft: '5%',
|
||||||
>
|
paddingRight: '5%',
|
||||||
<p className="modaltext">
|
paddingTop: 20,
|
||||||
{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:`}
|
<p className="modaltext">
|
||||||
<span
|
{t`Select the canvas you want to use. \
|
||||||
role="button"
|
Every canvas is unique and has different palettes, cooldown and requirements. \
|
||||||
tabIndex={0}
|
Archive of closed canvases can be accessed here:`}
|
||||||
className="modallink"
|
<span
|
||||||
onClick={showArchive}
|
role="button"
|
||||||
>{t`Archive`}</span>)
|
tabIndex={0}
|
||||||
|
className="modallink"
|
||||||
|
onClick={() => dispatch(changeWindowType(windowId, 'ARCHIVE'))}
|
||||||
|
>{t`Archive`}</span>)
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
Object.keys(canvases).map((canvasId) => (
|
||||||
|
(canvases[canvasId].hid && !showHiddenCanvases)
|
||||||
|
? null
|
||||||
|
: <CanvasItem canvasId={canvasId} canvas={canvases[canvasId]} />
|
||||||
|
))
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
{
|
);
|
||||||
Object.keys(canvases).map((canvasId) => (
|
};
|
||||||
(canvases[canvasId].hid && !showHiddenCanvases)
|
|
||||||
? null
|
|
||||||
: <CanvasItem canvasId={canvasId} canvas={canvases[canvasId]} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
export default React.memo(CanvasSelect);
|
||||||
return {
|
|
||||||
showArchive() {
|
|
||||||
dispatch(showArchiveModal());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
|
||||||
const {
|
|
||||||
canvases,
|
|
||||||
showHiddenCanvases,
|
|
||||||
} = state.canvas;
|
|
||||||
return { canvases, showHiddenCanvases };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(CanvasSelect);
|
|
||||||
|
|
|
@ -173,7 +173,7 @@ const Chat = ({
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
{(ownName) ? (
|
{(ownName) ? (
|
||||||
<div classNam="chatinput">
|
<div className="chatinput">
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => handleSubmit(e)}
|
onSubmit={(e) => handleSubmit(e)}
|
||||||
style={{ display: 'flex', flexDirection: 'row' }}
|
style={{ display: 'flex', flexDirection: 'row' }}
|
||||||
|
|
|
@ -1,35 +1,94 @@
|
||||||
/**
|
/*
|
||||||
*
|
* Form for requesting password-reset mail
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
import React, { useState } from 'react';
|
||||||
import React from 'react';
|
import { useDispatch } from 'react-redux';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import { showUserAreaModal } from '../../actions';
|
import { changeWindowType } from '../../actions';
|
||||||
import NewPasswordForm from '../NewPasswordForm';
|
import { validateEMail } from '../../utils/validation';
|
||||||
|
import { requestNewPassword } from '../../actions/fetch';
|
||||||
|
|
||||||
const ForgotPassword = ({ login }) => (
|
function validate(email) {
|
||||||
<p style={{ paddingLeft: '5%', paddingRight: '5%' }}>
|
const errors = [];
|
||||||
<p className="modaltext">
|
const mailerror = validateEMail(email);
|
||||||
{t`Enter your mail address and we will send you a new password:`}
|
if (mailerror) errors.push(mailerror);
|
||||||
</p><br />
|
return errors;
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
<NewPasswordForm back={login} />
|
|
||||||
<p>{t`Consider joining us on Guilded:`}
|
|
||||||
<a href="./guilded" target="_blank">pixelplanet.fun/guilded</a>
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
login() {
|
|
||||||
dispatch(showUserAreaModal());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
|
<div>
|
||||||
|
<p className="modalmessage">
|
||||||
|
{t`Sent you a mail with instructions to reset your password.`}
|
||||||
|
</p>
|
||||||
|
<button type="button" onClick={back}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ paddingLeft: '5%', paddingRight: '5%', textAlign: 'center' }}>
|
||||||
|
<p className="modaltext">
|
||||||
|
{t`Enter your mail address and we will send you a new password:`}
|
||||||
|
</p><br />
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error}><span>{t`Error`}</span>: {error}</p>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
style={inputStyles}
|
||||||
|
value={email}
|
||||||
|
onChange={(evt) => setEmail(evt.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={t`Email`}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<button type="submit">
|
||||||
|
{(submitting) ? '...' : t`Submit`}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={back}>{t`Cancel`}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ForgotPassword);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
|
||||||
const MinecraftModal = () => (
|
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
<p>You can also place pixels from our Minecraft Server at</p>
|
|
||||||
<p><input type="text" value="mc.pixelplanet.fun" readOnly /></p>
|
|
||||||
<p>Please Note that the Minecraft Server is down from time to time</p>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
function mapStateToProps(state: State) {
|
|
||||||
const { center } = state.user;
|
|
||||||
return { center };
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
content: connect(mapStateToProps)(MinecraftModal),
|
|
||||||
title: 'PixelPlanet Minecraft Server',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default data;
|
|
|
@ -1,36 +1,133 @@
|
||||||
/**
|
/*
|
||||||
*
|
* SignUp Form to register new user by mail
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
import {
|
||||||
|
validateEMail, validateName, validatePassword,
|
||||||
|
} from '../../utils/validation';
|
||||||
|
import { requestRegistration } from '../../actions/fetch';
|
||||||
|
|
||||||
import { showUserAreaModal } from '../../actions';
|
import { changeWindowType, loginUser } from '../../actions';
|
||||||
|
|
||||||
// import { send_registration } from '../ui/register';
|
|
||||||
import SignUpForm from '../SignUpForm';
|
|
||||||
|
|
||||||
|
|
||||||
const Register = ({ login }) => (
|
function validate(name, email, password, confirmPassword) {
|
||||||
<p style={{ paddingLeft: '5%', paddingRight: '5%' }}>
|
const errors = [];
|
||||||
<p className="modaltext">{t`Register new account here`}</p><br />
|
const mailerror = validateEMail(email);
|
||||||
<p style={{ textAlign: 'center' }}>
|
if (mailerror) errors.push(mailerror);
|
||||||
<SignUpForm back={login} />
|
const nameerror = validateName(name);
|
||||||
<p>{t`Consider joining us on Guilded:`}
|
if (nameerror) errors.push(nameerror);
|
||||||
<a href="./guilded" target="_blank">pixelplanet.fun/guilded</a>
|
const passworderror = validatePassword(password);
|
||||||
</p>
|
if (passworderror) errors.push(passworderror);
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
if (password !== confirmPassword) {
|
||||||
return {
|
errors.push('Passwords do not match');
|
||||||
login() {
|
}
|
||||||
dispatch(showUserAreaModal());
|
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 (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<form
|
||||||
|
style={{ paddingLeft: '5%', paddingRight: '5%' }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<p className="modaltext">{t`Register new account here`}</p><br />
|
||||||
|
{errors.map((error) => (
|
||||||
|
<p key={error} className="errormessage"><span>{t`Error`}</span>
|
||||||
|
: {error}</p>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
style={inputStyles}
|
||||||
|
value={name}
|
||||||
|
autoComplete="username"
|
||||||
|
onChange={(evt) => setName(evt.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={t`Name`}
|
||||||
|
/><br />
|
||||||
|
<input
|
||||||
|
style={inputStyles}
|
||||||
|
value={email}
|
||||||
|
autoComplete="email"
|
||||||
|
onChange={(evt) => setEmail(evt.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder={t`Email`}
|
||||||
|
/><br />
|
||||||
|
<input
|
||||||
|
style={inputStyles}
|
||||||
|
value={password}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(evt) => setPassword(evt.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder={t`Password`}
|
||||||
|
/><br />
|
||||||
|
<input
|
||||||
|
style={inputStyles}
|
||||||
|
value={confirmPassword}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(evt) => setConfirmPassword(evt.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder={t`Confirm Password`}
|
||||||
|
/><br />
|
||||||
|
<button type="submit">
|
||||||
|
{(submitting) ? '...' : t`Submit`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => dispatch(changeWindowType(windowId, 'USERAREA'))}
|
||||||
|
>
|
||||||
|
{t`Cancel`}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default React.memo(Register);
|
||||||
|
|
|
@ -120,7 +120,7 @@ function Settings({
|
||||||
chatNotify,
|
chatNotify,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<p style={{ paddingLeft: '5%', paddingRight: '5%', paddingTop: 30 }}>
|
<div style={{ paddingLeft: '5%', paddingRight: '5%', paddingTop: 30 }}>
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title={t`Show Grid`}
|
title={t`Show Grid`}
|
||||||
description={t`Turn on grid to highlight pixel borders.`}
|
description={t`Turn on grid to highlight pixel borders.`}
|
||||||
|
@ -203,7 +203,7 @@ function Settings({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { t } from 'ttag';
|
import { t } from 'ttag';
|
||||||
|
|
||||||
import type { State } from '../../reducers';
|
import LogInArea from '../LogInArea';
|
||||||
|
|
||||||
|
|
||||||
import {
|
|
||||||
showRegisterModal, showForgotPasswordModal, setName, setMailreg,
|
|
||||||
} from '../../actions';
|
|
||||||
import LogInForm from '../LogInForm';
|
|
||||||
import Tabs from '../Tabs';
|
import Tabs from '../Tabs';
|
||||||
import UserAreaContent from '../UserArea';
|
import UserAreaContent from '../UserAreaContent';
|
||||||
import Rankings from '../Rankings';
|
import Rankings from '../Rankings';
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
@ -23,136 +17,38 @@ const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ '.
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
const Admintools = React.lazy(() => import(/* webpackChunkName: "admintools" */ '../Admintools'));
|
const Admintools = React.lazy(() => import(/* webpackChunkName: "admintools" */ '../Admintools'));
|
||||||
|
|
||||||
const logoStyle = {
|
const UserArea = ({ windowId }) => {
|
||||||
marginRight: 5,
|
const name = useSelector((state) => state.user.name);
|
||||||
|
const userlvl = useSelector((state) => state.user.userlvl);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Tabs>
|
||||||
|
<div label={t`Profile`}>
|
||||||
|
{(name) ? <UserAreaContent /> : <LogInArea windowId={windowId} />}
|
||||||
|
</div>
|
||||||
|
<div label={t`Ranking`}>
|
||||||
|
<Rankings />
|
||||||
|
</div>
|
||||||
|
<div label={t`Converter`}>
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Converter />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
{userlvl && (
|
||||||
|
<div label={(userlvl === 1) ? t`Admintools` : t`Modtools`}>
|
||||||
|
<Suspense fallback={<div>{t`Loading...`}</div>}>
|
||||||
|
<Admintools />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
<br />
|
||||||
|
{t`Consider joining us on Guilded:`}
|
||||||
|
<a href="./guilded" target="_blank">pixelplanet.fun/guilded</a>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LogInArea = ({ register, forgotPassword, me }) => (
|
export default React.memo(UserArea);
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
<p className="modaltext">
|
|
||||||
{t`Login to access more features and stats.`}
|
|
||||||
</p><br />
|
|
||||||
<h2>{t`Login with Name or Mail:`}</h2>
|
|
||||||
<LogInForm me={me} />
|
|
||||||
<p
|
|
||||||
className="modallink"
|
|
||||||
onClick={forgotPassword}
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
{t`I forgot my Password.`}</p>
|
|
||||||
<h2>{t`or login with:`}</h2>
|
|
||||||
<a href="./api/auth/discord">
|
|
||||||
<img
|
|
||||||
style={logoStyle}
|
|
||||||
width={32}
|
|
||||||
src={`${window.ssv.assetserver}/discordlogo.svg`}
|
|
||||||
alt="Discord"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="./api/auth/google">
|
|
||||||
<img
|
|
||||||
style={logoStyle}
|
|
||||||
width={32}
|
|
||||||
src={`${window.ssv.assetserver}/googlelogo.svg`}
|
|
||||||
alt="Google"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="./api/auth/facebook">
|
|
||||||
<img
|
|
||||||
style={logoStyle}
|
|
||||||
width={32}
|
|
||||||
src={`${window.ssv.assetserver}/facebooklogo.svg`}
|
|
||||||
alt="Facebook"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="./api/auth/vk">
|
|
||||||
<img
|
|
||||||
style={logoStyle}
|
|
||||||
width={32}
|
|
||||||
src={`${window.ssv.assetserver}/vklogo.svg`}
|
|
||||||
alt="VK"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a href="./api/auth/reddit">
|
|
||||||
<img
|
|
||||||
style={logoStyle}
|
|
||||||
width={32}
|
|
||||||
src={`${window.ssv.assetserver}/redditlogo.svg`}
|
|
||||||
alt="Reddit"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h2>{t`or register here:`}</h2>
|
|
||||||
<button type="button" onClick={register}>{t`Register`}</button>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UserArea = ({
|
|
||||||
name,
|
|
||||||
register,
|
|
||||||
forgotPassword,
|
|
||||||
setUserName,
|
|
||||||
setUserMailreg,
|
|
||||||
userlvl,
|
|
||||||
}) => (
|
|
||||||
<p style={{ textAlign: 'center' }}>
|
|
||||||
{(name === null)
|
|
||||||
? (
|
|
||||||
<LogInArea
|
|
||||||
register={register}
|
|
||||||
forgotPassword={forgotPassword}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<Tabs>
|
|
||||||
<div label={t`Profile`}>
|
|
||||||
<UserAreaContent
|
|
||||||
setName={setUserName}
|
|
||||||
setMailreg={setUserMailreg}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div label={t`Ranking`}>
|
|
||||||
<Rankings />
|
|
||||||
</div>
|
|
||||||
<div label={t`Converter`}>
|
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<Converter />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
{userlvl && (
|
|
||||||
<div label={(userlvl === 1) ? t`Admintools` : t`Modtools`}>
|
|
||||||
<Suspense fallback={<div>{t`Loading...`}</div>}>
|
|
||||||
<Admintools />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
<p>{t`Consider joining us on Guilded:`}
|
|
||||||
<a href="./guilded" target="_blank">pixelplanet.fun/guilded</a>
|
|
||||||
</p>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Help from './Help';
|
import Help from './Help';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import UserArea from './UserArea';
|
import UserArea from './UserArea';
|
||||||
|
@ -14,7 +11,7 @@ import Chat from './Chat';
|
||||||
import ForgotPassword from './ForgotPassword';
|
import ForgotPassword from './ForgotPassword';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
NONE: <div />,
|
NONE: null,
|
||||||
HELP: Help,
|
HELP: Help,
|
||||||
SETTINGS: Settings,
|
SETTINGS: Settings,
|
||||||
USERAREA: UserArea,
|
USERAREA: UserArea,
|
||||||
|
@ -23,5 +20,5 @@ export default {
|
||||||
CHAT: Chat,
|
CHAT: Chat,
|
||||||
CANVAS_SELECTION: CanvasSelect,
|
CANVAS_SELECTION: CanvasSelect,
|
||||||
ARCHIVE: Archive,
|
ARCHIVE: Archive,
|
||||||
/* other modals */
|
/* other windows */
|
||||||
};
|
};
|
||||||
|
|
|
@ -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': {
|
case 'FOCUS_WINDOW': {
|
||||||
const {
|
const {
|
||||||
windowId,
|
windowId,
|
||||||
|
|
|
@ -239,7 +239,7 @@ tr:nth-child(even) {
|
||||||
border-width: thin;
|
border-width: thin;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 100%;
|
height: 22px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user