diff --git a/.eslintrc b/.eslintrc
index 15fbdd5..d54d531 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,7 +147,11 @@
"semi-spacing": "error",
"keyword-spacing": "warn",
"space-before-blocks": "error",
- "space-before-function-paren": ["error", "never"],
+ "space-before-function-paren": ["error", {
+ "anonymous": "never",
+ "named": "never",
+ "asyncArrow": "always"
+ }],
"space-in-parens": "warn",
"space-infix-ops": "warn",
"space-unary-ops": "error",
diff --git a/package.json b/package.json
index e0e2fd5..8bb1ab6 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dependencies": {
"babel-polyfill": "^6.3.14",
"classnames": "^2.1.3",
+ "copy-to-clipboard": "~3.0.8",
"debounce": "^1.0.0",
"flag-icon-css": "^2.8.0",
"intl": "^1.2.2",
@@ -43,6 +44,7 @@
"react-motion": "^0.5.0",
"react-redux": "^5.0.6",
"react-router-dom": "^4.1.1",
+ "react-textarea-autosize": "^6.0.0",
"react-transition-group": "^1.1.3",
"redux": "^3.0.4",
"redux-localstorage": "^0.4.1",
diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js
index 6e1947d..b26c497 100644
--- a/src/components/auth/actions.js
+++ b/src/components/auth/actions.js
@@ -370,7 +370,7 @@ export function oAuthValidate(oauthData: {
export function oAuthComplete(params: {accept?: bool} = {}) {
return wrapInLoader((dispatch, getState) =>
oauth.complete(getState().auth.oauth, params)
- .then((resp) => {
+ .then((resp: Object) => {
localStorage.removeItem('oauthData');
if (resp.redirectUri.startsWith('static_page')) {
diff --git a/src/components/auth/auth.scss b/src/components/auth/auth.scss
new file mode 100644
index 0000000..877a295
--- /dev/null
+++ b/src/components/auth/auth.scss
@@ -0,0 +1,3 @@
+.checkboxInput {
+ margin-top: 15px;
+}
diff --git a/src/components/auth/finish/Finish.js b/src/components/auth/finish/Finish.js
index 11d920e..70ba20b 100644
--- a/src/components/auth/finish/Finish.js
+++ b/src/components/auth/finish/Finish.js
@@ -6,6 +6,7 @@ import { FormattedMessage as Message } from 'react-intl';
import Helmet from 'react-helmet';
import { Button } from 'components/ui/form';
+import copy from 'services/copy';
import messages from './Finish.intl.json';
import styles from './finish.scss';
@@ -21,13 +22,8 @@ class Finish extends Component {
success: PropTypes.bool
};
- state = {
- isCopySupported: document.queryCommandSupported && document.queryCommandSupported('copy')
- };
-
render() {
const {appName, code, state, displayCode, success} = this.props;
- const {isCopySupported} = this.state;
const authData = JSON.stringify({
auth_code: code, // eslint-disable-line
state
@@ -53,18 +49,16 @@ class Finish extends Component {
- {isCopySupported ? (
-
- ) : (
- ''
- )}
+
) : (
@@ -91,28 +85,7 @@ class Finish extends Component {
onCopyClick = (event) => {
event.preventDefault();
- // http://stackoverflow.com/a/987376/5184751
-
- try {
- const selection = window.getSelection();
- const range = document.createRange();
- range.selectNodeContents(this.code);
- selection.removeAllRanges();
- selection.addRange(range);
-
- const successful = document.execCommand('copy');
- selection.removeAllRanges();
-
- // TODO: было бы ещё неплохо сделать какую-то анимацию, вроде "Скопировано",
- // ибо сейчас после клика как-то неубедительно, скопировалось оно или нет
- console.log('Copying text command was %s', successful ? 'successful' : 'unsuccessful');
- } catch (err) {
- // not critical
- }
- };
-
- setCode = (el) => {
- this.code = el;
+ copy(this.props.code);
};
}
diff --git a/src/components/auth/password/PasswordBody.js b/src/components/auth/password/PasswordBody.js
index cd47753..f6e96d4 100644
--- a/src/components/auth/password/PasswordBody.js
+++ b/src/components/auth/password/PasswordBody.js
@@ -4,6 +4,7 @@ import icons from 'components/ui/icons.scss';
import { Input, Checkbox } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
+import authStyles from 'components/auth/auth.scss';
import styles from './password.scss';
import messages from './Password.intl.json';
@@ -40,10 +41,12 @@ export default class PasswordBody extends BaseAuthBody {
placeholder={messages.accountPassword}
/>
-
+
+
+
);
}
diff --git a/src/components/auth/register/RegisterBody.js b/src/components/auth/register/RegisterBody.js
index e2060de..6f46ff5 100644
--- a/src/components/auth/register/RegisterBody.js
+++ b/src/components/auth/register/RegisterBody.js
@@ -7,6 +7,7 @@ import { Input, Checkbox, Captcha } from 'components/ui/form';
import BaseAuthBody from 'components/auth/BaseAuthBody';
import passwordMessages from 'components/auth/password/Password.intl.json';
+import styles from 'components/auth/auth.scss';
import messages from './Register.intl.json';
// TODO: password and username can be validate for length and sameness
@@ -56,19 +57,21 @@ export default class RegisterBody extends BaseAuthBody {
-
-
-
- )
- }} />
- }
- />
+
+
+
+
+ )
+ }} />
+ }
+ />
+
);
}
diff --git a/src/components/contact/ContactForm.js b/src/components/contact/ContactForm.js
index 1d77952..a2ed222 100644
--- a/src/components/contact/ContactForm.js
+++ b/src/components/contact/ContactForm.js
@@ -118,6 +118,8 @@ export class ContactForm extends Component {
required
label={messages.message}
skin="light"
+ minRows={6}
+ maxRows={6}
/>
diff --git a/src/components/dev/apps/ApplicationItem.js b/src/components/dev/apps/ApplicationItem.js
new file mode 100644
index 0000000..8f87ac7
--- /dev/null
+++ b/src/components/dev/apps/ApplicationItem.js
@@ -0,0 +1,246 @@
+// @flow
+import React, { Component } from 'react';
+import { FormattedMessage as Message } from 'react-intl';
+import { Link } from 'react-router-dom';
+
+import classNames from 'classnames';
+
+import { SKIN_LIGHT, COLOR_BLACK, COLOR_RED } from 'components/ui';
+import { Input, Button } from 'components/ui/form';
+import Collapse from 'components/ui/collapse';
+
+import styles from './applicationsIndex.scss';
+import messages from './ApplicationsIndex.intl.json';
+
+import type { Node } from 'react';
+import type { OauthAppResponse } from 'services/api/oauth';
+
+const ACTION_REVOKE_TOKENS = 'revoke-tokens';
+const ACTION_RESET_SECRET = 'reset-secret';
+const ACTION_DELETE = 'delete';
+
+export default class ApplicationItem extends Component<{
+ application: OauthAppResponse,
+ expand: bool,
+ onTileClick: (string) => void,
+ onResetSubmit: (string, bool) => Promise<*>,
+ onDeleteSubmit: (string) => Promise<*>,
+}, {
+ selectedAction: ?string,
+ isActionPerforming: bool,
+ detailsHeight: number,
+}> {
+ state = {
+ selectedAction: null,
+ isActionPerforming: false,
+ detailsHeight: 0,
+ };
+
+ render() {
+ const { application: app, expand } = this.props;
+ const { selectedAction, isActionPerforming } = this.state;
+
+ let actionContent: Node;
+ // eslint-disable-next-line
+ switch (selectedAction) {
+ case ACTION_REVOKE_TOKENS:
+ case ACTION_RESET_SECRET:
+ actionContent = (
+
+
+ {' '}
+
+
+
+
+
+ {isActionPerforming ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+ break;
+ case ACTION_DELETE:
+ actionContent = (
+
+
+ {' '}
+
+
+
+
+
+ {isActionPerforming ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+ break;
+ }
+
+ // TODO: @SleepWalker: нужно сделать так, чтобы форматирование числа пользователей шло через пробел
+
+ return (
+
+
+
+
+ {app.name}
+
+
+ Client ID: {app.clientId}
+ {typeof app.countUsers !== 'undefined' && (
+
+ {' | '}
+
+
+ )}
+
+
+
+
+
+
+
+
+ ,
+ }} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {actionContent}
+
+
+
+ );
+ }
+
+ onTileToggle = () => {
+ const { onTileClick, application } = this.props;
+ onTileClick(application.clientId);
+ };
+
+ onCollapseRest = () => {
+ if (!this.props.expand && this.state.selectedAction) {
+ this.setState({
+ selectedAction: null,
+ });
+ }
+ };
+
+ onActionButtonClick = (type: ?string) => () => {
+ this.setState({
+ selectedAction: type === this.state.selectedAction ? null : type,
+ });
+ };
+
+ onResetSubmit = (resetClientSecret: bool) => async () => {
+ const { onResetSubmit, application } = this.props;
+ this.setState({
+ isActionPerforming: true,
+ });
+ await onResetSubmit(application.clientId, resetClientSecret);
+ this.setState({
+ isActionPerforming: false,
+ selectedAction: null,
+ });
+ };
+
+ onSubmitDelete = () => {
+ const { onDeleteSubmit, application } = this.props;
+ this.setState({
+ isActionPerforming: true,
+ });
+ onDeleteSubmit(application.clientId);
+ };
+}
diff --git a/src/components/dev/apps/ApplicationsIndex.intl.json b/src/components/dev/apps/ApplicationsIndex.intl.json
new file mode 100644
index 0000000..683ca2c
--- /dev/null
+++ b/src/components/dev/apps/ApplicationsIndex.intl.json
@@ -0,0 +1,26 @@
+{
+ "accountsForDevelopers": "Ely.by Accounts for developers",
+ "accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
+ "ourDocumentation": "our documentation",
+ "ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
+ "feedback": "feedback",
+ "weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
+ "youMustAuthToBegin": "You have to authorize to start.",
+ "authorization": "Authorization",
+ "youDontHaveAnyApplication": "You don't have any app registered yet.",
+ "shallWeStart": "Shall we start?",
+ "addNew": "Add new",
+ "yourApplications": "Your applications:",
+ "countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
+ "ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
+ "revokeAllTokens": "Revoke all tokens",
+ "resetClientSecret": "Reset Client Secret",
+ "delete": "Delete",
+ "editDescription": "{icon} Edit description",
+ "allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
+ "appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
+ "takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
+ "cancel": "Cancel",
+ "continue": "Continue",
+ "performing": "Performing…"
+}
diff --git a/src/components/dev/apps/ApplicationsIndex.js b/src/components/dev/apps/ApplicationsIndex.js
new file mode 100644
index 0000000..5adde2b
--- /dev/null
+++ b/src/components/dev/apps/ApplicationsIndex.js
@@ -0,0 +1,198 @@
+// @flow
+import React, { Component } from 'react';
+import { FormattedMessage as Message } from 'react-intl';
+import { Helmet } from 'react-helmet';
+
+import styles from './applicationsIndex.scss';
+import messages from './ApplicationsIndex.intl.json';
+import cubeIcon from './icons/cube.svg';
+import loadingCubeIcon from './icons/loading-cube.svg';
+import toolsIcon from './icons/tools.svg';
+
+import ApplicationItem from './ApplicationItem';
+
+import { LinkButton } from 'components/ui/form';
+import { COLOR_GREEN, COLOR_BLUE } from 'components/ui';
+import { restoreScroll } from 'components/ui/scroll/scroll';
+
+import type { Node } from 'react';
+import type { OauthAppResponse } from 'services/api/oauth';
+
+type Props = {
+ location: {
+ hash: string,
+ },
+ displayForGuest: bool,
+ applications: Array,
+ isLoading: bool,
+ createContactPopup: () => void,
+ deleteApp: (string) => Promise,
+ resetApp: (string, bool) => Promise,
+};
+
+type State = {
+ expandedApp: ?string,
+};
+
+class ApplicationsIndex extends Component {
+ state = {
+ expandedApp: null,
+ };
+
+ appsRefs = {};
+
+ componentDidUpdate(prevProps: Props) {
+ const { applications, isLoading, location } = this.props;
+ if (isLoading !== prevProps.isLoading && applications.length) {
+ const hash = location.hash.substr(1);
+ if (hash !== '' && applications.some((app) => app.clientId === hash)) {
+ requestAnimationFrame(() => this.onTileClick(hash));
+ }
+ }
+ }
+
+ render() {
+ const { displayForGuest, applications, isLoading, resetApp, deleteApp } = this.props;
+ const { expandedApp } = this.state;
+
+ let content: Node;
+ if (displayForGuest) {
+ content = (
+
+

+
+
+
+
+ );
+ } else if (isLoading) {
+ content = (
+
+

+
+ );
+ } else if (applications.length > 0) {
+ content = (
+
+
+
+ {applications.map((app: OauthAppResponse) => (
+
{this.appsRefs[app.clientId] = elem;}}>
+
+
+ ))}
+
+
+ );
+ } else {
+ content = (
+
+

+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {(pageTitle: string) => (
+
+
+ {pageTitle}
+
+ )}
+
+
+
+
+
+
+ ),
+ }} />
+
+
+
+
+
+ ),
+ }} />
+
+
+
+ {content}
+
+ );
+ }
+
+ onTileClick = (clientId: string) => {
+ const expandedApp = this.state.expandedApp === clientId ? null : clientId;
+ this.setState({expandedApp}, () => {
+ if (expandedApp !== null) {
+ // TODO: @SleepWalker: мб у тебя есть идея, как это сделать более правильно и менее дёргано?
+ setTimeout(() => restoreScroll(this.appsRefs[clientId]), 150);
+ }
+ });
+ };
+
+ onContact = (event) => {
+ event.preventDefault();
+ this.props.createContactPopup();
+ };
+}
+
+import { connect } from 'react-redux';
+import { withRouter } from 'react-router';
+import { create as createPopup } from 'components/ui/popup/actions';
+import ContactForm from 'components/contact/ContactForm';
+
+export default withRouter(connect(null, {
+ createContactPopup: () => createPopup(ContactForm),
+})(ApplicationsIndex));
diff --git a/src/components/dev/apps/actions.js b/src/components/dev/apps/actions.js
new file mode 100644
index 0000000..9f4cf51
--- /dev/null
+++ b/src/components/dev/apps/actions.js
@@ -0,0 +1,68 @@
+// @flow
+
+import oauth from 'services/api/oauth';
+
+import type { Dispatch } from 'redux';
+import type { Apps } from './reducer';
+import type { OauthAppResponse } from 'services/api/oauth';
+import type { User } from 'components/user';
+
+export const SET_AVAILABLE = 'SET_AVAILABLE';
+export function setAppsList(apps: Array) {
+ return {
+ type: SET_AVAILABLE,
+ payload: apps,
+ };
+}
+
+export function getApp(state: {apps: Apps}, clientId: string): ?OauthAppResponse {
+ return state.apps.available.find((app) => app.clientId === clientId) || null;
+}
+
+export function fetchApp(clientId: string) {
+ return async (dispatch: Dispatch, getState: () => {apps: Apps}) => {
+ const fetchedApp = await oauth.getApp(clientId);
+ const { available } = getState().apps;
+ let newApps: Array;
+ if (available.some((app) => app.clientId === fetchedApp.clientId)) {
+ newApps = available.map((app) => app.clientId === fetchedApp.clientId ? fetchedApp : app);
+ } else {
+ newApps = [...available, fetchedApp];
+ }
+
+ return dispatch(setAppsList(newApps));
+ };
+}
+
+export function fetchAvailableApps() {
+ return async (dispatch: Dispatch, getState: () => {user: User}) => {
+ const { id } = getState().user;
+ if (!id) {
+ throw new Error('store.user.id is required for this action');
+ }
+
+ const apps = await oauth.getAppsByUser(id);
+
+ return dispatch(setAppsList(apps));
+ };
+}
+
+export function deleteApp(clientId: string) {
+ return async (dispatch: Dispatch, getState: () => {apps: Apps}) => {
+ await oauth.delete(clientId);
+ const apps = getState().apps.available.filter((app) => app.clientId !== clientId);
+
+ return dispatch(setAppsList(apps));
+ };
+}
+
+export function resetApp(clientId: string, resetSecret: bool) {
+ return async (dispatch: Dispatch, getState: () => {apps: Apps}) => {
+ const result = await oauth.reset(clientId, resetSecret);
+ if (resetSecret) {
+ const apps = getState().apps.available.map((app) => app.clientId === clientId ? result.data : app);
+
+ return dispatch(setAppsList(apps));
+ }
+ };
+}
diff --git a/src/components/dev/apps/applicationForm/ApplicationForm.intl.json b/src/components/dev/apps/applicationForm/ApplicationForm.intl.json
new file mode 100644
index 0000000..de0ebf8
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/ApplicationForm.intl.json
@@ -0,0 +1,20 @@
+{
+ "creatingApplication": "Creating an application",
+ "website": "Web site",
+ "minecraftServer": "Minecraft server",
+ "toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
+ "applicationName": "Application name:",
+ "appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
+ "description": "Description:",
+ "websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
+ "websiteLink": "Website link:",
+ "redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
+ "redirectUri": "Redirect URI:",
+ "createApplication": "Create application",
+ "serverName": "Server name:",
+ "ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
+ "serverIp": "Server IP:",
+ "youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
+ "updatingApplication": "Updating an application",
+ "updateApplication" : "Update application"
+}
diff --git a/src/components/dev/apps/applicationForm/ApplicationForm.js b/src/components/dev/apps/applicationForm/ApplicationForm.js
new file mode 100644
index 0000000..d0ee78e
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/ApplicationForm.js
@@ -0,0 +1,129 @@
+// @flow
+import React, { Component } from 'react';
+import { FormattedMessage as Message } from 'react-intl';
+import { Helmet } from 'react-helmet';
+
+import { Form, FormModel, Button } from 'components/ui/form';
+import { BackButton } from 'components/profile/ProfileForm';
+import { COLOR_GREEN } from 'components/ui';
+
+import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'components/dev/apps';
+import styles from 'components/profile/profileForm.scss';
+import messages from './ApplicationForm.intl.json';
+
+import ApplicationTypeSwitcher from './ApplicationTypeSwitcher';
+import WebsiteType from './WebsiteType';
+import MinecraftServerType from './MinecraftServerType';
+
+import type { ComponentType } from 'react';
+import type { MessageDescriptor } from 'react-intl';
+import type { OauthAppResponse } from 'services/api/oauth';
+
+const typeToForm: {
+ [key: string]: {
+ label: MessageDescriptor,
+ component: ComponentType,
+ },
+} = {
+ [TYPE_APPLICATION]: {
+ label: messages.website,
+ component: WebsiteType,
+ },
+ [TYPE_MINECRAFT_SERVER]: {
+ label: messages.minecraftServer,
+ component: MinecraftServerType,
+ },
+};
+
+const typeToLabel: {
+ [key: string]: MessageDescriptor,
+} = Object.keys(typeToForm).reduce((result, key: string) => {
+ result[key] = typeToForm[key].label;
+ return result;
+}, {});
+
+export default class ApplicationForm extends Component<{
+ app: OauthAppResponse,
+ form: FormModel,
+ displayTypeSwitcher?: bool,
+ type: ?string,
+ setType: (string) => void,
+ onSubmit: (FormModel) => Promise<*>,
+}> {
+ static displayName = 'ApplicationForm';
+
+ static defaultProps = {
+ setType: () => {},
+ };
+
+ render() {
+ const { type, setType, form, displayTypeSwitcher, app } = this.props;
+ const { component: FormComponent } = type && typeToForm[type] || {};
+ const isUpdate = app.clientId !== '';
+
+ return (
+
+ );
+ }
+
+ onFormSubmit = async () => {
+ const { form } = this.props;
+
+ try {
+ await this.props.onSubmit(form);
+ } catch (resp) {
+ if (resp.errors) {
+ form.setErrors(resp.errors);
+ return;
+ }
+
+ throw resp;
+ }
+ };
+}
diff --git a/src/components/dev/apps/applicationForm/ApplicationTypeSwitcher.js b/src/components/dev/apps/applicationForm/ApplicationTypeSwitcher.js
new file mode 100644
index 0000000..5ffab91
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/ApplicationTypeSwitcher.js
@@ -0,0 +1,40 @@
+// @flow
+import React, { Component } from 'react';
+
+import { SKIN_LIGHT } from 'components/ui';
+import { Radio } from 'components/ui/form';
+
+import styles from './applicationTypeSwitcher.scss';
+
+import type { MessageDescriptor } from 'react-intl';
+
+export default class ApplicationTypeSwitcher extends Component<{
+ appTypes: {
+ [key: string]: MessageDescriptor,
+ },
+ selectedType: ?string,
+ setType: (type: string) => void,
+}> {
+ render() {
+ const { appTypes, selectedType } = this.props;
+
+ return (
+
+ {Object.keys(appTypes).map((type: string) => (
+
+
+
+ ))}
+
+ );
+ }
+
+ onChange = (event: {target: {value: string}}) => {
+ this.props.setType(event.target.value);
+ }
+}
diff --git a/src/components/dev/apps/applicationForm/MinecraftServerType.js b/src/components/dev/apps/applicationForm/MinecraftServerType.js
new file mode 100644
index 0000000..4168683
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/MinecraftServerType.js
@@ -0,0 +1,59 @@
+// @flow
+import React, { Component } from 'react';
+import { FormattedMessage as Message } from 'react-intl';
+
+import styles from 'components/profile/profileForm.scss';
+import messages from './ApplicationForm.intl.json';
+
+import { Input, FormModel } from 'components/ui/form';
+import { SKIN_LIGHT } from 'components/ui';
+
+import type {OauthAppResponse} from 'services/api/oauth';
+
+export default class MinecraftServerType extends Component<{
+ form: FormModel,
+ app: OauthAppResponse,
+}> {
+ render() {
+ const { form, app } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/dev/apps/applicationForm/WebsiteType.js b/src/components/dev/apps/applicationForm/WebsiteType.js
new file mode 100644
index 0000000..29667bb
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/WebsiteType.js
@@ -0,0 +1,74 @@
+// @flow
+import React, { Component } from 'react';
+import { FormattedMessage as Message } from 'react-intl';
+
+import styles from 'components/profile/profileForm.scss';
+import messages from './ApplicationForm.intl.json';
+
+import { Input, TextArea, FormModel } from 'components/ui/form';
+import { SKIN_LIGHT } from 'components/ui';
+
+import type { OauthAppResponse } from 'services/api/oauth';
+
+export default class WebsiteType extends Component<{
+ form: FormModel,
+ app: OauthAppResponse,
+}> {
+ render() {
+ const { form, app } = this.props;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/dev/apps/applicationForm/applicationTypeSwitcher.scss b/src/components/dev/apps/applicationForm/applicationTypeSwitcher.scss
new file mode 100644
index 0000000..ed17ef4
--- /dev/null
+++ b/src/components/dev/apps/applicationForm/applicationTypeSwitcher.scss
@@ -0,0 +1,3 @@
+.radioContainer {
+ margin-top: 10px;
+}
diff --git a/src/components/dev/apps/applicationsIndex.scss b/src/components/dev/apps/applicationsIndex.scss
new file mode 100644
index 0000000..f2ccb3e
--- /dev/null
+++ b/src/components/dev/apps/applicationsIndex.scss
@@ -0,0 +1,252 @@
+@import '~components/ui/fonts.scss';
+@import '~components/ui/colors.scss';
+
+.container {
+ max-width: 500px;
+ margin: 0 auto;
+ background: white;
+ border-bottom: 10px solid #DDD8CE;
+
+ @media (max-width: 540px) {
+ margin: 0 20px;
+ }
+}
+
+.welcomeContainer {
+ padding: 30px;
+ background: #F5F5F5;
+ text-align: center;
+ border-bottom: 1px solid #EEEEEE;
+}
+
+.welcomeTitle {
+ font-size: 30px;
+ font-family: $font-family-title;
+ max-width: 245px;
+ margin: 0 auto 15px;
+ line-height: 1.2;
+}
+
+.welcomeTitleDelimiter {
+ width: 86px;
+ height: 3px;
+ background: $green;
+ margin: 0 auto 15px;
+}
+
+.welcomeParagraph {
+ color: #666666;
+ font-size: 14px;
+ margin-bottom: 15px;
+ line-height: 1.3;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+.emptyState {
+ padding: 30px 30px 50px;
+ text-align: center;
+}
+
+.emptyStateIcon {
+ width: 120px;
+ height: 120px;
+ margin-bottom: 20px;
+}
+
+@mixin emptyStateAnimation($order) {
+ animation:
+ slide-in-bottom
+ 1s // Total animation time
+ .2s + .2s * $order // Increase each next element delay
+ cubic-bezier(0.075, 0.82, 0.165, 1) // easeOutCirc
+ both
+ ;
+}
+
+.emptyStateText {
+ font-family: $font-family-title;
+ color: #666666;
+ font-size: 16px;
+ margin-bottom: 20px;
+ line-height: 20px;
+
+ > div {
+ &:nth-child(1) {
+ @include emptyStateAnimation(0);
+ }
+
+ &:nth-child(2) {
+ @include emptyStateAnimation(1);
+ }
+ }
+}
+
+.emptyStateActionButton {
+ @include emptyStateAnimation(2);
+}
+
+@keyframes slide-in-bottom {
+ 0% {
+ transform: translateY(50px);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+.loadingStateIcon {
+ composes: emptyStateIcon;
+
+ margin-bottom: 130px; // TODO: this is needed to render empty state without height jumping. Maybe it can be done more dynamically?
+}
+
+.appsListTitleContainer {
+ display: flex;
+ align-items: center;
+ padding: 20px 30px;
+ border-bottom: 1px solid #eee;
+}
+
+.appsListTitle {
+ font-family: $font-family-title;
+ font-size: 24px;
+ flex-grow: 1;
+}
+
+.appsListAddNewAppBtn {
+
+}
+
+.appsListContainer {
+ margin-bottom: 30px;
+}
+
+.appItemContainer {
+ border-bottom: 1px solid #eee;
+}
+
+.appItemTile {
+ padding: 15px 30px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ transition: background-color .25s;
+}
+
+.appTileTitle {
+ flex-grow: 1;
+}
+
+.appName {
+ font-family: $font-family-title;
+ font-size: 24px;
+}
+
+.appStats {
+ color: #999;
+ font-size: 14px;
+}
+
+.appItemToggle {
+
+}
+
+.appItemToggleIcon {
+ composes: arrowRight from 'components/ui/icons.scss';
+
+ position: relative;
+ left: 0;
+
+ font-size: 28px;
+ color: #ebe8e1;
+
+ transition: .25s;
+
+ .appItemTile:hover & {
+ color: #777;
+ }
+
+ .appExpanded & {
+ color: #777;
+ transform: rotate(360deg)!important; // Prevent it from hover rotating
+ }
+}
+
+.appDetailsContainer {
+ background: #F5F5F5;
+ border-top: 1px solid #eee;
+ padding: 5px 30px;
+}
+
+.appDetailsInfoField {
+ position: relative;
+ margin-bottom: 20px;
+}
+
+.editAppLink {
+ position: absolute;
+ top: 4px;
+ right: 0;
+
+ font-size: 12px;
+ color: #9A9A9A;
+ border-bottom: 0;
+}
+
+.pencilIcon {
+ composes: pencil from 'components/ui/icons.scss';
+
+ font-size: 14px;
+ position: relative;
+ bottom: 2px;
+}
+
+.appDetailsDescription {
+ font-size: 12px;
+ color: #9A9A9A;
+ line-height: 1.4;
+ margin-bottom: 20px;
+}
+
+.appActionsButtons {
+
+}
+
+.appActionButton {
+ margin: 0 10px 10px 0;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+}
+
+.appActionDescription {
+ composes: appDetailsDescription;
+
+ margin-top: 20px;
+}
+
+.continueActionButtonWrapper {
+ display: inline-block;
+ margin-left: 10px;
+}
+
+.continueActionLink {
+ composes: textLink from 'index.scss';
+
+ font-family: $font-family-title;
+ font-size: 14px;
+ color: #666;
+}
+
+.performingAction {
+ font-family: $font-family-title;
+ font-size: 14px;
+ color: #666;
+}
diff --git a/src/components/dev/apps/icons/cube.svg b/src/components/dev/apps/icons/cube.svg
new file mode 100644
index 0000000..d1610bc
--- /dev/null
+++ b/src/components/dev/apps/icons/cube.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/components/dev/apps/icons/loading-cube.svg b/src/components/dev/apps/icons/loading-cube.svg
new file mode 100644
index 0000000..d6742e8
--- /dev/null
+++ b/src/components/dev/apps/icons/loading-cube.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/components/dev/apps/icons/tools.svg b/src/components/dev/apps/icons/tools.svg
new file mode 100644
index 0000000..5590ca6
--- /dev/null
+++ b/src/components/dev/apps/icons/tools.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/components/dev/apps/index.js b/src/components/dev/apps/index.js
new file mode 100644
index 0000000..9dc3994
--- /dev/null
+++ b/src/components/dev/apps/index.js
@@ -0,0 +1,4 @@
+// @flow
+
+export const TYPE_APPLICATION = 'application';
+export const TYPE_MINECRAFT_SERVER = 'minecraft-server';
diff --git a/src/components/dev/apps/reducer.js b/src/components/dev/apps/reducer.js
new file mode 100644
index 0000000..4b0a535
--- /dev/null
+++ b/src/components/dev/apps/reducer.js
@@ -0,0 +1,29 @@
+// @flow
+import { SET_AVAILABLE } from './actions';
+import type { OauthAppResponse } from 'services/api/oauth';
+
+export type Apps = {
+ +available: Array,
+};
+
+const defaults: Apps = {
+ available: [],
+};
+
+export default function apps(
+ state: Apps = defaults,
+ {type, payload}: {type: string, payload: ?Object}
+) {
+ switch (type) {
+ case SET_AVAILABLE:
+ return {
+ ...state,
+ available: payload,
+ };
+
+ default:
+ return state || {
+ ...defaults
+ };
+ }
+}
diff --git a/src/components/footerMenu/FooterMenu.js b/src/components/footerMenu/FooterMenu.js
index 35f7b57..c4733f0 100644
--- a/src/components/footerMenu/FooterMenu.js
+++ b/src/components/footerMenu/FooterMenu.js
@@ -1,4 +1,4 @@
-import PropTypes from 'prop-types';
+// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
@@ -10,14 +10,12 @@ import { create as createPopup } from 'components/ui/popup/actions';
import styles from './footerMenu.scss';
import messages from './footerMenu.intl.json';
-class FooterMenu extends Component {
+class FooterMenu extends Component<{
+ createContactPopup: () => void,
+ createLanguageSwitcherPopup: () => void,
+}> {
static displayName = 'FooterMenu';
- static propTypes = {
- createContactPopup: PropTypes.func.isRequired,
- createLanguageSwitcherPopup: PropTypes.func.isRequired,
- };
-
render() {
return (
@@ -28,6 +26,10 @@ class FooterMenu extends Component {
+ {' | '}
+
+
+
diff --git a/src/components/footerMenu/footerMenu.intl.json b/src/components/footerMenu/footerMenu.intl.json
index 3f1edef..a71bef4 100644
--- a/src/components/footerMenu/footerMenu.intl.json
+++ b/src/components/footerMenu/footerMenu.intl.json
@@ -1,5 +1,6 @@
{
"rules": "Rules",
"contactUs": "Contact Us",
- "siteLanguage": "Site language"
+ "siteLanguage": "Site language",
+ "forDevelopers": "For developers"
}
diff --git a/src/components/profile/ProfileForm.js b/src/components/profile/ProfileForm.js
index ac6818d..0575d27 100644
--- a/src/components/profile/ProfileForm.js
+++ b/src/components/profile/ProfileForm.js
@@ -1,3 +1,4 @@
+// @flow
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
@@ -9,12 +10,20 @@ import styles from 'components/profile/profileForm.scss';
import messages from './ProfileForm.intl.json';
-export class BackButton extends FormComponent {
+export class BackButton extends FormComponent<{
+ to: string,
+}> {
static displayName = 'BackButton';
+ static defaultProps = {
+ to: '/',
+ };
+
render() {
+ const { to } = this.props;
+
return (
-
+
diff --git a/src/components/ui/bsod/BsodMiddleware.js b/src/components/ui/bsod/BsodMiddleware.js
index 355a487..1289bcc 100644
--- a/src/components/ui/bsod/BsodMiddleware.js
+++ b/src/components/ui/bsod/BsodMiddleware.js
@@ -13,7 +13,7 @@ export default function BsodMiddleware(dispatchBsod: Function, logger: Logger) {
(resp instanceof InternalServerError
&& resp.error.code !== ABORT_ERR
) || (resp.originalResponse
- && /404|5\d\d/.test((resp.originalResponse.status: string))
+ && /5\d\d/.test((resp.originalResponse.status: string))
)
)) {
dispatchBsod();
diff --git a/src/components/ui/buttons.scss b/src/components/ui/buttons.scss
index 333fbd8..8297422 100644
--- a/src/components/ui/buttons.scss
+++ b/src/components/ui/buttons.scss
@@ -47,7 +47,7 @@
composes: button;
height: 30px;
- padding: 0 7px;
+ padding: 0 15px;
font-size: 14px;
line-height: 30px;
}
@@ -75,6 +75,7 @@
@include button-theme('darkBlue', $dark_blue);
@include button-theme('lightViolet', $light_violet);
@include button-theme('violet', $violet);
+@include button-theme('red', $red);
.block {
display: block;
@@ -90,3 +91,8 @@
outline: none;
pointer-events: none;
}
+
+.black.disabled {
+ background: #95A5A6;
+ cursor: default;
+}
diff --git a/src/components/ui/collapse/Collapse.js b/src/components/ui/collapse/Collapse.js
new file mode 100644
index 0000000..359149a
--- /dev/null
+++ b/src/components/ui/collapse/Collapse.js
@@ -0,0 +1,85 @@
+// @flow
+import React, { Component } from 'react';
+import { Motion, spring } from 'react-motion';
+
+import MeasureHeight from 'components/MeasureHeight';
+
+import styles from './collapse.scss';
+
+import type { Node } from 'react';
+
+type Props = {
+ isOpened?: bool,
+ children: Node,
+ onRest: () => void,
+};
+
+export default class Collapse extends Component {
+ state = {
+ height: 0,
+ wasInitialized: false,
+ };
+
+ static defaultProps = {
+ onRest: () => {},
+ };
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (this.props.isOpened !== nextProps.isOpened && !this.state.wasInitialized) {
+ this.setState({
+ wasInitialized: true,
+ });
+ }
+ }
+
+ render() {
+ // TODO: @SleepWalker сейчас при первой отрисовке можно увидеть дёргание родительского блока. Надо пофиксить.
+ const { isOpened, children, onRest } = this.props;
+ const { height, wasInitialized } = this.state;
+
+ return (
+
+
+
+ {({top}) => (
+
+ {children}
+
+ )}
+
+
+
+ );
+ }
+
+ onUpdateHeight = (height: number) => {
+ this.setState({
+ height,
+ });
+ };
+
+ shouldMeasureHeight = () => {
+ return [
+ this.props.isOpened,
+ this.state.wasInitialized,
+ ].join('');
+ };
+}
diff --git a/src/components/ui/collapse/collapse.scss b/src/components/ui/collapse/collapse.scss
new file mode 100644
index 0000000..ed69d50
--- /dev/null
+++ b/src/components/ui/collapse/collapse.scss
@@ -0,0 +1,9 @@
+@import '~components/ui/colors.scss';
+
+.overflow {
+ overflow: hidden;
+}
+
+.content {
+
+}
diff --git a/src/components/ui/collapse/index.js b/src/components/ui/collapse/index.js
new file mode 100644
index 0000000..940f5df
--- /dev/null
+++ b/src/components/ui/collapse/index.js
@@ -0,0 +1,2 @@
+// @flow
+export { default } from './Collapse';
diff --git a/src/components/ui/form/Button.js b/src/components/ui/form/Button.js
index dcbdfbb..0a8bbe9 100644
--- a/src/components/ui/form/Button.js
+++ b/src/components/ui/form/Button.js
@@ -1,5 +1,6 @@
// @flow
import React from 'react';
+import type { ComponentType } from 'react';
import classNames from 'classnames';
@@ -9,19 +10,21 @@ import { COLOR_GREEN } from 'components/ui';
import FormComponent from './FormComponent';
import type { Color } from 'components/ui';
+import type { MessageDescriptor } from 'react-intl';
-export default class Button extends FormComponent {
- props: {
- label: string | {id: string},
- block: bool,
- small: bool,
- loading: bool,
- className: string,
- color: Color
- };
-
+export default class Button extends FormComponent<{
+ label: string | MessageDescriptor,
+ block?: bool,
+ small?: bool,
+ loading?: bool,
+ className?: string,
+ color?: Color,
+ disabled?: bool,
+ component?: string | ComponentType,
+} | HTMLButtonElement> {
static defaultProps = {
- color: COLOR_GREEN
+ color: COLOR_GREEN,
+ component: 'button',
};
render() {
@@ -29,23 +32,27 @@ export default class Button extends FormComponent {
color,
block,
small,
+ disabled,
className,
loading,
label,
+ component: ComponentProp,
...restProps
} = this.props;
return (
-
+
);
}
}
diff --git a/src/components/ui/form/Checkbox.js b/src/components/ui/form/Checkbox.js
index 36ed59b..5426950 100644
--- a/src/components/ui/form/Checkbox.js
+++ b/src/components/ui/form/Checkbox.js
@@ -1,31 +1,27 @@
-import PropTypes from 'prop-types';
+// @flow
import React from 'react';
import classNames from 'classnames';
-import { colors, skins, SKIN_DARK, COLOR_GREEN } from 'components/ui';
+import { SKIN_DARK, COLOR_GREEN } from 'components/ui';
import { omit } from 'functions';
import styles from './form.scss';
import FormInputComponent from './FormInputComponent';
-export default class Checkbox extends FormInputComponent {
- static displayName = 'Checkbox';
+import type { Color, Skin } from 'components/ui';
+import type { MessageDescriptor } from 'react-intl';
- static propTypes = {
- color: PropTypes.oneOf(colors),
- skin: PropTypes.oneOf(skins),
- label: PropTypes.oneOfType([
- PropTypes.shape({
- id: PropTypes.string
- }),
- PropTypes.string
- ]).isRequired
- };
+export default class Checkbox extends FormInputComponent<{
+ color: Color,
+ skin: Skin,
+ label: string | MessageDescriptor,
+}> {
+ static displayName = 'Checkbox';
static defaultProps = {
color: COLOR_GREEN,
- skin: SKIN_DARK
+ skin: SKIN_DARK,
};
render() {
@@ -34,13 +30,13 @@ export default class Checkbox extends FormInputComponent {
label = this.formatMessage(label);
- const props = omit(this.props, Object.keys(Checkbox.propTypes));
+ const props = omit(this.props, ['color', 'skin', 'label']);
return (
-
-