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 = () => (
+
+ {text('children', 'this is the button')}
+
+);
+
+export const SubmitButton = () => (
+ This is a submit button
+);
+
+export const DefaultTypeButton = () => Log In ;
+
+export const DisabledButton = () => Log In ;
+
+export const AnchorButton = () => (
+ Actually an anchor
+);
+
+export const ReactRouterLink = () => (
+ Actually a Link
+);
+
+export const ButtonWithIconBefore = () => (
+ }
+ >
+ Create
+
+);
+
+export const ButtonWithIconAfter = () => (
+ }
+ >
+ Create
+
+);
+
+export const InlineButtonWithIconAfter = () => (
+ }
+ kind={Button.kinds.inline}
+ >
+ File name
+
+);
+
+export const InlineIconOnlyButton = () => (
+ }
+ kind={Button.kinds.inline}
+ />
+);
diff --git a/client/common/icons.jsx b/client/common/icons.jsx
new file mode 100644
index 00000000..bbc2a662
--- /dev/null
+++ b/client/common/icons.jsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { prop } from '../theme';
+import SortArrowUp from '../images/sort-arrow-up.svg';
+import SortArrowDown from '../images/sort-arrow-down.svg';
+import Github from '../images/github.svg';
+import Google from '../images/google.svg';
+import Plus from '../images/plus-icon.svg';
+import Close from '../images/close.svg';
+import Exit from '../images/exit.svg';
+import DropdownArrow from '../images/down-filled-triangle.svg';
+import Preferences from '../images/preferences.svg';
+import Play from '../images/triangle-arrow-right.svg';
+
+// HOC that adds the right web accessibility props
+// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
+
+// could also give these a default size, color, etc. based on the theme
+// Need to add size to these - like small icon, medium icon, large icon. etc.
+function withLabel(SvgComponent) {
+ const Icon = (props) => {
+ const StyledIcon = styled(SvgComponent)`
+ &&& {
+ color: ${prop('Icon.default')};
+ & g, & path, & polygon {
+ opacity: 1;
+ fill: ${prop('Icon.default')};
+ }
+ &:hover {
+ color: ${prop('Icon.hover')};
+ & g, & path, & polygon {
+ opacity: 1;
+ fill: ${prop('Icon.hover')};
+ }
+ }
+ }
+ `;
+
+ const { 'aria-label': ariaLabel } = props;
+ if (ariaLabel) {
+ return ( );
+ }
+ return ( );
+ };
+
+ Icon.propTypes = {
+ 'aria-label': PropTypes.string
+ };
+
+ Icon.defaultProps = {
+ 'aria-label': null
+ };
+
+ return Icon;
+}
+
+export const SortArrowUpIcon = withLabel(SortArrowUp);
+export const SortArrowDownIcon = withLabel(SortArrowDown);
+export const GithubIcon = withLabel(Github);
+export const GoogleIcon = withLabel(Google);
+export const PlusIcon = withLabel(Plus);
+export const CloseIcon = withLabel(Close);
+export const ExitIcon = withLabel(Exit);
+export const DropdownArrowIcon = withLabel(DropdownArrow);
+export const PreferencesIcon = withLabel(Preferences);
+export const PlayIcon = withLabel(Play);
diff --git a/client/common/icons.stories.jsx b/client/common/icons.stories.jsx
new file mode 100644
index 00000000..3c3307ab
--- /dev/null
+++ b/client/common/icons.stories.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { select } from '@storybook/addon-knobs';
+
+import * as icons from './icons';
+
+export default {
+ title: 'Common/Icons',
+ component: icons
+};
+
+export const AllIcons = () => {
+ const names = Object.keys(icons);
+
+ const SelectedIcon = icons[select('name', names, names[0])];
+ return (
+
+ );
+};
diff --git a/client/components/AddRemoveButton.jsx b/client/components/AddRemoveButton.jsx
index f0ec1272..7350c177 100644
--- a/client/components/AddRemoveButton.jsx
+++ b/client/components/AddRemoveButton.jsx
@@ -1,17 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
-import InlineSVG from 'react-inlinesvg';
-const addIcon = require('../images/plus.svg');
-const removeIcon = require('../images/minus.svg');
+import AddIcon from '../images/plus.svg';
+import RemoveIcon from '../images/minus.svg';
const AddRemoveButton = ({ type, onClick }) => {
- const alt = type === 'add' ? 'add to collection' : 'remove from collection';
- const icon = type === 'add' ? addIcon : removeIcon;
+ const alt = type === 'add' ? 'Add to collection' : 'Remove from collection';
+ const Icon = type === 'add' ? AddIcon : RemoveIcon;
return (
-
-
+
+
);
};
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 (
-
+
-
+
- Back to Editor
+ {this.props.t('BackEditor')}
@@ -247,7 +258,7 @@ class Nav extends React.PureComponent {
return (
-
+
- File
-
+ {this.props.t('File')}
+
@@ -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()) &&
- Save
- {metaKeyName}+s
+ {this.props.t('Save')}
+ {metaKeyName}+S
}
{ 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')}
+
@@ -507,7 +518,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.handleKeyboardShortcuts}
>
- Keyboard Shortcuts
+ {this.props.t('KeyboardShortcuts')}
@@ -518,7 +529,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForHelp}
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
- >Reference
+ >{this.props.t('Reference')}
@@ -528,7 +539,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
- About
+ {this.props.t('About')}
@@ -537,18 +548,73 @@ class Nav extends React.PureComponent {
);
}
+ renderLanguageMenu(navDropdownState) {
+ return (
+
+
+ {
+ if (this.state.dropdownOpen !== 'none') {
+ this.setDropdown('lang');
+ }
+ }}
+ >
+ {this.props.t('Lang')}
+
+
+
+
+
+ this.handleLangSelection(e)}
+ >
+ Italian (Test Fallback)
+
+
+
+ this.handleLangSelection(e)}
+ >English
+
+
+
+ this.handleLangSelection(e)}
+ >
+ Español
+
+
+
+
+
+ );
+ }
+
+
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')}
+
@@ -585,10 +651,10 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
- My sketches
+ {this.props.t('MySketches')}
- {__process.env.UI_COLLECTIONS_ENABLED &&
+ {getConfig('UI_COLLECTIONS_ENABLED') &&
- My collections
+ {this.props.t('MyCollections')}
}
@@ -607,7 +673,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
- My assets
+ {this.props.t('MyAssets')}
@@ -617,7 +683,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
- Settings
+ {this.props.t('Settings')}
@@ -626,7 +692,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForAccount}
onBlur={this.handleBlur}
>
- Log out
+ {this.props.t('LogOut')}
@@ -636,7 +702,7 @@ class Nav extends React.PureComponent {
}
renderUserMenu(navDropdownState) {
- const isLoginEnabled = __process.env.LOGIN_ENABLED;
+ const isLoginEnabled = getConfig('LOGIN_ENABLED');
const isAuthenticated = this.props.user.authenticated;
if (isLoginEnabled && isAuthenticated) {
@@ -679,25 +745,21 @@ class Nav extends React.PureComponent {
account: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'account'
+ }),
+ lang: classNames({
+ 'nav__item': true,
+ 'nav__item--open': this.state.dropdownOpen === 'lang'
})
};
return (
- { this.node = node; }}>
- {this.renderLeftLayout(navDropdownState)}
- {this.renderUserMenu(navDropdownState)}
- {/*
-
- This is a preview version of the editor, that has not yet been officially released.
- It is in development, you can report bugs
here .
- Please use with caution.
-
- */}
-
+
+ { this.node = node; }}>
+ {this.renderLeftLayout(navDropdownState)}
+ {getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
+ {this.renderUserMenu(navDropdownState)}
+
+
);
}
}
@@ -745,7 +807,9 @@ Nav.propTypes = {
}).isRequired,
params: PropTypes.shape({
username: PropTypes.string
- })
+ }),
+ t: PropTypes.func.isRequired
+
};
Nav.defaultProps = {
@@ -778,5 +842,5 @@ const mapDispatchToProps = {
setAllAccessibleOutput
};
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav));
+export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
export { Nav as NavComponent };
diff --git a/client/components/NavBasic.jsx b/client/components/NavBasic.jsx
index 88c9f313..411329d6 100644
--- a/client/components/NavBasic.jsx
+++ b/client/components/NavBasic.jsx
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
-const logoUrl = require('../images/p5js-logo-small.svg');
-const arrowUrl = require('../images/triangle-arrow-left.svg');
+import LogoIcon from '../images/p5js-logo-small.svg';
+import ArrowIcon from '../images/triangle-arrow-left.svg';
class NavBasic extends React.PureComponent {
static defaultProps = {
@@ -15,13 +14,13 @@ class NavBasic extends React.PureComponent {
{ this.node = node; }}>
-
+
{ this.props.onBack && (
-
+
Back to the editor
diff --git a/client/components/PreviewNav.jsx b/client/components/PreviewNav.jsx
index 7169621d..6a983e19 100644
--- a/client/components/PreviewNav.jsx
+++ b/client/components/PreviewNav.jsx
@@ -1,24 +1,23 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
-import InlineSVG from 'react-inlinesvg';
-const logoUrl = require('../images/p5js-logo-small.svg');
-const editorUrl = require('../images/code.svg');
+import LogoIcon from '../images/p5js-logo-small.svg';
+import CodeIcon from '../images/code.svg';
const PreviewNav = ({ owner, project }) => (
-
+
{project.name}
by
{owner.username}
-
-
+
+
diff --git a/client/components/__test__/FileNode.test.jsx b/client/components/__test__/FileNode.test.jsx
deleted file mode 100644
index b70ebf14..00000000
--- a/client/components/__test__/FileNode.test.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { FileNode } from '../../modules/IDE/components/FileNode';
-
-beforeAll(() => {});
-describe(' ', () => {
- let component;
- let props = {};
-
- describe('with valid props', () => {
- beforeEach(() => {
- props = {
- ...props,
- id: '0',
- children: [],
- name: 'test.jsx',
- fileType: 'dunno',
- setSelectedFile: jest.fn(),
- deleteFile: jest.fn(),
- updateFileName: jest.fn(),
- resetSelectedFile: jest.fn(),
- newFile: jest.fn(),
- newFolder: jest.fn(),
- showFolderChildren: jest.fn(),
- hideFolderChildren: jest.fn(),
- canEdit: true,
- authenticated: false
- };
- component = shallow( );
- });
-
- describe('when changing name', () => {
- let input;
- let renameTriggerButton;
- const changeName = (newFileName) => {
- renameTriggerButton.simulate('click');
- input.simulate('change', { target: { value: newFileName } });
- input.simulate('blur');
- };
-
- beforeEach(() => {
- input = component.find('.sidebar__file-item-input');
- renameTriggerButton = component
- .find('.sidebar__file-item-option')
- .first();
- component.setState({ isEditing: true });
- });
- it('should render', () => expect(component).toBeDefined());
-
- // it('should debug', () => console.log(component.debug()));
-
- describe('to a valid filename', () => {
- const newName = 'newname.jsx';
- beforeEach(() => changeName(newName));
-
- it('should save the name', () => {
- expect(props.updateFileName).toBeCalledWith(props.id, newName);
- });
- });
-
- describe('to an empty filename', () => {
- const newName = '';
- beforeEach(() => changeName(newName));
-
- it('should not save the name', () => {
- expect(props.updateFileName).not.toHaveBeenCalled();
- });
- });
- });
- });
-});
diff --git a/client/components/__test__/Nav.test.jsx b/client/components/__test__/Nav.test.jsx
index f9261cfc..ba7d3fd6 100644
--- a/client/components/__test__/Nav.test.jsx
+++ b/client/components/__test__/Nav.test.jsx
@@ -1,9 +1,9 @@
import React from 'react';
-import { shallow } from 'enzyme';
-import renderer from 'react-test-renderer';
+import { render } from '@testing-library/react';
-import { NavComponent } from './../Nav';
+
+import { NavComponent } from '../Nav';
describe('Nav', () => {
const props = {
@@ -44,19 +44,12 @@ describe('Nav', () => {
setToastText: jest.fn(),
rootFile: {
id: 'root-file'
- }
+ },
+ t: jest.fn()
};
- const getWrapper = () => shallow( );
-
- test('it renders main navigation', () => {
- const nav = getWrapper();
- expect(nav.exists('.nav')).toEqual(true);
- });
it('renders correctly', () => {
- const tree = renderer
- .create( )
- .toJSON();
- expect(tree).toMatchSnapshot();
+ const { asFragment } = render( );
+ expect(asFragment()).toMatchSnapshot();
});
});
diff --git a/client/components/__test__/__snapshots__/Nav.test.jsx.snap b/client/components/__test__/__snapshots__/Nav.test.jsx.snap
index 1445d628..c2a0b9d6 100644
--- a/client/components/__test__/__snapshots__/Nav.test.jsx.snap
+++ b/client/components/__test__/__snapshots__/Nav.test.jsx.snap
@@ -1,333 +1,219 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nav renders correctly 1`] = `
-
-
-
+
-
-
-
- File
-
-
-
-
- New
-
+
-
- Duplicate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- Share
+
+
+
+
+
+
+
+ ⇧+Tab
+
+
+
+
+
+
+ ⌃+F
+
+
+
+
+
+
+ ⌃+G
+
+
+
+
+
+
+ ⇧+⌃+G
+
+
+
+
-
- Download
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⌃+Enter
+
+
+
+
+
+
+ ⇧+⌃+Enter
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- Edit
-
-
-
-
-
-
- Tidy Code
-
- ⇧
- +Tab
-
-
-
-
-
- Find
-
- Ctrl
- +F
-
-
-
-
-
- Find Next
-
- Ctrl
- +G
-
-
-
-
-
- Find Previous
-
- ⇧
- +
- Ctrl
- +G
-
-
-
-
-
-
-
-
- Sketch
-
-
-
-
-
-
- Add File
-
-
-
-
- Add Folder
-
-
-
-
- Run
-
- Ctrl
- +Enter
-
-
-
-
-
- Stop
-
- ⇧
- +
- Ctrl
- +Enter
-
-
-
-
-
-
-
-
- Help
-
-
-
-
-
-
-
+
+
+
`;
diff --git a/client/components/mobile/Footer.jsx b/client/components/mobile/Footer.jsx
new file mode 100644
index 00000000..5d82d3c7
--- /dev/null
+++ b/client/components/mobile/Footer.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import styled from 'styled-components';
+import { prop, remSize } from '../../theme';
+
+const background = prop('MobilePanel.default.background');
+const textColor = prop('primaryTextColor');
+
+const Footer = styled.div`
+ position: fixed;
+ width: 100%;
+ background: ${background};
+ color: ${textColor};
+ padding: ${remSize(12)};
+ padding-left: ${remSize(32)};
+ z-index: 1;
+
+ bottom: 0;
+`;
+
+export default Footer;
diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx
new file mode 100644
index 00000000..1f7f7a29
--- /dev/null
+++ b/client/components/mobile/Header.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+import { prop, remSize } from '../../theme';
+
+const background = prop('MobilePanel.default.background');
+const textColor = prop('primaryTextColor');
+
+
+const HeaderDiv = styled.div`
+ position: fixed;
+ width: 100%;
+ background: ${props => (props.transparent ? 'transparent' : background)};
+ color: ${textColor};
+ padding: ${remSize(12)};
+ padding-left: ${remSize(16)};
+ padding-right: ${remSize(16)};
+ z-index: 1;
+
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+
+ // TODO:
+ svg {
+ max-height: ${remSize(32)};
+ padding: ${remSize(4)}
+ }
+`;
+
+const IconContainer = styled.div`
+ margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
+ display: flex;
+`;
+
+
+const TitleContainer = styled.div`
+ margin-left: ${remSize(4)};
+ margin-right: auto;
+
+ ${props => props.padded && `h2{
+ padding-top: ${remSize(10)};
+ padding-bottom: ${remSize(10)};
+ }`}
+`;
+
+const Header = ({
+ title, subtitle, leftButton, children, transparent
+}) => (
+
+ {leftButton}
+
+ {title && {title} }
+ {subtitle && {subtitle} }
+
+
+ {children}
+
+
+);
+
+Header.propTypes = {
+ title: PropTypes.string,
+ subtitle: PropTypes.string,
+ leftButton: PropTypes.element,
+ children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
+ transparent: PropTypes.bool
+};
+
+Header.defaultProps = {
+ title: null,
+ subtitle: null,
+ leftButton: null,
+ children: [],
+ transparent: false
+};
+
+export default Header;
diff --git a/client/components/mobile/IDEWrapper.jsx b/client/components/mobile/IDEWrapper.jsx
new file mode 100644
index 00000000..0982cf81
--- /dev/null
+++ b/client/components/mobile/IDEWrapper.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import styled from 'styled-components';
+import { remSize } from '../../theme';
+
+export default styled.div`
+ z-index: 0;
+ margin-top: ${remSize(16)};
+`;
diff --git a/client/components/mobile/IconButton.jsx b/client/components/mobile/IconButton.jsx
new file mode 100644
index 00000000..08f05311
--- /dev/null
+++ b/client/components/mobile/IconButton.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import Button from '../../common/Button';
+import { remSize } from '../../theme';
+
+const ButtonWrapper = styled(Button)`
+width: ${remSize(48)};
+> svg {
+ width: 100%;
+ height: 100%;
+}
+`;
+
+const IconButton = (props) => {
+ const { icon, ...otherProps } = props;
+ const Icon = icon;
+
+ return ( }
+ kind={Button.kinds.inline}
+ focusable="false"
+ {...otherProps}
+ />);
+};
+
+IconButton.propTypes = {
+ icon: PropTypes.func.isRequired
+};
+
+export default IconButton;
diff --git a/client/components/mobile/MobileScreen.jsx b/client/components/mobile/MobileScreen.jsx
new file mode 100644
index 00000000..e78baa2c
--- /dev/null
+++ b/client/components/mobile/MobileScreen.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const Screen = ({ children, fullscreen }) => (
+
+ {children}
+
+);
+
+Screen.defaultProps = {
+ fullscreen: false
+};
+
+Screen.propTypes = {
+ children: PropTypes.node.isRequired,
+ fullscreen: PropTypes.bool
+};
+
+export default Screen;
diff --git a/client/components/mobile/PreferencePicker.jsx b/client/components/mobile/PreferencePicker.jsx
new file mode 100644
index 00000000..0e2e085a
--- /dev/null
+++ b/client/components/mobile/PreferencePicker.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { prop, remSize } from '../../theme';
+
+
+const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))`
+ color: ${prop('primaryTextColor')};
+`;
+
+const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))`
+ flex-wrap: nowrap !important;
+ align-items: baseline !important;
+ justify-items: space-between;
+`;
+
+const OptionLabel = styled.label.attrs({ className: 'preference__option' })`
+ font-size: ${remSize(14)} !important;
+`;
+
+const PreferencePicker = ({
+ title, value, onSelect, options,
+}) => (
+
+ {title}
+
+ {options.map(option => (
+
+ onSelect(option.value)}
+ aria-label={option.ariaLabel}
+ name={option.name}
+ key={`${option.name}-${option.id}-input`}
+ id={option.id}
+ className="preference__radio-button"
+ value={option.value}
+ checked={value === option.value}
+ />
+
+ {option.label}
+
+ ))}
+
+
+);
+
+PreferencePicker.defaultProps = {
+ options: []
+};
+
+PreferencePicker.propTypes = {
+ title: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string,
+ name: PropTypes.string,
+ label: PropTypes.string,
+ ariaLabel: PropTypes.string,
+ })),
+ onSelect: PropTypes.func.isRequired,
+};
+
+export default PreferencePicker;
diff --git a/client/i18n.js b/client/i18n.js
new file mode 100644
index 00000000..7d79bf8c
--- /dev/null
+++ b/client/i18n.js
@@ -0,0 +1,38 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import Backend from 'i18next-http-backend';
+
+const fallbackLng = ['en-US'];
+const availableLanguages = ['en-US', 'es-419'];
+
+const options = {
+ loadPath: 'locales/{{lng}}/translations.json',
+ requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
+ mode: 'no-cors'
+ },
+ allowMultiLoading: false, // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading
+};
+
+i18n
+ .use(initReactI18next) // pass the i18n instance to react-i18next.
+ .use(LanguageDetector)// to detect the language from currentBrowser
+ .use(Backend) // to fetch the data from server
+ .init({
+ lng: 'en-US',
+ defaultNS: 'WebEditor',
+ fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier
+ debug: false,
+ backend: options,
+ getAsync: false,
+ initImmediate: false,
+ useSuspense: true,
+ whitelist: availableLanguages,
+ interpolation: {
+ escapeValue: false, // react already safes from xss
+ },
+ saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler
+ missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys
+ });
+
+export default i18n;
diff --git a/client/images/console-debug-contrast.svg b/client/images/console-debug-contrast.svg
new file mode 100644
index 00000000..76906983
--- /dev/null
+++ b/client/images/console-debug-contrast.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/client/images/console-debug-light.svg b/client/images/console-debug-light.svg
index 8bdc7ae3..0e039342 100644
--- a/client/images/console-debug-light.svg
+++ b/client/images/console-debug-light.svg
@@ -50,5 +50,5 @@
+ fill="#0071AD" />
diff --git a/client/images/console-error-contrast.svg b/client/images/console-error-contrast.svg
new file mode 100644
index 00000000..b455509b
--- /dev/null
+++ b/client/images/console-error-contrast.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/client/images/console-info-contrast.svg b/client/images/console-info-contrast.svg
new file mode 100644
index 00000000..7c37d673
--- /dev/null
+++ b/client/images/console-info-contrast.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/client/images/console-info-dark.svg b/client/images/console-info-dark.svg
index c116d093..869795c5 100644
--- a/client/images/console-info-dark.svg
+++ b/client/images/console-info-dark.svg
@@ -50,5 +50,5 @@
+ fill="#D9D9D9" />
diff --git a/client/images/console-info-light.svg b/client/images/console-info-light.svg
index 24b74263..eaf8be4d 100644
--- a/client/images/console-info-light.svg
+++ b/client/images/console-info-light.svg
@@ -50,5 +50,5 @@
+ fill="#4D4D4D" />
diff --git a/client/images/console-warn-contrast.svg b/client/images/console-warn-contrast.svg
new file mode 100644
index 00000000..6811e3d4
--- /dev/null
+++ b/client/images/console-warn-contrast.svg
@@ -0,0 +1,54 @@
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
diff --git a/client/images/console-warn-light.svg b/client/images/console-warn-light.svg
index 6e76d14a..548ee946 100644
--- a/client/images/console-warn-light.svg
+++ b/client/images/console-warn-light.svg
@@ -50,5 +50,5 @@
+ fill="#996B00" />
diff --git a/client/images/down-arrow.svg b/client/images/down-arrow.svg
index f6eb1e87..a1dc41d5 100644
--- a/client/images/down-arrow.svg
+++ b/client/images/down-arrow.svg
@@ -3,8 +3,8 @@
-
-
+
+
diff --git a/client/images/exit.svg b/client/images/exit.svg
index dd19e998..b6c0cc74 100644
--- a/client/images/exit.svg
+++ b/client/images/exit.svg
@@ -5,7 +5,7 @@
-
+
diff --git a/client/images/google.svg b/client/images/google.svg
index 542fea64..005c874f 100644
--- a/client/images/google.svg
+++ b/client/images/google.svg
@@ -1,47 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
diff --git a/client/images/sort-arrow-down.svg b/client/images/sort-arrow-down.svg
index f9a1fc8e..574bbc25 100644
--- a/client/images/sort-arrow-down.svg
+++ b/client/images/sort-arrow-down.svg
@@ -1,9 +1,8 @@
-
-
-
-
+
+
+
+
diff --git a/client/images/sort-arrow-up.svg b/client/images/sort-arrow-up.svg
index 4fc7ca5d..60635a84 100644
--- a/client/images/sort-arrow-up.svg
+++ b/client/images/sort-arrow-up.svg
@@ -1,9 +1,8 @@
-
-
-
-
+
+
+
+
diff --git a/client/images/triangle-arrow-down-white.svg b/client/images/triangle-arrow-down-white.svg
index a66e9ff6..75fd5567 100644
--- a/client/images/triangle-arrow-down-white.svg
+++ b/client/images/triangle-arrow-down-white.svg
@@ -3,7 +3,7 @@
Created with Sketch.
-
+
diff --git a/client/images/triangle-arrow-down.svg b/client/images/triangle-arrow-down.svg
index a66e9ff6..47ab5d10 100644
--- a/client/images/triangle-arrow-down.svg
+++ b/client/images/triangle-arrow-down.svg
@@ -3,7 +3,7 @@
Created with Sketch.
-
+
diff --git a/client/images/triangle-arrow-right.svg b/client/images/triangle-arrow-right.svg
index a99148b2..222020a8 100644
--- a/client/images/triangle-arrow-right.svg
+++ b/client/images/triangle-arrow-right.svg
@@ -3,7 +3,7 @@
Created with Sketch.
-
+
diff --git a/client/images/up-arrow.svg b/client/images/up-arrow.svg
index a5a3fb18..c4a90f1d 100644
--- a/client/images/up-arrow.svg
+++ b/client/images/up-arrow.svg
@@ -3,8 +3,8 @@
-
-
+
+
diff --git a/client/index.jsx b/client/index.jsx
index 09f6eba0..bf8c2263 100644
--- a/client/index.jsx
+++ b/client/index.jsx
@@ -1,10 +1,14 @@
-import React from 'react';
+import React, { Suspense } from 'react';
import { render } from 'react-dom';
import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
+
import configureStore from './store';
import routes from './routes';
+import ThemeProvider from './modules/App/components/ThemeProvider';
+import Loader from './modules/App/components/loader';
+import i18n from './i18n';
require('./styles/main.scss');
@@ -18,13 +22,17 @@ const store = configureStore(initialState);
const App = () => (
-
+
+
+
);
const HotApp = hot(App);
render(
- ,
+ )}>
+
+ ,
document.getElementById('root')
);
diff --git a/client/index.stories.mdx b/client/index.stories.mdx
new file mode 100644
index 00000000..50a676d1
--- /dev/null
+++ b/client/index.stories.mdx
@@ -0,0 +1,7 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
+
+
+# Welcome to the P5.js Web Editor Style Guide
+
+This guide will contain all the components in the project, with examples of how they can be reused.
diff --git a/client/jest.setup.js b/client/jest.setup.js
new file mode 100644
index 00000000..79652c74
--- /dev/null
+++ b/client/jest.setup.js
@@ -0,0 +1,5 @@
+import '@babel/polyfill';
+
+// See: https://github.com/testing-library/jest-dom
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx
index c9d9b3a1..af441a9d 100644
--- a/client/modules/App/App.jsx
+++ b/client/modules/App/App.jsx
@@ -1,11 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
+import getConfig from '../../utils/getConfig';
import DevTools from './components/DevTools';
import { setPreviousPath } from '../IDE/actions/ide';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-
class App extends React.Component {
constructor(props, context) {
super(props, context);
@@ -35,7 +34,7 @@ class App extends React.Component {
render() {
return (
- {this.state.isMounted && !window.devToolsExtension && __process.env.NODE_ENV === 'development' && }
+ {this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && }
{this.props.children}
);
diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx
index 415a9a0e..4e54b615 100644
--- a/client/modules/App/components/Overlay.jsx
+++ b/client/modules/App/components/Overlay.jsx
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
-const exitUrl = require('../../../images/exit.svg');
+import ExitIcon from '../../../images/exit.svg';
class Overlay extends React.Component {
constructor(props) {
@@ -81,8 +80,8 @@ class Overlay extends React.Component {
{title}
{actions}
-
-
+
+
diff --git a/client/modules/App/components/ThemeProvider.jsx b/client/modules/App/components/ThemeProvider.jsx
new file mode 100644
index 00000000..eb6dcc9b
--- /dev/null
+++ b/client/modules/App/components/ThemeProvider.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { ThemeProvider } from 'styled-components';
+
+import theme, { Theme } from '../../../theme';
+
+const Provider = ({ children, currentTheme }) => (
+
+ {children}
+
+);
+
+Provider.propTypes = {
+ children: PropTypes.node.isRequired,
+ currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired,
+};
+
+function mapStateToProps(state) {
+ return {
+ currentTheme: state.preferences.theme,
+ };
+}
+
+
+export default connect(mapStateToProps)(Provider);
diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js
index 483e6d4e..79df4285 100644
--- a/client/modules/IDE/actions/assets.js
+++ b/client/modules/IDE/actions/assets.js
@@ -1,10 +1,7 @@
-import axios from 'axios';
+import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
-
function setAssets(assets, totalSize) {
return {
type: ActionTypes.SET_ASSETS,
@@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
export function getAssets() {
return (dispatch) => {
dispatch(startLoader());
- axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true })
+ apiClient.get('/S3/objects')
.then((response) => {
dispatch(setAssets(response.data.assets, response.data.totalSize));
dispatch(stopLoader());
@@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
export function deleteAssetRequest(assetKey) {
return (dispatch) => {
- axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
+ apiClient.delete(`/S3/${assetKey}`)
.then((response) => {
dispatch(deleteAsset(assetKey));
})
diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js
index 3bd99dc1..3aa954ac 100644
--- a/client/modules/IDE/actions/collections.js
+++ b/client/modules/IDE/actions/collections.js
@@ -1,11 +1,9 @@
-import axios from 'axios';
import { browserHistory } from 'react-router';
+import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader';
import { setToastText, showToast } from './toast';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
const TOAST_DISPLAY_TIME_MS = 1500;
@@ -15,11 +13,11 @@ export function getCollections(username) {
dispatch(startLoader());
let url;
if (username) {
- url = `${ROOT_URL}/${username}/collections`;
+ url = `/${username}/collections`;
} else {
- url = `${ROOT_URL}/collections`;
+ url = '/collections';
}
- axios.get(url, { withCredentials: true })
+ apiClient.get(url)
.then((response) => {
dispatch({
type: ActionTypes.SET_COLLECTIONS,
@@ -27,7 +25,8 @@ export function getCollections(username) {
});
dispatch(stopLoader());
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
@@ -40,8 +39,8 @@ export function getCollections(username) {
export function createCollection(collection) {
return (dispatch) => {
dispatch(startLoader());
- const url = `${ROOT_URL}/collections`;
- return axios.post(url, collection, { withCredentials: true })
+ const url = '/collections';
+ return apiClient.post(url, collection)
.then((response) => {
dispatch({
type: ActionTypes.CREATE_COLLECTION
@@ -57,7 +56,8 @@ export function createCollection(collection) {
browserHistory.push(location);
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
console.error('Error creating collection', response.data);
dispatch({
type: ActionTypes.ERROR,
@@ -71,8 +71,8 @@ export function createCollection(collection) {
export function addToCollection(collectionId, projectId) {
return (dispatch) => {
dispatch(startLoader());
- const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
- return axios.post(url, { withCredentials: true })
+ const url = `/collections/${collectionId}/${projectId}`;
+ return apiClient.post(url)
.then((response) => {
dispatch({
type: ActionTypes.ADD_TO_COLLECTION,
@@ -87,7 +87,8 @@ export function addToCollection(collectionId, projectId) {
return response.data;
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
@@ -102,8 +103,8 @@ export function addToCollection(collectionId, projectId) {
export function removeFromCollection(collectionId, projectId) {
return (dispatch) => {
dispatch(startLoader());
- const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
- return axios.delete(url, { withCredentials: true })
+ const url = `/collections/${collectionId}/${projectId}`;
+ return apiClient.delete(url)
.then((response) => {
dispatch({
type: ActionTypes.REMOVE_FROM_COLLECTION,
@@ -118,7 +119,8 @@ export function removeFromCollection(collectionId, projectId) {
return response.data;
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
@@ -132,8 +134,8 @@ export function removeFromCollection(collectionId, projectId) {
export function editCollection(collectionId, { name, description }) {
return (dispatch) => {
- const url = `${ROOT_URL}/collections/${collectionId}`;
- return axios.patch(url, { name, description }, { withCredentials: true })
+ const url = `/collections/${collectionId}`;
+ return apiClient.patch(url, { name, description })
.then((response) => {
dispatch({
type: ActionTypes.EDIT_COLLECTION,
@@ -141,7 +143,8 @@ export function editCollection(collectionId, { name, description }) {
});
return response.data;
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
@@ -154,8 +157,8 @@ export function editCollection(collectionId, { name, description }) {
export function deleteCollection(collectionId) {
return (dispatch) => {
- const url = `${ROOT_URL}/collections/${collectionId}`;
- return axios.delete(url, { withCredentials: true })
+ const url = `/collections/${collectionId}`;
+ return apiClient.delete(url)
.then((response) => {
dispatch({
type: ActionTypes.DELETE_COLLECTION,
@@ -164,7 +167,8 @@ export function deleteCollection(collectionId) {
});
return response.data;
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js
index 5edcf74d..e17e46c5 100644
--- a/client/modules/IDE/actions/files.js
+++ b/client/modules/IDE/actions/files.js
@@ -1,13 +1,11 @@
-import axios from 'axios';
import objectID from 'bson-objectid';
import blobUtil from 'blob-util';
import { reset } from 'redux-form';
+import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants';
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
import { setProjectSavedTime } from './project';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
function appendToFilename(filename, string) {
const dotIndex = filename.lastIndexOf('.');
@@ -50,7 +48,7 @@ export function createFile(formProps) {
parentId,
children: []
};
- axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
+ apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => {
dispatch({
type: ActionTypes.CREATE_FILE,
@@ -65,10 +63,13 @@ export function createFile(formProps) {
// });
dispatch(setUnsavedChanges(true));
})
- .catch(response => dispatch({
- type: ActionTypes.ERROR,
- error: response.data
- }));
+ .catch((error) => {
+ const { response } = error;
+ dispatch({
+ type: ActionTypes.ERROR,
+ error: response.data
+ });
+ });
} else {
const id = objectID().toHexString();
dispatch({
@@ -103,7 +104,7 @@ export function createFolder(formProps) {
parentId,
fileType: 'folder'
};
- axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
+ apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => {
dispatch({
type: ActionTypes.CREATE_FILE,
@@ -113,10 +114,13 @@ export function createFolder(formProps) {
dispatch(setProjectSavedTime(response.data.project.updatedAt));
dispatch(closeNewFolderModal());
})
- .catch(response => dispatch({
- type: ActionTypes.ERROR,
- error: response.data
- }));
+ .catch((error) => {
+ const { response } = error;
+ dispatch({
+ type: ActionTypes.ERROR,
+ error: response.data
+ });
+ });
} else {
const id = objectID().toHexString();
dispatch({
@@ -155,7 +159,7 @@ export function deleteFile(id, parentId) {
parentId
}
};
- axios.delete(`${ROOT_URL}/projects/${state.project.id}/files/${id}`, deleteConfig, { withCredentials: true })
+ apiClient.delete(`/projects/${state.project.id}/files/${id}`, deleteConfig)
.then(() => {
dispatch({
type: ActionTypes.DELETE_FILE,
@@ -163,7 +167,8 @@ export function deleteFile(id, parentId) {
parentId
});
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js
index ae8e2438..a182da76 100644
--- a/client/modules/IDE/actions/preferences.js
+++ b/client/modules/IDE/actions/preferences.js
@@ -1,17 +1,17 @@
-import axios from 'axios';
+import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
-
function updatePreferences(formParams, dispatch) {
- axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true })
+ apiClient.put('/preferences', formParams)
.then(() => {
})
- .catch(response => dispatch({
- type: ActionTypes.ERROR,
- error: response.data
- }));
+ .catch((error) => {
+ const { response } = error;
+ dispatch({
+ type: ActionTypes.ERROR,
+ error: response.data
+ });
+ });
}
export function setFontSize(value) {
diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js
index 2621d5c6..9055d484 100644
--- a/client/modules/IDE/actions/project.js
+++ b/client/modules/IDE/actions/project.js
@@ -1,8 +1,9 @@
import { browserHistory } from 'react-router';
-import axios from 'axios';
import objectID from 'bson-objectid';
import each from 'async/each';
import isEqual from 'lodash/isEqual';
+import apiClient from '../../../utils/apiClient';
+import getConfig from '../../../utils/getConfig';
import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast';
import {
@@ -14,8 +15,7 @@ import {
} from './ide';
import { clearState, saveState } from '../../../persistState';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
+const ROOT_URL = getConfig('API_URL');
export function setProject(project) {
return {
@@ -49,18 +49,21 @@ export function setNewProject(project) {
};
}
-export function getProject(id) {
+export function getProject(id, username) {
return (dispatch, getState) => {
dispatch(justOpenedProject());
- axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
+ apiClient.get(`/${username}/projects/${id}`)
.then((response) => {
dispatch(setProject(response.data));
dispatch(setUnsavedChanges(false));
})
- .catch(response => dispatch({
- type: ActionTypes.ERROR,
- error: response.data
- }));
+ .catch((error) => {
+ const { response } = error;
+ dispatch({
+ type: ActionTypes.ERROR,
+ error: response.data
+ });
+ });
};
}
@@ -139,7 +142,7 @@ export function saveProject(selectedFile = null, autosave = false) {
fileToUpdate.content = selectedFile.content;
}
if (state.project.id) {
- return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
+ return apiClient.put(`/projects/${state.project.id}`, formParams)
.then((response) => {
dispatch(endSavingProject());
dispatch(setUnsavedChanges(false));
@@ -152,17 +155,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) {
if (state.ide.justOpenedProject && state.preferences.autosave) {
dispatch(showToast(5500));
- dispatch(setToastText('Project saved.'));
+ dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject());
} else {
dispatch(showToast(1500));
- dispatch(setToastText('Project saved.'));
+ dispatch(setToastText('Sketch saved.'));
}
}
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch(endSavingProject());
+ dispatch(setToastText('Failed to save sketch.'));
+ dispatch(showToast(1500));
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else if (response.status === 409) {
@@ -173,7 +179,7 @@ export function saveProject(selectedFile = null, autosave = false) {
});
}
- return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
+ return apiClient.post('/projects', formParams)
.then((response) => {
dispatch(endSavingProject());
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
@@ -191,17 +197,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) {
if (state.preferences.autosave) {
dispatch(showToast(5500));
- dispatch(setToastText('Project saved.'));
+ dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject());
} else {
dispatch(showToast(1500));
- dispatch(setToastText('Project saved.'));
+ dispatch(setToastText('Sketch saved.'));
}
}
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch(endSavingProject());
+ dispatch(setToastText('Failed to save sketch.'));
+ dispatch(showToast(1500));
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {
@@ -255,7 +264,7 @@ export function cloneProject(id) {
if (!id) {
resolve(getState());
} else {
- fetch(`${ROOT_URL}/projects/${id}`)
+ apiClient.get(`/projects/${id}`)
.then(res => res.json())
.then(data => resolve({
files: data.files,
@@ -282,7 +291,7 @@ export function cloneProject(id) {
const formParams = {
url: file.url
};
- axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true })
+ apiClient.post('/S3/copy', formParams)
.then((response) => {
file.url = response.data.url;
callback(null);
@@ -293,15 +302,18 @@ export function cloneProject(id) {
}, (err) => {
// if not errors in duplicating the files on S3, then duplicate it
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
- axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
+ apiClient.post('/projects', formParams)
.then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch(setNewProject(response.data));
})
- .catch(response => dispatch({
- type: ActionTypes.PROJECT_SAVE_FAIL,
- error: response.data
- }));
+ .catch((error) => {
+ const { response } = error;
+ dispatch({
+ type: ActionTypes.PROJECT_SAVE_FAIL,
+ error: response.data
+ });
+ });
});
});
};
@@ -329,7 +341,7 @@ export function setProjectSavedTime(updatedAt) {
export function changeProjectName(id, newName) {
return (dispatch, getState) => {
const state = getState();
- axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true })
+ apiClient.put(`/projects/${id}`, { name: newName })
.then((response) => {
if (response.status === 200) {
dispatch({
@@ -344,8 +356,8 @@ export function changeProjectName(id, newName) {
}
}
})
- .catch((response) => {
- console.log(response);
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data
@@ -356,7 +368,7 @@ export function changeProjectName(id, newName) {
export function deleteProject(id) {
return (dispatch, getState) => {
- axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
+ apiClient.delete(`/projects/${id}`)
.then(() => {
const state = getState();
if (id === state.project.id) {
@@ -368,7 +380,8 @@ export function deleteProject(id) {
id
});
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {
diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js
index 446c50cc..d41747b5 100644
--- a/client/modules/IDE/actions/projects.js
+++ b/client/modules/IDE/actions/projects.js
@@ -1,21 +1,18 @@
-import axios from 'axios';
+import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const ROOT_URL = __process.env.API_URL;
-
// eslint-disable-next-line
export function getProjects(username) {
return (dispatch) => {
dispatch(startLoader());
let url;
if (username) {
- url = `${ROOT_URL}/${username}/projects`;
+ url = `/${username}/projects`;
} else {
- url = `${ROOT_URL}/projects`;
+ url = '/projects';
}
- axios.get(url, { withCredentials: true })
+ apiClient.get(url)
.then((response) => {
dispatch({
type: ActionTypes.SET_PROJECTS,
@@ -23,7 +20,8 @@ export function getProjects(username) {
});
dispatch(stopLoader());
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
dispatch({
type: ActionTypes.ERROR,
error: response.data
diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js
index 602ac3e5..eca1ffe6 100644
--- a/client/modules/IDE/actions/uploader.js
+++ b/client/modules/IDE/actions/uploader.js
@@ -1,11 +1,10 @@
-import axios from 'axios';
+import apiClient from '../../../utils/apiClient';
+import getConfig from '../../../utils/getConfig';
import { createFile } from './files';
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const s3BucketHttps = __process.env.S3_BUCKET_URL_BASE ||
- `https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
-const ROOT_URL = __process.env.API_URL;
+const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') ||
+ `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
function localIntercept(file, options = {}) {
@@ -46,18 +45,13 @@ export function dropzoneAcceptCallback(userId, file, done) {
});
} else {
file.postData = []; // eslint-disable-line
- axios.post(
- `${ROOT_URL}/S3/sign`, {
- name: file.name,
- type: file.type,
- size: file.size,
- userId
+ apiClient.post('/S3/sign', {
+ name: file.name,
+ type: file.type,
+ size: file.size,
+ userId
// _csrf: document.getElementById('__createPostToken').value
- },
- {
- withCredentials: true
- }
- )
+ })
.then((response) => {
file.custom_status = 'ready'; // eslint-disable-line
file.postData = response.data; // eslint-disable-line
@@ -65,7 +59,8 @@ export function dropzoneAcceptCallback(userId, file, done) {
file.previewTemplate.className += ' uploading'; // eslint-disable-line
done();
})
- .catch((response) => {
+ .catch((error) => {
+ const { response } = error;
file.custom_status = 'rejected'; // eslint-disable-line
if (response.data && response.data.responseText && response.data.responseText.message) {
done(response.data.responseText.message);
diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx
index a8133e98..81bca172 100644
--- a/client/modules/IDE/components/About.jsx
+++ b/client/modules/IDE/components/About.jsx
@@ -1,19 +1,19 @@
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet';
-
-const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
-// const playUrl = require('../../../images/play.svg');
-const asteriskUrl = require('../../../images/p5-asterisk.svg');
+import { useTranslation } from 'react-i18next';
+import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
+// import PlayIcon from '../../../images/play.svg';
+import AsteriskIcon from '../../../images/p5-asterisk.svg';
function About(props) {
+ const { t } = useTranslation();
return (
- p5.js Web Editor | About
+ p5.js Web Editor | About
-
New to p5.js?
+
{t('NewP5')}
-
- Examples
+
+ {t('Examples')}
@@ -43,21 +43,21 @@ function About(props) {
target="_blank"
rel="noopener noreferrer"
>
-
- Learn
+
+ {t('Learn')}
-
Resources
+
{t('Resources')}
-
- Libraries
+
+ {t('Libraries')}
@@ -66,8 +66,8 @@ function About(props) {
target="_blank"
rel="noopener noreferrer"
>
-
- Reference
+
+ {t('Reference')}
@@ -76,8 +76,8 @@ function About(props) {
target="_blank"
rel="noopener noreferrer"
>
-
- Forum
+
+ {t('Forum')}
@@ -87,7 +87,7 @@ function About(props) {
href="https://github.com/processing/p5.js-web-editor"
target="_blank"
rel="noopener noreferrer"
- >Contribute
+ >{t('Contribute')}
@@ -95,7 +95,7 @@ function About(props) {
href="https://github.com/processing/p5.js-web-editor/issues/new"
target="_blank"
rel="noopener noreferrer"
- >Report a bug
+ >{t('Report')}
diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx
index da834dc2..4c2e0371 100644
--- a/client/modules/IDE/components/AssetList.jsx
+++ b/client/modules/IDE/components/AssetList.jsx
@@ -5,11 +5,10 @@ import { bindActionCreators } from 'redux';
import { Link } from 'react-router';
import { Helmet } from 'react-helmet';
import prettyBytes from 'pretty-bytes';
-import InlineSVG from 'react-inlinesvg';
import Loader from '../../App/components/loader';
import * as AssetActions from '../actions/assets';
-import downFilledTriangle from '../../../images/down-filled-triangle.svg';
+import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
class AssetListRowBase extends React.Component {
constructor(props) {
@@ -86,8 +85,9 @@ class AssetListRowBase extends React.Component {
onClick={this.toggleOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
+ aria-label="Toggle Open/Close Asset Options"
>
-
+
{optionsOpen &&
+
{this.getAssetsTitle()}
@@ -195,7 +195,7 @@ class AssetList extends React.Component {
{assetList.map(asset => )}
}
-
+
);
}
}
diff --git a/client/modules/IDE/components/AssetSize.jsx b/client/modules/IDE/components/AssetSize.jsx
index 2e4c1282..cf2356e2 100644
--- a/client/modules/IDE/components/AssetSize.jsx
+++ b/client/modules/IDE/components/AssetSize.jsx
@@ -3,8 +3,9 @@ import React from 'react';
import { connect } from 'react-redux';
import prettyBytes from 'pretty-bytes';
-const __process = (typeof global !== 'undefined' ? global : window).process;
-const limit = __process.env.UPLOAD_LIMIT || 250000000;
+import getConfig from '../../../utils/getConfig';
+
+const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const MAX_SIZE_B = limit;
const formatPercent = (percent) => {
diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx
index e39d3c86..251f3e3c 100644
--- a/client/modules/IDE/components/CollectionList/CollectionList.jsx
+++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Helmet } from 'react-helmet';
-import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import classNames from 'classnames';
@@ -19,8 +18,8 @@ import { SketchSearchbar } from '../Searchbar';
import CollectionListRow from './CollectionListRow';
-const arrowUp = require('../../../../images/sort-arrow-up.svg');
-const arrowDown = require('../../../../images/sort-arrow-down.svg');
+import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
+import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
class CollectionList extends React.Component {
constructor(props) {
@@ -83,21 +82,43 @@ class CollectionList extends React.Component {
return null;
}
+ _getButtonLabel = (fieldName, displayName) => {
+ const { field, direction } = this.props.sorting;
+ let buttonLabel;
+ if (field !== fieldName) {
+ if (field === 'name') {
+ buttonLabel = `Sort by ${displayName} ascending.`;
+ } else {
+ buttonLabel = `Sort by ${displayName} descending.`;
+ }
+ } else if (direction === SortingActions.DIRECTION.ASC) {
+ buttonLabel = `Sort by ${displayName} descending.`;
+ } else {
+ buttonLabel = `Sort by ${displayName} ascending.`;
+ }
+ return buttonLabel;
+ }
+
_renderFieldHeader = (fieldName, displayName) => {
const { field, direction } = this.props.sorting;
const headerClass = classNames({
'sketches-table__header': true,
'sketches-table__header--selected': field === fieldName
});
+ const buttonLabel = this._getButtonLabel(fieldName, displayName);
return (
- this.props.toggleDirectionForField(fieldName)}>
+ this.props.toggleDirectionForField(fieldName)}
+ aria-label={buttonLabel}
+ >
{displayName}
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
-
+
}
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
-
+
}
@@ -108,7 +129,7 @@ class CollectionList extends React.Component {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return (
-
+
{this.getTitle()}
@@ -155,7 +176,7 @@ class CollectionList extends React.Component {
)
}
-
+
);
}
}
diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
index 0f7f9a53..37f992e4 100644
--- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
+++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx
@@ -1,7 +1,6 @@
import format from 'date-fns/format';
import PropTypes from 'prop-types';
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
@@ -10,7 +9,7 @@ import * as CollectionsActions from '../../actions/collections';
import * as IdeActions from '../../actions/ide';
import * as ToastActions from '../../actions/toast';
-const downFilledTriangle = require('../../../../images/down-filled-triangle.svg');
+import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg';
class CollectionListRowBase extends React.Component {
static projectInCollection(project, collection) {
@@ -129,8 +128,9 @@ class CollectionListRowBase extends React.Component {
onClick={this.toggleOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
+ aria-label="Toggle Open/Close collection options"
>
-
+
{optionsOpen &&
-
+
+
Console
-
+
Clear
-
+
-
-
+
+
-
+
{ this.consoleMessages = element; }} className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent) => {
const { method, times } = consoleEvent;
@@ -118,7 +127,7 @@ class Console extends React.Component {
);
})}
-
+
);
}
}
diff --git a/client/modules/IDE/components/CopyableInput.jsx b/client/modules/IDE/components/CopyableInput.jsx
index 2ebc9547..cbce2f3e 100644
--- a/client/modules/IDE/components/CopyableInput.jsx
+++ b/client/modules/IDE/components/CopyableInput.jsx
@@ -1,10 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import Clipboard from 'clipboard';
-import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
-import shareUrl from '../../../images/share.svg';
+import ShareIcon from '../../../images/share.svg';
class CopyableInput extends React.Component {
constructor(props) {
@@ -70,8 +69,9 @@ class CopyableInput extends React.Component {
rel="noopener noreferrer"
href={value}
className="copyable-input__preview"
+ aria-label={`Open ${label} view in new tab`}
>
-
+
}
diff --git a/client/modules/IDE/components/EditableInput.jsx b/client/modules/IDE/components/EditableInput.jsx
index eef5e0b8..c10f3d70 100644
--- a/client/modules/IDE/components/EditableInput.jsx
+++ b/client/modules/IDE/components/EditableInput.jsx
@@ -1,13 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
-const editIconUrl = require('../../../images/pencil.svg');
-
-function EditIcon() {
- return ;
-}
+import EditIcon from '../../../images/pencil.svg';
+// TODO I think this needs a description prop so that it's accessible
function EditableInput({
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
}) {
@@ -52,9 +48,13 @@ function EditableInput({
return (
-
+
{displayValue}
-
+
+
- { this.codemirrorContainer = element; }} className={editorHolderClass} >
-
+ { this.codemirrorContainer = element; }} className={editorHolderClass} >
+
diff --git a/client/modules/IDE/components/Feedback.jsx b/client/modules/IDE/components/Feedback.jsx
index 49b1b42d..409c1dcd 100644
--- a/client/modules/IDE/components/Feedback.jsx
+++ b/client/modules/IDE/components/Feedback.jsx
@@ -1,7 +1,6 @@
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet';
-import githubLogoUrl from '../../../images/github.svg';
+import GitHubLogo from '../../../images/github.svg';
function Feedback(props) {
return (
@@ -24,7 +23,7 @@ function Feedback(props) {
className="feedback__github-link"
>
Go to Github
-
+
diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index 4d560fc1..57238cf8 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
-import downArrowUrl from '../../../images/down-filled-triangle.svg';
-import folderRightUrl from '../../../images/triangle-arrow-right.svg';
-import folderDownUrl from '../../../images/triangle-arrow-down.svg';
-import fileUrl from '../../../images/file.svg';
+import DownArrowIcon from '../../../images/down-filled-triangle.svg';
+import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
+import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
+import FileIcon from '../../../images/file.svg';
export class FileNode extends React.Component {
constructor(props) {
@@ -185,7 +184,7 @@ export class FileNode extends React.Component {
{ isFile &&
-
+
}
{ isFolder &&
@@ -193,24 +192,28 @@ export class FileNode extends React.Component {
-
+
-
+
}
{this.state.updatedName}
{ this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0"
onClick={this.toggleFileOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
-
+
{name.touched && name.error && {name.error} }
diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx
index 36c3cc4d..412d9895 100644
--- a/client/modules/IDE/components/NewFileModal.jsx
+++ b/client/modules/IDE/components/NewFileModal.jsx
@@ -3,13 +3,12 @@ import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { reduxForm } from 'redux-form';
-import InlineSVG from 'react-inlinesvg';
import NewFileForm from './NewFileForm';
import { closeNewFileModal } from '../actions/ide';
import { createFile } from '../actions/files';
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
-const exitUrl = require('../../../images/exit.svg');
+import ExitIcon from '../../../images/exit.svg';
// At some point this will probably be generalized to a generic modal
@@ -35,8 +34,12 @@ class NewFileModal extends React.Component {
{name.touched && name.error && {name.error} }
diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx
index 4af43a89..13029d6c 100644
--- a/client/modules/IDE/components/NewFolderModal.jsx
+++ b/client/modules/IDE/components/NewFolderModal.jsx
@@ -1,10 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import { reduxForm } from 'redux-form';
-import InlineSVG from 'react-inlinesvg';
import NewFolderForm from './NewFolderForm';
-const exitUrl = require('../../../images/exit.svg');
+import ExitIcon from '../../../images/exit.svg';
class NewFolderModal extends React.Component {
componentDidMount() {
@@ -17,8 +16,12 @@ class NewFolderModal extends React.Component {
Create Folder
-
-
+
+
diff --git a/client/modules/IDE/components/Preferences.jsx b/client/modules/IDE/components/Preferences.jsx
index 5b2a2898..1a658c3c 100644
--- a/client/modules/IDE/components/Preferences.jsx
+++ b/client/modules/IDE/components/Preferences.jsx
@@ -1,15 +1,15 @@
import PropTypes from 'prop-types';
import React from 'react';
-import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
+import { withTranslation } from 'react-i18next';
// import { bindActionCreators } from 'redux';
// import { connect } from 'react-redux';
// import * as PreferencesActions from '../actions/preferences';
-const plusUrl = require('../../../images/plus.svg');
-const minusUrl = require('../../../images/minus.svg');
-const beepUrl = require('../../../sounds/audioAlert.mp3');
+import PlusIcon from '../../../images/plus.svg';
+import MinusIcon from '../../../images/minus.svg';
+import beepUrl from '../../../sounds/audioAlert.mp3';
class Preferences extends React.Component {
constructor(props) {
@@ -99,13 +99,13 @@ class Preferences extends React.Component {
- General Settings
- Accessibility
+ {this.props.t('GeneralSettings')}
+ {this.props.t('Accessibility')}
-
Text size
+
{this.props.t('TextSize')}
-
- Decrease
+
+ {this.props.t('Decrease')}
@@ -361,6 +361,7 @@ Preferences.propTypes = {
setLintWarning: PropTypes.func.isRequired,
theme: PropTypes.string.isRequired,
setTheme: PropTypes.func.isRequired,
+ t: PropTypes.func.isRequired,
};
-export default Preferences;
+export default withTranslation('WebEditor')(Preferences);
diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx
index ef19c96d..ef33bb3f 100644
--- a/client/modules/IDE/components/PreviewFrame.jsx
+++ b/client/modules/IDE/components/PreviewFrame.jsx
@@ -22,6 +22,23 @@ import {
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
from '../../../utils/consoleUtils';
+
+const shouldRenderSketch = (props, prevProps = undefined) => {
+ const { isPlaying, previewIsRefreshing, fullView } = props;
+
+ // if the user explicitly clicks on the play button
+ if (isPlaying && previewIsRefreshing) return true;
+
+ if (!prevProps) return false;
+
+ return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender
+ || props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences
+ || props.textOutput !== prevProps.textOutput
+ || props.gridOutput !== prevProps.gridOutput
+ || props.soundOutput !== prevProps.soundOutput
+ || (fullView && props.files[0].id !== prevProps.files[0].id));
+};
+
class PreviewFrame extends React.Component {
constructor(props) {
super(props);
@@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
componentDidMount() {
window.addEventListener('message', this.handleConsoleEvent);
+
+ const props = {
+ ...this.props,
+ previewIsRefreshing: this.props.previewIsRefreshing,
+ isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
+ };
+ if (shouldRenderSketch(props)) this.renderSketch();
}
componentDidUpdate(prevProps) {
- // if sketch starts or stops playing, want to rerender
- if (this.props.isPlaying !== prevProps.isPlaying) {
- this.renderSketch();
- return;
- }
-
- // if the user explicitly clicks on the play button
- if (this.props.isPlaying && this.props.previewIsRefreshing) {
- this.renderSketch();
- return;
- }
-
- // if user switches textoutput preferences
- if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
- this.renderSketch();
- return;
- }
-
- if (this.props.textOutput !== prevProps.textOutput) {
- this.renderSketch();
- return;
- }
-
- if (this.props.gridOutput !== prevProps.gridOutput) {
- this.renderSketch();
- return;
- }
-
- if (this.props.soundOutput !== prevProps.soundOutput) {
- this.renderSketch();
- return;
- }
-
- if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
- this.renderSketch();
- }
-
+ if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
// small bug - if autorefresh is on, and the usr changes files
// in the sketch, preview will reload
}
componentWillUnmount() {
window.removeEventListener('message', this.handleConsoleEvent);
- ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
+ const iframeBody = this.iframeElement.contentDocument.body;
+ if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
}
handleConsoleEvent(messageEvent) {
@@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
+ const quoteCharacter = jsFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
+
if (resolvedFile) {
if (resolvedFile.url) {
- newContent = newContent.replace(filePath, resolvedFile.url);
+ newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
// could also pull file from API instead of using bloburl
const blobURL = getBlobUrl(resolvedFile);
this.props.setBlobUrl(resolvedFile, blobURL);
- const filePathRegex = new RegExp(filePath, 'gi');
- newContent = newContent.replace(filePathRegex, blobURL);
+
+ newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter);
}
}
}
@@ -274,10 +265,11 @@ class PreviewFrame extends React.Component {
cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
+ const quoteCharacter = cssFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
- newContent = newContent.replace(filePath, resolvedFile.url);
+ newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
}
}
}
@@ -353,6 +345,8 @@ class PreviewFrame extends React.Component {
'preview-frame': true,
'preview-frame--full-view': this.props.fullView
});
+ const sandboxAttributes =
+ 'allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals allow-downloads';
return (