Merge branch 'develop' into p5js-to-1.0.0

This commit is contained in:
Cassie Tarakajian 2020-07-30 13:42:33 -04:00
commit 6eb51665d9
199 changed files with 21403 additions and 4206 deletions

View File

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

View File

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

View File

@ -77,5 +77,13 @@
"__SERVER__": true,
"__DISABLE_SSR__": 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)
- [Beginning Work](#beginning-work)
- [Contribution Guides](#contribution-guides)
- [Writing Commit Messages](#writing-commit-messages)
- [Tips](#tips)
## Code of Conduct
@ -62,45 +60,9 @@ If you feel like an issue is tagged incorrectly (e.g. it's low priority and you
If you'd like to work on an issue, please comment on it to let the maintainers know, so that they can assign it to you. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
Then, follow the [installation guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md) to get the project building and working on your computer.
Then, look at the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md) for instructions on how to install the project locally and follow the right development workflow.
### Contribution Guides
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/)
* [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/)
## Writing Commit Messages
Good commit messages serve at least three important purposes:
* They speed up the reviewing process.
* They help us write good release notes.
* They help future maintainers understand your change and the reasons behind it.
Structure your commit message like this:
```
Short (50 chars or less) summary of changes ( involving Fixes #Issue-number keyword )
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded by a
single space, with blank lines in between, but conventions vary here
```
* Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed".
* Always leave the second line blank.
* Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened.
## Tips
* If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`.

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:
* [ ] has no linting errors (`npm run lint`)

3
.gitignore vendored
View File

@ -17,3 +17,6 @@ cert_chain.crt
localhost.crt
localhost.key
privkey.pem
storybook-static
duplicates.json

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
skip_cleanup: true
on:
branch: master
branch: release
tags: true
- provider: script
script: ./deploy_staging.sh
skip_cleanup: true
on:
branch: feature/public-api
branch: develop
env:
global:

View File

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

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

80
client/common/icons.jsx Normal file
View File

@ -0,0 +1,80 @@
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';
import Code from '../images/code.svg';
import Terminal from '../images/terminal.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);
export const TerminalIcon = withLabel(Terminal);

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

View File

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

View File

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

View File

@ -1,24 +1,23 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
const logoUrl = require('../images/p5js-logo-small.svg');
const editorUrl = require('../images/code.svg');
import LogoIcon from '../images/p5js-logo-small.svg';
import CodeIcon from '../images/code.svg';
const PreviewNav = ({ owner, project }) => (
<nav className="nav preview-nav">
<div className="nav__items-left">
<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>
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
<p className="toolbar__project-owner">by</p>
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
</div>
<div className="nav__items-right">
<Link to={`/${owner.username}/sketches/${project.id}`}>
<InlineSVG className="preview-nav__editor-svg" src={editorUrl} />
<Link to={`/${owner.username}/sketches/${project.id}`} aria-label="Edit Sketch" >
<CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
</Link>
</div>
</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 { shallow } from 'enzyme';
import renderer from 'react-test-renderer';
import { render } from '@testing-library/react';
import { NavComponent } from './../Nav';
import { NavComponent } from '../Nav';
describe('Nav', () => {
const props = {
@ -44,19 +44,12 @@ describe('Nav', () => {
setToastText: jest.fn(),
rootFile: {
id: 'root-file'
}
},
t: jest.fn()
};
const getWrapper = () => shallow(<NavComponent {...props} />);
test('it renders main navigation', () => {
const nav = getWrapper();
expect(nav.exists('.nav')).toEqual(true);
});
it('renders correctly', () => {
const tree = renderer
.create(<NavComponent {...props} />)
.toJSON();
expect(tree).toMatchSnapshot();
const { asFragment } = render(<NavComponent {...props} />);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -1,333 +1,219 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nav renders correctly 1`] = `
<nav
className="nav"
title="main-navigation"
>
<ul
className="nav__items-left"
>
<li
className="nav__item-logo"
<DocumentFragment>
<header>
<nav
class="nav"
title="main-navigation"
>
<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
className="nav__dropdown"
class="nav__items-left"
>
<li
className="nav__dropdown-item"
class="nav__item-logo"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
New
</button>
<test-file-stub
aria-label="p5.js Logo"
classname="svg__logo"
focusable="false"
role="img"
/>
</li>
<li
className="nav__dropdown-item"
class="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Duplicate
<button>
<span
class="nav__item-header"
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</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
className="nav__dropdown-item"
class="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Share
<button>
<span
class="nav__item-header"
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</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
className="nav__dropdown-item"
class="nav__item"
>
<button
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
Download
<button>
<span
class="nav__item-header"
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</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
className="nav__dropdown-item"
class="nav__item"
>
<a
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
style={Object {}}
<button>
<span
class="nav__item-header"
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
focusable="false"
/>
</button>
<ul
class="nav__dropdown"
>
Open
</a>
<li
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>
</ul>
</li>
<li
className="nav__item"
>
<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>
</nav>
</header>
</DocumentFragment>
`;

View File

@ -0,0 +1,35 @@
import React from 'react';
import styled from 'styled-components';
import { bindActionCreators } from 'redux';
import { useDispatch, useSelector } from 'react-redux';
import { remSize } from '../../theme';
import IconButton from './IconButton';
import { TerminalIcon } from '../../common/icons';
import * as IDEActions from '../../modules/IDE/actions/ide';
const BottomBarContent = styled.h2`
padding: ${remSize(8)};
svg {
max-height: ${remSize(32)};
}
`;
export default () => {
const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch());
const { consoleIsExpanded } = useSelector(state => state.ide);
const actions = [{ icon: TerminalIcon, aria: 'Say Something', action: consoleIsExpanded ? collapseConsole : expandConsole }];
return (
<BottomBarContent>
{actions.map(({ icon, aria, action }) =>
(<IconButton
icon={icon}
aria-label={aria}
key={`bottom-bar-${aria}`}
onClick={() => action()}
/>))}
</BottomBarContent>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import styled from 'styled-components';
import { prop, grays } from '../../theme';
const background = prop('MobilePanel.default.background');
const textColor = prop('primaryTextColor');
export default styled.div`
position: fixed;
width: 100%;
bottom: 0;
background: ${background};
color: ${textColor};
& > * + * { border-top: dashed 1px ${prop('Separator')} }
`;

View File

@ -0,0 +1,79 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { prop, remSize } from '../../theme';
const background = transparent => prop(transparent ? 'backgroundColor' : 'MobilePanel.default.background');
const textColor = prop('primaryTextColor');
const HeaderDiv = styled.div`
position: fixed;
width: 100%;
background: ${props => background(props.transparent === true)};
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;
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
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="#007BBB" />
fill="#0071AD" />
</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
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="#a3a3a3" />
fill="#D9D9D9" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -50,5 +50,5 @@
<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="#7D7D7D" />
fill="#4D4D4D" />
</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
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
id="path2"
fill="#FAAF00" />
fill="#996B00" />
</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 -->
<!-- <desc>Created with Sketch.</desc> -->
<defs></defs>
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#333333">
<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="#AAA">
<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>
</g>

Before

Width:  |  Height:  |  Size: 979 B

After

Width:  |  Height:  |  Size: 968 B

View File

@ -5,7 +5,7 @@
<!-- <desc>Created with Sketch.</desc> -->
<defs></defs>
<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>
</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"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<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"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#FBBB00;" d="M113.47,309.408L95.648,375.94l-65.139,1.378C11.042,341.211,0,299.9,0,256
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
C103.821,274.792,107.225,292.797,113.47,309.408z"/>
<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
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 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">
<g>
<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 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>
<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>
<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>
</g>
</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"?>
<!-- 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">
<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"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/>
</g>
<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">
<g transform="translate(0.666667, 0.333333)">
<polygon points="4.56711429 7.96345714 0.103114286 0.1872 8.99022857 0.1872"></polygon>
</g>
</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"?>
<!-- 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">
<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"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/>
</g>
<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">
<g transform="translate(-0.200000, -0.200000)" >
<polygon points="4.93361111 0.202222222 9.75583333 8.6025 0.155833333 8.6025"></polygon>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 543 B

View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="8" width="22" height="16" rx="2" fill="#333333"/>
<path d="M24 21H14V20H24V21Z" fill="#F0F0F0"/>
<path d="M10.4081 16.0231L8.3676 18.0637C8.27757 18.1537 8.15754 18.1537 8.06752 18.0637C7.97749 17.9736 7.97749 17.8536 8.06752 17.7636L9.95802 15.8731L8.06752 13.9826C7.97749 13.8926 7.97749 13.7725 8.06752 13.6675C8.15754 13.5775 8.27757 13.5775 8.3676 13.6675L10.4081 15.723C10.4532 15.753 10.4832 15.8131 10.4832 15.8731C10.4832 15.9181 10.4532 15.9781 10.4081 16.0231Z" fill="#F0F0F0"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

View File

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc>
<defs></defs>
<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)">
<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>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 695 B

View File

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc>
<defs></defs>
<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)">
<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>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 698 B

View File

@ -3,7 +3,7 @@
<desc>Created with Sketch.</desc>
<defs></defs>
<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)">
<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>

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 -->
<!-- <desc>Created with Sketch.</desc> -->
<defs></defs>
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#333333">
<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="#AAA">
<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>
</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 { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import configureStore from './store';
import routes from './routes';
import ThemeProvider from './modules/App/components/ThemeProvider';
import Loader from './modules/App/components/loader';
import i18n from './i18n';
require('./styles/main.scss');
@ -18,13 +22,17 @@ const store = configureStore(initialState);
const App = () => (
<Provider store={store}>
<Router history={history} routes={routes(store)} />
<ThemeProvider>
<Router history={history} routes={routes(store)} />
</ThemeProvider>
</Provider>
);
const HotApp = hot(App);
render(
<HotApp />,
<Suspense fallback={(<Loader />)}>
<HotApp />
</Suspense>,
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 React from 'react';
import { connect } from 'react-redux';
import getConfig from '../../utils/getConfig';
import DevTools from './components/DevTools';
import { setPreviousPath } from '../IDE/actions/ide';
const __process = (typeof global !== 'undefined' ? global : window).process;
class App extends React.Component {
constructor(props, context) {
super(props, context);
@ -35,7 +34,7 @@ class App extends React.Component {
render() {
return (
<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}
</div>
);

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
import ExitIcon from '../../../images/exit.svg';
class Overlay extends React.Component {
constructor(props) {
@ -81,8 +80,8 @@ class Overlay extends React.Component {
<h2 className="overlay__title">{title}</h2>
<div className="overlay__actions">
{actions}
<button className="overlay__close-button" onClick={this.close} >
<InlineSVG src={exitUrl} alt="close overlay" />
<button className="overlay__close-button" onClick={this.close} aria-label={`Close ${title} overlay`} >
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
</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 { startLoader, stopLoader } from './loader';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function setAssets(assets, totalSize) {
return {
type: ActionTypes.SET_ASSETS,
@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
export function getAssets() {
return (dispatch) => {
dispatch(startLoader());
axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true })
apiClient.get('/S3/objects')
.then((response) => {
dispatch(setAssets(response.data.assets, response.data.totalSize));
dispatch(stopLoader());
@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
export function deleteAssetRequest(assetKey) {
return (dispatch) => {
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
apiClient.delete(`/S3/${assetKey}`)
.then((response) => {
dispatch(deleteAsset(assetKey));
})

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,144 +1,144 @@
import PropTypes from 'prop-types';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import React, { useRef } from 'react';
import { bindActionCreators } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import classNames from 'classnames';
import { Console as ConsoleFeed } from 'console-feed';
import {
CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES,
CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES
} from '../../../styles/components/_console-feed.scss';
import warnLightUrl from '../../../images/console-warn-light.svg';
import warnDarkUrl from '../../../images/console-warn-dark.svg';
import errorLightUrl from '../../../images/console-error-light.svg';
import errorDarkUrl from '../../../images/console-error-dark.svg';
import debugLightUrl from '../../../images/console-debug-light.svg';
import debugDarkUrl from '../../../images/console-debug-dark.svg';
import infoLightUrl from '../../../images/console-info-light.svg';
import infoDarkUrl from '../../../images/console-info-dark.svg';
import warnLightUrl from '../../../images/console-warn-light.svg?byUrl';
import warnDarkUrl from '../../../images/console-warn-dark.svg?byUrl';
import warnContrastUrl from '../../../images/console-warn-contrast.svg?byUrl';
import errorLightUrl from '../../../images/console-error-light.svg?byUrl';
import errorDarkUrl from '../../../images/console-error-dark.svg?byUrl';
import errorContrastUrl from '../../../images/console-error-contrast.svg?byUrl';
import debugLightUrl from '../../../images/console-debug-light.svg?byUrl';
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');
const downArrowUrl = require('../../../images/down-arrow.svg');
import UpArrowIcon from '../../../images/up-arrow.svg';
import DownArrowIcon from '../../../images/down-arrow.svg';
class Console extends React.Component {
componentDidUpdate(prevProps) {
this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight;
if (this.props.theme !== prevProps.theme) {
this.props.clearConsole();
this.props.dispatchConsoleEvent(this.props.consoleEvents);
}
import * as IDEActions from '../../IDE/actions/ide';
import * as ConsoleActions from '../../IDE/actions/console';
import { useDidUpdate } from '../../../utils/custom-hooks';
if (this.props.fontSize !== prevProps.fontSize) {
this.props.clearConsole();
this.props.dispatchConsoleEvent(this.props.consoleEvents);
}
const getConsoleFeedStyle = (theme, times, fontSize) => {
const style = {};
const CONSOLE_FEED_LIGHT_ICONS = {
LOG_WARN_ICON: `url(${warnLightUrl})`,
LOG_ERROR_ICON: `url(${errorLightUrl})`,
LOG_DEBUG_ICON: `url(${debugLightUrl})`,
LOG_INFO_ICON: `url(${infoLightUrl})`
};
const CONSOLE_FEED_DARK_ICONS = {
LOG_WARN_ICON: `url(${warnDarkUrl})`,
LOG_ERROR_ICON: `url(${errorDarkUrl})`,
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
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 = {
TREENODE_LINE_HEIGHT: 1.2,
BASE_FONT_SIZE: fontSize,
ARROW_FONT_SIZE: fontSize,
LOG_ICON_WIDTH: fontSize,
LOG_ICON_HEIGHT: 1.45 * fontSize,
};
if (times > 1) {
Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS);
}
getConsoleFeedStyle(theme, times) {
const style = {};
const CONSOLE_FEED_LIGHT_ICONS = {
LOG_WARN_ICON: `url(${warnLightUrl})`,
LOG_ERROR_ICON: `url(${errorLightUrl})`,
LOG_DEBUG_ICON: `url(${debugLightUrl})`,
LOG_INFO_ICON: `url(${infoLightUrl})`
};
const CONSOLE_FEED_DARK_ICONS = {
LOG_WARN_ICON: `url(${warnDarkUrl})`,
LOG_ERROR_ICON: `url(${errorDarkUrl})`,
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
LOG_INFO_ICON: `url(${infoDarkUrl})`
};
const CONSOLE_FEED_SIZES = {
TREENODE_LINE_HEIGHT: 1.2,
BASE_FONT_SIZE: this.props.fontSize,
ARROW_FONT_SIZE: this.props.fontSize,
LOG_ICON_WIDTH: this.props.fontSize,
LOG_ICON_HEIGHT: 1.45 * this.props.fontSize,
};
if (times > 1) {
Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS);
}
switch (theme) {
case 'light':
return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style);
case 'dark':
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
case 'contrast':
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
default:
return '';
}
switch (theme) {
case 'light':
return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style);
case 'dark':
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
case 'contrast':
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style);
default:
return '';
}
};
render() {
const consoleClass = classNames({
'preview-console': true,
'preview-console--collapsed': !this.props.isExpanded
});
const Console = () => {
const consoleEvents = useSelector(state => state.console);
const isExpanded = useSelector(state => state.ide.consoleIsExpanded);
const { theme, fontSize } = useSelector(state => state.preferences);
return (
<div className={consoleClass} role="main">
<div className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2>
<div className="preview-console__header-buttons">
<button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
Clear
</button>
<button
className="preview-console__collapse"
onClick={this.props.collapseConsole}
aria-label="collapse console"
>
<InlineSVG src={downArrowUrl} />
</button>
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console">
<InlineSVG src={upArrowUrl} />
</button>
</div>
const {
collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent
} = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch());
useDidUpdate(() => {
clearConsole();
dispatchConsoleEvent(consoleEvents);
}, [theme, fontSize]);
const cm = useRef({});
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
const consoleClass = classNames({
'preview-console': true,
'preview-console--collapsed': !isExpanded
});
return (
<section className={consoleClass} >
<header className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2>
<div className="preview-console__header-buttons">
<button className="preview-console__clear" onClick={clearConsole} aria-label="Clear console">
Clear
</button>
<button
className="preview-console__collapse"
onClick={collapseConsole}
aria-label="Close console"
>
<DownArrowIcon focusable="false" aria-hidden="true" />
</button>
<button className="preview-console__expand" onClick={expandConsole} aria-label="Open console" >
<UpArrowIcon focusable="false" aria-hidden="true" />
</button>
</div>
<div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent) => {
const { method, times } = consoleEvent;
const { theme } = this.props;
return (
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
{ times > 1 &&
<div
className="preview-console__logged-times"
style={{ fontSize: this.props.fontSize, borderRadius: this.props.fontSize / 2 }}
>
{times}
</div>
}
<ConsoleFeed
styles={this.getConsoleFeedStyle(theme, times)}
logs={[consoleEvent]}
/>
</header>
<div ref={cm} className="preview-console__messages">
{consoleEvents.map((consoleEvent) => {
const { method, times } = consoleEvent;
return (
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
{ times > 1 &&
<div
className="preview-console__logged-times"
style={{ fontSize, borderRadius: fontSize / 2 }}
>
{times}
</div>
);
})}
</div>
}
<ConsoleFeed
styles={getConsoleFeedStyle(theme, times, fontSize)}
logs={[consoleEvent]}
/>
</div>
);
})}
</div>
);
}
}
Console.propTypes = {
consoleEvents: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})),
isExpanded: PropTypes.bool.isRequired,
collapseConsole: PropTypes.func.isRequired,
expandConsole: PropTypes.func.isRequired,
clearConsole: PropTypes.func.isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
theme: PropTypes.string.isRequired,
fontSize: PropTypes.number.isRequired
</section>
);
};
Console.defaultProps = {
consoleEvents: []
};
export default Console;

View File

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

View File

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

View File

@ -24,7 +24,6 @@ import 'codemirror/addon/edit/matchbrackets';
import { JSHINT } from 'jshint';
import { CSSLint } from 'csslint';
import { HTMLHint } from 'htmlhint';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import { debounce } from 'lodash';
import '../../../utils/htmlmixed';
@ -36,6 +35,11 @@ import { metaKey, } from '../../../utils/metaKey';
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);
const beautifyCSS = beautifyJS.css;
@ -45,11 +49,6 @@ window.JSHINT = JSHINT;
window.CSSLint = CSSLint;
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 INDENTATION_AMOUNT = 2;
@ -315,29 +314,30 @@ class Editor extends React.Component {
});
return (
<section
role="main"
className={editorSectionClass}
>
<section className={editorSectionClass} >
<header className="editor__header">
<button
aria-label="collapse file navigation"
aria-label="Open Sketch files navigation"
className="sidebar__contract"
onClick={this.props.collapseSidebar}
>
<InlineSVG src={leftArrowUrl} />
<LeftArrowIcon focusable="false" aria-hidden="true" />
</button>
<button
aria-label="expand file navigation"
aria-label="Close sketch files navigation"
className="sidebar__expand"
onClick={this.props.expandSidebar}
>
<InlineSVG src={rightArrowUrl} />
<RightArrowIcon focusable="false" aria-hidden="true" />
</button>
<div className="editor__file-name">
<span>
{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>
<Timer
projectSavedTime={this.props.projectSavedTime}
@ -345,8 +345,8 @@ class Editor extends React.Component {
/>
</div>
</header>
<div ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
</div>
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
</article>
<EditorAccessibility
lintMessages={this.props.lintMessages}
/>

View File

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

View File

@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
import downArrowUrl from '../../../images/down-filled-triangle.svg';
import folderRightUrl from '../../../images/triangle-arrow-right.svg';
import folderDownUrl from '../../../images/triangle-arrow-down.svg';
import fileUrl from '../../../images/file.svg';
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
import FileIcon from '../../../images/file.svg';
export class FileNode extends React.Component {
constructor(props) {
@ -185,7 +184,7 @@ export class FileNode extends React.Component {
<span className="file-item__spacer"></span>
{ isFile &&
<span className="sidebar__file-item-icon">
<InlineSVG src={fileUrl} />
<FileIcon focusable="false" aria-hidden="true" />
</span>
}
{ isFolder &&
@ -193,24 +192,28 @@ export class FileNode extends React.Component {
<button
className="sidebar__file-item-closed"
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
className="sidebar__file-item-open"
onClick={this.hideFolderChildren}
aria-label="Close file contents"
>
<InlineSVG className="folder-down" src={folderDownUrl} />
<FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" />
</button>
</div>
}
<button
aria-label="Name"
className="sidebar__file-item-name"
onClick={this.handleFileClick}
>
{this.state.updatedName}
</button>
<input
data-testid="input"
type="text"
className="sidebar__file-item-input"
value={this.state.updatedName}
@ -222,14 +225,14 @@ export class FileNode extends React.Component {
/>
<button
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; }}
tabIndex="0"
onClick={this.toggleFileOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
<InlineSVG src={downArrowUrl} />
<DownArrowIcon focusable="false" aria-hidden="true" />
</button>
<div className="sidebar__file-item-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 { connect } from 'react-redux';
import * as UploaderActions from '../actions/uploader';
import getConfig from '../../../utils/getConfig';
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
const __process = (typeof global !== 'undefined' ? global : window).process;
const s3Bucket = __process.env.S3_BUCKET_URL_BASE ||
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
const s3Bucket = getConfig('S3_BUCKET_URL_BASE') ||
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
class FileUploader extends React.Component {
componentDidMount() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
export const optionsOnOff = (name, onLabel = 'On', offLabel = 'Off') => [
{
value: true, label: onLabel, ariaLabel: `${name} on`, name: `${name}`, id: `${name}-on`.replace(' ', '-')
},
{
value: false, label: offLabel, ariaLabel: `${name} off`, name: `${name}`, id: `${name}-off`.replace(' ', '-')
},
];
export const optionsPickOne = (name, ...options) => options.map(option => ({
value: option,
label: option,
ariaLabel: `${option} ${name} on`,
name: `${option} ${name}`,
id: `${option}-${name}-on`.replace(' ', '-')
}));
const nameToValueName = x => (x && x.toLowerCase().replace(/#|_|-/g, ' '));
// preferenceOnOff: name, value and onSelect are mandatory. propname is optional
export const preferenceOnOff = (name, value, onSelect, propname) => ({
title: name,
value,
options: optionsOnOff(propname || nameToValueName(name)),
onSelect
});

View File

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

View File

@ -22,6 +22,23 @@ import {
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
from '../../../utils/consoleUtils';
const shouldRenderSketch = (props, prevProps = undefined) => {
const { isPlaying, previewIsRefreshing, fullView } = props;
// if the user explicitly clicks on the play button
if (isPlaying && previewIsRefreshing) return true;
if (!prevProps) return false;
return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender
|| props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences
|| props.textOutput !== prevProps.textOutput
|| props.gridOutput !== prevProps.gridOutput
|| props.soundOutput !== prevProps.soundOutput
|| (fullView && props.files[0].id !== prevProps.files[0].id));
};
class PreviewFrame extends React.Component {
constructor(props) {
super(props);
@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
componentDidMount() {
window.addEventListener('message', this.handleConsoleEvent);
const props = {
...this.props,
previewIsRefreshing: this.props.previewIsRefreshing,
isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
};
if (shouldRenderSketch(props)) this.renderSketch();
}
componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender
if (this.props.isPlaying !== prevProps.isPlaying) {
this.renderSketch();
return;
}
// if the user explicitly clicks on the play button
if (this.props.isPlaying && this.props.previewIsRefreshing) {
this.renderSketch();
return;
}
// if user switches textoutput preferences
if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
this.renderSketch();
return;
}
if (this.props.textOutput !== prevProps.textOutput) {
this.renderSketch();
return;
}
if (this.props.gridOutput !== prevProps.gridOutput) {
this.renderSketch();
return;
}
if (this.props.soundOutput !== prevProps.soundOutput) {
this.renderSketch();
return;
}
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
this.renderSketch();
}
if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
// small bug - if autorefresh is on, and the usr changes files
// in the sketch, preview will reload
}
componentWillUnmount() {
window.removeEventListener('message', this.handleConsoleEvent);
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
const iframeBody = this.iframeElement.contentDocument.body;
if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
}
handleConsoleEvent(messageEvent) {
@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const quoteCharacter = jsFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
newContent = newContent.replace(filePath, resolvedFile.url);
newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
// could also pull file from API instead of using bloburl
const blobURL = getBlobUrl(resolvedFile);
this.props.setBlobUrl(resolvedFile, blobURL);
const filePathRegex = new RegExp(filePath, 'gi');
newContent = newContent.replace(filePathRegex, blobURL);
newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter);
}
}
}
@ -274,10 +265,11 @@ class PreviewFrame extends React.Component {
cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2);
const quoteCharacter = cssFileString.substr(0, 1);
const resolvedFile = resolvePathToFile(filePath, files);
if (resolvedFile) {
if (resolvedFile.url) {
newContent = newContent.replace(filePath, resolvedFile.url);
newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
}
}
}
@ -353,6 +345,8 @@ class PreviewFrame extends React.Component {
'preview-frame': true,
'preview-frame--full-view': this.props.fullView
});
const sandboxAttributes =
'allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals allow-downloads';
return (
<iframe
id="canvas_frame"
@ -362,7 +356,7 @@ class PreviewFrame extends React.Component {
frameBorder="0"
title="sketch preview"
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,
cmController: PropTypes.shape({
getContent: PropTypes.func
})
}),
};
PreviewFrame.defaultProps = {

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { throttle } from 'lodash';
const searchIcon = require('../../../../images/magnifyingglass.svg');
import SearchIcon from '../../../../images/magnifyingglass.svg';
class Searchbar extends React.Component {
constructor(props) {
@ -24,20 +23,13 @@ class Searchbar extends React.Component {
});
}
handleSearchEnter = (e) => {
if (e.key === 'Enter') {
this.searchChange();
}
}
searchChange = () => {
if (this.state.searchValue.trim().length === 0) return;
this.props.setSearchTerm(this.state.searchValue.trim());
};
handleSearchChange = (e) => {
this.setState({ searchValue: e.target.value }, () => {
this.throttledSearchChange(this.state.searchValue);
this.throttledSearchChange(this.state.searchValue.trim());
});
}
@ -46,7 +38,7 @@ class Searchbar extends React.Component {
return (
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
<div className="searchbar__button">
<InlineSVG className="searchbar__icon" src={searchIcon} />
<SearchIcon className="searchbar__icon" focusable="false" aria-hidden="true" />
</div>
<input
className="searchbar__input"
@ -54,7 +46,6 @@ class Searchbar extends React.Component {
value={searchValue}
placeholder={this.props.searchLabel}
onChange={this.handleSearchChange}
onKeyUp={this.handleSearchEnter}
/>
<button
className="searchbar__clear-button"

View File

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

View File

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

View File

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

View File

@ -3,37 +3,50 @@ import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
import * as IDEActions from '../actions/ide';
import * as preferenceActions from '../actions/preferences';
import * as projectActions from '../actions/project';
const playUrl = require('../../../images/play.svg');
const stopUrl = require('../../../images/stop.svg');
const preferencesUrl = require('../../../images/preferences.svg');
const editProjectNameUrl = require('../../../images/pencil.svg');
import PlayIcon from '../../../images/play.svg';
import StopIcon from '../../../images/stop.svg';
import PreferencesIcon from '../../../images/preferences.svg';
import EditProjectNameIcon from '../../../images/pencil.svg';
class Toolbar extends React.Component {
constructor(props) {
super(props);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
this.state = {
projectNameInputValue: props.project.name,
};
}
handleKeyPress(event) {
if (event.key === 'Enter') {
this.props.hideEditProjectName();
this.projectNameInput.blur();
}
}
handleProjectNameChange(event) {
this.props.setProjectName(event.target.value);
this.setState({ projectNameInputValue: event.target.value });
}
validateProjectName() {
if ((this.props.project.name.trim()).length === 0) {
this.props.setProjectName(this.originalProjectName);
handleProjectNameSave() {
const newProjectName = this.state.projectNameInputValue.trim();
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
});
const canEditProjectName = this.canEditProjectName();
return (
<div className="toolbar">
<button
@ -70,25 +85,25 @@ class Toolbar extends React.Component {
this.props.setTextOutput(true);
this.props.setGridOutput(true);
}}
aria-label="play sketch"
aria-label="Play sketch"
disabled={this.props.infiniteLoop}
>
<InlineSVG src={playUrl} alt="Play Sketch" />
<PlayIcon focusable="false" aria-hidden="true" />
</button>
<button
className={playButtonClass}
onClick={this.props.startSketch}
aria-label="play only visual sketch"
aria-label="Play only visual sketch"
disabled={this.props.infiniteLoop}
>
<InlineSVG src={playUrl} alt="Play only visual Sketch" />
<PlayIcon focusable="false" aria-hidden="true" />
</button>
<button
className={stopButtonClass}
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>
<div className="toolbar__autorefresh">
<input
@ -104,38 +119,36 @@ class Toolbar extends React.Component {
</label>
</div>
<div className={nameContainerClass}>
<a
<button
className="toolbar__project-name"
href={this.props.owner ? `/${this.props.owner.username}/sketches/${this.props.project.id}` : ''}
onClick={(e) => {
if (this.canEditProjectName()) {
e.preventDefault();
this.originalProjectName = this.props.project.name;
onClick={() => {
if (canEditProjectName) {
this.props.showEditProjectName();
setTimeout(() => this.projectNameInput.focus(), 0);
}
}}
disabled={!canEditProjectName}
aria-label="Edit sketch name"
>
<span>{this.props.project.name}</span>
{
this.canEditProjectName() &&
<InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" />
canEditProjectName &&
<EditProjectNameIcon
className="toolbar__edit-name-button"
focusable="false"
aria-hidden="true"
/>
}
</a>
</button>
<input
type="text"
maxLength="128"
className="toolbar__project-name-input"
value={this.props.project.name}
aria-label="New sketch name"
value={this.state.projectNameInputValue}
onChange={this.handleProjectNameChange}
ref={(element) => { this.projectNameInput = element; }}
onBlur={() => {
this.validateProjectName();
this.props.hideEditProjectName();
if (this.props.project.id) {
this.props.saveProject();
}
}}
onBlur={this.handleProjectNameSave}
onKeyPress={this.handleKeyPress}
/>
{(() => { // eslint-disable-line
@ -151,9 +164,9 @@ class Toolbar extends React.Component {
<button
className={preferencesButtonClass}
onClick={this.props.openPreferences}
aria-label="preferences"
aria-label="Open Preferences"
>
<InlineSVG src={preferencesUrl} alt="Preferences" />
<PreferencesIcon focusable="false" aria-hidden="true" />
</button>
</div>
);
@ -210,4 +223,5 @@ const mapDispatchToProps = {
...projectActions,
};
export const ToolbarComponent = 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 { connect } from 'react-redux';
import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import prettyBytes from 'pretty-bytes';
import getConfig from '../../../utils/getConfig';
import FileUploader from './FileUploader';
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 = __process.env.UPLOAD_LIMIT || 250000000;
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limitText = prettyBytes(limit);
class UploadFileModal extends React.Component {
@ -33,8 +32,12 @@ class UploadFileModal extends React.Component {
<div className="modal-content">
<div className="modal__header">
<h2 className="modal__title">Upload File</h2>
<button className="modal__exit-button" onClick={this.props.closeModal}>
<InlineSVG src={exitUrl} alt="Close New File Modal" />
<button
className="modal__exit-button"
onClick={this.props.closeModal}
aria-label="Close upload file modal"
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
{ this.props.reachedTotalSizeLimit &&

View File

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

View File

@ -3,13 +3,14 @@ import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { withTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet';
import SplitPane from 'react-split-pane';
import Editor from '../components/Editor';
import Sidebar from '../components/Sidebar';
import PreviewFrame from '../components/PreviewFrame';
import Toolbar from '../components/Toolbar';
import Preferences from '../components/Preferences';
import Preferences from '../components/Preferences/index';
import NewFileModal from '../components/NewFileModal';
import NewFolderModal from '../components/NewFolderModal';
import UploadFileModal from '../components/UploadFileModal';
@ -34,11 +35,38 @@ import AddToCollectionList from '../components/AddToCollectionList';
import Feedback from '../components/Feedback';
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 {
constructor(props) {
super(props);
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
this.state = {
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
@ -53,18 +81,18 @@ class IDEView extends React.Component {
this.props.stopSketch();
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) {
this.props.getProject(id);
this.props.getProject(id, username);
}
}
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
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;
}
@ -92,7 +120,7 @@ class IDEView extends React.Component {
}
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.selectedFile.name === prevProps.selectedFile.name &&
@ -113,7 +141,7 @@ class IDEView extends React.Component {
}
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;
}
isUserOwner() {
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
}
handleGlobalKeydown(e) {
// 83 === s
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
e.preventDefault();
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());
} else if (this.props.user.authenticated) {
this.props.cloneProject();
@ -170,42 +194,34 @@ class IDEView extends React.Component {
} else {
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
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;
}
}
handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
render() {
return (
<div className="ide">
<Helmet>
<title>p5.js Web Editor | {this.props.project.name}</title>
<title>{getTitle(this.props)}</title>
</Helmet>
{this.props.toast.isVisible && <Toast />}
<Nav
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
warnIfUnsavedChanges={this.handleUnsavedChanges}
cmController={this.cmController}
/>
<Toolbar />
<Toolbar key={this.props.project.id} />
{this.props.ide.preferencesIsVisible &&
<Overlay
title="Settings"
title={this.props.t('Settings')}
ariaLabel="settings"
closeOverlay={this.props.closePreferences}
>
@ -231,7 +247,7 @@ class IDEView extends React.Component {
/>
</Overlay>
}
<div className="editor-preview-container">
<main className="editor-preview-container">
<SplitPane
split="vertical"
size={this.state.sidebarSize}
@ -300,7 +316,7 @@ class IDEView extends React.Component {
isExpanded={this.props.ide.sidebarIsExpanded}
expandSidebar={this.props.expandSidebar}
collapseSidebar={this.props.collapseSidebar}
isUserOwner={this.isUserOwner()}
isUserOwner={isUserOwner(this.props)}
clearConsole={this.props.clearConsole}
consoleEvents={this.props.console}
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
@ -308,20 +324,11 @@ class IDEView extends React.Component {
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible}
provideController={(ctl) => { this.cmController = ctl; }}
/>
<Console
fontSize={this.props.preferences.fontSize}
consoleEvents={this.props.console}
isExpanded={this.props.ide.consoleIsExpanded}
expandConsole={this.props.expandConsole}
collapseConsole={this.props.collapseConsole}
clearConsole={this.props.clearConsole}
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
theme={this.props.preferences.theme}
/>
<Console />
</SplitPane>
<div className="preview-frame-holder">
<section className="preview-frame-holder">
<header className="preview-frame__header">
<h2 className="preview-frame__title">Preview</h2>
<h2 className="preview-frame__title">{this.props.t('Preview')}</h2>
</header>
<div className="preview-frame__content">
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
@ -362,10 +369,10 @@ class IDEView extends React.Component {
cmController={this.cmController}
/>
</div>
</div>
</section>
</SplitPane>
</SplitPane>
</div>
</main>
{ this.props.ide.modalIsVisible &&
<NewFileModal />
}
@ -382,7 +389,7 @@ class IDEView extends React.Component {
}
{ this.props.location.pathname === '/about' &&
<Overlay
title="About"
title={this.props.t('About')}
previousPath={this.props.ide.previousPath}
ariaLabel="about"
>
@ -428,7 +435,7 @@ class IDEView extends React.Component {
}
{this.props.ide.keyboardShortcutVisible &&
<Overlay
title="Keyboard Shortcuts"
title={this.props.t('KeyboardShortcuts')}
ariaLabel="keyboard shortcuts"
closeOverlay={this.props.closeKeyboardShortcutModal}
>
@ -562,6 +569,7 @@ IDEView.propTypes = {
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired,
closeNewFolderModal: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired,
closeShareModal: PropTypes.func.isRequired,
showEditorOptions: PropTypes.func.isRequired,
@ -590,12 +598,12 @@ IDEView.propTypes = {
showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
persistState: PropTypes.func.isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired,
startSketch: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
closeUploadFileModal: PropTypes.func.isRequired
closeUploadFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
function mapStateToProps(state) {
@ -632,4 +640,4 @@ function mapDispatchToProps(dispatch) {
);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));

View File

@ -0,0 +1,262 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { useState } from 'react';
import styled from 'styled-components';
// 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';
import Console from '../components/Console';
import { remSize } from '../../../theme';
import ActionStrip from '../../../components/mobile/ActionStrip';
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
const Expander = styled.div`
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
`;
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>
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
<ActionStrip />
</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', '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 result =

View File

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

View File

@ -0,0 +1,92 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect, useSelector, useDispatch } 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';
import { optionsOnOff, optionsPickOne, preferenceOnOff } from '../IDE/components/Preferences/PreferenceCreators';
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(68)};
`;
const SectionHeader = styled.h2`
color: ${prop('primaryTextColor')};
padding-top: ${remSize(32)};
`;
const SectionSubeader = styled.h3`
color: ${prop('primaryTextColor')};
`;
const MobilePreferences = () => {
// Props
const {
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, lineNumbers, lintWarning
} = useSelector(state => state.preferences);
// Actions
const {
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, setLineNumbers, setLintWarning,
} = bindActionCreators({ ...PreferencesActions, ...IdeActions }, useDispatch());
const generalSettings = [
{
title: 'Theme',
value: theme,
options: optionsPickOne('theme', 'light', 'dark', 'contrast'),
onSelect: x => setTheme(x) // setTheme
},
preferenceOnOff('Autosave', autosave, setAutosave, 'autosave'),
preferenceOnOff('Word Wrap', linewrap, setLinewrap, 'linewrap')
];
const outputSettings = [
preferenceOnOff('Plain-text', textOutput, setTextOutput, 'text output'),
preferenceOnOff('Table-text', gridOutput, setGridOutput, 'table output'),
preferenceOnOff('Lint Warning Sound', soundOutput, setSoundOutput, 'sound output')
];
const accessibilitySettings = [
preferenceOnOff('Line Numbers', lineNumbers, setLineNumbers),
preferenceOnOff('Lint Warning Sound', lintWarning, setLintWarning)
];
return (
<Screen fullscreen>
<section>
<Header transparent title="Preferences">
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
</Header>
<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>);
};
export default withRouter(MobilePreferences);

View File

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect, useSelector, useDispatch } 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 Console from '../IDE/components/Console';
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';
import Footer from '../../components/mobile/Footer';
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(68)};
`;
const MobileSketchView = (props) => {
const { files, ide, preferences } = useSelector(state => state);
const htmlFile = useSelector(state => getHTMLFile(state.files));
const projectName = useSelector(state => state.project.name);
const selectedFile = useSelector(state => state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'));
const {
setTextOutput, setGridOutput, setSoundOutput, dispatchConsoleEvent,
endSketchRefresh, stopSketch, setBlobUrl, expandConsole, clearConsole
} = bindActionCreators({
...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions
}, useDispatch());
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>
<Footer>
<Console />
</Footer>
</Screen>);
};
export default MobileSketchView;

View File

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

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