diff --git a/package.json b/package.json index 10250a77..5683ff3d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-dom-confetti": "^0.2.0", - "react-dropzone": "^12.0.4", + "react-dropzone": "^14.2.3", "react-foundation-components": "git+https://github.com/golos-blockchain/react-foundation-components.git#6606fd5529f1ccbc77cd8d33a8ce139fdf8f9a11", "react-intl": "^5.24.6", "react-notification": "^6.8.5", diff --git a/src/components/modules/CreateGroup.jsx b/src/components/modules/CreateGroup.jsx index 97defcc3..16c33654 100644 --- a/src/components/modules/CreateGroup.jsx +++ b/src/components/modules/CreateGroup.jsx @@ -5,7 +5,6 @@ import { Map } from 'immutable' import { api, formatter } from 'golos-lib-js' import { Asset, Price, AssetEditor } from 'golos-lib-js/lib/utils' import tt from 'counterpart' -import getSlug from 'speakingurl' import g from 'app/redux/GlobalReducer' import transaction from 'app/redux/TransactionReducer' @@ -15,13 +14,15 @@ import Icon from 'app/components/elements/Icon' import LoadingIndicator from 'app/components/elements/LoadingIndicator' import FormikAgent from 'app/components/elements/donate/FormikUtils' import Stepper from 'app/components/elements/messages/Stepper' +import GroupName, { validateNameStep } from 'app/components/modules/groups/GroupName' +import GroupLogo from 'app/components/modules/groups/GroupLogo' -const STEPS = { +const STEPS = () => { return { name: tt('create_group_jsx.step_name'), logo: tt('create_group_jsx.step_logo'), admin: tt('create_group_jsx.step_admin'), create: tt('create_group_jsx.step_create') -} +} } class CreateGroup extends React.Component { constructor(props) { @@ -32,7 +33,9 @@ class CreateGroup extends React.Component { title: '', name: '', is_encrypted: true, - privacy: 'public_group' + privacy: 'public_group', + + logo: '', } } this.stepperRef = React.createRef() @@ -80,28 +83,9 @@ class CreateGroup extends React.Component { validate = async (values) => { const errors = {} - if (!values.title) { - errors.title = tt('g.required') - } - if (values.name) { - if (values.name.length < 3) { - errors.name = tt('create_group_jsx.group_min_length') - } else { - let group - try { - console.time('x') - group = await api.getGroupsAsync({ - start_group: values.name, - limit: 1 - }) - console.timeEnd('x') - } catch (err) { - console.error(err) - } - if (group && group[0]) { - errors.name = tt('create_group_jsx.group_already_exists') - } - } + const { step } = this.state + if (step === 'name') { + await validateNameStep(values, errors, (validating) => this.setState({ validating })) } return errors } @@ -117,37 +101,8 @@ class CreateGroup extends React.Component { }) } - onTitleChange = (e, setFieldValue, setFieldTouched) => { - const { value } = e.target - if (value.trimLeft() !== value) { - return - } - setFieldValue('title', value) - let link = getSlug(value) - setFieldValue('name', link) - setFieldTouched('name', true) - this.setState({ - showName: true - }) - } - - onNameChange = (e, setFieldValue) => { - const { value } = e.target - for (const c of value) { - if ((c > 'z' || c < 'a') && c !== '-' && c !== '_') { - return - } - } - setFieldValue('name', value) - } - - onPrivacyChange = (e, setFieldValue) => { - setFieldValue('privacy', e.target.value) - setFieldValue('is_encrypted', true) - } - render() { - const { showName, step, loaded, createError } = this.state + const { step, loaded, createError, validating } = this.state let form if (!loaded) { @@ -181,83 +136,19 @@ class CreateGroup extends React.Component { {({ handleSubmit, isSubmitting, isValid, values, setFieldValue, setFieldTouched, handleChange, }) => { - const disabled = !isValid + const disabled = !isValid || validating return (
- {step === 'name' ? -
-
- {tt('create_group_jsx.title')} -
-
- this.onTitleChange(e, setFieldValue, setFieldTouched)} - autoFocus - /> - -
-
- - {showName ?
-
- {tt('create_group_jsx.name')} -
-
- this.onNameChange(e, setFieldValue)} - /> - -
-
: null} - -
-
- {tt('create_group_jsx.access')} - -
-
- this.onPrivacyChange(e, setFieldValue)} - > - - - - - -
-
- -
-
- - -
-
-
: - } + {step === 'name' ? : + step === 'logo' ? : + } - + {isSubmitting ?
: - } diff --git a/src/components/modules/CreateGroup.scss b/src/components/modules/CreateGroup.scss index f7eddd78..3780db74 100644 --- a/src/components/modules/CreateGroup.scss +++ b/src/components/modules/CreateGroup.scss @@ -1,4 +1,7 @@ .CreateGroup { + h3 { + padding-left: 0.75rem; + } .next-button { width: 48px; height: 48px; diff --git a/src/components/modules/groups/GroupLogo.jsx b/src/components/modules/groups/GroupLogo.jsx new file mode 100644 index 00000000..9c2290e7 --- /dev/null +++ b/src/components/modules/groups/GroupLogo.jsx @@ -0,0 +1,144 @@ +import React from 'react' +import DropZone from 'react-dropzone' +import { connect } from 'react-redux' +import { Field, ErrorMessage, } from 'formik' +import tt from 'counterpart' + +import Input from 'app/components/elements/common/Input'; +import PictureSvg from 'app/assets/icons/editor-toolbar/picture.svg'; +import DialogManager from 'app/components/elements/common/DialogManager' + +class GroupLogo extends React.Component { + state = {} + + constructor(props) { + super(props) + } + + uploadLogo = (file, name, setFieldValue) => { + const { notify } = this.props + const { uploadImage } = this.props + this.setState({ uploading: true }) + uploadImage(file, progress => { + if (progress.url) { + alert(progress.url) + } + if (progress.error) { + const { error } = progress; + notify(error, 10000) + } + this.setState({ uploading: false }) + }) + } + + _onDrop = (acceptedFiles, rejectedFiles, setFieldValue) => { + const file = acceptedFiles[0] + + if (!file) { + if (rejectedFiles.length) { + DialogManager.alert( + tt('post_editor.please_insert_only_image_files') + ) + } + return + } + + this.uploadLogo(file, file.name, setFieldValue) + }; + + _onInputKeyDown = e => { + if (e.which === keyCodes.ENTER) { + e.preventDefault(); + //this.props.onClose({ + //e.target.value, + //}); + } + }; + + + render() { + const { values, setFieldValue, setFieldTouched } = this.props + const { uploading } = this.state + + const selectorStyleCover = uploading ? + { + whiteSpace: `nowrap`, + display: `flex`, + alignItems: `center`, + padding: `0 6px`, + pointerEvents: `none`, + cursor: `default`, + opacity: `0.6` + } : + { + display: `flex`, + alignItems: `center`, + padding: `0 6px` + } + + return +
+
+ {tt('create_group_jsx.logo_desc')} +
+
+
+
+ + {({getRootProps, getInputProps}) => (
+ + + + {tt('create_group_jsx.logo_upload')} + +
)} +
+
+
+
+ {tt('create_group_jsx.logo_link')}: +
+ + {({ field, form }) => } + +
+
+
+ + } +} + +export default connect( + // mapStateToProps + (state, ownProps) => { + }, + dispatch => ({ + uploadImage: (file, progress) => { + dispatch({ + type: 'user/UPLOAD_IMAGE', + payload: {file, progress}, + }) + }, + notify: (message, dismiss = 3000) => { + dispatch({type: 'ADD_NOTIFICATION', payload: { + key: 'group_logo_' + Date.now(), + message, + dismissAfter: dismiss} + }); + } + }) +)(GroupLogo) diff --git a/src/components/modules/groups/GroupName.jsx b/src/components/modules/groups/GroupName.jsx new file mode 100644 index 00000000..4ad41173 --- /dev/null +++ b/src/components/modules/groups/GroupName.jsx @@ -0,0 +1,155 @@ +import React from 'react' +import { Field, ErrorMessage, } from 'formik' +import getSlug from 'speakingurl' +import tt from 'counterpart' +import { api } from 'golos-lib-js' + +import Icon from 'app/components/elements/Icon' + +export async function validateNameStep(values, errors, setValidating) { + if (!values.title) { + errors.title = tt('g.required') + } + if (values.name) { + if (values.name.length < 3) { + errors.name = tt('create_group_jsx.group_min_length') + } else { + setValidating(true) + let group + for (let i = 0; i < 3; ++i) { + try { + console.time('group_exists') + group = await api.getGroupsAsync({ + start_group: values.name, + limit: 1 + }) + console.timeEnd('group_exists') + break + } catch (err) { + console.error(err) + errors.name = 'Blockchain unavailable :(' + } + } + if (group && group[0]) { + errors.name = tt('create_group_jsx.group_already_exists') + } + setValidating(false) + } + } +} + +export default class GroupName extends React.Component { + state = {} + + constructor(props) { + super(props) + } + + onTitleChange = (e, setFieldValue, setFieldTouched) => { + const { value } = e.target + if (value.trimLeft() !== value) { + return + } + setFieldValue('title', value) + let link = getSlug(value) + setFieldValue('name', link) + setFieldTouched('name', true) + this.setState({ + showName: true + }) + } + + onNameChange = (e, setFieldValue) => { + const { value } = e.target + for (let i = 0; i < value.length; ++i) { + const c = value[i] + const is_alpha = c >= 'a' && c <= 'z' + const is_digit = c >= '0' && c <= '9' + const is_dash = c == '-' + const is_ul = c == '_' + if (i == 0) { + if (!is_alpha && !is_digit) return; + } else { + if (!is_alpha && !is_digit && !is_dash && !is_ul) return; + } + } + setFieldValue('name', value) + } + + onPrivacyChange = (e, setFieldValue) => { + setFieldValue('privacy', e.target.value) + setFieldValue('is_encrypted', true) + } + + render() { + const { values, setFieldValue, setFieldTouched } = this.props + const { showName } = this.state + return +
+
+ {tt('create_group_jsx.title')} +
+
+ this.onTitleChange(e, setFieldValue, setFieldTouched)} + autoFocus + /> + +
+
+ + {showName ?
+
+ {tt('create_group_jsx.name')} +
+
+ this.onNameChange(e, setFieldValue)} + /> + +
+
: null} + +
+
+ {tt('create_group_jsx.access')} + +
+
+ this.onPrivacyChange(e, setFieldValue)} + > + + + + + +
+
+ +
+
+ + +
+
+
+ } +} diff --git a/src/locales/en.json b/src/locales/en.json index 9bcf9a2a..347048d4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -105,7 +105,10 @@ "golos_power_too_low": "To create group you should have Golos Power at least ", "golos_power_too_low2": "That is not enough ", "golos_power_too_low3": "Your Golos Power is ", - "deposit_gp": "Increase Golos Power" + "deposit_gp": "Increase Golos Power", + "logo_desc": "The group logo is like a user’s avatar...", + "logo_upload": "Upload logo", + "logo_link": "Add logo from the URL" }, "emoji_i18n": { "categoriesLabel": "Категории", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 84bce760..d9d60424 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -102,11 +102,15 @@ "step_admin": "Администратор", "step_create": "Создать!", "group_already_exists": "Такая группа уже существует.", + "validating": "Проверка существования группы...", "group_min_length": "Минимум 3 символа.", "golos_power_too_low": "Для создания группы нужна Сила Голоса не менее ", "golos_power_too_low2": "Вам не хватает ", "golos_power_too_low3": "Ваша Сила Голоса - ", - "deposit_gp": "Пополнить Силу Голоса" + "deposit_gp": "Пополнить Силу Голоса", + "logo_desc": "Логотип группы - это как аватарка у пользователя...", + "logo_upload": "Загрузить логотип", + "logo_link": "Добавить логотип ссылкой" }, "emoji_i18n": { "categoriesLabel": "Категории", diff --git a/yarn.lock b/yarn.lock index 949eb17f..cd4d7610 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4753,12 +4753,12 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -file-selector@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" - integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== dependencies: - tslib "^2.0.3" + tslib "^2.4.0" filelist@^1.0.1: version "1.0.2" @@ -8285,13 +8285,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-dropzone@^12.0.4: - version "12.0.4" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-12.0.4.tgz#b88eeaa2c7118f7fd042404682b17a1d466f2fcf" - integrity sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang== +react-dropzone@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== dependencies: attr-accept "^2.2.2" - file-selector "^0.4.0" + file-selector "^0.6.0" prop-types "^15.8.1" react-error-overlay@^6.0.10: @@ -9678,6 +9678,11 @@ tslib@^2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"