Merge branch 'develop' into login-signup-issue

This commit is contained in:
Cassie Tarakajian 2020-07-13 16:50:18 -04:00
commit fd06f5ff9a
182 changed files with 20851 additions and 3978 deletions

View file

@ -6,6 +6,7 @@
"env": { "env": {
"production": { "production": {
"plugins": [ "plugins": [
"babel-plugin-styled-components",
"transform-react-remove-prop-types", "transform-react-remove-prop-types",
"@babel/plugin-transform-react-constant-elements", "@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-react-inline-elements", "@babel/plugin-transform-react-inline-elements",
@ -48,6 +49,7 @@
}, },
"development": { "development": {
"plugins": [ "plugins": [
"babel-plugin-styled-components",
"react-hot-loader/babel" "react-hot-loader/babel"
] ]
} }

View file

@ -2,6 +2,7 @@ API_URL=/editor
AWS_ACCESS_KEY=<your-aws-access-key> AWS_ACCESS_KEY=<your-aws-access-key>
AWS_REGION=<your-aws-region> AWS_REGION=<your-aws-region>
AWS_SECRET_KEY=<your-aws-secret-key> AWS_SECRET_KEY=<your-aws-secret-key>
CORS_ALLOW_LOCALHOST=true
EMAIL_SENDER=<transactional-email-sender> EMAIL_SENDER=<transactional-email-sender>
EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production
EXAMPLE_USER_EMAIL=examples@p5js.org EXAMPLE_USER_EMAIL=examples@p5js.org

View file

@ -77,5 +77,13 @@
"__SERVER__": true, "__SERVER__": true,
"__DISABLE_SSR__": true, "__DISABLE_SSR__": true,
"__DEVTOOLS__": true "__DEVTOOLS__": true
} },
"overrides": [
{
"files": ["*.stories.jsx"],
"rules": {
"import/no-extraneous-dependencies": "off"
}
}
]
} }

View file

@ -15,8 +15,6 @@ Hello! We welcome community contributions to the p5.js Web Editor. Contributing
- [Issue Search and Tagging](#issue-search-and-tagging) - [Issue Search and Tagging](#issue-search-and-tagging)
- [Beginning Work](#beginning-work) - [Beginning Work](#beginning-work)
- [Contribution Guides](#contribution-guides) - [Contribution Guides](#contribution-guides)
- [Writing Commit Messages](#writing-commit-messages)
- [Tips](#tips)
## Code of Conduct ## 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. 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 ### Contribution Guides
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/) * [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/) * [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`.

3
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
custom: https://processingfoundation.org/support github: processing
custom: https://processingfoundation.org/

View file

@ -1,3 +1,5 @@
Fixes #issue-number
I have verified that this pull request: I have verified that this pull request:
* [ ] has no linting errors (`npm run lint`) * [ ] has no linting errors (`npm run lint`)

2
.gitignore vendored
View file

@ -17,3 +17,5 @@ cert_chain.crt
localhost.crt localhost.crt
localhost.key localhost.key
privkey.pem privkey.pem
storybook-static

29
.storybook/main.js Normal file
View file

@ -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;
},
};

31
.storybook/preview.js Normal file
View file

@ -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 => <ThemeProvider theme={theme}>{storyFn()}</ThemeProvider>);

View file

@ -37,12 +37,13 @@ deploy:
script: ./deploy.sh script: ./deploy.sh
skip_cleanup: true skip_cleanup: true
on: on:
branch: master branch: release
tags: true
- provider: script - provider: script
script: ./deploy_staging.sh script: ./deploy_staging.sh
skip_cleanup: true skip_cleanup: true
on: on:
branch: feature/public-api branch: develop
env: env:
global: global:

View file

@ -14,6 +14,7 @@ COPY .babelrc index.js nodemon.json ./
COPY ./webpack ./webpack COPY ./webpack ./webpack
COPY client ./client COPY client ./client
COPY server ./server COPY server ./server
COPY translations/locales ./translations/locales
CMD ["npm", "start"] CMD ["npm", "start"]
FROM development as build FROM development as build

240
client/common/Button.jsx Normal file
View file

@ -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 && <span>{children}</span>}{iconAfter}</>;
let StyledComponent = StyledButton;
if (kind === kinds.inline) {
StyledComponent = StyledInlineButton;
} else if (kind === kinds.icon) {
StyledComponent = StyledIconButton;
}
if (href) {
return (
<StyledComponent
kind={kind}
as="a"
aria-label={ariaLabel}
href={href}
{...props}
>
{content}
</StyledComponent>
);
}
if (to) {
return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>;
}
return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>;
};
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 <a> 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;

View file

@ -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 = () => (
<Button
disabled={boolean('disabled', false)}
type="submit"
label={text('label', 'submit')}
>
{text('children', 'this is the button')}
</Button>
);
export const SubmitButton = () => (
<Button type="submit" label="submit">This is a submit button</Button>
);
export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>;
export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>;
export const AnchorButton = () => (
<Button href="http://p5js.org" label="submit">Actually an anchor</Button>
);
export const ReactRouterLink = () => (
<Button to="./somewhere" label="submit">Actually a Link</Button>
);
export const ButtonWithIconBefore = () => (
<Button
iconBefore={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const ButtonWithIconAfter = () => (
<Button
iconAfter={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const InlineButtonWithIconAfter = () => (
<Button
iconAfter={<DropdownArrowIcon />}
kind={Button.kinds.inline}
>
File name
</Button>
);
export const InlineIconOnlyButton = () => (
<Button
aria-label="Add to collection"
iconBefore={<PlusIcon />}
kind={Button.kinds.inline}
/>
);

76
client/common/icons.jsx Normal file
View file

@ -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 (<StyledIcon
{...props}
aria-label={ariaLabel}
role="img"
focusable="false"
/>);
}
return (<StyledIcon
{...props}
aria-hidden
focusable="false"
/>);
};
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);

View file

@ -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 (
<SelectedIcon />
);
};

View file

@ -1,17 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import InlineSVG from 'react-inlinesvg';
const addIcon = require('../images/plus.svg'); import AddIcon from '../images/plus.svg';
const removeIcon = require('../images/minus.svg'); import RemoveIcon from '../images/minus.svg';
const AddRemoveButton = ({ type, onClick }) => { const AddRemoveButton = ({ type, onClick }) => {
const alt = type === 'add' ? 'add to collection' : 'remove from collection'; const alt = type === 'add' ? 'Add to collection' : 'Remove from collection';
const icon = type === 'add' ? addIcon : removeIcon; const Icon = type === 'add' ? AddIcon : RemoveIcon;
return ( return (
<button className="overlay__close-button" onClick={onClick}> <button
<InlineSVG src={icon} alt={alt} /> className="overlay__close-button"
onClick={onClick}
aria-label={alt}
>
<Icon focusable="false" aria-hidden="true" />
</button> </button>
); );
}; };

View file

@ -3,21 +3,21 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { Link } from 'react-router'; import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames'; import classNames from 'classnames';
import { withTranslation } from 'react-i18next';
import i18next from 'i18next';
import * as IDEActions from '../modules/IDE/actions/ide'; import * as IDEActions from '../modules/IDE/actions/ide';
import * as toastActions from '../modules/IDE/actions/toast'; import * as toastActions from '../modules/IDE/actions/toast';
import * as projectActions from '../modules/IDE/actions/project'; import * as projectActions from '../modules/IDE/actions/project';
import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
import { logoutUser } from '../modules/User/actions'; import { logoutUser } from '../modules/User/actions';
import getConfig from '../utils/getConfig';
import { metaKeyName, } from '../utils/metaKey'; import { metaKeyName, } from '../utils/metaKey';
import caretLeft from '../images/left-arrow.svg';
const triangleUrl = require('../images/down-filled-triangle.svg'); import CaretLeftIcon from '../images/left-arrow.svg';
const logoUrl = require('../images/p5js-logo-small.svg'); import TriangleIcon from '../images/down-filled-triangle.svg';
import LogoIcon from '../images/p5js-logo-small.svg';
const __process = (typeof global !== 'undefined' ? global : window).process;
class Nav extends React.PureComponent { class Nav extends React.PureComponent {
constructor(props) { constructor(props) {
@ -57,6 +57,10 @@ class Nav extends React.PureComponent {
this.handleFocusForHelp = this.handleFocus.bind(this, 'help'); this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account'); this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
this.handleFocusForAccount = this.handleFocus.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); this.closeDropDown = this.closeDropDown.bind(this);
} }
@ -165,6 +169,13 @@ class Nav extends React.PureComponent {
this.setDropdown('none'); this.setDropdown('none');
} }
handleLangSelection(event) {
i18next.changeLanguage(event.target.value);
this.props.showToast(1500);
this.props.setToastText('LangChange');
this.setDropdown('none');
}
handleLogout() { handleLogout() {
this.props.logoutUser(); this.props.logoutUser();
this.setDropdown('none'); this.setDropdown('none');
@ -229,13 +240,13 @@ class Nav extends React.PureComponent {
return ( return (
<ul className="nav__items-left"> <ul className="nav__items-left">
<li className="nav__item-logo"> <li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" /> <LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
</li> </li>
<li className="nav__item nav__item--no-icon"> <li className="nav__item nav__item--no-icon">
<Link to="/" className="nav__back-link"> <Link to="/" className="nav__back-link">
<InlineSVG src={caretLeft} className="nav__back-icon" /> <CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" />
<span className="nav__item-header"> <span className="nav__item-header">
Back to Editor {this.props.t('BackEditor')}
</span> </span>
</Link> </Link>
</li> </li>
@ -247,7 +258,7 @@ class Nav extends React.PureComponent {
return ( return (
<ul className="nav__items-left"> <ul className="nav__items-left">
<li className="nav__item-logo"> <li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" /> <LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
</li> </li>
<li className={navDropdownState.file}> <li className={navDropdownState.file}>
<button <button
@ -260,8 +271,8 @@ class Nav extends React.PureComponent {
} }
}} }}
> >
<span className="nav__item-header">File</span> <span className="nav__item-header">{this.props.t('File')}</span>
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} /> <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button> </button>
<ul className="nav__dropdown"> <ul className="nav__dropdown">
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -270,18 +281,18 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile} onFocus={this.handleFocusForFile}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
New {this.props.t('New')}
</button> </button>
</li> </li>
{ __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) && { getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<button <button
onClick={this.handleSave} onClick={this.handleSave}
onFocus={this.handleFocusForFile} onFocus={this.handleFocusForFile}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Save {this.props.t('Save')}
<span className="nav__keyboard-shortcut">{metaKeyName}+s</span> <span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
</button> </button>
</li> } </li> }
{ this.props.project.id && this.props.user.authenticated && { this.props.project.id && this.props.user.authenticated &&
@ -291,7 +302,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile} onFocus={this.handleFocusForFile}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Duplicate {this.props.t('Duplicate')}
</button> </button>
</li> } </li> }
{ this.props.project.id && { this.props.project.id &&
@ -301,7 +312,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile} onFocus={this.handleFocusForFile}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Share {this.props.t('Share')}
</button> </button>
</li> } </li> }
{ this.props.project.id && { this.props.project.id &&
@ -311,7 +322,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile} onFocus={this.handleFocusForFile}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Download {this.props.t('Download')}
</button> </button>
</li> } </li> }
{ this.props.user.authenticated && { this.props.user.authenticated &&
@ -322,10 +333,10 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
Open {this.props.t('Open')}
</Link> </Link>
</li> } </li> }
{__process.env.UI_COLLECTIONS_ENABLED && {getConfig('UI_COLLECTIONS_ENABLED') &&
this.props.user.authenticated && this.props.user.authenticated &&
this.props.project.id && this.props.project.id &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -335,10 +346,10 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
Add to Collection {this.props.t('AddToCollection')}
</Link> </Link>
</li>} </li>}
{ __process.env.EXAMPLES_ENABLED && { getConfig('EXAMPLES_ENABLED') &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<Link <Link
to="/p5/sketches" to="/p5/sketches"
@ -346,7 +357,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
Examples {this.props.t('Examples')}
</Link> </Link>
</li> } </li> }
</ul> </ul>
@ -362,8 +373,8 @@ class Nav extends React.PureComponent {
} }
}} }}
> >
<span className="nav__item-header">Edit</span> <span className="nav__item-header">{this.props.t('Edit')}</span>
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} /> <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button> </button>
<ul className="nav__dropdown" > <ul className="nav__dropdown" >
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -375,7 +386,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForEdit} onFocus={this.handleFocusForEdit}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Tidy Code {this.props.t('TidyCode')}
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span> <span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
</button> </button>
</li> </li>
@ -385,7 +396,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForEdit} onFocus={this.handleFocusForEdit}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Find {this.props.t('Find')}
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span> <span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
</button> </button>
</li> </li>
@ -395,7 +406,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForEdit} onFocus={this.handleFocusForEdit}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Find Next {this.props.t('FindNext')}
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span> <span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
</button> </button>
</li> </li>
@ -405,7 +416,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForEdit} onFocus={this.handleFocusForEdit}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Find Previous {this.props.t('FindPrevious')}
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span> <span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
</button> </button>
</li> </li>
@ -422,8 +433,8 @@ class Nav extends React.PureComponent {
} }
}} }}
> >
<span className="nav__item-header">Sketch</span> <span className="nav__item-header">{this.props.t('Sketch')}</span>
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} /> <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button> </button>
<ul className="nav__dropdown"> <ul className="nav__dropdown">
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -432,7 +443,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForSketch} onFocus={this.handleFocusForSketch}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Add File {this.props.t('AddFile')}
</button> </button>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -441,7 +452,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForSketch} onFocus={this.handleFocusForSketch}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Add Folder {this.props.t('AddFolder')}
</button> </button>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -450,7 +461,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForSketch} onFocus={this.handleFocusForSketch}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Run {this.props.t('Run')}
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span> <span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
</button> </button>
</li> </li>
@ -460,7 +471,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForSketch} onFocus={this.handleFocusForSketch}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Stop {this.props.t('Stop')}
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span> <span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
</button> </button>
</li> </li>
@ -497,8 +508,8 @@ class Nav extends React.PureComponent {
} }
}} }}
> >
<span className="nav__item-header">Help</span> <span className="nav__item-header">{this.props.t('Help')}</span>
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} /> <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button> </button>
<ul className="nav__dropdown"> <ul className="nav__dropdown">
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -507,7 +518,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.handleKeyboardShortcuts} onClick={this.handleKeyboardShortcuts}
> >
Keyboard Shortcuts {this.props.t('KeyboardShortcuts')}
</button> </button>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -518,7 +529,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForHelp} onFocus={this.handleFocusForHelp}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
>Reference >{this.props.t('Reference')}
</a> </a>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -528,7 +539,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
About {this.props.t('About')}
</Link> </Link>
</li> </li>
</ul> </ul>
@ -537,18 +548,73 @@ class Nav extends React.PureComponent {
); );
} }
renderLanguageMenu(navDropdownState) {
return (
<ul className="nav__items-right" title="user-menu">
<li className={navDropdownState.lang}>
<button
onClick={this.toggleDropdownForLang}
onBlur={this.handleBlur}
onFocus={this.clearHideTimeout}
onMouseOver={() => {
if (this.state.dropdownOpen !== 'none') {
this.setDropdown('lang');
}
}}
>
<span className="nav__item-header"> {this.props.t('Lang')}</span>
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button>
<ul className="nav__dropdown">
<li className="nav__dropdown-item">
<button
onFocus={this.handleFocusForLang}
onBlur={this.handleBlur}
value="it"
onClick={e => this.handleLangSelection(e)}
>
Italian (Test Fallback)
</button>
</li>
<li className="nav__dropdown-item">
<button
onFocus={this.handleFocusForLang}
onBlur={this.handleBlur}
value="en-US"
onClick={e => this.handleLangSelection(e)}
>English
</button>
</li>
<li className="nav__dropdown-item">
<button
onFocus={this.handleFocusForLang}
onBlur={this.handleBlur}
value="es-419"
onClick={e => this.handleLangSelection(e)}
>
Español
</button>
</li>
</ul>
</li>
</ul>
);
}
renderUnauthenticatedUserMenu(navDropdownState) { renderUnauthenticatedUserMenu(navDropdownState) {
return ( return (
<ul className="nav__items-right" title="user-menu"> <ul className="nav__items-right" title="user-menu">
<li className="nav__item"> <li className="nav__item">
<Link to="/login"> <Link to="/login" className="nav__auth-button">
<span className="nav__item-header">Log in</span> <span className="nav__item-header">{this.props.t('Login')}</span>
</Link> </Link>
</li> </li>
<span className="nav__item-spacer">or</span> <span className="nav__item-or">{this.props.t('LoginOr')}</span>
<li className="nav__item"> <li className="nav__item">
<Link to="/signup"> <Link to="/signup" className="nav__auth-button">
<span className="nav__item-header">Sign up</span> <span className="nav__item-header">{this.props.t('SignUp')}</span>
</Link> </Link>
</li> </li>
</ul> </ul>
@ -559,7 +625,7 @@ class Nav extends React.PureComponent {
return ( return (
<ul className="nav__items-right" title="user-menu"> <ul className="nav__items-right" title="user-menu">
<li className="nav__item"> <li className="nav__item">
<span>Hello, {this.props.user.username}!</span> <span>{this.props.t('Hello')}, {this.props.user.username}!</span>
</li> </li>
<span className="nav__item-spacer">|</span> <span className="nav__item-spacer">|</span>
<li className={navDropdownState.account}> <li className={navDropdownState.account}>
@ -574,8 +640,8 @@ class Nav extends React.PureComponent {
} }
}} }}
> >
My Account {this.props.t('MyAccount')}
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} /> <TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button> </button>
<ul className="nav__dropdown"> <ul className="nav__dropdown">
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -585,10 +651,10 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
My sketches {this.props.t('MySketches')}
</Link> </Link>
</li> </li>
{__process.env.UI_COLLECTIONS_ENABLED && {getConfig('UI_COLLECTIONS_ENABLED') &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<Link <Link
to={`/${this.props.user.username}/collections`} to={`/${this.props.user.username}/collections`}
@ -596,7 +662,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
My collections {this.props.t('MyCollections')}
</Link> </Link>
</li> </li>
} }
@ -607,7 +673,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
My assets {this.props.t('MyAssets')}
</Link> </Link>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -617,7 +683,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur} onBlur={this.handleBlur}
onClick={this.setDropdownForNone} onClick={this.setDropdownForNone}
> >
Settings {this.props.t('Settings')}
</Link> </Link>
</li> </li>
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -626,7 +692,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForAccount} onFocus={this.handleFocusForAccount}
onBlur={this.handleBlur} onBlur={this.handleBlur}
> >
Log out {this.props.t('LogOut')}
</button> </button>
</li> </li>
</ul> </ul>
@ -636,7 +702,7 @@ class Nav extends React.PureComponent {
} }
renderUserMenu(navDropdownState) { renderUserMenu(navDropdownState) {
const isLoginEnabled = __process.env.LOGIN_ENABLED; const isLoginEnabled = getConfig('LOGIN_ENABLED');
const isAuthenticated = this.props.user.authenticated; const isAuthenticated = this.props.user.authenticated;
if (isLoginEnabled && isAuthenticated) { if (isLoginEnabled && isAuthenticated) {
@ -679,25 +745,21 @@ class Nav extends React.PureComponent {
account: classNames({ account: classNames({
'nav__item': true, 'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'account' 'nav__item--open': this.state.dropdownOpen === 'account'
}),
lang: classNames({
'nav__item': true,
'nav__item--open': this.state.dropdownOpen === 'lang'
}) })
}; };
return ( return (
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}> <header>
{this.renderLeftLayout(navDropdownState)} <nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
{this.renderUserMenu(navDropdownState)} {this.renderLeftLayout(navDropdownState)}
{/* {getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
<div className="nav__announce"> {this.renderUserMenu(navDropdownState)}
This is a preview version of the editor, that has not yet been officially released. </nav>
It is in development, you can report bugs <a </header>
href="https://github.com/processing/p5.js-web-editor/issues"
target="_blank"
rel="noopener noreferrer"
>here</a>.
Please use with caution.
</div>
*/}
</nav>
); );
} }
} }
@ -745,7 +807,9 @@ Nav.propTypes = {
}).isRequired, }).isRequired,
params: PropTypes.shape({ params: PropTypes.shape({
username: PropTypes.string username: PropTypes.string
}) }),
t: PropTypes.func.isRequired
}; };
Nav.defaultProps = { Nav.defaultProps = {
@ -778,5 +842,5 @@ const mapDispatchToProps = {
setAllAccessibleOutput setAllAccessibleOutput
}; };
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)); export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
export { Nav as NavComponent }; export { Nav as NavComponent };

View file

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
const logoUrl = require('../images/p5js-logo-small.svg'); import LogoIcon from '../images/p5js-logo-small.svg';
const arrowUrl = require('../images/triangle-arrow-left.svg'); import ArrowIcon from '../images/triangle-arrow-left.svg';
class NavBasic extends React.PureComponent { class NavBasic extends React.PureComponent {
static defaultProps = { static defaultProps = {
@ -15,13 +14,13 @@ class NavBasic extends React.PureComponent {
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}> <nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
<ul className="nav__items-left"> <ul className="nav__items-left">
<li className="nav__item-logo"> <li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" /> <LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
</li> </li>
{ this.props.onBack && ( { this.props.onBack && (
<li className="nav__item"> <li className="nav__item">
<button onClick={this.props.onBack}> <button onClick={this.props.onBack}>
<span className="nav__item-header"> <span className="nav__item-header">
<InlineSVG src={arrowUrl} alt="Left arrow" /> <ArrowIcon focusable="false" aria-hidden="true" />
</span> </span>
Back to the editor Back to the editor
</button> </button>

View file

@ -1,24 +1,23 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
const logoUrl = require('../images/p5js-logo-small.svg'); import LogoIcon from '../images/p5js-logo-small.svg';
const editorUrl = require('../images/code.svg'); import CodeIcon from '../images/code.svg';
const PreviewNav = ({ owner, project }) => ( const PreviewNav = ({ owner, project }) => (
<nav className="nav preview-nav"> <nav className="nav preview-nav">
<div className="nav__items-left"> <div className="nav__items-left">
<div className="nav__item-logo"> <div className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" /> <LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
</div> </div>
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link> <Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
<p className="toolbar__project-owner">by</p> <p className="toolbar__project-owner">by</p>
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link> <Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
</div> </div>
<div className="nav__items-right"> <div className="nav__items-right">
<Link to={`/${owner.username}/sketches/${project.id}`}> <Link to={`/${owner.username}/sketches/${project.id}`} aria-label="Edit Sketch" >
<InlineSVG className="preview-nav__editor-svg" src={editorUrl} /> <CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
</Link> </Link>
</div> </div>
</nav> </nav>

View file

@ -1,71 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FileNode } from '../../modules/IDE/components/FileNode';
beforeAll(() => {});
describe('<FileNode />', () => {
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(<FileNode {...props} />);
});
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();
});
});
});
});
});

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { render } from '@testing-library/react';
import renderer from 'react-test-renderer';
import { NavComponent } from './../Nav';
import { NavComponent } from '../Nav';
describe('Nav', () => { describe('Nav', () => {
const props = { const props = {
@ -44,19 +44,12 @@ describe('Nav', () => {
setToastText: jest.fn(), setToastText: jest.fn(),
rootFile: { rootFile: {
id: 'root-file' id: 'root-file'
} },
t: jest.fn()
}; };
const getWrapper = () => shallow(<NavComponent {...props} />);
test('it renders main navigation', () => {
const nav = getWrapper();
expect(nav.exists('.nav')).toEqual(true);
});
it('renders correctly', () => { it('renders correctly', () => {
const tree = renderer const { asFragment } = render(<NavComponent {...props} />);
.create(<NavComponent {...props} />) expect(asFragment()).toMatchSnapshot();
.toJSON();
expect(tree).toMatchSnapshot();
}); });
}); });

View file

@ -1,333 +1,219 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nav renders correctly 1`] = ` exports[`Nav renders correctly 1`] = `
<nav <DocumentFragment>
className="nav" <header>
title="main-navigation" <nav
> class="nav"
<ul title="main-navigation"
className="nav__items-left"
>
<li
className="nav__item-logo"
> >
<span
className="isvg loading svg__logo"
/>
</li>
<li
className="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}
>
<span
className="nav__item-header"
>
File
</span>
<span
className="isvg loading nav__item-header-triangle"
/>
</button>
<ul <ul
className="nav__dropdown" class="nav__items-left"
> >
<li <li
className="nav__dropdown-item" class="nav__item-logo"
> >
<button <test-file-stub
onBlur={[Function]} aria-label="p5.js Logo"
onClick={[Function]} classname="svg__logo"
onFocus={[Function]} focusable="false"
> role="img"
New />
</button>
</li> </li>
<li <li
className="nav__dropdown-item" class="nav__item"
> >
<button <button>
onBlur={[Function]} <span
onClick={[Function]} class="nav__item-header"
onFocus={[Function]} />
> <test-file-stub
Duplicate aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</button> </button>
<ul
class="nav__dropdown"
>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<a />
</li>
</ul>
</li> </li>
<li <li
className="nav__dropdown-item" class="nav__item"
> >
<button <button>
onBlur={[Function]} <span
onClick={[Function]} class="nav__item-header"
onFocus={[Function]} />
> <test-file-stub
Share aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</button> </button>
<ul
class="nav__dropdown"
>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⇧+Tab
</span>
</button>
</li>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⌃+F
</span>
</button>
</li>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⌃+G
</span>
</button>
</li>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⇧+⌃+G
</span>
</button>
</li>
</ul>
</li> </li>
<li <li
className="nav__dropdown-item" class="nav__item"
> >
<button <button>
onBlur={[Function]} <span
onClick={[Function]} class="nav__item-header"
onFocus={[Function]} />
> <test-file-stub
Download aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</button> </button>
<ul
class="nav__dropdown"
>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⌃+Enter
</span>
</button>
</li>
<li
class="nav__dropdown-item"
>
<button>
<span
class="nav__keyboard-shortcut"
>
⇧+⌃+Enter
</span>
</button>
</li>
</ul>
</li> </li>
<li <li
className="nav__dropdown-item" class="nav__item"
> >
<a <button>
onBlur={[Function]} <span
onClick={[Function]} class="nav__item-header"
onFocus={[Function]} />
style={Object {}} <test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</button>
<ul
class="nav__dropdown"
> >
Open <li
</a> class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<a
href="https://p5js.org/reference/"
rel="noopener noreferrer"
target="_blank"
/>
</li>
<li
class="nav__dropdown-item"
>
<a />
</li>
</ul>
</li> </li>
</ul> </ul>
</li> </nav>
<li </header>
className="nav__item" </DocumentFragment>
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}
>
<span
className="nav__item-header"
>
Edit
</span>
<span
className="isvg loading nav__item-header-triangle"
/>
</button>
<ul
className="nav__dropdown"
>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Tidy Code
<span
className="nav__keyboard-shortcut"
>
+Tab
</span>
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Find
<span
className="nav__keyboard-shortcut"
>
Ctrl
+F
</span>
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Find Next
<span
className="nav__keyboard-shortcut"
>
Ctrl
+G
</span>
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Find Previous
<span
className="nav__keyboard-shortcut"
>
+
Ctrl
+G
</span>
</button>
</li>
</ul>
</li>
<li
className="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}
>
<span
className="nav__item-header"
>
Sketch
</span>
<span
className="isvg loading nav__item-header-triangle"
/>
</button>
<ul
className="nav__dropdown"
>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Add File
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Add Folder
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Run
<span
className="nav__keyboard-shortcut"
>
Ctrl
+Enter
</span>
</button>
</li>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Stop
<span
className="nav__keyboard-shortcut"
>
+
Ctrl
+Enter
</span>
</button>
</li>
</ul>
</li>
<li
className="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}
>
<span
className="nav__item-header"
>
Help
</span>
<span
className="isvg loading nav__item-header-triangle"
/>
</button>
<ul
className="nav__dropdown"
>
<li
className="nav__dropdown-item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Keyboard Shortcuts
</button>
</li>
<li
className="nav__dropdown-item"
>
<a
href="https://p5js.org/reference/"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
rel="noopener noreferrer"
target="_blank"
>
Reference
</a>
</li>
<li
className="nav__dropdown-item"
>
<a
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
style={Object {}}
>
About
</a>
</li>
</ul>
</li>
</ul>
</nav>
`; `;

View file

@ -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;

View file

@ -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
}) => (
<HeaderDiv transparent={transparent}>
{leftButton}
<TitleContainer padded={subtitle === null}>
{title && <h2>{title}</h2>}
{subtitle && <h3>{subtitle}</h3>}
</TitleContainer>
<IconContainer>
{children}
</IconContainer>
</HeaderDiv>
);
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;

View file

@ -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)};
`;

View file

@ -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 (<ButtonWrapper
iconBefore={<Icon />}
kind={Button.kinds.inline}
focusable="false"
{...otherProps}
/>);
};
IconButton.propTypes = {
icon: PropTypes.func.isRequired
};
export default IconButton;

View file

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
const Screen = ({ children, fullscreen }) => (
<div className={fullscreen && 'fullscreen-preview'}>
{children}
</div>
);
Screen.defaultProps = {
fullscreen: false
};
Screen.propTypes = {
children: PropTypes.node.isRequired,
fullscreen: PropTypes.bool
};
export default Screen;

View file

@ -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,
}) => (
<Preference>
<PreferenceTitle>{title}</PreferenceTitle>
<div className="preference__options">
{options.map(option => (
<React.Fragment key={`${option.name}-${option.id}`} >
<input
type="radio"
onChange={() => 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}
/>
<OptionLabel
key={`${option.name}-${option.id}-label`}
htmlFor={option.id}
>
{option.label}
</OptionLabel>
</React.Fragment>))}
</div>
</Preference>
);
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;

38
client/i18n.js Normal file
View file

@ -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;

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="100%"
height="100%"
viewBox="0 0 24 24"
id="svg4"
sodipodi:docname="console-debug-contrast.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="23.6"
inkscape:cx="5"
inkscape:cy="5"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
id="path2"
fill="#38B6F5" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -50,5 +50,5 @@
<path <path
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z" d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
id="path2" id="path2"
fill="#007BBB" /> fill="#0071AD" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="100%"
height="100%"
viewBox="0 0 24 24"
id="svg4"
sodipodi:docname="console-error-contrast.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="23.6"
inkscape:cx="5"
inkscape:cy="5"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
id="path2"
fill="#EA7B7D" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="100%"
height="100%"
viewBox="0 0 24 24"
id="svg4"
sodipodi:docname="console-info-contrast.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="23.6"
inkscape:cx="5"
inkscape:cy="5"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
id="path2"
fill="#D9D9D9" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -50,5 +50,5 @@
<path <path
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
id="path2" id="path2"
fill="#a3a3a3" /> fill="#D9D9D9" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -50,5 +50,5 @@
<path <path
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
id="path2" id="path2"
fill="#7D7D7D" /> fill="#4D4D4D" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="100%"
height="100%"
viewBox="0 0 24 24"
id="svg4"
sodipodi:docname="console-warn-contrast.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview6"
showgrid="false"
inkscape:zoom="23.6"
inkscape:cx="5"
inkscape:cy="5"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
id="path2"
fill="#f5bc38" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -50,5 +50,5 @@
<path <path
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
id="path2" id="path2"
fill="#FAAF00" /> fill="#996B00" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -3,8 +3,8 @@
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
<!-- <desc>Created with Sketch.</desc> --> <!-- <desc>Created with Sketch.</desc> -->
<defs></defs> <defs></defs>
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962"> <g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1.0">
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#333333"> <g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#AAA">
<g id="Icons" transform="translate(16.000000, 1063.000000)"> <g id="Icons" transform="translate(16.000000, 1063.000000)">
<polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon> <polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon>
</g> </g>

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 968 B

View file

@ -5,7 +5,7 @@
<!-- <desc>Created with Sketch.</desc> --> <!-- <desc>Created with Sketch.</desc> -->
<defs></defs> <defs></defs>
<g id="exit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="exit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard-1" fill="#D8D8D8"> <g id="Artboard-1" fill="#AAA">
<path d="M8,5.87867966 L2.69669914,0.575378798 L0.575378798,2.69669914 L5.87867966,8 L0.575378798,13.3033009 L2.69669914,15.4246212 L8,10.1213203 L13.3033009,15.4246212 L15.4246212,13.3033009 L10.1213203,8 L15.4246212,2.69669914 L13.3033009,0.575378798 L8,5.87867966 Z" id="exit"></path> <path d="M8,5.87867966 L2.69669914,0.575378798 L0.575378798,2.69669914 L5.87867966,8 L0.575378798,13.3033009 L2.69669914,15.4246212 L8,10.1213203 L13.3033009,15.4246212 L15.4246212,13.3033009 L10.1213203,8 L15.4246212,2.69669914 L13.3033009,0.575378798 L8,5.87867966 Z" id="exit"></path>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 823 B

View file

@ -1,47 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <g>
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> <path d="M7.091875,19.338 L5.978,23.49625 L1.9068125,23.582375 C0.690125,21.3256875 0,18.74375 0,16 C0,13.3468125 0.64525,10.8448125 1.789,8.64175 L1.789875,8.64175 L5.414375,9.30625 L7.002125,12.909 C6.6698125,13.8778125 6.4886875,14.9178125 6.4886875,16 C6.4888125,17.1745 6.7015625,18.2998125 7.091875,19.338 Z" fill="#FBBB00"></path>
<path style="fill:#FBBB00;" d="M113.47,309.408L95.648,375.94l-65.139,1.378C11.042,341.211,0,299.9,0,256 <path d="M31.7204375,13.011 C31.9041875,13.978875 32,14.9784375 32,16 C32,17.1455 31.8795625,18.262875 31.650125,19.3406875 C30.87125,23.008375 28.8360625,26.211 26.01675,28.477375 L26.015875,28.4765 L21.450625,28.2435625 L20.8045,24.210125 C22.67525,23.113 24.13725,21.3960625 24.907375,19.3406875 L16.35175,19.3406875 L16.35175,13.011 L25.0321875,13.011 L31.7204375,13.011 Z" fill="#518EF8"></path>
c0-42.451,10.324-82.483,28.624-117.732h0.014l57.992,10.632l25.404,57.644c-5.317,15.501-8.215,32.141-8.215,49.456 <path d="M26.0158125,28.4765 L26.0166875,28.477375 C23.27475,30.6813125 19.791625,32 16,32 C9.9068125,32 4.60925,28.5943125 1.9068125,23.5824375 L7.091875,19.3380625 C8.4430625,22.9441875 11.92175,25.51125 16,25.51125 C17.7529375,25.51125 19.3951875,25.037375 20.804375,24.210125 L26.0158125,28.4765 Z" fill="#28B446"></path>
C103.821,274.792,107.225,292.797,113.47,309.408z"/> <path d="M26.21275,3.6835 L21.0294375,7.927 C19.571,7.015375 17.847,6.48875 16,6.48875 C11.8294375,6.48875 8.2856875,9.1735625 7.0021875,12.909 L1.789875,8.64175 L1.789,8.64175 C4.451875,3.5076875 9.81625,0 16,0 C19.8821875,0 23.44175,1.382875 26.21275,3.6835 Z" fill="#F14336"></path>
<path style="fill:#518EF8;" d="M507.527,208.176C510.467,223.662,512,239.655,512,256c0,18.328-1.927,36.206-5.598,53.451 </g>
c-12.462,58.683-45.025,109.925-90.134,146.187l-0.014-0.014l-73.044-3.727l-10.338-64.535
c29.932-17.554,53.324-45.025,65.646-77.911h-136.89V208.176h138.887L507.527,208.176L507.527,208.176z"/>
<path style="fill:#28B446;" d="M416.253,455.624l0.014,0.014C372.396,490.901,316.666,512,256,512
c-97.491,0-182.252-54.491-225.491-134.681l82.961-67.91c21.619,57.698,77.278,98.771,142.53,98.771
c28.047,0,54.323-7.582,76.87-20.818L416.253,455.624z"/>
<path style="fill:#F14336;" d="M419.404,58.936l-82.933,67.896c-23.335-14.586-50.919-23.012-80.471-23.012
c-66.729,0-123.429,42.957-143.965,102.724l-83.397-68.276h-0.014C71.23,56.123,157.06,0,256,0
C318.115,0,375.068,22.126,419.404,58.936z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"> <g transform="translate(0.666667, 0.333333)">
<g> <polygon points="4.56711429 7.96345714 0.103114286 0.1872 8.99022857 0.1872"></polygon>
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/> </g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 539 B

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"> <g transform="translate(-0.200000, -0.200000)" >
<g> <polygon points="4.93361111 0.202222222 9.75583333 8.6025 0.155833333 8.6025"></polygon>
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/> </g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 543 B

View file

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5"> <g id="environment" transform="translate(-26.000000, -42.000000)" fill="#FFF">
<g id="libraries" transform="translate(21.000000, 32.000000)"> <g id="libraries" transform="translate(21.000000, 32.000000)">
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon> <polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
</g> </g>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 695 B

View file

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5"> <g id="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
<g id="libraries" transform="translate(21.000000, 32.000000)"> <g id="libraries" transform="translate(21.000000, 32.000000)">
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon> <polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
</g> </g>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 698 B

View file

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5"> <g id="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
<g id="libraries" transform="translate(21.000000, 32.000000)"> <g id="libraries" transform="translate(21.000000, 32.000000)">
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(90.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon> <polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(90.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
</g> </g>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 698 B

View file

@ -3,8 +3,8 @@
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
<!-- <desc>Created with Sketch.</desc> --> <!-- <desc>Created with Sketch.</desc> -->
<defs></defs> <defs></defs>
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962"> <g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1">
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#333333"> <g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#AAA">
<g id="Icons" transform="translate(16.000000, 1063.000000)"> <g id="Icons" transform="translate(16.000000, 1063.000000)">
<polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon> <polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon>
</g> </g>

Before

Width:  |  Height:  |  Size: 873 B

After

Width:  |  Height:  |  Size: 860 B

View file

@ -1,10 +1,14 @@
import React from 'react'; import React, { Suspense } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router'; import { Router, browserHistory } from 'react-router';
import configureStore from './store'; import configureStore from './store';
import routes from './routes'; 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'); require('./styles/main.scss');
@ -18,13 +22,17 @@ const store = configureStore(initialState);
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
<Router history={history} routes={routes(store)} /> <ThemeProvider>
<Router history={history} routes={routes(store)} />
</ThemeProvider>
</Provider> </Provider>
); );
const HotApp = hot(App); const HotApp = hot(App);
render( render(
<HotApp />, <Suspense fallback={(<Loader />)}>
<HotApp />
</Suspense>,
document.getElementById('root') document.getElementById('root')
); );

7
client/index.stories.mdx Normal file
View file

@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title=" |Intro" />
# 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.

5
client/jest.setup.js Normal file
View file

@ -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';

View file

@ -1,11 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import getConfig from '../../utils/getConfig';
import DevTools from './components/DevTools'; import DevTools from './components/DevTools';
import { setPreviousPath } from '../IDE/actions/ide'; import { setPreviousPath } from '../IDE/actions/ide';
const __process = (typeof global !== 'undefined' ? global : window).process;
class App extends React.Component { class App extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -35,7 +34,7 @@ class App extends React.Component {
render() { render() {
return ( return (
<div className="app"> <div className="app">
{this.state.isMounted && !window.devToolsExtension && __process.env.NODE_ENV === 'development' && <DevTools />} {this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />}
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg'); import ExitIcon from '../../../images/exit.svg';
class Overlay extends React.Component { class Overlay extends React.Component {
constructor(props) { constructor(props) {
@ -81,8 +80,8 @@ class Overlay extends React.Component {
<h2 className="overlay__title">{title}</h2> <h2 className="overlay__title">{title}</h2>
<div className="overlay__actions"> <div className="overlay__actions">
{actions} {actions}
<button className="overlay__close-button" onClick={this.close} > <button className="overlay__close-button" onClick={this.close} aria-label={`Close ${title} overlay`} >
<InlineSVG src={exitUrl} alt="close overlay" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
</header> </header>

View file

@ -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 }) => (
<ThemeProvider theme={{ ...theme[currentTheme] }}>
{children}
</ThemeProvider>
);
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);

View file

@ -1,10 +1,7 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function setAssets(assets, totalSize) { function setAssets(assets, totalSize) {
return { return {
type: ActionTypes.SET_ASSETS, type: ActionTypes.SET_ASSETS,
@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
export function getAssets() { export function getAssets() {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true }) apiClient.get('/S3/objects')
.then((response) => { .then((response) => {
dispatch(setAssets(response.data.assets, response.data.totalSize)); dispatch(setAssets(response.data.assets, response.data.totalSize));
dispatch(stopLoader()); dispatch(stopLoader());
@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
export function deleteAssetRequest(assetKey) { export function deleteAssetRequest(assetKey) {
return (dispatch) => { return (dispatch) => {
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true }) apiClient.delete(`/S3/${assetKey}`)
.then((response) => { .then((response) => {
dispatch(deleteAsset(assetKey)); dispatch(deleteAsset(assetKey));
}) })

View file

@ -1,11 +1,9 @@
import axios from 'axios';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
import { setToastText, showToast } from './toast'; 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; const TOAST_DISPLAY_TIME_MS = 1500;
@ -15,11 +13,11 @@ export function getCollections(username) {
dispatch(startLoader()); dispatch(startLoader());
let url; let url;
if (username) { if (username) {
url = `${ROOT_URL}/${username}/collections`; url = `/${username}/collections`;
} else { } else {
url = `${ROOT_URL}/collections`; url = '/collections';
} }
axios.get(url, { withCredentials: true }) apiClient.get(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_COLLECTIONS, type: ActionTypes.SET_COLLECTIONS,
@ -27,7 +25,8 @@ export function getCollections(username) {
}); });
dispatch(stopLoader()); dispatch(stopLoader());
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data
@ -40,8 +39,8 @@ export function getCollections(username) {
export function createCollection(collection) { export function createCollection(collection) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections`; const url = '/collections';
return axios.post(url, collection, { withCredentials: true }) return apiClient.post(url, collection)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_COLLECTION type: ActionTypes.CREATE_COLLECTION
@ -57,7 +56,8 @@ export function createCollection(collection) {
browserHistory.push(location); browserHistory.push(location);
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
console.error('Error creating collection', response.data); console.error('Error creating collection', response.data);
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
@ -71,8 +71,8 @@ export function createCollection(collection) {
export function addToCollection(collectionId, projectId) { export function addToCollection(collectionId, projectId) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; const url = `/collections/${collectionId}/${projectId}`;
return axios.post(url, { withCredentials: true }) return apiClient.post(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.ADD_TO_COLLECTION, type: ActionTypes.ADD_TO_COLLECTION,
@ -87,7 +87,8 @@ export function addToCollection(collectionId, projectId) {
return response.data; return response.data;
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data
@ -102,8 +103,8 @@ export function addToCollection(collectionId, projectId) {
export function removeFromCollection(collectionId, projectId) { export function removeFromCollection(collectionId, projectId) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; const url = `/collections/${collectionId}/${projectId}`;
return axios.delete(url, { withCredentials: true }) return apiClient.delete(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.REMOVE_FROM_COLLECTION, type: ActionTypes.REMOVE_FROM_COLLECTION,
@ -118,7 +119,8 @@ export function removeFromCollection(collectionId, projectId) {
return response.data; return response.data;
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data
@ -132,8 +134,8 @@ export function removeFromCollection(collectionId, projectId) {
export function editCollection(collectionId, { name, description }) { export function editCollection(collectionId, { name, description }) {
return (dispatch) => { return (dispatch) => {
const url = `${ROOT_URL}/collections/${collectionId}`; const url = `/collections/${collectionId}`;
return axios.patch(url, { name, description }, { withCredentials: true }) return apiClient.patch(url, { name, description })
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.EDIT_COLLECTION, type: ActionTypes.EDIT_COLLECTION,
@ -141,7 +143,8 @@ export function editCollection(collectionId, { name, description }) {
}); });
return response.data; return response.data;
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data
@ -154,8 +157,8 @@ export function editCollection(collectionId, { name, description }) {
export function deleteCollection(collectionId) { export function deleteCollection(collectionId) {
return (dispatch) => { return (dispatch) => {
const url = `${ROOT_URL}/collections/${collectionId}`; const url = `/collections/${collectionId}`;
return axios.delete(url, { withCredentials: true }) return apiClient.delete(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.DELETE_COLLECTION, type: ActionTypes.DELETE_COLLECTION,
@ -164,7 +167,8 @@ export function deleteCollection(collectionId) {
}); });
return response.data; return response.data;
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data

View file

@ -1,13 +1,11 @@
import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import blobUtil from 'blob-util'; import blobUtil from 'blob-util';
import { reset } from 'redux-form'; import { reset } from 'redux-form';
import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide'; import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
import { setProjectSavedTime } from './project'; import { setProjectSavedTime } from './project';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function appendToFilename(filename, string) { function appendToFilename(filename, string) {
const dotIndex = filename.lastIndexOf('.'); const dotIndex = filename.lastIndexOf('.');
@ -50,7 +48,7 @@ export function createFile(formProps) {
parentId, parentId,
children: [] children: []
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
@ -65,10 +63,13 @@ export function createFile(formProps) {
// }); // });
dispatch(setUnsavedChanges(true)); dispatch(setUnsavedChanges(true));
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
error: response.data dispatch({
})); type: ActionTypes.ERROR,
error: response.data
});
});
} else { } else {
const id = objectID().toHexString(); const id = objectID().toHexString();
dispatch({ dispatch({
@ -103,7 +104,7 @@ export function createFolder(formProps) {
parentId, parentId,
fileType: 'folder' fileType: 'folder'
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
@ -113,10 +114,13 @@ export function createFolder(formProps) {
dispatch(setProjectSavedTime(response.data.project.updatedAt)); dispatch(setProjectSavedTime(response.data.project.updatedAt));
dispatch(closeNewFolderModal()); dispatch(closeNewFolderModal());
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
error: response.data dispatch({
})); type: ActionTypes.ERROR,
error: response.data
});
});
} else { } else {
const id = objectID().toHexString(); const id = objectID().toHexString();
dispatch({ dispatch({
@ -155,7 +159,7 @@ export function deleteFile(id, parentId) {
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(() => { .then(() => {
dispatch({ dispatch({
type: ActionTypes.DELETE_FILE, type: ActionTypes.DELETE_FILE,
@ -163,7 +167,8 @@ export function deleteFile(id, parentId) {
parentId parentId
}); });
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data

View file

@ -1,17 +1,17 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function updatePreferences(formParams, dispatch) { function updatePreferences(formParams, dispatch) {
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true }) apiClient.put('/preferences', formParams)
.then(() => { .then(() => {
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
error: response.data dispatch({
})); type: ActionTypes.ERROR,
error: response.data
});
});
} }
export function setFontSize(value) { export function setFontSize(value) {

View file

@ -1,8 +1,9 @@
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import each from 'async/each'; import each from 'async/each';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import apiClient from '../../../utils/apiClient';
import getConfig from '../../../utils/getConfig';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast'; import { showToast, setToastText } from './toast';
import { import {
@ -14,8 +15,7 @@ import {
} from './ide'; } from './ide';
import { clearState, saveState } from '../../../persistState'; import { clearState, saveState } from '../../../persistState';
const __process = (typeof global !== 'undefined' ? global : window).process; const ROOT_URL = getConfig('API_URL');
const ROOT_URL = __process.env.API_URL;
export function setProject(project) { export function setProject(project) {
return { return {
@ -49,18 +49,21 @@ export function setNewProject(project) {
}; };
} }
export function getProject(id) { export function getProject(id, username) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(justOpenedProject()); dispatch(justOpenedProject());
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) apiClient.get(`/${username}/projects/${id}`)
.then((response) => { .then((response) => {
dispatch(setProject(response.data)); dispatch(setProject(response.data));
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
error: response.data dispatch({
})); type: ActionTypes.ERROR,
error: response.data
});
});
}; };
} }
@ -139,7 +142,7 @@ export function saveProject(selectedFile = null, autosave = false) {
fileToUpdate.content = selectedFile.content; fileToUpdate.content = selectedFile.content;
} }
if (state.project.id) { 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) => { .then((response) => {
dispatch(endSavingProject()); dispatch(endSavingProject());
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));
@ -152,17 +155,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) { if (!autosave) {
if (state.ide.justOpenedProject && state.preferences.autosave) { if (state.ide.justOpenedProject && state.preferences.autosave) {
dispatch(showToast(5500)); dispatch(showToast(5500));
dispatch(setToastText('Project saved.')); dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500); setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject()); dispatch(resetJustOpenedProject());
} else { } else {
dispatch(showToast(1500)); dispatch(showToast(1500));
dispatch(setToastText('Project saved.')); dispatch(setToastText('Sketch saved.'));
} }
} }
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch(endSavingProject()); dispatch(endSavingProject());
dispatch(setToastText('Failed to save sketch.'));
dispatch(showToast(1500));
if (response.status === 403) { if (response.status === 403) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} else if (response.status === 409) { } 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) => { .then((response) => {
dispatch(endSavingProject()); dispatch(endSavingProject());
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
@ -191,17 +197,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) { if (!autosave) {
if (state.preferences.autosave) { if (state.preferences.autosave) {
dispatch(showToast(5500)); dispatch(showToast(5500));
dispatch(setToastText('Project saved.')); dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500); setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject()); dispatch(resetJustOpenedProject());
} else { } else {
dispatch(showToast(1500)); dispatch(showToast(1500));
dispatch(setToastText('Project saved.')); dispatch(setToastText('Sketch saved.'));
} }
} }
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch(endSavingProject()); dispatch(endSavingProject());
dispatch(setToastText('Failed to save sketch.'));
dispatch(showToast(1500));
if (response.status === 403) { if (response.status === 403) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} else { } else {
@ -255,7 +264,7 @@ export function cloneProject(id) {
if (!id) { if (!id) {
resolve(getState()); resolve(getState());
} else { } else {
fetch(`${ROOT_URL}/projects/${id}`) apiClient.get(`/projects/${id}`)
.then(res => res.json()) .then(res => res.json())
.then(data => resolve({ .then(data => resolve({
files: data.files, files: data.files,
@ -282,7 +291,7 @@ export function cloneProject(id) {
const formParams = { const formParams = {
url: file.url url: file.url
}; };
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true }) apiClient.post('/S3/copy', formParams)
.then((response) => { .then((response) => {
file.url = response.data.url; file.url = response.data.url;
callback(null); callback(null);
@ -293,15 +302,18 @@ export function cloneProject(id) {
}, (err) => { }, (err) => {
// if not errors in duplicating the files on S3, then duplicate it // if not errors in duplicating the files on S3, then duplicate it
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles }); 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) => { .then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch(setNewProject(response.data)); dispatch(setNewProject(response.data));
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.PROJECT_SAVE_FAIL, const { response } = error;
error: response.data dispatch({
})); type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data
});
});
}); });
}); });
}; };
@ -329,7 +341,7 @@ export function setProjectSavedTime(updatedAt) {
export function changeProjectName(id, newName) { export function changeProjectName(id, newName) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true }) apiClient.put(`/projects/${id}`, { name: newName })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
dispatch({ dispatch({
@ -344,8 +356,8 @@ export function changeProjectName(id, newName) {
} }
} }
}) })
.catch((response) => { .catch((error) => {
console.log(response); const { response } = error;
dispatch({ dispatch({
type: ActionTypes.PROJECT_SAVE_FAIL, type: ActionTypes.PROJECT_SAVE_FAIL,
error: response.data error: response.data
@ -356,7 +368,7 @@ export function changeProjectName(id, newName) {
export function deleteProject(id) { export function deleteProject(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) apiClient.delete(`/projects/${id}`)
.then(() => { .then(() => {
const state = getState(); const state = getState();
if (id === state.project.id) { if (id === state.project.id) {
@ -368,7 +380,8 @@ export function deleteProject(id) {
id id
}); });
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
if (response.status === 403) { if (response.status === 403) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} else { } else {

View file

@ -1,21 +1,18 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
// eslint-disable-next-line // eslint-disable-next-line
export function getProjects(username) { export function getProjects(username) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
let url; let url;
if (username) { if (username) {
url = `${ROOT_URL}/${username}/projects`; url = `/${username}/projects`;
} else { } else {
url = `${ROOT_URL}/projects`; url = '/projects';
} }
axios.get(url, { withCredentials: true }) apiClient.get(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECTS, type: ActionTypes.SET_PROJECTS,
@ -23,7 +20,8 @@ export function getProjects(username) {
}); });
dispatch(stopLoader()); dispatch(stopLoader());
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data

View file

@ -1,11 +1,10 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import getConfig from '../../../utils/getConfig';
import { createFile } from './files'; import { createFile } from './files';
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
const __process = (typeof global !== 'undefined' ? global : window).process; const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') ||
const s3BucketHttps = __process.env.S3_BUCKET_URL_BASE || `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
const ROOT_URL = __process.env.API_URL;
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
function localIntercept(file, options = {}) { function localIntercept(file, options = {}) {
@ -46,18 +45,13 @@ export function dropzoneAcceptCallback(userId, file, done) {
}); });
} else { } else {
file.postData = []; // eslint-disable-line file.postData = []; // eslint-disable-line
axios.post( apiClient.post('/S3/sign', {
`${ROOT_URL}/S3/sign`, { name: file.name,
name: file.name, type: file.type,
type: file.type, size: file.size,
size: file.size, userId
userId
// _csrf: document.getElementById('__createPostToken').value // _csrf: document.getElementById('__createPostToken').value
}, })
{
withCredentials: true
}
)
.then((response) => { .then((response) => {
file.custom_status = 'ready'; // eslint-disable-line file.custom_status = 'ready'; // eslint-disable-line
file.postData = response.data; // 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 file.previewTemplate.className += ' uploading'; // eslint-disable-line
done(); done();
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
file.custom_status = 'rejected'; // eslint-disable-line file.custom_status = 'rejected'; // eslint-disable-line
if (response.data && response.data.responseText && response.data.responseText.message) { if (response.data && response.data.responseText && response.data.responseText.message) {
done(response.data.responseText.message); done(response.data.responseText.message);

View file

@ -1,19 +1,19 @@
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
const squareLogoUrl = require('../../../images/p5js-square-logo.svg'); import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
// const playUrl = require('../../../images/play.svg'); // import PlayIcon from '../../../images/play.svg';
const asteriskUrl = require('../../../images/p5-asterisk.svg'); import AsteriskIcon from '../../../images/p5-asterisk.svg';
function About(props) { function About(props) {
const { t } = useTranslation();
return ( return (
<div className="about__content"> <div className="about__content">
<Helmet> <Helmet>
<title>p5.js Web Editor | About</title> <title>p5.js Web Editor | About </title>
</Helmet> </Helmet>
<div className="about__content-column"> <div className="about__content-column">
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" /> <SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
{/* Video button to hello p5 video page */} {/* Video button to hello p5 video page */}
{/* <p className="about__play-video"> {/* <p className="about__play-video">
<a <a
@ -21,20 +21,20 @@ function About(props) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" /> <PlayIcon className="about__play-video-button" title="Play Hello Video" />
Play hello! video</a> Play hello! video</a>
</p> */} </p> */}
</div> </div>
<div className="about__content-column"> <div className="about__content-column">
<h3 className="about__content-column-title">New to p5.js?</h3> <h3 className="about__content-column-title">{t('NewP5')}</h3>
<p className="about__content-column-list"> <p className="about__content-column-list">
<a <a
href="https://p5js.org/examples/" href="https://p5js.org/examples/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Examples {t('Examples')}
</a> </a>
</p> </p>
<p className="about__content-column-list"> <p className="about__content-column-list">
@ -43,21 +43,21 @@ function About(props) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Learn {t('Learn')}
</a> </a>
</p> </p>
</div> </div>
<div className="about__content-column"> <div className="about__content-column">
<h3 className="about__content-column-title">Resources</h3> <h3 className="about__content-column-title">{t('Resources')}</h3>
<p className="about__content-column-list"> <p className="about__content-column-list">
<a <a
href="https://p5js.org/libraries/" href="https://p5js.org/libraries/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Libraries {t('Libraries')}
</a> </a>
</p> </p>
<p className="about__content-column-list"> <p className="about__content-column-list">
@ -66,8 +66,8 @@ function About(props) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Reference {t('Reference')}
</a> </a>
</p> </p>
<p className="about__content-column-list"> <p className="about__content-column-list">
@ -76,8 +76,8 @@ function About(props) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Forum {t('Forum')}
</a> </a>
</p> </p>
</div> </div>
@ -87,7 +87,7 @@ function About(props) {
href="https://github.com/processing/p5.js-web-editor" href="https://github.com/processing/p5.js-web-editor"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>Contribute >{t('Contribute')}
</a> </a>
</p> </p>
<p className="about__footer-list"> <p className="about__footer-list">
@ -95,7 +95,7 @@ function About(props) {
href="https://github.com/processing/p5.js-web-editor/issues/new" href="https://github.com/processing/p5.js-web-editor/issues/new"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>Report a bug >{t('Report')}
</a> </a>
</p> </p>
<p className="about__footer-list"> <p className="about__footer-list">

View file

@ -5,11 +5,10 @@ import { bindActionCreators } from 'redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import InlineSVG from 'react-inlinesvg';
import Loader from '../../App/components/loader'; import Loader from '../../App/components/loader';
import * as AssetActions from '../actions/assets'; 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 { class AssetListRowBase extends React.Component {
constructor(props) { constructor(props) {
@ -86,8 +85,9 @@ class AssetListRowBase extends React.Component {
onClick={this.toggleOptions} onClick={this.toggleOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
aria-label="Toggle Open/Close Asset Options"
> >
<InlineSVG src={downFilledTriangle} alt="Menu" /> <DownFilledTriangleIcon focusable="false" aria-hidden="true" />
</button> </button>
{optionsOpen && {optionsOpen &&
<ul <ul
@ -175,7 +175,7 @@ class AssetList extends React.Component {
render() { render() {
const { assetList } = this.props; const { assetList } = this.props;
return ( return (
<div className="asset-table-container"> <article className="asset-table-container">
<Helmet> <Helmet>
<title>{this.getAssetsTitle()}</title> <title>{this.getAssetsTitle()}</title>
</Helmet> </Helmet>
@ -195,7 +195,7 @@ class AssetList extends React.Component {
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)} {assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
</tbody> </tbody>
</table>} </table>}
</div> </article>
); );
} }
} }

View file

@ -3,8 +3,9 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
const __process = (typeof global !== 'undefined' ? global : window).process; import getConfig from '../../../utils/getConfig';
const limit = __process.env.UPLOAD_LIMIT || 250000000;
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const MAX_SIZE_B = limit; const MAX_SIZE_B = limit;
const formatPercent = (percent) => { const formatPercent = (percent) => {

View file

@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import classNames from 'classnames'; import classNames from 'classnames';
@ -19,8 +18,8 @@ import { SketchSearchbar } from '../Searchbar';
import CollectionListRow from './CollectionListRow'; import CollectionListRow from './CollectionListRow';
const arrowUp = require('../../../../images/sort-arrow-up.svg'); import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
const arrowDown = require('../../../../images/sort-arrow-down.svg'); import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
class CollectionList extends React.Component { class CollectionList extends React.Component {
constructor(props) { constructor(props) {
@ -83,21 +82,43 @@ class CollectionList extends React.Component {
return null; 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) => { _renderFieldHeader = (fieldName, displayName) => {
const { field, direction } = this.props.sorting; const { field, direction } = this.props.sorting;
const headerClass = classNames({ const headerClass = classNames({
'sketches-table__header': true, 'sketches-table__header': true,
'sketches-table__header--selected': field === fieldName 'sketches-table__header--selected': field === fieldName
}); });
const buttonLabel = this._getButtonLabel(fieldName, displayName);
return ( return (
<th scope="col"> <th scope="col">
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}> <button
className="sketch-list__sort-button"
onClick={() => this.props.toggleDirectionForField(fieldName)}
aria-label={buttonLabel}
>
<span className={headerClass}>{displayName}</span> <span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC && {field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<InlineSVG src={arrowUp} /> <ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
} }
{field === fieldName && direction === SortingActions.DIRECTION.DESC && {field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<InlineSVG src={arrowDown} /> <ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
} }
</button> </button>
</th> </th>
@ -108,7 +129,7 @@ class CollectionList extends React.Component {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username; const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return ( return (
<div className="sketches-table-container"> <article className="sketches-table-container">
<Helmet> <Helmet>
<title>{this.getTitle()}</title> <title>{this.getTitle()}</title>
</Helmet> </Helmet>
@ -155,7 +176,7 @@ class CollectionList extends React.Component {
</Overlay> </Overlay>
) )
} }
</div> </article>
); );
} }
} }

View file

@ -1,7 +1,6 @@
import format from 'date-fns/format'; import format from 'date-fns/format';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -10,7 +9,7 @@ import * as CollectionsActions from '../../actions/collections';
import * as IdeActions from '../../actions/ide'; import * as IdeActions from '../../actions/ide';
import * as ToastActions from '../../actions/toast'; 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 { class CollectionListRowBase extends React.Component {
static projectInCollection(project, collection) { static projectInCollection(project, collection) {
@ -129,8 +128,9 @@ class CollectionListRowBase extends React.Component {
onClick={this.toggleOptions} onClick={this.toggleOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
aria-label="Toggle Open/Close collection options"
> >
<InlineSVG src={downFilledTriangle} alt="Menu" /> <DownFilledTriangleIcon title="Menu" />
</button> </button>
{optionsOpen && {optionsOpen &&
<ul <ul

View file

@ -1,23 +1,26 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames'; import classNames from 'classnames';
import { Console as ConsoleFeed } from 'console-feed'; import { Console as ConsoleFeed } from 'console-feed';
import { import {
CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES,
CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES
} from '../../../styles/components/_console-feed.scss'; } from '../../../styles/components/_console-feed.scss';
import warnLightUrl from '../../../images/console-warn-light.svg'; import warnLightUrl from '../../../images/console-warn-light.svg?byUrl';
import warnDarkUrl from '../../../images/console-warn-dark.svg'; import warnDarkUrl from '../../../images/console-warn-dark.svg?byUrl';
import errorLightUrl from '../../../images/console-error-light.svg'; import warnContrastUrl from '../../../images/console-warn-contrast.svg?byUrl';
import errorDarkUrl from '../../../images/console-error-dark.svg'; import errorLightUrl from '../../../images/console-error-light.svg?byUrl';
import debugLightUrl from '../../../images/console-debug-light.svg'; import errorDarkUrl from '../../../images/console-error-dark.svg?byUrl';
import debugDarkUrl from '../../../images/console-debug-dark.svg'; import errorContrastUrl from '../../../images/console-error-contrast.svg?byUrl';
import infoLightUrl from '../../../images/console-info-light.svg'; import debugLightUrl from '../../../images/console-debug-light.svg?byUrl';
import infoDarkUrl from '../../../images/console-info-dark.svg'; import debugDarkUrl from '../../../images/console-debug-dark.svg?byUrl';
import debugContrastUrl from '../../../images/console-debug-contrast.svg?byUrl';
import infoLightUrl from '../../../images/console-info-light.svg?byUrl';
import infoDarkUrl from '../../../images/console-info-dark.svg?byUrl';
import infoContrastUrl from '../../../images/console-info-contrast.svg?byUrl';
const upArrowUrl = require('../../../images/up-arrow.svg'); import UpArrowIcon from '../../../images/up-arrow.svg';
const downArrowUrl = require('../../../images/down-arrow.svg'); import DownArrowIcon from '../../../images/down-arrow.svg';
class Console extends React.Component { class Console extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -47,6 +50,12 @@ class Console extends React.Component {
LOG_DEBUG_ICON: `url(${debugDarkUrl})`, LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
LOG_INFO_ICON: `url(${infoDarkUrl})` LOG_INFO_ICON: `url(${infoDarkUrl})`
}; };
const CONSOLE_FEED_CONTRAST_ICONS = {
LOG_WARN_ICON: `url(${warnContrastUrl})`,
LOG_ERROR_ICON: `url(${errorContrastUrl})`,
LOG_DEBUG_ICON: `url(${debugContrastUrl})`,
LOG_INFO_ICON: `url(${infoContrastUrl})`
};
const CONSOLE_FEED_SIZES = { const CONSOLE_FEED_SIZES = {
TREENODE_LINE_HEIGHT: 1.2, TREENODE_LINE_HEIGHT: 1.2,
BASE_FONT_SIZE: this.props.fontSize, BASE_FONT_SIZE: this.props.fontSize,
@ -64,7 +73,7 @@ class Console extends React.Component {
case 'dark': case 'dark':
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style); return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
case 'contrast': case 'contrast':
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style); return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style);
default: default:
return ''; return '';
} }
@ -77,25 +86,25 @@ class Console extends React.Component {
}); });
return ( return (
<div className={consoleClass} role="main"> <section className={consoleClass} >
<div className="preview-console__header"> <header className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2> <h2 className="preview-console__header-title">Console</h2>
<div className="preview-console__header-buttons"> <div className="preview-console__header-buttons">
<button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console"> <button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="Clear console">
Clear Clear
</button> </button>
<button <button
className="preview-console__collapse" className="preview-console__collapse"
onClick={this.props.collapseConsole} onClick={this.props.collapseConsole}
aria-label="collapse console" aria-label="Close console"
> >
<InlineSVG src={downArrowUrl} /> <DownArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console"> <button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="Open console" >
<InlineSVG src={upArrowUrl} /> <UpArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </header>
<div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages"> <div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent) => { {this.props.consoleEvents.map((consoleEvent) => {
const { method, times } = consoleEvent; const { method, times } = consoleEvent;
@ -118,7 +127,7 @@ class Console extends React.Component {
); );
})} })}
</div> </div>
</div> </section>
); );
} }
} }

View file

@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames'; import classNames from 'classnames';
import shareUrl from '../../../images/share.svg'; import ShareIcon from '../../../images/share.svg';
class CopyableInput extends React.Component { class CopyableInput extends React.Component {
constructor(props) { constructor(props) {
@ -70,8 +69,9 @@ class CopyableInput extends React.Component {
rel="noopener noreferrer" rel="noopener noreferrer"
href={value} href={value}
className="copyable-input__preview" className="copyable-input__preview"
aria-label={`Open ${label} view in new tab`}
> >
<InlineSVG src={shareUrl} alt={`open ${label} view in new tab`} /> <ShareIcon focusable="false" aria-hidden="true" />
</a> </a>
} }
</div> </div>

View file

@ -1,13 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
const editIconUrl = require('../../../images/pencil.svg'); import EditIcon from '../../../images/pencil.svg';
function EditIcon() {
return <InlineSVG className="editable-input__icon" src={editIconUrl} alt="Edit" />;
}
// TODO I think this needs a description prop so that it's accessible
function EditableInput({ function EditableInput({
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
}) { }) {
@ -52,9 +48,13 @@ function EditableInput({
return ( return (
<span className={classes}> <span className={classes}>
<button className="editable-input__label" onClick={beginEditing}> <button
className="editable-input__label"
onClick={beginEditing}
aria-label={`Edit ${displayValue} value`}
>
<span>{displayValue}</span> <span>{displayValue}</span>
<EditIcon /> <EditIcon className="editable-input__icon" focusable="false" aria-hidden="true" />
</button> </button>
<InputComponent <InputComponent

View file

@ -24,7 +24,6 @@ import 'codemirror/addon/edit/matchbrackets';
import { JSHINT } from 'jshint'; import { JSHINT } from 'jshint';
import { CSSLint } from 'csslint'; import { CSSLint } from 'csslint';
import { HTMLHint } from 'htmlhint'; import { HTMLHint } from 'htmlhint';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames'; import classNames from 'classnames';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import '../../../utils/htmlmixed'; import '../../../utils/htmlmixed';
@ -36,6 +35,11 @@ import { metaKey, } from '../../../utils/metaKey';
import search from '../../../utils/codemirror-search'; import search from '../../../utils/codemirror-search';
import beepUrl from '../../../sounds/audioAlert.mp3';
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
import RightArrowIcon from '../../../images/right-arrow.svg';
import LeftArrowIcon from '../../../images/left-arrow.svg';
search(CodeMirror); search(CodeMirror);
const beautifyCSS = beautifyJS.css; const beautifyCSS = beautifyJS.css;
@ -45,11 +49,6 @@ window.JSHINT = JSHINT;
window.CSSLint = CSSLint; window.CSSLint = CSSLint;
window.HTMLHint = HTMLHint; window.HTMLHint = HTMLHint;
const beepUrl = require('../../../sounds/audioAlert.mp3');
const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg');
const rightArrowUrl = require('../../../images/right-arrow.svg');
const leftArrowUrl = require('../../../images/left-arrow.svg');
const IS_TAB_INDENT = false; const IS_TAB_INDENT = false;
const INDENTATION_AMOUNT = 2; const INDENTATION_AMOUNT = 2;
@ -315,29 +314,30 @@ class Editor extends React.Component {
}); });
return ( return (
<section <section className={editorSectionClass} >
role="main"
className={editorSectionClass}
>
<header className="editor__header"> <header className="editor__header">
<button <button
aria-label="collapse file navigation" aria-label="Open Sketch files navigation"
className="sidebar__contract" className="sidebar__contract"
onClick={this.props.collapseSidebar} onClick={this.props.collapseSidebar}
> >
<InlineSVG src={leftArrowUrl} /> <LeftArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<button <button
aria-label="expand file navigation" aria-label="Close sketch files navigation"
className="sidebar__expand" className="sidebar__expand"
onClick={this.props.expandSidebar} onClick={this.props.expandSidebar}
> >
<InlineSVG src={rightArrowUrl} /> <RightArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<div className="editor__file-name"> <div className="editor__file-name">
<span> <span>
{this.props.file.name} {this.props.file.name}
{this.props.unsavedChanges ? <InlineSVG src={unsavedChangesDotUrl} /> : null} <span className="editor__unsaved-changes">
{this.props.unsavedChanges ?
<UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" /> :
null}
</span>
</span> </span>
<Timer <Timer
projectSavedTime={this.props.projectSavedTime} projectSavedTime={this.props.projectSavedTime}
@ -345,8 +345,8 @@ class Editor extends React.Component {
/> />
</div> </div>
</header> </header>
<div ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} > <article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
</div> </article>
<EditorAccessibility <EditorAccessibility
lintMessages={this.props.lintMessages} lintMessages={this.props.lintMessages}
/> />

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import githubLogoUrl from '../../../images/github.svg'; import GitHubLogo from '../../../images/github.svg';
function Feedback(props) { function Feedback(props) {
return ( return (
@ -24,7 +23,7 @@ function Feedback(props) {
className="feedback__github-link" className="feedback__github-link"
> >
Go to Github Go to Github
<InlineSVG className="feedback__github-logo" src={githubLogoUrl} /> <GitHubLogo className="feedback__github-logo" focusable="false" aria-hidden="true" />
</a> </a>
</p> </p>
</div> </div>

View file

@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames'; import classNames from 'classnames';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files'; import * as FileActions from '../actions/files';
import downArrowUrl from '../../../images/down-filled-triangle.svg'; import DownArrowIcon from '../../../images/down-filled-triangle.svg';
import folderRightUrl from '../../../images/triangle-arrow-right.svg'; import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
import folderDownUrl from '../../../images/triangle-arrow-down.svg'; import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
import fileUrl from '../../../images/file.svg'; import FileIcon from '../../../images/file.svg';
export class FileNode extends React.Component { export class FileNode extends React.Component {
constructor(props) { constructor(props) {
@ -185,7 +184,7 @@ export class FileNode extends React.Component {
<span className="file-item__spacer"></span> <span className="file-item__spacer"></span>
{ isFile && { isFile &&
<span className="sidebar__file-item-icon"> <span className="sidebar__file-item-icon">
<InlineSVG src={fileUrl} /> <FileIcon focusable="false" aria-hidden="true" />
</span> </span>
} }
{ isFolder && { isFolder &&
@ -193,24 +192,28 @@ export class FileNode extends React.Component {
<button <button
className="sidebar__file-item-closed" className="sidebar__file-item-closed"
onClick={this.showFolderChildren} onClick={this.showFolderChildren}
aria-label="Open folder contents"
> >
<InlineSVG className="folder-right" src={folderRightUrl} /> <FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" />
</button> </button>
<button <button
className="sidebar__file-item-open" className="sidebar__file-item-open"
onClick={this.hideFolderChildren} onClick={this.hideFolderChildren}
aria-label="Close file contents"
> >
<InlineSVG className="folder-down" src={folderDownUrl} /> <FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
} }
<button <button
aria-label="Name"
className="sidebar__file-item-name" className="sidebar__file-item-name"
onClick={this.handleFileClick} onClick={this.handleFileClick}
> >
{this.state.updatedName} {this.state.updatedName}
</button> </button>
<input <input
data-testid="input"
type="text" type="text"
className="sidebar__file-item-input" className="sidebar__file-item-input"
value={this.state.updatedName} value={this.state.updatedName}
@ -222,14 +225,14 @@ export class FileNode extends React.Component {
/> />
<button <button
className="sidebar__file-item-show-options" className="sidebar__file-item-show-options"
aria-label="view file options" aria-label="Toggle open/close file options"
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }} ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0" tabIndex="0"
onClick={this.toggleFileOptions} onClick={this.toggleFileOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
<InlineSVG src={downArrowUrl} /> <DownArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<div className="sidebar__file-item-options"> <div className="sidebar__file-item-options">
<ul title="file options"> <ul title="file options">

View file

@ -0,0 +1,31 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { FileNode } from './FileNode';
export default {
title: 'IDE/FileNode',
component: FileNode
};
export const Show = () => (
<FileNode
id="nodeId"
parantId="parentId"
name="File name"
fileType="jpeg"
isSelectedFile
isFolderClosed={false}
setSelectedFile={action('setSelectedFile')}
deleteFile={action('deleteFile')}
updateFileName={action('updateFileName')}
resetSelectedFile={action('resetSelectedFile')}
newFile={action('newFile')}
newFolder={action('newFolder')}
showFolderChildren={action('showFolderChildren')}
hideFolderChildren={action('hideFolderChildren')}
openUploadFileModal={action('openUploadFileModal')}
canEdit
authenticated
/>
);

View file

@ -0,0 +1,127 @@
import React from 'react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { FileNode } from './FileNode';
describe('<FileNode />', () => {
const changeName = (newFileName) => {
const renameButton = screen.getByText(/Rename/i);
fireEvent.click(renameButton);
const input = screen.getByTestId('input');
fireEvent.change(input, { target: { value: newFileName } });
fireEvent.blur(input);
};
const expectFileNameToBe = async (expectedName) => {
const name = screen.getByLabelText(/Name/i);
await waitFor(() => within(name).queryByText(expectedName));
};
const renderFileNode = (fileType, extraProps = {}) => {
const props = {
...extraProps,
id: '0',
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
fileType,
canEdit: true,
children: [],
authenticated: false,
setSelectedFile: jest.fn(),
deleteFile: jest.fn(),
updateFileName: jest.fn(),
resetSelectedFile: jest.fn(),
newFile: jest.fn(),
newFolder: jest.fn(),
showFolderChildren: jest.fn(),
hideFolderChildren: jest.fn(),
openUploadFileModal: jest.fn(),
setProjectName: jest.fn(),
};
render(<FileNode {...props} />);
return props;
};
describe('fileType: file', () => {
it('cannot change to an empty name', async () => {
const props = renderFileNode('file');
changeName('');
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
it('can change to a valid filename', async () => {
const newName = 'newname.jsx';
const props = renderFileNode('file');
changeName(newName);
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
await expectFileNameToBe(newName);
});
it('must have an extension', async () => {
const newName = 'newname';
const props = renderFileNode('file');
changeName(newName);
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
it('can change to a different extension', async () => {
const newName = 'newname.gif';
const props = renderFileNode('file');
changeName(newName);
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
it('cannot be just an extension', async () => {
const newName = '.jsx';
const props = renderFileNode('file');
changeName(newName);
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
});
describe('fileType: folder', () => {
it('cannot change to an empty name', async () => {
const props = renderFileNode('folder');
changeName('');
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
it('can change to another name', async () => {
const newName = 'foldername';
const props = renderFileNode('folder');
changeName(newName);
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
await expectFileNameToBe(newName);
});
it('cannot have a file extension', async () => {
const newName = 'foldername.jsx';
const props = renderFileNode('folder');
changeName(newName);
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
await expectFileNameToBe(props.name);
});
});
});

View file

@ -4,11 +4,11 @@ import Dropzone from 'dropzone';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as UploaderActions from '../actions/uploader'; import * as UploaderActions from '../actions/uploader';
import getConfig from '../../../utils/getConfig';
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
const __process = (typeof global !== 'undefined' ? global : window).process; const s3Bucket = getConfig('S3_BUCKET_URL_BASE') ||
const s3Bucket = __process.env.S3_BUCKET_URL_BASE || `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
class FileUploader extends React.Component { class FileUploader extends React.Component {
componentDidMount() { componentDidMount() {

View file

@ -1,92 +1,91 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { metaKeyName, } from '../../../utils/metaKey'; import { metaKeyName, } from '../../../utils/metaKey';
function KeyboardShortcutModal() { function KeyboardShortcutModal() {
const { t } = useTranslation();
return ( return (
<ul className="keyboard-shortcuts" title="keyboard shortcuts"> <div className="keyboard-shortcuts">
<li className="keyboard-shortcut-item"> <h3 className="keyboard-shortcuts__title">{t('CodeEditing')}</h3>
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span> <p className="keyboard-shortcuts__description">
<span>Tidy</span> {t('Code editing keyboard shortcuts follow')} <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">{t('Sublime Text shortcuts')}</a>.
</li> </p>
<li className="keyboard-shortcut-item"> <ul className="keyboard-shortcuts__list">
<span className="keyboard-shortcut__command"> <li className="keyboard-shortcut-item">
{metaKeyName} + S <span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
</span> <span>{t('Tidy')}</span>
<span>Save</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + F
{metaKeyName} + F </span>
</span> <span>{t('FindText')}</span>
<span>Find Text</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + G
{metaKeyName} + G </span>
</span> <span>{t('FindNextTextMatch')}</span>
<span>Find Next Text Match</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + {'\u21E7'} + G
{metaKeyName} + {'\u21E7'} + G </span>
</span> <span>{t('FindPreviousTextMatch')}</span>
<span>Find Previous Text Match</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + [
{metaKeyName} + [ </span>
</span> <span>{t('IndentCodeLeft')}</span>
<span>Indent Code Left</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + ]
{metaKeyName} + ] </span>
</span> <span>{t('IndentCodeRight')}</span>
<span>Indent Code Right</span> </li>
</li> <li className="keyboard-shortcut-item">
<li className="keyboard-shortcut-item"> <span className="keyboard-shortcut__command">
<span className="keyboard-shortcut__command"> {metaKeyName} + /
{metaKeyName} + / </span>
</span> <span>{t('CommentLine')}</span>
<span>Comment Line</span> </li>
</li> </ul>
<li className="keyboard-shortcut-item"> <h3 className="keyboard-shortcuts__title">General</h3>
<span className="keyboard-shortcut__command"> <ul className="keyboard-shortcuts__list">
{metaKeyName} + Enter <li className="keyboard-shortcut-item">
</span> <span className="keyboard-shortcut__command">
<span>Start Sketch</span> {metaKeyName} + S
</li> </span>
<li className="keyboard-shortcut-item"> <span>{t('Save')}</span>
<span className="keyboard-shortcut__command"> </li>
{metaKeyName} + {'\u21E7'} + Enter <li className="keyboard-shortcut-item">
</span> <span className="keyboard-shortcut__command">
<span>Stop Sketch</span> {metaKeyName} + Enter
</li> </span>
<li className="keyboard-shortcut-item"> <span>{t('StartSketch')}</span>
<span className="keyboard-shortcut__command"> </li>
{metaKeyName} + {'\u21E7'} + 1 <li className="keyboard-shortcut-item">
</span> <span className="keyboard-shortcut__command">
<span>Turn on Accessible Output</span> {metaKeyName} + {'\u21E7'} + Enter
</li> </span>
<li className="keyboard-shortcut-item"> <span>{t('StopSketch')}</span>
<span className="keyboard-shortcut__command"> </li>
{metaKeyName} + {'\u21E7'} + 2 <li className="keyboard-shortcut-item">
</span> <span className="keyboard-shortcut__command">
<span>Turn off Accessible Output</span> {metaKeyName} + {'\u21E7'} + 1
</li> </span>
<li className="keyboard-shortcut-item"> <span>{t('TurnOnAccessibleOutput')}</span>
<span className="keyboard-shortcut__command"> </li>
{metaKeyName} + B <li className="keyboard-shortcut-item">
</span> <span className="keyboard-shortcut__command">
<span>Toggle Sidebar</span> {metaKeyName} + {'\u21E7'} + 2
</li> </span>
<li className="keyboard-shortcut-item"> <span>{t('TurnOffAccessibleOutput')}</span>
<span className="keyboard-shortcut__command"> </li>
Ctrl + ` </ul>
</span> </div>
<span>Toggle Console</span>
</li>
</ul>
); );
} }

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
class NewFileForm extends React.Component { class NewFileForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -33,7 +35,10 @@ class NewFileForm extends React.Component {
{...domOnlyProps(name)} {...domOnlyProps(name)}
ref={(element) => { this.fileName = element; }} ref={(element) => { this.fileName = element; }}
/> />
<input type="submit" value="Add File" aria-label="add file" /> <Button
type="submit"
>Add File
</Button>
</div> </div>
{name.touched && name.error && <span className="form-error">{name.error}</span>} {name.touched && name.error && <span className="form-error">{name.error}</span>}
</form> </form>

View file

@ -3,13 +3,12 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux'; import { bindActionCreators, compose } from 'redux';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import InlineSVG from 'react-inlinesvg';
import NewFileForm from './NewFileForm'; import NewFileForm from './NewFileForm';
import { closeNewFileModal } from '../actions/ide'; import { closeNewFileModal } from '../actions/ide';
import { createFile } from '../actions/files'; import { createFile } from '../actions/files';
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; 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 // At some point this will probably be generalized to a generic modal
@ -35,8 +34,12 @@ class NewFileModal extends React.Component {
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Create File</h2> <h2 className="modal__title">Create File</h2>
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}> <button
<InlineSVG src={exitUrl} alt="Close New File Modal" /> className="modal__exit-button"
onClick={this.props.closeNewFileModal}
aria-label="Close New File Modal"
>
<ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
<NewFileForm <NewFileForm

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
class NewFolderForm extends React.Component { class NewFolderForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -34,7 +36,10 @@ class NewFolderForm extends React.Component {
ref={(element) => { this.fileName = element; }} ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)} {...domOnlyProps(name)}
/> />
<input type="submit" value="Add Folder" aria-label="add folder" /> <Button
type="submit"
>Add Folder
</Button>
</div> </div>
{name.touched && name.error && <span className="form-error">{name.error}</span>} {name.touched && name.error && <span className="form-error">{name.error}</span>}
</form> </form>

View file

@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import InlineSVG from 'react-inlinesvg';
import NewFolderForm from './NewFolderForm'; import NewFolderForm from './NewFolderForm';
const exitUrl = require('../../../images/exit.svg'); import ExitIcon from '../../../images/exit.svg';
class NewFolderModal extends React.Component { class NewFolderModal extends React.Component {
componentDidMount() { componentDidMount() {
@ -17,8 +16,12 @@ class NewFolderModal extends React.Component {
<div className="modal-content-folder"> <div className="modal-content-folder">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Create Folder</h2> <h2 className="modal__title">Create Folder</h2>
<button className="modal__exit-button" onClick={this.props.closeModal}> <button
<InlineSVG src={exitUrl} alt="Close New Folder Modal" /> className="modal__exit-button"
onClick={this.props.closeModal}
aria-label="Close New Folder Modal"
>
<ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
<NewFolderForm {...this.props} /> <NewFolderForm {...this.props} />

View file

@ -1,15 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { withTranslation } from 'react-i18next';
// import { bindActionCreators } from 'redux'; // import { bindActionCreators } from 'redux';
// import { connect } from 'react-redux'; // import { connect } from 'react-redux';
// import * as PreferencesActions from '../actions/preferences'; // import * as PreferencesActions from '../actions/preferences';
const plusUrl = require('../../../images/plus.svg'); import PlusIcon from '../../../images/plus.svg';
const minusUrl = require('../../../images/minus.svg'); import MinusIcon from '../../../images/minus.svg';
const beepUrl = require('../../../sounds/audioAlert.mp3'); import beepUrl from '../../../sounds/audioAlert.mp3';
class Preferences extends React.Component { class Preferences extends React.Component {
constructor(props) { constructor(props) {
@ -99,13 +99,13 @@ class Preferences extends React.Component {
<Tabs> <Tabs>
<TabList> <TabList>
<div className="tabs__titles"> <div className="tabs__titles">
<Tab><h4 className="tabs__title">General Settings</h4></Tab> <Tab><h4 className="tabs__title">{this.props.t('GeneralSettings')}</h4></Tab>
<Tab><h4 className="tabs__title">Accessibility</h4></Tab> <Tab><h4 className="tabs__title">{this.props.t('Accessibility')}</h4></Tab>
</div> </div>
</TabList> </TabList>
<TabPanel> <TabPanel>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Theme</h4> <h4 className="preference__title">{this.props.t('Theme')}</h4>
<div className="preference__options"> <div className="preference__options">
<input <input
type="radio" type="radio"
@ -117,7 +117,7 @@ class Preferences extends React.Component {
value="light" value="light"
checked={this.props.theme === 'light'} checked={this.props.theme === 'light'}
/> />
<label htmlFor="light-theme-on" className="preference__option">Light</label> <label htmlFor="light-theme-on" className="preference__option">{this.props.t('Light')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setTheme('dark')} onChange={() => this.props.setTheme('dark')}
@ -128,7 +128,7 @@ class Preferences extends React.Component {
value="dark" value="dark"
checked={this.props.theme === 'dark'} checked={this.props.theme === 'dark'}
/> />
<label htmlFor="dark-theme-on" className="preference__option">Dark</label> <label htmlFor="dark-theme-on" className="preference__option">{this.props.t('Dark')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setTheme('contrast')} onChange={() => this.props.setTheme('contrast')}
@ -139,19 +139,19 @@ class Preferences extends React.Component {
value="contrast" value="contrast"
checked={this.props.theme === 'contrast'} checked={this.props.theme === 'contrast'}
/> />
<label htmlFor="high-contrast-theme-on" className="preference__option">High Contrast</label> <label htmlFor="high-contrast-theme-on" className="preference__option">{this.props.t('HighContrast')}</label>
</div> </div>
</div> </div>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Text size</h4> <h4 className="preference__title">{this.props.t('TextSize')}</h4>
<button <button
className="preference__minus-button" className="preference__minus-button"
onClick={this.decreaseFontSize} onClick={this.decreaseFontSize}
aria-label="decrease font size" aria-label="decrease font size"
disabled={this.state.fontSize <= 8} disabled={this.state.fontSize <= 8}
> >
<InlineSVG src={minusUrl} alt="Decrease Font Size" /> <MinusIcon focusable="false" aria-hidden="true" />
<h6 className="preference__label">Decrease</h6> <h6 className="preference__label">{this.props.t('Decrease')}</h6>
</button> </button>
<form onSubmit={this.onFontInputSubmit}> <form onSubmit={this.onFontInputSubmit}>
<input <input
@ -171,12 +171,12 @@ class Preferences extends React.Component {
aria-label="increase font size" aria-label="increase font size"
disabled={this.state.fontSize >= 36} disabled={this.state.fontSize >= 36}
> >
<InlineSVG src={plusUrl} alt="Increase Font Size" /> <PlusIcon focusable="false" aria-hidden="true" />
<h6 className="preference__label">Increase</h6> <h6 className="preference__label">{this.props.t('Increase')}</h6>
</button> </button>
</div> </div>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Autosave</h4> <h4 className="preference__title">{this.props.t('Autosave')}</h4>
<div className="preference__options"> <div className="preference__options">
<input <input
type="radio" type="radio"
@ -188,7 +188,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={this.props.autosave} checked={this.props.autosave}
/> />
<label htmlFor="autosave-on" className="preference__option">On</label> <label htmlFor="autosave-on" className="preference__option">{this.props.t('On')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setAutosave(false)} onChange={() => this.props.setAutosave(false)}
@ -199,11 +199,11 @@ class Preferences extends React.Component {
value="Off" value="Off"
checked={!this.props.autosave} checked={!this.props.autosave}
/> />
<label htmlFor="autosave-off" className="preference__option">Off</label> <label htmlFor="autosave-off" className="preference__option">{this.props.t('Off')}</label>
</div> </div>
</div> </div>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Word Wrap</h4> <h4 className="preference__title">{this.props.t('WordWrap')}</h4>
<div className="preference__options"> <div className="preference__options">
<input <input
type="radio" type="radio"
@ -215,7 +215,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={this.props.linewrap} checked={this.props.linewrap}
/> />
<label htmlFor="linewrap-on" className="preference__option">On</label> <label htmlFor="linewrap-on" className="preference__option">{this.props.t('On')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setLinewrap(false)} onChange={() => this.props.setLinewrap(false)}
@ -226,13 +226,13 @@ class Preferences extends React.Component {
value="Off" value="Off"
checked={!this.props.linewrap} checked={!this.props.linewrap}
/> />
<label htmlFor="linewrap-off" className="preference__option">Off</label> <label htmlFor="linewrap-off" className="preference__option">{this.props.t('Off')}</label>
</div> </div>
</div> </div>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Line numbers</h4> <h4 className="preference__title">{this.props.t('LineNumbers')}</h4>
<div className="preference__options"> <div className="preference__options">
<input <input
type="radio" type="radio"
@ -244,7 +244,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={this.props.lineNumbers} checked={this.props.lineNumbers}
/> />
<label htmlFor="line-numbers-on" className="preference__option">On</label> <label htmlFor="line-numbers-on" className="preference__option">{this.props.t('On')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setLineNumbers(false)} onChange={() => this.props.setLineNumbers(false)}
@ -255,11 +255,11 @@ class Preferences extends React.Component {
value="Off" value="Off"
checked={!this.props.lineNumbers} checked={!this.props.lineNumbers}
/> />
<label htmlFor="line-numbers-off" className="preference__option">Off</label> <label htmlFor="line-numbers-off" className="preference__option">{this.props.t('Off')}</label>
</div> </div>
</div> </div>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Lint warning sound</h4> <h4 className="preference__title">{this.props.t('LintWarningSound')}</h4>
<div className="preference__options"> <div className="preference__options">
<input <input
type="radio" type="radio"
@ -271,7 +271,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={this.props.lintWarning} checked={this.props.lintWarning}
/> />
<label htmlFor="lint-warning-on" className="preference__option">On</label> <label htmlFor="lint-warning-on" className="preference__option">{this.props.t('On')}</label>
<input <input
type="radio" type="radio"
onChange={() => this.props.setLintWarning(false)} onChange={() => this.props.setLintWarning(false)}
@ -282,19 +282,19 @@ class Preferences extends React.Component {
value="Off" value="Off"
checked={!this.props.lintWarning} checked={!this.props.lintWarning}
/> />
<label htmlFor="lint-warning-off" className="preference__option">Off</label> <label htmlFor="lint-warning-off" className="preference__option">{this.props.t('Off')}</label>
<button <button
className="preference__preview-button" className="preference__preview-button"
onClick={() => beep.play()} onClick={() => beep.play()}
aria-label="preview sound" aria-label="preview sound"
> >
Preview sound {this.props.t('PreviewSound')}
</button> </button>
</div> </div>
</div> </div>
<div className="preference"> <div className="preference">
<h4 className="preference__title">Accessible text-based canvas</h4> <h4 className="preference__title">{this.props.t('AccessibleTextBasedCanvas')}</h4>
<h6 className="preference__subtitle">Used with screen reader</h6> <h6 className="preference__subtitle">{this.props.t('UsedScreenReader')}</h6>
<div className="preference__options"> <div className="preference__options">
<input <input
@ -308,7 +308,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={(this.props.textOutput)} checked={(this.props.textOutput)}
/> />
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label> <label htmlFor="text-output-on" className="preference__option preference__canvas">{this.props.t('PlainText')}</label>
<input <input
type="checkbox" type="checkbox"
onChange={(event) => { onChange={(event) => {
@ -320,7 +320,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={(this.props.gridOutput)} checked={(this.props.gridOutput)}
/> />
<label htmlFor="table-output-on" className="preference__option preference__canvas">Table-text</label> <label htmlFor="table-output-on" className="preference__option preference__canvas">{this.props.t('TableText')}</label>
<input <input
type="checkbox" type="checkbox"
onChange={(event) => { onChange={(event) => {
@ -332,7 +332,7 @@ class Preferences extends React.Component {
value="On" value="On"
checked={(this.props.soundOutput)} checked={(this.props.soundOutput)}
/> />
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label> <label htmlFor="sound-output-on" className="preference__option preference__canvas">{this.props.t('Sound')}</label>
</div> </div>
</div> </div>
</TabPanel> </TabPanel>
@ -361,6 +361,7 @@ Preferences.propTypes = {
setLintWarning: PropTypes.func.isRequired, setLintWarning: PropTypes.func.isRequired,
theme: PropTypes.string.isRequired, theme: PropTypes.string.isRequired,
setTheme: PropTypes.func.isRequired, setTheme: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
export default Preferences; export default withTranslation('WebEditor')(Preferences);

View file

@ -22,6 +22,23 @@ import {
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets } import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
from '../../../utils/consoleUtils'; 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 { class PreviewFrame extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
componentDidMount() { componentDidMount() {
window.addEventListener('message', this.handleConsoleEvent); window.addEventListener('message', this.handleConsoleEvent);
const props = {
...this.props,
previewIsRefreshing: this.props.previewIsRefreshing,
isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
};
if (shouldRenderSketch(props)) this.renderSketch();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
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();
}
// small bug - if autorefresh is on, and the usr changes files // small bug - if autorefresh is on, and the usr changes files
// in the sketch, preview will reload // in the sketch, preview will reload
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('message', this.handleConsoleEvent); window.removeEventListener('message', this.handleConsoleEvent);
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body); const iframeBody = this.iframeElement.contentDocument.body;
if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
} }
handleConsoleEvent(messageEvent) { handleConsoleEvent(messageEvent) {
@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
jsFileStrings.forEach((jsFileString) => { jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) { if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2); const filePath = jsFileString.substr(1, jsFileString.length - 2);
const quoteCharacter = jsFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files); const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) { if (resolvedFile) {
if (resolvedFile.url) { 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)) { } else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
// could also pull file from API instead of using bloburl // could also pull file from API instead of using bloburl
const blobURL = getBlobUrl(resolvedFile); const blobURL = getBlobUrl(resolvedFile);
this.props.setBlobUrl(resolvedFile, blobURL); 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) => { cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) { if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2); const filePath = cssFileString.substr(1, cssFileString.length - 2);
const quoteCharacter = cssFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files); const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) { if (resolvedFile) {
if (resolvedFile.url) { 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': true,
'preview-frame--full-view': this.props.fullView '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 ( return (
<iframe <iframe
id="canvas_frame" id="canvas_frame"
@ -362,7 +356,7 @@ class PreviewFrame extends React.Component {
frameBorder="0" frameBorder="0"
title="sketch preview" title="sketch preview"
ref={(element) => { this.iframeElement = element; }} ref={(element) => { this.iframeElement = element; }}
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals" sandbox={sandboxAttributes}
/> />
); );
} }
@ -393,7 +387,7 @@ PreviewFrame.propTypes = {
clearConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired,
cmController: PropTypes.shape({ cmController: PropTypes.shape({
getContent: PropTypes.func getContent: PropTypes.func
}) }),
}; };
PreviewFrame.defaultProps = { PreviewFrame.defaultProps = {

View file

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import InlineSVG from 'react-inlinesvg';
const check = require('../../../../images/check_encircled.svg'); import CheckIcon from '../../../../images/check_encircled.svg';
const close = require('../../../../images/close.svg'); import CloseIcon from '../../../../images/close.svg';
const Icons = ({ isAdded }) => { const Icons = ({ isAdded }) => {
const classes = [ const classes = [
@ -13,9 +12,9 @@ const Icons = ({ isAdded }) => {
return ( return (
<div className={classes}> <div className={classes}>
<InlineSVG className="quick-add__remove-icon" src={close} alt="Remove from collection" /> <CloseIcon className="quick-add__remove-icon" role="img" aria-label="Descending" focusable="false" />
<InlineSVG className="quick-add__in-icon" src={check} alt="In collection" /> <CheckIcon className="quick-add__in-icon" role="img" aria-label="Descending" focusable="false" />
<InlineSVG className="quick-add__add-icon" src={close} alt="Add to collection" /> <CloseIcon className="quick-add__add-icon" role="img" aria-label="Descending" focusable="false" />
</div> </div>
); );
}; };

View file

@ -6,22 +6,25 @@ import Icons from './Icons';
const Item = ({ const Item = ({
isAdded, onSelect, name, url isAdded, onSelect, name, url
}) => ( }) => {
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ } const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection';
<button className="quick-add__item-toggle" onClick={onSelect}> return (
<Icons isAdded={isAdded} /> <li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
</button> <button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}>
<span className="quick-add__item-name">{name}</span> <Icons isAdded={isAdded} />
<Link </button>
className="quick-add__item-view" <span className="quick-add__item-name">{name}</span>
to={url} <Link
target="_blank" className="quick-add__item-view"
onClick={e => e.stopPropogation()} to={url}
> target="_blank"
View onClick={e => e.stopPropogation()}
</Link> >
</li> View
); </Link>
</li>
);
};
const ItemType = PropTypes.shape({ const ItemType = PropTypes.shape({
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,

View file

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
const searchIcon = require('../../../../images/magnifyingglass.svg'); import SearchIcon from '../../../../images/magnifyingglass.svg';
class Searchbar extends React.Component { class Searchbar extends React.Component {
constructor(props) { constructor(props) {
@ -46,7 +45,7 @@ class Searchbar extends React.Component {
return ( return (
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}> <div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
<div className="searchbar__button"> <div className="searchbar__button">
<InlineSVG className="searchbar__icon" src={searchIcon} /> <SearchIcon className="searchbar__icon" focusable="false" aria-hidden="true" />
</div> </div>
<input <input
className="searchbar__input" className="searchbar__input"

View file

@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
import ConnectedFileNode from './FileNode'; import ConnectedFileNode from './FileNode';
const downArrowUrl = require('../../../images/down-filled-triangle.svg'); import DownArrowIcon from '../../../images/down-filled-triangle.svg';
class Sidebar extends React.Component { class Sidebar extends React.Component {
constructor(props) { constructor(props) {
@ -69,14 +68,14 @@ class Sidebar extends React.Component {
const rootFile = this.props.files.filter(file => file.name === 'root')[0]; const rootFile = this.props.files.filter(file => file.name === 'root')[0];
return ( return (
<nav className={sidebarClass} title="file-navigation" > <section className={sidebarClass}>
<div className="sidebar__header" onContextMenu={this.toggleProjectOptions}> <header className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
<h3 className="sidebar__title"> <h3 className="sidebar__title">
<span>Sketch Files</span> <span>Sketch Files</span>
</h3> </h3>
<div className="sidebar__icons"> <div className="sidebar__icons">
<button <button
aria-label="add file or folder" aria-label="Toggle open/close sketch file options"
className="sidebar__add" className="sidebar__add"
tabIndex="0" tabIndex="0"
ref={(element) => { this.sidebarOptions = element; }} ref={(element) => { this.sidebarOptions = element; }}
@ -84,7 +83,7 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
> >
<InlineSVG src={downArrowUrl} /> <DownArrowIcon focusable="false" aria-hidden="true" />
</button> </button>
<ul className="sidebar__project-options"> <ul className="sidebar__project-options">
<li> <li>
@ -131,12 +130,12 @@ class Sidebar extends React.Component {
} }
</ul> </ul>
</div> </div>
</div> </header>
<ConnectedFileNode <ConnectedFileNode
id={rootFile.id} id={rootFile.id}
canEdit={canEditProject} canEdit={canEditProject}
/> />
</nav> </section>
); );
} }
} }

View file

@ -2,7 +2,6 @@ import format from 'date-fns/format';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -19,9 +18,9 @@ import Loader from '../../App/components/loader';
import Overlay from '../../App/components/Overlay'; import Overlay from '../../App/components/Overlay';
import AddToCollectionList from './AddToCollectionList'; import AddToCollectionList from './AddToCollectionList';
const arrowUp = require('../../../images/sort-arrow-up.svg'); import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
const arrowDown = require('../../../images/sort-arrow-down.svg'); import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
class SketchListRowBase extends React.Component { class SketchListRowBase extends React.Component {
constructor(props) { constructor(props) {
@ -168,8 +167,9 @@ class SketchListRowBase extends React.Component {
onClick={this.toggleOptions} onClick={this.toggleOptions}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent} onFocus={this.onFocusComponent}
aria-label="Toggle Open/Close Sketch Options"
> >
<InlineSVG src={downFilledTriangle} alt="Menu" /> <DownFilledTriangleIcon focusable="false" aria-hidden="true" />
</button> </button>
{optionsOpen && {optionsOpen &&
<ul <ul
@ -326,15 +326,15 @@ class SketchList extends React.Component {
super(props); super(props);
this.props.getProjects(this.props.username); this.props.getProjects(this.props.username);
this.props.resetSorting(); this.props.resetSorting();
this._renderFieldHeader = this._renderFieldHeader.bind(this);
this.state = { this.state = {
isInitialDataLoad: true, isInitialDataLoad: true,
}; };
} }
componentWillReceiveProps(nextProps) { componentDidUpdate(prevProps) {
if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) { if (this.props.sketches !== prevProps.sketches && Array.isArray(this.props.sketches)) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ this.setState({
isInitialDataLoad: false, isInitialDataLoad: false,
}); });
@ -368,21 +368,43 @@ class SketchList extends React.Component {
return null; return null;
} }
_renderFieldHeader(fieldName, displayName) { _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 { field, direction } = this.props.sorting;
const headerClass = classNames({ const headerClass = classNames({
'sketches-table__header': true, 'sketches-table__header': true,
'sketches-table__header--selected': field === fieldName 'sketches-table__header--selected': field === fieldName
}); });
const buttonLabel = this._getButtonLabel(fieldName, displayName);
return ( return (
<th scope="col"> <th scope="col">
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}> <button
className="sketch-list__sort-button"
onClick={() => this.props.toggleDirectionForField(fieldName)}
aria-label={buttonLabel}
>
<span className={headerClass}>{displayName}</span> <span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC && {field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<InlineSVG src={arrowUp} /> <ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
} }
{field === fieldName && direction === SortingActions.DIRECTION.DESC && {field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<InlineSVG src={arrowDown} /> <ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
} }
</button> </button>
</th> </th>
@ -392,7 +414,7 @@ class SketchList extends React.Component {
render() { render() {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username; const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return ( return (
<div className="sketches-table-container"> <article className="sketches-table-container">
<Helmet> <Helmet>
<title>{this.getSketchesTitle()}</title> <title>{this.getSketchesTitle()}</title>
</Helmet> </Helmet>
@ -435,7 +457,7 @@ class SketchList extends React.Component {
/> />
</Overlay> </Overlay>
} }
</div> </article>
); );
} }
} }

View file

@ -2,19 +2,20 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg'; import { useTranslation } from 'react-i18next';
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
const exitUrl = require('../../../images/exit.svg'); import ExitIcon from '../../../images/exit.svg';
function Toast(props) { function Toast(props) {
const { t } = useTranslation('WebEditor');
return ( return (
<section className="toast"> <section className="toast">
<p> <p>
{props.text} {t(props.text)}
</p> </p>
<button className="toast__close" onClick={props.hideToast}> <button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" >
<InlineSVG src={exitUrl} alt="Close Keyboard Shortcuts Overlay" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</section> </section>
); );

View file

@ -3,37 +3,50 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import classNames from 'classnames'; import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as preferenceActions from '../actions/preferences'; import * as preferenceActions from '../actions/preferences';
import * as projectActions from '../actions/project'; import * as projectActions from '../actions/project';
const playUrl = require('../../../images/play.svg'); import PlayIcon from '../../../images/play.svg';
const stopUrl = require('../../../images/stop.svg'); import StopIcon from '../../../images/stop.svg';
const preferencesUrl = require('../../../images/preferences.svg'); import PreferencesIcon from '../../../images/preferences.svg';
const editProjectNameUrl = require('../../../images/pencil.svg'); import EditProjectNameIcon from '../../../images/pencil.svg';
class Toolbar extends React.Component { class Toolbar extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleKeyPress = this.handleKeyPress.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleProjectNameChange = this.handleProjectNameChange.bind(this); this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
this.state = {
projectNameInputValue: props.project.name,
};
} }
handleKeyPress(event) { handleKeyPress(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.props.hideEditProjectName(); this.props.hideEditProjectName();
this.projectNameInput.blur();
} }
} }
handleProjectNameChange(event) { handleProjectNameChange(event) {
this.props.setProjectName(event.target.value); this.setState({ projectNameInputValue: event.target.value });
} }
validateProjectName() { handleProjectNameSave() {
if ((this.props.project.name.trim()).length === 0) { const newProjectName = this.state.projectNameInputValue.trim();
this.props.setProjectName(this.originalProjectName); if (newProjectName.length === 0) {
this.setState({
projectNameInputValue: this.props.project.name,
});
} else {
this.props.setProjectName(newProjectName);
this.props.hideEditProjectName();
if (this.props.project.id) {
this.props.saveProject();
}
} }
} }
@ -61,6 +74,8 @@ class Toolbar extends React.Component {
'toolbar__project-name-container--editing': this.props.project.isEditingName 'toolbar__project-name-container--editing': this.props.project.isEditingName
}); });
const canEditProjectName = this.canEditProjectName();
return ( return (
<div className="toolbar"> <div className="toolbar">
<button <button
@ -70,25 +85,25 @@ class Toolbar extends React.Component {
this.props.setTextOutput(true); this.props.setTextOutput(true);
this.props.setGridOutput(true); this.props.setGridOutput(true);
}} }}
aria-label="play sketch" aria-label="Play sketch"
disabled={this.props.infiniteLoop} disabled={this.props.infiniteLoop}
> >
<InlineSVG src={playUrl} alt="Play Sketch" /> <PlayIcon focusable="false" aria-hidden="true" />
</button> </button>
<button <button
className={playButtonClass} className={playButtonClass}
onClick={this.props.startSketch} onClick={this.props.startSketch}
aria-label="play only visual sketch" aria-label="Play only visual sketch"
disabled={this.props.infiniteLoop} disabled={this.props.infiniteLoop}
> >
<InlineSVG src={playUrl} alt="Play only visual Sketch" /> <PlayIcon focusable="false" aria-hidden="true" />
</button> </button>
<button <button
className={stopButtonClass} className={stopButtonClass}
onClick={this.props.stopSketch} onClick={this.props.stopSketch}
aria-label="stop sketch" aria-label="Stop sketch"
> >
<InlineSVG src={stopUrl} alt="Stop Sketch" /> <StopIcon focusable="false" aria-hidden="true" />
</button> </button>
<div className="toolbar__autorefresh"> <div className="toolbar__autorefresh">
<input <input
@ -104,38 +119,36 @@ class Toolbar extends React.Component {
</label> </label>
</div> </div>
<div className={nameContainerClass}> <div className={nameContainerClass}>
<a <button
className="toolbar__project-name" className="toolbar__project-name"
href={this.props.owner ? `/${this.props.owner.username}/sketches/${this.props.project.id}` : ''} onClick={() => {
onClick={(e) => { if (canEditProjectName) {
if (this.canEditProjectName()) {
e.preventDefault();
this.originalProjectName = this.props.project.name;
this.props.showEditProjectName(); this.props.showEditProjectName();
setTimeout(() => this.projectNameInput.focus(), 0); setTimeout(() => this.projectNameInput.focus(), 0);
} }
}} }}
disabled={!canEditProjectName}
aria-label="Edit sketch name"
> >
<span>{this.props.project.name}</span> <span>{this.props.project.name}</span>
{ {
this.canEditProjectName() && canEditProjectName &&
<InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" /> <EditProjectNameIcon
className="toolbar__edit-name-button"
focusable="false"
aria-hidden="true"
/>
} }
</a> </button>
<input <input
type="text" type="text"
maxLength="128" maxLength="128"
className="toolbar__project-name-input" className="toolbar__project-name-input"
value={this.props.project.name} aria-label="New sketch name"
value={this.state.projectNameInputValue}
onChange={this.handleProjectNameChange} onChange={this.handleProjectNameChange}
ref={(element) => { this.projectNameInput = element; }} ref={(element) => { this.projectNameInput = element; }}
onBlur={() => { onBlur={this.handleProjectNameSave}
this.validateProjectName();
this.props.hideEditProjectName();
if (this.props.project.id) {
this.props.saveProject();
}
}}
onKeyPress={this.handleKeyPress} onKeyPress={this.handleKeyPress}
/> />
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
@ -151,9 +164,9 @@ class Toolbar extends React.Component {
<button <button
className={preferencesButtonClass} className={preferencesButtonClass}
onClick={this.props.openPreferences} onClick={this.props.openPreferences}
aria-label="preferences" aria-label="Open Preferences"
> >
<InlineSVG src={preferencesUrl} alt="Preferences" /> <PreferencesIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
); );
@ -210,4 +223,5 @@ const mapDispatchToProps = {
...projectActions, ...projectActions,
}; };
export const ToolbarComponent = Toolbar;
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar); export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);

View file

@ -0,0 +1,84 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import lodash from 'lodash';
import { ToolbarComponent } from './Toolbar';
const renderComponent = (extraProps = {}) => {
const props = lodash.merge({
isPlaying: false,
preferencesIsVisible: false,
stopSketch: jest.fn(),
setProjectName: jest.fn(),
openPreferences: jest.fn(),
showEditProjectName: jest.fn(),
hideEditProjectName: jest.fn(),
infiniteLoop: false,
autorefresh: false,
setAutorefresh: jest.fn(),
setTextOutput: jest.fn(),
setGridOutput: jest.fn(),
startSketch: jest.fn(),
startAccessibleSketch: jest.fn(),
saveProject: jest.fn(),
currentUser: 'me',
originalProjectName: 'testname',
owner: {
username: 'me'
},
project: {
name: 'testname',
isEditingName: false,
id: 'id',
},
}, extraProps);
render(<ToolbarComponent {...props} />);
return props;
};
describe('<ToolbarComponent />', () => {
it('sketch owner can switch to sketch name editing mode', async () => {
const props = renderComponent();
const sketchName = screen.getByLabelText('Edit sketch name');
fireEvent.click(sketchName);
await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled());
});
it('non-owner can\t switch to sketch editing mode', async () => {
const props = renderComponent({ currentUser: 'not-me' });
const sketchName = screen.getByLabelText('Edit sketch name');
fireEvent.click(sketchName);
expect(sketchName).toBeDisabled();
await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled());
});
it('sketch owner can change name', async () => {
const props = renderComponent({ project: { isEditingName: true } });
const sketchNameInput = screen.getByLabelText('New sketch name');
fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } });
fireEvent.blur(sketchNameInput);
await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name'));
await waitFor(() => expect(props.saveProject).toHaveBeenCalled());
});
it('sketch owner can\'t change to empty name', async () => {
const props = renderComponent({ project: { isEditingName: true } });
const sketchNameInput = screen.getByLabelText('New sketch name');
fireEvent.change(sketchNameInput, { target: { value: '' } });
fireEvent.blur(sketchNameInput);
await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled());
await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled());
});
});

View file

@ -2,14 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import getConfig from '../../../utils/getConfig';
import FileUploader from './FileUploader'; import FileUploader from './FileUploader';
import { getreachedTotalSizeLimit } from '../selectors/users'; import { getreachedTotalSizeLimit } from '../selectors/users';
import exitUrl from '../../../images/exit.svg'; import ExitIcon from '../../../images/exit.svg';
const __process = (typeof global !== 'undefined' ? global : window).process; const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limit = __process.env.UPLOAD_LIMIT || 250000000;
const limitText = prettyBytes(limit); const limitText = prettyBytes(limit);
class UploadFileModal extends React.Component { class UploadFileModal extends React.Component {
@ -33,8 +32,12 @@ class UploadFileModal extends React.Component {
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Upload File</h2> <h2 className="modal__title">Upload File</h2>
<button className="modal__exit-button" onClick={this.props.closeModal}> <button
<InlineSVG src={exitUrl} alt="Close New File Modal" /> className="modal__exit-button"
onClick={this.props.closeModal}
aria-label="Close upload file modal"
>
<ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
</div> </div>
{ this.props.reachedTotalSizeLimit && { this.props.reachedTotalSizeLimit &&

View file

@ -10,7 +10,7 @@ import * as ProjectActions from '../actions/project';
class FullView extends React.Component { class FullView extends React.Component {
componentDidMount() { componentDidMount() {
this.props.getProject(this.props.params.project_id); this.props.getProject(this.props.params.project_id, this.props.params.username);
} }
ident = () => {} ident = () => {}
@ -25,7 +25,7 @@ class FullView extends React.Component {
owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }} owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }}
project={{ name: this.props.project.name, id: this.props.params.project_id }} project={{ name: this.props.project.name, id: this.props.params.project_id }}
/> />
<div className="preview-frame-holder"> <main className="preview-frame-holder">
<PreviewFrame <PreviewFrame
htmlFile={this.props.htmlFile} htmlFile={this.props.htmlFile}
jsFiles={this.props.jsFiles} jsFiles={this.props.jsFiles}
@ -48,7 +48,7 @@ class FullView extends React.Component {
expandConsole={this.ident} expandConsole={this.ident}
clearConsole={this.ident} clearConsole={this.ident}
/> />
</div> </main>
</div> </div>
); );
} }
@ -56,7 +56,8 @@ class FullView extends React.Component {
FullView.propTypes = { FullView.propTypes = {
params: PropTypes.shape({ params: PropTypes.shape({
project_id: PropTypes.string project_id: PropTypes.string,
username: PropTypes.string
}).isRequired, }).isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,

View file

@ -3,6 +3,7 @@ import React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { withTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import Editor from '../components/Editor'; import Editor from '../components/Editor';
@ -34,11 +35,38 @@ import AddToCollectionList from '../components/AddToCollectionList';
import Feedback from '../components/Feedback'; import Feedback from '../components/Feedback';
import { CollectionSearchbar } from '../components/Searchbar'; import { CollectionSearchbar } from '../components/Searchbar';
function getTitle(props) {
const { id } = props.project;
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
}
function isUserOwner(props) {
return props.project.owner && props.project.owner.id === props.user.id;
}
function warnIfUnsavedChanges(props) { // eslint-disable-line
const { route } = props.route;
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn
props.persistState();
window.onbeforeunload = null;
} else if (route && (props.location.pathname === '/login' || props.location.pathname === '/signup')) {
// don't warn
props.persistState();
window.onbeforeunload = null;
} else if (props.ide.unsavedChanges) {
if (!window.confirm(props.t('WarningUnsavedChanges'))) {
return false;
}
props.setUnsavedChanges(false);
return true;
}
}
class IDEView extends React.Component { class IDEView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this); this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
this.state = { this.state = {
consoleSize: props.ide.consoleIsExpanded ? 150 : 29, consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
@ -53,18 +81,18 @@ class IDEView extends React.Component {
this.props.stopSketch(); this.props.stopSketch();
if (this.props.params.project_id) { if (this.props.params.project_id) {
const id = this.props.params.project_id; const { project_id: id, username } = this.props.params;
if (id !== this.props.project.id) { if (id !== this.props.project.id) {
this.props.getProject(id); this.props.getProject(id, username);
} }
} }
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false); document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route)); this.props.router.setRouteLeaveHook(this.props.route, this.handleUnsavedChanges);
window.onbeforeunload = () => this.warnIfUnsavedChanges(); window.onbeforeunload = this.handleUnsavedChanges;
this.autosaveInterval = null; this.autosaveInterval = null;
} }
@ -92,7 +120,7 @@ class IDEView extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.isUserOwner() && this.props.project.id) { if (isUserOwner(this.props) && this.props.project.id) {
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) { if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
if ( if (
this.props.selectedFile.name === prevProps.selectedFile.name && this.props.selectedFile.name === prevProps.selectedFile.name &&
@ -113,7 +141,7 @@ class IDEView extends React.Component {
} }
if (this.props.route.path !== prevProps.route.path) { if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route)); this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props));
} }
} }
@ -123,16 +151,12 @@ class IDEView extends React.Component {
this.autosaveInterval = null; this.autosaveInterval = null;
} }
isUserOwner() {
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
}
handleGlobalKeydown(e) { handleGlobalKeydown(e) {
// 83 === s // 83 === s
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) { if (isUserOwner(this.props) || (this.props.user.authenticated && !this.props.project.owner)) {
this.props.saveProject(this.cmController.getContent()); this.props.saveProject(this.cmController.getContent());
} else if (this.props.user.authenticated) { } else if (this.props.user.authenticated) {
this.props.cloneProject(); this.props.cloneProject();
@ -170,42 +194,34 @@ class IDEView extends React.Component {
} else { } else {
this.props.expandConsole(); this.props.expandConsole();
} }
} else if (e.keyCode === 27) {
if (this.props.ide.newFolderModalVisible) {
this.props.closeNewFolderModal();
} else if (this.props.ide.uploadFileModalVisible) {
this.props.closeUploadFileModal();
} else if (this.props.ide.modalIsVisible) {
this.props.closeNewFileModal();
}
} }
} }
warnIfUnsavedChanges(route) { // eslint-disable-line handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
// don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (this.props.ide.unsavedChanges) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
return false;
}
this.props.setUnsavedChanges(false);
return true;
}
}
render() { render() {
return ( return (
<div className="ide"> <div className="ide">
<Helmet> <Helmet>
<title>p5.js Web Editor | {this.props.project.name}</title> <title>{getTitle(this.props)}</title>
</Helmet> </Helmet>
{this.props.toast.isVisible && <Toast />} {this.props.toast.isVisible && <Toast />}
<Nav <Nav
warnIfUnsavedChanges={this.warnIfUnsavedChanges} warnIfUnsavedChanges={this.handleUnsavedChanges}
cmController={this.cmController} cmController={this.cmController}
/> />
<Toolbar /> <Toolbar key={this.props.project.id} />
{this.props.ide.preferencesIsVisible && {this.props.ide.preferencesIsVisible &&
<Overlay <Overlay
title="Settings" title={this.props.t('Settings')}
ariaLabel="settings" ariaLabel="settings"
closeOverlay={this.props.closePreferences} closeOverlay={this.props.closePreferences}
> >
@ -231,7 +247,7 @@ class IDEView extends React.Component {
/> />
</Overlay> </Overlay>
} }
<div className="editor-preview-container"> <main className="editor-preview-container">
<SplitPane <SplitPane
split="vertical" split="vertical"
size={this.state.sidebarSize} size={this.state.sidebarSize}
@ -300,7 +316,7 @@ class IDEView extends React.Component {
isExpanded={this.props.ide.sidebarIsExpanded} isExpanded={this.props.ide.sidebarIsExpanded}
expandSidebar={this.props.expandSidebar} expandSidebar={this.props.expandSidebar}
collapseSidebar={this.props.collapseSidebar} collapseSidebar={this.props.collapseSidebar}
isUserOwner={this.isUserOwner()} isUserOwner={isUserOwner(this.props)}
clearConsole={this.props.clearConsole} clearConsole={this.props.clearConsole}
consoleEvents={this.props.console} consoleEvents={this.props.console}
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning} showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
@ -319,9 +335,9 @@ class IDEView extends React.Component {
theme={this.props.preferences.theme} theme={this.props.preferences.theme}
/> />
</SplitPane> </SplitPane>
<div className="preview-frame-holder"> <section className="preview-frame-holder">
<header className="preview-frame__header"> <header className="preview-frame__header">
<h2 className="preview-frame__title">Preview</h2> <h2 className="preview-frame__title">{this.props.t('Preview')}</h2>
</header> </header>
<div className="preview-frame__content"> <div className="preview-frame__content">
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}> <div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
@ -362,10 +378,10 @@ class IDEView extends React.Component {
cmController={this.cmController} cmController={this.cmController}
/> />
</div> </div>
</div> </section>
</SplitPane> </SplitPane>
</SplitPane> </SplitPane>
</div> </main>
{ this.props.ide.modalIsVisible && { this.props.ide.modalIsVisible &&
<NewFileModal /> <NewFileModal />
} }
@ -382,7 +398,7 @@ class IDEView extends React.Component {
} }
{ this.props.location.pathname === '/about' && { this.props.location.pathname === '/about' &&
<Overlay <Overlay
title="About" title={this.props.t('About')}
previousPath={this.props.ide.previousPath} previousPath={this.props.ide.previousPath}
ariaLabel="about" ariaLabel="about"
> >
@ -428,7 +444,7 @@ class IDEView extends React.Component {
} }
{this.props.ide.keyboardShortcutVisible && {this.props.ide.keyboardShortcutVisible &&
<Overlay <Overlay
title="Keyboard Shortcuts" title={this.props.t('KeyboardShortcuts')}
ariaLabel="keyboard shortcuts" ariaLabel="keyboard shortcuts"
closeOverlay={this.props.closeKeyboardShortcutModal} closeOverlay={this.props.closeKeyboardShortcutModal}
> >
@ -562,6 +578,7 @@ IDEView.propTypes = {
closeProjectOptions: PropTypes.func.isRequired, closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired, newFolder: PropTypes.func.isRequired,
closeNewFolderModal: PropTypes.func.isRequired, closeNewFolderModal: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired, createFolder: PropTypes.func.isRequired,
closeShareModal: PropTypes.func.isRequired, closeShareModal: PropTypes.func.isRequired,
showEditorOptions: PropTypes.func.isRequired, showEditorOptions: PropTypes.func.isRequired,
@ -590,12 +607,12 @@ IDEView.propTypes = {
showErrorModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired, hideErrorModal: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired, clearPersistedState: PropTypes.func.isRequired,
persistState: PropTypes.func.isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired, showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired, hideRuntimeErrorWarning: PropTypes.func.isRequired,
startSketch: PropTypes.func.isRequired, startSketch: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired, openUploadFileModal: PropTypes.func.isRequired,
closeUploadFileModal: PropTypes.func.isRequired closeUploadFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
@ -632,4 +649,6 @@ function mapDispatchToProps(dispatch) {
); );
} }
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));

View file

@ -0,0 +1,250 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { useState } from 'react';
// Imports to be Refactored
import { bindActionCreators } from 'redux';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project';
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
import * as PreferencesActions from '../actions/preferences';
import * as UserActions from '../../User/actions';
import * as ToastActions from '../actions/toast';
import * as ConsoleActions from '../actions/console';
import { getHTMLFile } from '../reducers/files';
// Local Imports
import Editor from '../components/Editor';
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
import IconButton from '../../../components/mobile/IconButton';
import Header from '../../../components/mobile/Header';
import Screen from '../../../components/mobile/MobileScreen';
import Footer from '../../../components/mobile/Footer';
import IDEWrapper from '../../../components/mobile/IDEWrapper';
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
const MobileIDEView = (props) => {
const {
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
selectedFile, updateFileContent, files,
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges,
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
} = props;
const [tmController, setTmController] = useState(null); // eslint-disable-line
const [overlay, setOverlay] = useState(null); // eslint-disable-line
return (
<Screen fullscreen>
<Header
title={project.name}
subtitle={selectedFile.name}
leftButton={
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
}
>
<IconButton
to="/mobile/preferences"
onClick={() => setOverlay('preferences')}
icon={PreferencesIcon}
aria-label="Open preferences menu"
/>
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
</Header>
<IDEWrapper>
<Editor
lintWarning={preferences.lintWarning}
linewrap={preferences.linewrap}
lintMessages={editorAccessibility.lintMessages}
updateLintMessage={updateLintMessage}
clearLintMessage={clearLintMessage}
file={selectedFile}
updateFileContent={updateFileContent}
fontSize={preferences.fontSize}
lineNumbers={preferences.lineNumbers}
files={files}
editorOptionsVisible={ide.editorOptionsVisible}
showEditorOptions={showEditorOptions}
closeEditorOptions={closeEditorOptions}
showKeyboardShortcutModal={showKeyboardShortcutModal}
setUnsavedChanges={setUnsavedChanges}
isPlaying={ide.isPlaying}
theme={preferences.theme}
startRefreshSketch={startRefreshSketch}
stopSketch={stopSketch}
autorefresh={preferences.autorefresh}
unsavedChanges={ide.unsavedChanges}
projectSavedTime={project.updatedAt}
isExpanded={ide.sidebarIsExpanded}
expandSidebar={expandSidebar}
collapseSidebar={collapseSidebar}
isUserOwner={isUserOwner(props)}
clearConsole={clearConsole}
consoleEvents={console}
showRuntimeErrorWarning={showRuntimeErrorWarning}
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
provideController={setTmController}
/>
</IDEWrapper>
<Footer><h2>Bottom Bar</h2></Footer>
</Screen>
);
};
MobileIDEView.propTypes = {
preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired,
autosave: PropTypes.bool.isRequired,
linewrap: PropTypes.bool.isRequired,
lineNumbers: PropTypes.bool.isRequired,
lintWarning: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
gridOutput: PropTypes.bool.isRequired,
soundOutput: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
autorefresh: PropTypes.bool.isRequired
}).isRequired,
ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.array,
modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFolderModalVisible: PropTypes.bool.isRequired,
shareModalVisible: PropTypes.bool.isRequired,
shareModalProjectId: PropTypes.string.isRequired,
shareModalProjectName: PropTypes.string.isRequired,
shareModalProjectUsername: PropTypes.string.isRequired,
editorOptionsVisible: PropTypes.bool.isRequired,
keyboardShortcutVisible: PropTypes.bool.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
infiniteLoop: PropTypes.bool.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
infiniteLoopMessage: PropTypes.string.isRequired,
projectSavedTime: PropTypes.string,
previousPath: PropTypes.string.isRequired,
justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired
}).isRequired,
editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired,
}).isRequired,
project: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string.isRequired,
owner: PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string
}),
updatedAt: PropTypes.string
}).isRequired,
startSketch: PropTypes.func.isRequired,
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
updateFileContent: PropTypes.func.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
closeEditorOptions: PropTypes.func.isRequired,
showEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired,
setUnsavedChanges: PropTypes.func.isRequired,
startRefreshSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
collapseSidebar: PropTypes.func.isRequired,
clearConsole: PropTypes.func.isRequired,
console: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})).isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired,
user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired,
id: PropTypes.string,
username: PropTypes.string
}).isRequired,
};
function mapStateToProps(state) {
return {
files: state.files,
selectedFile: state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'),
htmlFile: getHTMLFile(state.files),
ide: state.ide,
preferences: state.preferences,
editorAccessibility: state.editorAccessibility,
user: state.user,
project: state.project,
toast: state.toast,
console: state.console
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
Object.assign(
{},
EditorAccessibilityActions,
FileActions,
ProjectActions,
IDEActions,
PreferencesActions,
UserActions,
ToastActions,
ConsoleActions
),
dispatch
);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));

View file

@ -37,6 +37,11 @@ const getSortedCollections = createSelector(
return orderBy(collections, 'name', 'desc'); return orderBy(collections, 'name', 'desc');
} }
return orderBy(collections, 'name', 'asc'); return orderBy(collections, 'name', 'asc');
} else if (field === 'numItems') {
if (direction === DIRECTION.DESC) {
return orderBy(collections, 'items.length', 'desc');
}
return orderBy(collections, 'items.length', 'asc');
} }
const sortedCollections = [...collections].sort((a, b) => { const sortedCollections = [...collections].sort((a, b) => {
const result = const result =

View file

@ -1,10 +1,10 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getConfig from '../../../utils/getConfig';
const __process = (typeof global !== 'undefined' ? global : window).process;
const getAuthenticated = state => state.user.authenticated; const getAuthenticated = state => state.user.authenticated;
const getTotalSize = state => state.user.totalSize; const getTotalSize = state => state.user.totalSize;
const getAssetsTotalSize = state => state.assets.totalSize; const getAssetsTotalSize = state => state.assets.totalSize;
const limit = __process.env.UPLOAD_LIMIT || 250000000; const limit = getConfig('UPLOAD_LIMIT') || 250000000;
export const getCanUploadMedia = createSelector( export const getCanUploadMedia = createSelector(
getAuthenticated, getAuthenticated,

View file

@ -0,0 +1,226 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import * as PreferencesActions from '../IDE/actions/preferences';
import * as IdeActions from '../IDE/actions/ide';
import IconButton from '../../components/mobile/IconButton';
import Screen from '../../components/mobile/MobileScreen';
import Header from '../../components/mobile/Header';
import PreferencePicker from '../../components/mobile/PreferencePicker';
import { ExitIcon } from '../../common/icons';
import { remSize, prop } from '../../theme';
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(68)};
`;
const SettingsHeader = styled(Header)`
background: transparent;
`;
const SectionHeader = styled.h2`
color: ${prop('primaryTextColor')};
padding-top: ${remSize(32)};
`;
const SectionSubeader = styled.h3`
color: ${prop('primaryTextColor')};
`;
const MobilePreferences = (props) => {
const {
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, lineNumbers, lintWarning
} = props;
const {
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, setLineNumbers, setLintWarning
} = props;
const generalSettings = [
{
title: 'Theme',
value: theme,
options: [
{
value: 'light', label: 'light', ariaLabel: 'light theme on', name: 'light theme', id: 'light-theme-on'
},
{
value: 'dark', label: 'dark', ariaLabel: 'dark theme on', name: 'dark theme', id: 'dark-theme-on'
},
{
value: 'contrast',
label: 'contrast',
ariaLabel: 'contrast theme on',
name: 'contrast theme',
id: 'contrast-theme-on'
}
],
onSelect: x => setTheme(x) // setTheme
},
{
title: 'Autosave',
value: autosave,
options: [
{
value: true, label: 'On', ariaLabel: 'autosave on', name: 'autosave', id: 'autosave-on'
},
{
value: false, label: 'Off', ariaLabel: 'autosave off', name: 'autosave', id: 'autosave-off'
},
],
onSelect: x => setAutosave(x) // setAutosave
},
{
title: 'Word Wrap',
value: linewrap,
options: [
{
value: true, label: 'On', ariaLabel: 'linewrap on', name: 'linewrap', id: 'linewrap-on'
},
{
value: false, label: 'Off', ariaLabel: 'linewrap off', name: 'linewrap', id: 'linewrap-off'
},
],
onSelect: x => setLinewrap(x)
}
];
const outputSettings = [
{
title: 'Plain-text',
value: textOutput,
options: [
{
value: true, label: 'On', ariaLabel: 'text output on', name: 'text output', id: 'text-output-on'
},
{
value: false, label: 'Off', ariaLabel: 'text output off', name: 'text output', id: 'text-output-off'
},
],
onSelect: x => setTextOutput(x)
},
{
title: 'Table-text',
value: gridOutput,
options: [
{
value: true, label: 'On', ariaLabel: 'table output on', name: 'table output', id: 'table-output-on'
},
{
value: false, label: 'Off', ariaLabel: 'table output off', name: 'table output', id: 'table-output-off'
},
],
onSelect: x => setGridOutput(x)
},
{
title: 'Sound',
value: soundOutput,
options: [
{
value: true, label: 'On', ariaLabel: 'sound output on', name: 'sound output', id: 'sound-output-on'
},
{
value: false, label: 'Off', ariaLabel: 'sound output off', name: 'sound output', id: 'sound-output-off'
},
],
onSelect: x => setSoundOutput(x)
},
];
const accessibilitySettings = [
{
title: 'Line Numbers',
value: lineNumbers,
options: [
{
value: true, label: 'On', ariaLabel: 'line numbers on', name: 'line numbers', id: 'line-numbers-on'
},
{
value: false, label: 'Off', ariaLabel: 'line numbers off', name: 'line numbers', id: 'line-numbers-off'
},
],
onSelect: x => setLineNumbers(x)
},
{
title: 'Lint Warning Sound',
value: lintWarning,
options: [
{
value: true, label: 'On', ariaLabel: 'lint warning on', name: 'lint warning', id: 'lint-warning-on'
},
{
value: false, label: 'Off', ariaLabel: 'lint warning off', name: 'lint warning', id: 'lint-warning-off'
},
],
onSelect: x => setLintWarning(x)
},
];
return (
<Screen fullscreen>
<section>
<SettingsHeader transparent title="Preferences">
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
</SettingsHeader>
<section className="preferences">
<Content>
<SectionHeader>General Settings</SectionHeader>
{ generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
<SectionHeader>Accessibility</SectionHeader>
{ accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
<SectionHeader>Accessible Output</SectionHeader>
<SectionSubeader>Used with screen reader</SectionSubeader>
{ outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
</Content>
</section>
</section>
</Screen>);
};
MobilePreferences.propTypes = {
fontSize: PropTypes.number.isRequired,
lineNumbers: PropTypes.bool.isRequired,
autosave: PropTypes.bool.isRequired,
linewrap: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
gridOutput: PropTypes.bool.isRequired,
soundOutput: PropTypes.bool.isRequired,
lintWarning: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
setLinewrap: PropTypes.func.isRequired,
setLintWarning: PropTypes.func.isRequired,
setTheme: PropTypes.func.isRequired,
setFontSize: PropTypes.func.isRequired,
setLineNumbers: PropTypes.func.isRequired,
setAutosave: PropTypes.func.isRequired,
setTextOutput: PropTypes.func.isRequired,
setGridOutput: PropTypes.func.isRequired,
setSoundOutput: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
...state.preferences,
});
const mapDispatchToProps = dispatch => bindActionCreators({
...PreferencesActions,
...IdeActions
}, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobilePreferences));

View file

@ -0,0 +1,184 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import styled from 'styled-components';
import Header from '../../components/mobile/Header';
import IconButton from '../../components/mobile/IconButton';
import PreviewFrame from '../IDE/components/PreviewFrame';
import Screen from '../../components/mobile/MobileScreen';
import * as ProjectActions from '../IDE/actions/project';
import * as IDEActions from '../IDE/actions/ide';
import * as PreferencesActions from '../IDE/actions/preferences';
import * as ConsoleActions from '../IDE/actions/console';
import * as FilesActions from '../IDE/actions/files';
import { getHTMLFile } from '../IDE/reducers/files';
import { ExitIcon } from '../../common/icons';
import { remSize } from '../../theme';
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(68)};
`;
const MobileSketchView = (props) => {
// TODO: useSelector requires react-redux ^7.1.0
// const htmlFile = useSelector(state => getHTMLFile(state.files));
// const jsFiles = useSelector(state => getJSFiles(state.files));
// const cssFiles = useSelector(state => getCSSFiles(state.files));
// const files = useSelector(state => state.files);
const {
htmlFile, files, selectedFile, projectName
} = props;
// Actions
const {
setTextOutput, setGridOutput, setSoundOutput,
endSketchRefresh, stopSketch,
dispatchConsoleEvent, expandConsole, clearConsole,
setBlobUrl,
} = props;
const { preferences, ide } = props;
return (
<Screen fullscreen>
<Header
leftButton={
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
}
title={projectName}
/>
<Content>
<PreviewFrame
htmlFile={htmlFile}
files={files}
head={<link type="text/css" rel="stylesheet" href="/preview-styles.css" />}
content={selectedFile.content}
isPlaying
isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying}
previewIsRefreshing={ide.previewIsRefreshing}
textOutput={preferences.textOutput}
gridOutput={preferences.gridOutput}
soundOutput={preferences.soundOutput}
autorefresh={preferences.autorefresh}
setTextOutput={setTextOutput}
setGridOutput={setGridOutput}
setSoundOutput={setSoundOutput}
dispatchConsoleEvent={dispatchConsoleEvent}
endSketchRefresh={endSketchRefresh}
stopSketch={stopSketch}
setBlobUrl={setBlobUrl}
expandConsole={expandConsole}
clearConsole={clearConsole}
/>
</Content>
</Screen>);
};
MobileSketchView.propTypes = {
params: PropTypes.shape({
project_id: PropTypes.string,
username: PropTypes.string
}).isRequired,
htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired,
autosave: PropTypes.bool.isRequired,
linewrap: PropTypes.bool.isRequired,
lineNumbers: PropTypes.bool.isRequired,
lintWarning: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
gridOutput: PropTypes.bool.isRequired,
soundOutput: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
autorefresh: PropTypes.bool.isRequired
}).isRequired,
ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.array,
modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFolderModalVisible: PropTypes.bool.isRequired,
shareModalVisible: PropTypes.bool.isRequired,
shareModalProjectId: PropTypes.string.isRequired,
shareModalProjectName: PropTypes.string.isRequired,
shareModalProjectUsername: PropTypes.string.isRequired,
editorOptionsVisible: PropTypes.bool.isRequired,
keyboardShortcutVisible: PropTypes.bool.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
infiniteLoop: PropTypes.bool.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
infiniteLoopMessage: PropTypes.string.isRequired,
projectSavedTime: PropTypes.string,
previousPath: PropTypes.string.isRequired,
justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired
}).isRequired,
projectName: PropTypes.string.isRequired,
setTextOutput: PropTypes.func.isRequired,
setGridOutput: PropTypes.func.isRequired,
setSoundOutput: PropTypes.func.isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
endSketchRefresh: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
setBlobUrl: PropTypes.func.isRequired,
expandConsole: PropTypes.func.isRequired,
clearConsole: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
htmlFile: getHTMLFile(state.files),
projectName: state.project.name,
files: state.files,
ide: state.ide,
preferences: state.preferences,
selectedFile: state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'),
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(MobileSketchView);

View file

@ -1,13 +1,9 @@
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios';
import * as ActionTypes from '../../constants'; import * as ActionTypes from '../../constants';
import apiClient from '../../utils/apiClient';
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
import { showToast, setToastText } from '../IDE/actions/toast'; import { showToast, setToastText } from '../IDE/actions/toast';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
export function authError(error) { export function authError(error) {
return { return {
type: ActionTypes.AUTH_ERROR, type: ActionTypes.AUTH_ERROR,
@ -17,7 +13,7 @@ export function authError(error) {
export function signUpUser(previousPath, formValues) { export function signUpUser(previousPath, formValues) {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true }) apiClient.post('/signup', formValues)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.AUTH_USER, type: ActionTypes.AUTH_USER,
@ -26,12 +22,15 @@ export function signUpUser(previousPath, formValues) {
dispatch(justOpenedProject()); dispatch(justOpenedProject());
browserHistory.push(previousPath); browserHistory.push(previousPath);
}) })
.catch(response => dispatch(authError(response.data.error))); .catch((error) => {
const { response } = error;
dispatch(authError(response.data.error));
});
}; };
} }
export function loginUser(formValues) { export function loginUser(formValues) {
return axios.post(`${ROOT_URL}/login`, formValues, { withCredentials: true }); return apiClient.post('/login', formValues);
} }
export function loginUserSuccess(user) { export function loginUserSuccess(user) {
@ -71,7 +70,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
export function getUser() { export function getUser() {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) apiClient.get('/session')
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.AUTH_USER, type: ActionTypes.AUTH_USER,
@ -82,7 +81,8 @@ export function getUser() {
preferences: response.data.preferences preferences: response.data.preferences
}); });
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
const message = response.message || response.data.error; const message = response.message || response.data.error;
dispatch(authError(message)); dispatch(authError(message));
}); });
@ -91,14 +91,15 @@ export function getUser() {
export function validateSession() { export function validateSession() {
return (dispatch, getState) => { return (dispatch, getState) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) apiClient.get('/session')
.then((response) => { .then((response) => {
const state = getState(); const state = getState();
if (state.user.username !== response.data.username) { if (state.user.username !== response.data.username) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} }
}) })
.catch((response) => { .catch((error) => {
const { response } = error;
if (response.status === 404) { if (response.status === 404) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} }
@ -108,13 +109,16 @@ export function validateSession() {
export function logoutUser() { export function logoutUser() {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/logout`, { withCredentials: true }) apiClient.get('/logout')
.then(() => { .then(() => {
dispatch({ dispatch({
type: ActionTypes.UNAUTH_USER type: ActionTypes.UNAUTH_USER
}); });
}) })
.catch(response => dispatch(authError(response.data.error))); .catch((error) => {
const { response } = error;
dispatch(authError(response.data.error));
});
}; };
} }
@ -123,14 +127,17 @@ export function initiateResetPassword(formValues) {
dispatch({ dispatch({
type: ActionTypes.RESET_PASSWORD_INITIATE type: ActionTypes.RESET_PASSWORD_INITIATE
}); });
axios.post(`${ROOT_URL}/reset-password`, formValues, { withCredentials: true }) apiClient.post('/reset-password', formValues)
.then(() => { .then(() => {
// do nothing // do nothing
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
message: response.data dispatch({
})); type: ActionTypes.ERROR,
message: response.data
});
});
}; };
} }
@ -139,14 +146,17 @@ export function initiateVerification() {
dispatch({ dispatch({
type: ActionTypes.EMAIL_VERIFICATION_INITIATE type: ActionTypes.EMAIL_VERIFICATION_INITIATE
}); });
axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true }) apiClient.post('/verify/send', {})
.then(() => { .then(() => {
// do nothing // do nothing
}) })
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.ERROR, const { response } = error;
message: response.data dispatch({
})); type: ActionTypes.ERROR,
message: response.data
});
});
}; };
} }
@ -156,15 +166,18 @@ export function verifyEmailConfirmation(token) {
type: ActionTypes.EMAIL_VERIFICATION_VERIFY, type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
state: 'checking', state: 'checking',
}); });
return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true }) return apiClient.get(`/verify?t=${token}`, {})
.then(response => dispatch({ .then(response => dispatch({
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED, type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
message: response.data, message: response.data,
})) }))
.catch(response => dispatch({ .catch((error) => {
type: ActionTypes.EMAIL_VERIFICATION_INVALID, const { response } = error;
message: response.data dispatch({
})); type: ActionTypes.EMAIL_VERIFICATION_INVALID,
message: response.data
});
});
}; };
} }
@ -177,7 +190,7 @@ export function resetPasswordReset() {
export function validateResetPasswordToken(token) { export function validateResetPasswordToken(token) {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/reset-password/${token}`) apiClient.get(`/reset-password/${token}`)
.then(() => { .then(() => {
// do nothing if the token is valid // do nothing if the token is valid
}) })
@ -189,7 +202,7 @@ export function validateResetPasswordToken(token) {
export function updatePassword(token, formValues) { export function updatePassword(token, formValues) {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/reset-password/${token}`, formValues) apiClient.post(`/reset-password/${token}`, formValues)
.then((response) => { .then((response) => {
dispatch(loginUserSuccess(response.data)); dispatch(loginUserSuccess(response.data));
browserHistory.push('/'); browserHistory.push('/');
@ -209,14 +222,17 @@ export function updateSettingsSuccess(user) {
export function updateSettings(formValues) { export function updateSettings(formValues) {
return dispatch => return dispatch =>
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true }) apiClient.put('/account', formValues)
.then((response) => { .then((response) => {
dispatch(updateSettingsSuccess(response.data)); dispatch(updateSettingsSuccess(response.data));
browserHistory.push('/'); browserHistory.push('/');
dispatch(showToast(5500)); dispatch(showToast(5500));
dispatch(setToastText('Settings saved.')); dispatch(setToastText('Settings saved.'));
}) })
.catch(response => Promise.reject(new Error(response.data.error))); .catch((error) => {
const { response } = error;
Promise.reject(new Error(response.data.error));
});
} }
export function createApiKeySuccess(user) { export function createApiKeySuccess(user) {
@ -228,21 +244,27 @@ export function createApiKeySuccess(user) {
export function createApiKey(label) { export function createApiKey(label) {
return dispatch => return dispatch =>
axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true }) apiClient.post('/account/api-keys', { label })
.then((response) => { .then((response) => {
dispatch(createApiKeySuccess(response.data)); dispatch(createApiKeySuccess(response.data));
}) })
.catch(response => Promise.reject(new Error(response.data.error))); .catch((error) => {
const { response } = error;
Promise.reject(new Error(response.data.error));
});
} }
export function removeApiKey(keyId) { export function removeApiKey(keyId) {
return dispatch => return dispatch =>
axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true }) apiClient.delete(`/account/api-keys/${keyId}`)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.API_KEY_REMOVED, type: ActionTypes.API_KEY_REMOVED,
user: response.data user: response.data
}); });
}) })
.catch(response => Promise.reject(new Error(response.data.error))); .catch((error) => {
const { response } = error;
Promise.reject(new Error(response.data.error));
});
} }

View file

@ -1,13 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import Button from '../../../common/Button';
import { PlusIcon } from '../../../common/icons';
import CopyableInput from '../../IDE/components/CopyableInput'; import CopyableInput from '../../IDE/components/CopyableInput';
import APIKeyList from './APIKeyList'; import APIKeyList from './APIKeyList';
const plusIcon = require('../../../images/plus-icon.svg');
export const APIKeyPropType = PropTypes.shape({ export const APIKeyPropType = PropTypes.shape({
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired,
token: PropTypes.object, token: PropTypes.object,
@ -81,14 +80,14 @@ class APIKeyForm extends React.Component {
type="text" type="text"
value={this.state.keyLabel} value={this.state.keyLabel}
/> />
<button <Button
className="api-key-form__create-button"
disabled={this.state.keyLabel === ''} disabled={this.state.keyLabel === ''}
iconBefore={<PlusIcon />}
label="Create new key"
type="submit" type="submit"
> >
<InlineSVG src={plusIcon} className="api-key-form__create-icon" />
Create Create
</button> </Button>
</form> </form>
{ {

View file

@ -1,13 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import format from 'date-fns/format'; import format from 'date-fns/format';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { APIKeyPropType } from './APIKeyForm'; import { APIKeyPropType } from './APIKeyForm';
const trashCan = require('../../../images/trash-can.svg'); import TrashCanIcon from '../../../images/trash-can.svg';
function APIKeyList({ apiKeys, onRemove }) { function APIKeyList({ apiKeys, onRemove }) {
return ( return (
@ -32,8 +31,12 @@ function APIKeyList({ apiKeys, onRemove }) {
<td>{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}</td> <td>{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}</td>
<td>{lastUsed}</td> <td>{lastUsed}</td>
<td className="api-key-list__action"> <td className="api-key-list__action">
<button className="api-key-list__delete-button" onClick={() => onRemove(key)}> <button
<InlineSVG src={trashCan} alt="Delete Key" /> className="api-key-list__delete-button"
onClick={() => onRemove(key)}
aria-label="Delete API Key"
>
<TrashCanIcon focusable="false" aria-hidden="true" />
</button> </button>
</td> </td>
</tr> </tr>

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function AccountForm(props) { function AccountForm(props) {
const { const {
@ -44,11 +45,10 @@ function AccountForm(props) {
<span className="form__status"> Confirmation sent, check your email.</span> <span className="form__status"> Confirmation sent, check your email.</span>
) : ) :
( (
<button <Button
className="form__action"
onClick={handleInitiateVerification} onClick={handleInitiateVerification}
>Resend confirmation email >Resend confirmation email
</button> </Button>
) )
} }
</p> </p>
@ -92,12 +92,11 @@ function AccountForm(props) {
/> />
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>} {newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
</p> </p>
<input <Button
type="submit" type="submit"
disabled={submitting || invalid || pristine} disabled={submitting || invalid || pristine}
value="Save All Settings" >Save All Settings
aria-label="updateSettings" </Button>
/>
</form> </form>
); );
} }

Some files were not shown because too many files have changed in this diff Show more