diff --git a/.babelrc b/.babelrc index 3f488527..d5b6bd5d 100644 --- a/.babelrc +++ b/.babelrc @@ -6,6 +6,7 @@ "env": { "production": { "plugins": [ + "babel-plugin-styled-components", "transform-react-remove-prop-types", "@babel/plugin-transform-react-constant-elements", "@babel/plugin-transform-react-inline-elements", @@ -48,6 +49,7 @@ }, "development": { "plugins": [ + "babel-plugin-styled-components", "react-hot-loader/babel" ] } diff --git a/.env.example b/.env.example index 5e696e25..3a070317 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ API_URL=/editor AWS_ACCESS_KEY= AWS_REGION= AWS_SECRET_KEY= +CORS_ALLOW_LOCALHOST=true EMAIL_SENDER= EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production EXAMPLE_USER_EMAIL=examples@p5js.org diff --git a/.eslintrc b/.eslintrc index 70fdcc0c..1122f7e2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -77,5 +77,13 @@ "__SERVER__": true, "__DISABLE_SSR__": true, "__DEVTOOLS__": true - } + }, + "overrides": [ + { + "files": ["*.stories.jsx"], + "rules": { + "import/no-extraneous-dependencies": "off" + } + } + ] } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4c8fe8b7..fc813e0c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,8 +15,6 @@ Hello! We welcome community contributions to the p5.js Web Editor. Contributing - [Issue Search and Tagging](#issue-search-and-tagging) - [Beginning Work](#beginning-work) - [Contribution Guides](#contribution-guides) - - [Writing Commit Messages](#writing-commit-messages) - - [Tips](#tips) ## Code of Conduct @@ -62,45 +60,9 @@ If you feel like an issue is tagged incorrectly (e.g. it's low priority and you If you'd like to work on an issue, please comment on it to let the maintainers know, so that they can assign it to you. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort. -Then, follow the [installation guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md) to get the project building and working on your computer. +Then, look at the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md) for instructions on how to install the project locally and follow the right development workflow. ### Contribution Guides * [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/) * [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/) - -## Writing Commit Messages - -Good commit messages serve at least three important purposes: - -* They speed up the reviewing process. -* They help us write good release notes. -* They help future maintainers understand your change and the reasons behind it. - -Structure your commit message like this: - - ``` - Short (50 chars or less) summary of changes ( involving Fixes #Issue-number keyword ) - - More detailed explanatory text, if necessary. Wrap it to about 72 - characters or so. In some contexts, the first line is treated as the - subject of an email and the rest of the text as the body. The blank - line separating the summary from the body is critical (unless you omit - the body entirely); tools like rebase can get confused if you run the - two together. - - Further paragraphs come after blank lines. - - - Bullet points are okay, too - - - Typically a hyphen or asterisk is used for the bullet, preceded by a - single space, with blank lines in between, but conventions vary here - ``` - -* Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed". -* Always leave the second line blank. -* Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened. - -## Tips - -* If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`. \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 82c461c7..93c4b27b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ -custom: https://processingfoundation.org/support +github: processing +custom: https://processingfoundation.org/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cbe15e3f..95cb6f48 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,5 @@ +Fixes #issue-number + I have verified that this pull request: * [ ] has no linting errors (`npm run lint`) diff --git a/.gitignore b/.gitignore index 91400122..e36b805f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ cert_chain.crt localhost.crt localhost.key privkey.pem + +storybook-static diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..b7d42a9a --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,29 @@ +const path = require('path'); + +module.exports = { + stories: ['../client/**/*.stories.(jsx|mdx)'], + addons: [ + '@storybook/addon-actions', + '@storybook/addon-docs', + '@storybook/addon-knobs', + '@storybook/addon-links', + 'storybook-addon-theme-playground/dist/register' + ], + webpackFinal: async config => { + // do mutation to the config + + const rules = config.module.rules; + + // modify storybook's file-loader rule to avoid conflicts with svgr + const fileLoaderRule = rules.find(rule => rule.test.test('.svg')); + fileLoaderRule.exclude = path.resolve(__dirname, '../client'); + + // use svgr for svg files + rules.push({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }) + + return config; + }, +}; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 00000000..da394031 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { addDecorator, addParameters } from '@storybook/react'; +import { withKnobs } from "@storybook/addon-knobs"; +import { withThemePlayground } from 'storybook-addon-theme-playground'; +import { ThemeProvider } from "styled-components"; + +import theme, { Theme } from '../client/theme'; + +addDecorator(withKnobs); + +const themeConfigs = Object.values(Theme).map( + name => { + return { name, theme: theme[name] }; + } +); + +addDecorator(withThemePlayground({ + theme: themeConfigs, + provider: ThemeProvider +})); + +addParameters({ + options: { + /** + * display the top-level grouping as a "root" in the sidebar + */ + showRoots: true, + }, +}) + +// addDecorator(storyFn => {storyFn()}); diff --git a/.travis.yml b/.travis.yml index da57c24a..16287434 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,12 +37,13 @@ deploy: script: ./deploy.sh skip_cleanup: true on: - branch: master + branch: release + tags: true - provider: script script: ./deploy_staging.sh skip_cleanup: true on: - branch: feature/public-api + branch: develop env: global: diff --git a/Dockerfile b/Dockerfile index d2364735..743d7d90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY .babelrc index.js nodemon.json ./ COPY ./webpack ./webpack COPY client ./client COPY server ./server +COPY translations/locales ./translations/locales CMD ["npm", "start"] FROM development as build diff --git a/client/common/Button.jsx b/client/common/Button.jsx new file mode 100644 index 00000000..20bd9c25 --- /dev/null +++ b/client/common/Button.jsx @@ -0,0 +1,240 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { Link } from 'react-router'; + +import { remSize, prop } from '../theme'; + +const kinds = { + block: 'block', + icon: 'icon', + inline: 'inline', +}; + +// The '&&&' will increase the specificity of the +// component's CSS so that it overrides the more +// general global styles +const StyledButton = styled.button` + &&& { + display: flex; + justify-content: center; + align-items: center; + + width: max-content; + text-decoration: none; + + color: ${prop('Button.default.foreground')}; + background-color: ${prop('Button.default.background')}; + cursor: pointer; + border: 2px solid ${prop('Button.default.border')}; + border-radius: 2px; + padding: ${remSize(8)} ${remSize(25)}; + line-height: 1; + + svg * { + fill: ${prop('Button.default.foreground')}; + } + + &:hover:not(:disabled) { + color: ${prop('Button.hover.foreground')}; + background-color: ${prop('Button.hover.background')}; + border-color: ${prop('Button.hover.border')}; + + svg * { + fill: ${prop('Button.hover.foreground')}; + } + } + + &:active:not(:disabled) { + color: ${prop('Button.active.foreground')}; + background-color: ${prop('Button.active.background')}; + + svg * { + fill: ${prop('Button.active.foreground')}; + } + } + + &:disabled { + color: ${prop('Button.disabled.foreground')}; + background-color: ${prop('Button.disabled.background')}; + border-color: ${prop('Button.disabled.border')}; + cursor: not-allowed; + + svg * { + fill: ${prop('Button.disabled.foreground')}; + } + } + + > * + * { + margin-left: ${remSize(8)}; + } + } +`; + +const StyledInlineButton = styled.button` + &&& { + display: flex; + justify-content: center; + align-items: center; + + text-decoration: none; + + color: ${prop('primaryTextColor')}; + cursor: pointer; + border: none; + line-height: 1; + + svg * { + fill: ${prop('primaryTextColor')}; + } + + &:disabled { + cursor: not-allowed; + } + + > * + * { + margin-left: ${remSize(8)}; + } + } +`; + +const StyledIconButton = styled.button` + &&& { + display: flex; + justify-content: center; + align-items: center; + + width: ${remSize(32)}px; + height: ${remSize(32)}px; + text-decoration: none; + + color: ${prop('Button.default.foreground')}; + background-color: ${prop('Button.hover.background')}; + cursor: pointer; + border: 1px solid transparent; + border-radius: 50%; + padding: ${remSize(8)} ${remSize(25)}; + line-height: 1; + + &:hover:not(:disabled) { + color: ${prop('Button.hover.foreground')}; + background-color: ${prop('Button.hover.background')}; + + svg * { + fill: ${prop('Button.hover.foreground')}; + } + } + + &:active:not(:disabled) { + color: ${prop('Button.active.foreground')}; + background-color: ${prop('Button.active.background')}; + + svg * { + fill: ${prop('Button.active.foreground')}; + } + } + + &:disabled { + color: ${prop('Button.disabled.foreground')}; + background-color: ${prop('Button.disabled.background')}; + cursor: not-allowed; + } + + > * + * { + margin-left: ${remSize(8)}; + } + } +`; + +/** + * A Button performs an primary action + */ +const Button = ({ + children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props +}) => { + const hasChildren = React.Children.count(children) > 0; + const content = <>{iconBefore}{hasChildren && {children}}{iconAfter}; + let StyledComponent = StyledButton; + + if (kind === kinds.inline) { + StyledComponent = StyledInlineButton; + } else if (kind === kinds.icon) { + StyledComponent = StyledIconButton; + } + + if (href) { + return ( + + {content} + + ); + } + + if (to) { + return {content}; + } + + return {content}; +}; + +Button.defaultProps = { + 'children': null, + 'disabled': false, + 'iconAfter': null, + 'iconBefore': null, + 'kind': kinds.block, + 'href': null, + 'aria-label': null, + 'to': null, + 'type': 'button', +}; + +Button.kinds = kinds; + +Button.propTypes = { + /** + * The visible part of the button, telling the user what + * the action is + */ + 'children': PropTypes.element, + /** + If the button can be activated or not + */ + 'disabled': PropTypes.bool, + /** + * SVG icon to place after child content + */ + 'iconAfter': PropTypes.element, + /** + * SVG icon to place before child content + */ + 'iconBefore': PropTypes.element, + /** + * The kind of button - determines how it appears visually + */ + 'kind': PropTypes.oneOf(Object.values(kinds)), + /** + * Specifying an href will use an to link to the URL + */ + 'href': PropTypes.string, + /* + * An ARIA Label used for accessibility + */ + 'aria-label': PropTypes.string, + /** + * Specifying a to URL will use a react-router Link + */ + 'to': PropTypes.string, + /** + * If using a button, then type is defines the type of button + */ + 'type': PropTypes.oneOf(['button', 'submit']), +}; + +export default Button; diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx new file mode 100644 index 00000000..bf1a02c8 --- /dev/null +++ b/client/common/Button.stories.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { boolean, text } from '@storybook/addon-knobs'; + +import Button from './Button'; +import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons'; + +export default { + title: 'Common/Button', + component: Button +}; + +export const AllFeatures = () => ( + +); + +export const SubmitButton = () => ( + +); + +export const DefaultTypeButton = () => ; + +export const DisabledButton = () => ; + +export const AnchorButton = () => ( + +); + +export const ReactRouterLink = () => ( + +); + +export const ButtonWithIconBefore = () => ( + +); + +export const ButtonWithIconAfter = () => ( + +); + +export const InlineButtonWithIconAfter = () => ( + +); + +export const InlineIconOnlyButton = () => ( + ); }; diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 8e9cde7a..4c27d6fa 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -3,21 +3,21 @@ import React from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import { Link } from 'react-router'; -import InlineSVG from 'react-inlinesvg'; import classNames from 'classnames'; +import { withTranslation } from 'react-i18next'; +import i18next from 'i18next'; import * as IDEActions from '../modules/IDE/actions/ide'; import * as toastActions from '../modules/IDE/actions/toast'; import * as projectActions from '../modules/IDE/actions/project'; import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; import { logoutUser } from '../modules/User/actions'; +import getConfig from '../utils/getConfig'; import { metaKeyName, } from '../utils/metaKey'; -import caretLeft from '../images/left-arrow.svg'; -const triangleUrl = require('../images/down-filled-triangle.svg'); -const logoUrl = require('../images/p5js-logo-small.svg'); - -const __process = (typeof global !== 'undefined' ? global : window).process; +import CaretLeftIcon from '../images/left-arrow.svg'; +import TriangleIcon from '../images/down-filled-triangle.svg'; +import LogoIcon from '../images/p5js-logo-small.svg'; class Nav extends React.PureComponent { constructor(props) { @@ -57,6 +57,10 @@ class Nav extends React.PureComponent { this.handleFocusForHelp = this.handleFocus.bind(this, 'help'); this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account'); this.handleFocusForAccount = this.handleFocus.bind(this, 'account'); + this.toggleDropdownForLang = this.toggleDropdown.bind(this, 'lang'); + this.handleFocusForLang = this.handleFocus.bind(this, 'lang'); + this.handleLangSelection = this.handleLangSelection.bind(this); + this.closeDropDown = this.closeDropDown.bind(this); } @@ -165,6 +169,13 @@ class Nav extends React.PureComponent { this.setDropdown('none'); } + handleLangSelection(event) { + i18next.changeLanguage(event.target.value); + this.props.showToast(1500); + this.props.setToastText('LangChange'); + this.setDropdown('none'); + } + handleLogout() { this.props.logoutUser(); this.setDropdown('none'); @@ -229,13 +240,13 @@ class Nav extends React.PureComponent { return (
  • - +
  • - +
  • @@ -247,7 +258,7 @@ class Nav extends React.PureComponent { return (
    • - +
      • @@ -270,18 +281,18 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForFile} onBlur={this.handleBlur} > - New + {this.props.t('New')}
      • - { __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) && + { getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
      • } { this.props.project.id && this.props.user.authenticated && @@ -291,7 +302,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForFile} onBlur={this.handleBlur} > - Duplicate + {this.props.t('Duplicate')} } { this.props.project.id && @@ -301,7 +312,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForFile} onBlur={this.handleBlur} > - Share + {this.props.t('Share')} } { this.props.project.id && @@ -311,7 +322,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForFile} onBlur={this.handleBlur} > - Download + {this.props.t('Download')} } { this.props.user.authenticated && @@ -322,10 +333,10 @@ class Nav extends React.PureComponent { onBlur={this.handleBlur} onClick={this.setDropdownForNone} > - Open + {this.props.t('Open')} } - {__process.env.UI_COLLECTIONS_ENABLED && + {getConfig('UI_COLLECTIONS_ENABLED') && this.props.user.authenticated && this.props.project.id &&
      • @@ -335,10 +346,10 @@ class Nav extends React.PureComponent { onBlur={this.handleBlur} onClick={this.setDropdownForNone} > - Add to Collection + {this.props.t('AddToCollection')}
      • } - { __process.env.EXAMPLES_ENABLED && + { getConfig('EXAMPLES_ENABLED') &&
      • - Examples + {this.props.t('Examples')}
      • }
      @@ -362,8 +373,8 @@ class Nav extends React.PureComponent { } }} > - Edit - + {this.props.t('Edit')} +
      • @@ -375,7 +386,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForEdit} onBlur={this.handleBlur} > - Tidy Code + {this.props.t('TidyCode')} {'\u21E7'}+Tab
      • @@ -385,7 +396,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForEdit} onBlur={this.handleBlur} > - Find + {this.props.t('Find')} {metaKeyName}+F @@ -395,7 +406,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForEdit} onBlur={this.handleBlur} > - Find Next + {this.props.t('FindNext')} {metaKeyName}+G @@ -405,7 +416,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForEdit} onBlur={this.handleBlur} > - Find Previous + {this.props.t('FindPrevious')} {'\u21E7'}+{metaKeyName}+G @@ -422,8 +433,8 @@ class Nav extends React.PureComponent { } }} > - Sketch - + {this.props.t('Sketch')} +
        • @@ -432,7 +443,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForSketch} onBlur={this.handleBlur} > - Add File + {this.props.t('AddFile')}
        • @@ -441,7 +452,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForSketch} onBlur={this.handleBlur} > - Add Folder + {this.props.t('AddFolder')}
        • @@ -450,7 +461,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForSketch} onBlur={this.handleBlur} > - Run + {this.props.t('Run')} {metaKeyName}+Enter
        • @@ -460,7 +471,7 @@ class Nav extends React.PureComponent { onFocus={this.handleFocusForSketch} onBlur={this.handleBlur} > - Stop + {this.props.t('Stop')} {'\u21E7'}+{metaKeyName}+Enter @@ -497,8 +508,8 @@ class Nav extends React.PureComponent { } }} > - Help - + {this.props.t('Help')} +
          @@ -537,18 +548,73 @@ class Nav extends React.PureComponent { ); } + renderLanguageMenu(navDropdownState) { + return ( +
            +
          • + +
              + +
            • + +
            • +
            • + +
            • +
            • + +
            • +
            +
          • +
          + ); + } + + renderUnauthenticatedUserMenu(navDropdownState) { return (
          • - - Log in + + {this.props.t('Login')}
          • - or + {this.props.t('LoginOr')}
          • - - Sign up + + {this.props.t('SignUp')}
          @@ -559,7 +625,7 @@ class Nav extends React.PureComponent { return (
          • - Hello, {this.props.user.username}! + {this.props.t('Hello')}, {this.props.user.username}!
          • |
          • @@ -574,8 +640,8 @@ class Nav extends React.PureComponent { } }} > - My Account - + {this.props.t('MyAccount')} +