Merge branch 'develop' into chore/split-test-runners

This commit is contained in:
Cassie Tarakajian 2020-07-06 12:38:22 -04:00
commit 67e132d4eb
35 changed files with 1387 additions and 585 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { remSize, prop } from '../theme';
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';
@ -10,6 +10,8 @@ import Plus from '../images/plus-icon.svg';
import Close from '../images/close.svg';
import Exit from '../images/exit.svg';
import DropdownArrow from '../images/down-filled-triangle.svg';
import Preferences from '../images/preferences.svg';
import Play from '../images/triangle-arrow-right.svg';
// HOC that adds the right web accessibility props
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
@ -70,3 +72,5 @@ export const PlusIcon = withLabel(Plus);
export const CloseIcon = withLabel(Close);
export const ExitIcon = withLabel(Exit);
export const DropdownArrowIcon = withLabel(DropdownArrow);
export const PreferencesIcon = withLabel(Preferences);
export const PlayIcon = withLabel(Play);

View File

@ -4,6 +4,8 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { Link } from 'react-router';
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';
@ -55,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);
}
@ -163,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');
@ -233,7 +246,7 @@ class Nav extends React.PureComponent {
<Link to="/" className="nav__back-link">
<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>
@ -258,7 +271,7 @@ class Nav extends React.PureComponent {
}
}}
>
<span className="nav__item-header">File</span>
<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">
@ -268,7 +281,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile}
onBlur={this.handleBlur}
>
New
{this.props.t('New')}
</button>
</li>
{ getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
@ -278,7 +291,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForFile}
onBlur={this.handleBlur}
>
Save
{this.props.t('Save')}
<span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
</button>
</li> }
@ -289,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 &&
@ -299,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 &&
@ -309,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 &&
@ -320,7 +333,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
Open
{this.props.t('Open')}
</Link>
</li> }
{getConfig('UI_COLLECTIONS_ENABLED') &&
@ -333,7 +346,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
Add to Collection
{this.props.t('AddToCollection')}
</Link>
</li>}
{ getConfig('EXAMPLES_ENABLED') &&
@ -344,7 +357,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
Examples
{this.props.t('Examples')}
</Link>
</li> }
</ul>
@ -360,7 +373,7 @@ class Nav extends React.PureComponent {
}
}}
>
<span className="nav__item-header">Edit</span>
<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" >
@ -373,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>
@ -383,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>
@ -393,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>
@ -403,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>
@ -420,7 +433,7 @@ class Nav extends React.PureComponent {
}
}}
>
<span className="nav__item-header">Sketch</span>
<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">
@ -430,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">
@ -439,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">
@ -448,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>
@ -458,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>
@ -495,7 +508,7 @@ class Nav extends React.PureComponent {
}
}}
>
<span className="nav__item-header">Help</span>
<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">
@ -505,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">
@ -516,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">
@ -526,7 +539,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
About
{this.props.t('About')}
</Link>
</li>
</ul>
@ -535,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" className="nav__auth-button">
<span className="nav__item-header">Log in</span>
<span className="nav__item-header">{this.props.t('Login')}</span>
</Link>
</li>
<span className="nav__item-or">or</span>
<span className="nav__item-or">{this.props.t('LoginOr')}</span>
<li className="nav__item">
<Link to="/signup" className="nav__auth-button">
<span className="nav__item-header">Sign up</span>
<span className="nav__item-header">{this.props.t('SignUp')}</span>
</Link>
</li>
</ul>
@ -557,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}>
@ -572,7 +640,7 @@ class Nav extends React.PureComponent {
}
}}
>
My Account
{this.props.t('MyAccount')}
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
</button>
<ul className="nav__dropdown">
@ -583,7 +651,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
My sketches
{this.props.t('MySketches')}
</Link>
</li>
{getConfig('UI_COLLECTIONS_ENABLED') &&
@ -594,7 +662,7 @@ class Nav extends React.PureComponent {
onBlur={this.handleBlur}
onClick={this.setDropdownForNone}
>
My collections
{this.props.t('MyCollections')}
</Link>
</li>
}
@ -605,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">
@ -615,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">
@ -624,7 +692,7 @@ class Nav extends React.PureComponent {
onFocus={this.handleFocusForAccount}
onBlur={this.handleBlur}
>
Log out
{this.props.t('LogOut')}
</button>
</li>
</ul>
@ -677,6 +745,10 @@ 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'
})
};
@ -684,6 +756,7 @@ class Nav extends React.PureComponent {
<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>
@ -734,7 +807,9 @@ Nav.propTypes = {
}).isRequired,
params: PropTypes.shape({
username: PropTypes.string
})
}),
t: PropTypes.func.isRequired
};
Nav.defaultProps = {
@ -767,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

@ -44,7 +44,8 @@ describe('Nav', () => {
setToastText: jest.fn(),
rootFile: {
id: 'root-file'
}
},
t: jest.fn()
};
it('renders correctly', () => {

View File

@ -26,9 +26,7 @@ exports[`Nav renders correctly 1`] = `
<button>
<span
class="nav__item-header"
>
File
</span>
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
@ -41,37 +39,27 @@ exports[`Nav renders correctly 1`] = `
<li
class="nav__dropdown-item"
>
<button>
New
</button>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button>
Duplicate
</button>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button>
Share
</button>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button>
Download
</button>
<button />
</li>
<li
class="nav__dropdown-item"
>
<a>
Open
</a>
<a />
</li>
</ul>
</li>
@ -81,9 +69,7 @@ exports[`Nav renders correctly 1`] = `
<button>
<span
class="nav__item-header"
>
Edit
</span>
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
@ -97,7 +83,6 @@ exports[`Nav renders correctly 1`] = `
class="nav__dropdown-item"
>
<button>
Tidy Code
<span
class="nav__keyboard-shortcut"
>
@ -109,7 +94,6 @@ exports[`Nav renders correctly 1`] = `
class="nav__dropdown-item"
>
<button>
Find
<span
class="nav__keyboard-shortcut"
>
@ -121,7 +105,6 @@ exports[`Nav renders correctly 1`] = `
class="nav__dropdown-item"
>
<button>
Find Next
<span
class="nav__keyboard-shortcut"
>
@ -133,7 +116,6 @@ exports[`Nav renders correctly 1`] = `
class="nav__dropdown-item"
>
<button>
Find Previous
<span
class="nav__keyboard-shortcut"
>
@ -149,9 +131,7 @@ exports[`Nav renders correctly 1`] = `
<button>
<span
class="nav__item-header"
>
Sketch
</span>
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
@ -164,22 +144,17 @@ exports[`Nav renders correctly 1`] = `
<li
class="nav__dropdown-item"
>
<button>
Add File
</button>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button />
</li>
<li
class="nav__dropdown-item"
>
<button>
Add Folder
</button>
</li>
<li
class="nav__dropdown-item"
>
<button>
Run
<span
class="nav__keyboard-shortcut"
>
@ -191,7 +166,6 @@ exports[`Nav renders correctly 1`] = `
class="nav__dropdown-item"
>
<button>
Stop
<span
class="nav__keyboard-shortcut"
>
@ -207,9 +181,7 @@ exports[`Nav renders correctly 1`] = `
<button>
<span
class="nav__item-header"
>
Help
</span>
/>
<test-file-stub
aria-hidden="true"
classname="nav__item-header-triangle"
@ -222,9 +194,7 @@ exports[`Nav renders correctly 1`] = `
<li
class="nav__dropdown-item"
>
<button>
Keyboard Shortcuts
</button>
<button />
</li>
<li
class="nav__dropdown-item"
@ -233,16 +203,12 @@ exports[`Nav renders correctly 1`] = `
href="https://p5js.org/reference/"
rel="noopener noreferrer"
target="_blank"
>
Reference
</a>
/>
</li>
<li
class="nav__dropdown-item"
>
<a>
About
</a>
<a />
</li>
</ul>
</li>

View File

@ -0,0 +1,20 @@
import React from 'react';
import styled from 'styled-components';
import { prop, remSize } from '../../theme';
const background = prop('MobilePanel.default.background');
const textColor = prop('primaryTextColor');
const Footer = styled.div`
position: fixed;
width: 100%;
background: ${background};
color: ${textColor};
padding: ${remSize(12)};
padding-left: ${remSize(32)};
z-index: 1;
bottom: 0;
`;
export default Footer;

View File

@ -0,0 +1,80 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { prop, remSize } from '../../theme';
const background = prop('MobilePanel.default.background');
const textColor = prop('primaryTextColor');
const HeaderDiv = styled.div`
position: fixed;
width: 100%;
background: ${props => (props.transparent ? 'transparent' : background)};
color: ${textColor};
padding: ${remSize(12)};
padding-left: ${remSize(16)};
padding-right: ${remSize(16)};
z-index: 1;
display: flex;
flex: 1;
flex-direction: row;
justify-content: flex-start;
align-items: center;
// TODO:
svg {
max-height: ${remSize(32)};
padding: ${remSize(4)}
}
`;
const IconContainer = styled.div`
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
display: flex;
`;
const TitleContainer = styled.div`
margin-left: ${remSize(4)};
margin-right: auto;
${props => props.padded && `h2{
padding-top: ${remSize(10)};
padding-bottom: ${remSize(10)};
}`}
`;
const Header = ({
title, subtitle, leftButton, children, transparent
}) => (
<HeaderDiv transparent={transparent}>
{leftButton}
<TitleContainer padded={subtitle === null}>
{title && <h2>{title}</h2>}
{subtitle && <h3>{subtitle}</h3>}
</TitleContainer>
<IconContainer>
{children}
</IconContainer>
</HeaderDiv>
);
Header.propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
leftButton: PropTypes.element,
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
transparent: PropTypes.bool
};
Header.defaultProps = {
title: null,
subtitle: null,
leftButton: null,
children: [],
transparent: false
};
export default Header;

View File

@ -0,0 +1,8 @@
import React from 'react';
import styled from 'styled-components';
import { remSize } from '../../theme';
export default styled.div`
z-index: 0;
margin-top: ${remSize(16)};
`;

View File

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Button from '../../common/Button';
import { remSize } from '../../theme';
const ButtonWrapper = styled(Button)`
width: ${remSize(48)};
> svg {
width: 100%;
height: 100%;
}
`;
const IconButton = (props) => {
const { icon, ...otherProps } = props;
const Icon = icon;
return (<ButtonWrapper
iconBefore={<Icon />}
kind={Button.kinds.inline}
focusable="false"
{...otherProps}
/>);
};
IconButton.propTypes = {
icon: PropTypes.func.isRequired
};
export default IconButton;

View File

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

View File

@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { prop, remSize } from '../../theme';
const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))`
color: ${prop('primaryTextColor')};
`;
const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))`
flex-wrap: nowrap !important;
align-items: baseline !important;
justify-items: space-between;
`;
const OptionLabel = styled.label.attrs({ className: 'preference__option' })`
font-size: ${remSize(14)} !important;
`;
const PreferencePicker = ({
title, value, onSelect, options,
}) => (
<Preference>
<PreferenceTitle>{title}</PreferenceTitle>
<div className="preference__options">
{options.map(option => (
<React.Fragment key={`${option.name}-${option.id}`} >
<input
type="radio"
onChange={() => onSelect(option.value)}
aria-label={option.ariaLabel}
name={option.name}
key={`${option.name}-${option.id}-input`}
id={option.id}
className="preference__radio-button"
value={option.value}
checked={value === option.value}
/>
<OptionLabel
key={`${option.name}-${option.id}-label`}
htmlFor={option.id}
>
{option.label}
</OptionLabel>
</React.Fragment>))}
</div>
</Preference>
);
PreferencePicker.defaultProps = {
options: []
};
PreferencePicker.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
ariaLabel: PropTypes.string,
})),
onSelect: PropTypes.func.isRequired,
};
export default PreferencePicker;

38
client/i18n.js Normal file
View File

@ -0,0 +1,38 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
const fallbackLng = ['en-US'];
const availableLanguages = ['en-US', 'es-419'];
const options = {
loadPath: '/translations/{{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

@ -1,4 +1,4 @@
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';
@ -7,6 +7,8 @@ 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');
@ -29,6 +31,8 @@ const App = () => (
const HotApp = hot(App);
render(
<HotApp />,
<Suspense fallback={(<Loader />)}>
<HotApp />
</Suspense>,
document.getElementById('root')
);

View File

@ -155,18 +155,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) {
if (state.ide.justOpenedProject && state.preferences.autosave) {
dispatch(showToast(5500));
dispatch(setToastText('Project saved.'));
dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject());
} else {
dispatch(showToast(1500));
dispatch(setToastText('Project saved.'));
dispatch(setToastText('Sketch saved.'));
}
}
})
.catch((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) {
@ -195,18 +197,20 @@ export function saveProject(selectedFile = null, autosave = false) {
if (!autosave) {
if (state.preferences.autosave) {
dispatch(showToast(5500));
dispatch(setToastText('Project saved.'));
dispatch(setToastText('Sketch saved.'));
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
dispatch(resetJustOpenedProject());
} else {
dispatch(showToast(1500));
dispatch(setToastText('Project saved.'));
dispatch(setToastText('Sketch saved.'));
}
}
})
.catch((error) => {
const { response } = error;
dispatch(endSavingProject());
dispatch(setToastText('Failed to save sketch.'));
dispatch(showToast(1500));
if (response.status === 403) {
dispatch(showErrorModal('staleSession'));
} else {

View File

@ -1,15 +1,16 @@
import React from 'react';
import { Helmet } from 'react-helmet';
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">
<SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
@ -25,7 +26,7 @@ function About(props) {
</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/"
@ -33,7 +34,7 @@ function About(props) {
rel="noopener noreferrer"
>
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Examples
{t('Examples')}
</a>
</p>
<p className="about__content-column-list">
@ -43,12 +44,12 @@ function About(props) {
rel="noopener noreferrer"
>
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Learn
{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/"
@ -56,7 +57,7 @@ function About(props) {
rel="noopener noreferrer"
>
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Libraries
{t('Libraries')}
</a>
</p>
<p className="about__content-column-list">
@ -66,7 +67,7 @@ function About(props) {
rel="noopener noreferrer"
>
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Reference
{t('Reference')}
</a>
</p>
<p className="about__content-column-list">
@ -76,7 +77,7 @@ function About(props) {
rel="noopener noreferrer"
>
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
Forum
{t('Forum')}
</a>
</p>
</div>
@ -86,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">
@ -94,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

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

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
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';
@ -98,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"
@ -116,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')}
@ -127,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')}
@ -138,11 +139,11 @@ 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}
@ -150,7 +151,7 @@ class Preferences extends React.Component {
disabled={this.state.fontSize <= 8}
>
<MinusIcon focusable="false" aria-hidden="true" />
<h6 className="preference__label">Decrease</h6>
<h6 className="preference__label">{this.props.t('Decrease')}</h6>
</button>
<form onSubmit={this.onFontInputSubmit}>
<input
@ -171,11 +172,11 @@ class Preferences extends React.Component {
disabled={this.state.fontSize >= 36}
>
<PlusIcon focusable="false" aria-hidden="true" />
<h6 className="preference__label">Increase</h6>
<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"
@ -187,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)}
@ -198,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"
@ -214,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)}
@ -225,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"
@ -243,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)}
@ -254,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"
@ -270,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)}
@ -281,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
@ -307,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) => {
@ -319,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) => {
@ -331,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>
@ -360,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,46 +47,17 @@ 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
}
@ -249,16 +237,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 +264,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);
}
}
}
@ -395,7 +386,7 @@ PreviewFrame.propTypes = {
clearConsole: PropTypes.func.isRequired,
cmController: PropTypes.shape({
getContent: PropTypes.func
})
}),
};
PreviewFrame.defaultProps = {

View File

@ -2,15 +2,17 @@ import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next';
import * as ToastActions from '../actions/toast';
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} aria-label="Close Alert" >
<ExitIcon focusable="false" aria-hidden="true" />

View File

@ -3,6 +3,7 @@ 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';
@ -34,6 +35,15 @@ 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;
}
class IDEView extends React.Component {
constructor(props) {
super(props);
@ -92,7 +102,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 &&
@ -123,21 +133,12 @@ class IDEView extends React.Component {
this.autosaveInterval = null;
}
getTitle = () => {
const { id } = this.props.project;
return id ? `p5.js Web Editor | ${this.props.project.name}` : 'p5.js Web Editor';
}
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();
@ -196,7 +197,7 @@ class IDEView extends React.Component {
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.')) {
if (!window.confirm(this.props.t('WarningUnsavedChanges'))) {
return false;
}
this.props.setUnsavedChanges(false);
@ -208,7 +209,7 @@ class IDEView extends React.Component {
return (
<div className="ide">
<Helmet>
<title>{this.getTitle()}</title>
<title>{getTitle(this.props)}</title>
</Helmet>
{this.props.toast.isVisible && <Toast />}
<Nav
@ -218,7 +219,7 @@ class IDEView extends React.Component {
<Toolbar key={this.props.project.id} />
{this.props.ide.preferencesIsVisible &&
<Overlay
title="Settings"
title={this.props.t('Settings')}
ariaLabel="settings"
closeOverlay={this.props.closePreferences}
>
@ -313,7 +314,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}
@ -334,7 +335,7 @@ class IDEView extends React.Component {
</SplitPane>
<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; }}>
@ -395,7 +396,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"
>
@ -441,7 +442,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}
>
@ -609,7 +610,8 @@ IDEView.propTypes = {
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) {
@ -646,4 +648,6 @@ function mapDispatchToProps(dispatch) {
);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));

View File

@ -1,7 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { useState } from 'react';
@ -20,93 +18,47 @@ import { getHTMLFile } from '../reducers/files';
// Local Imports
import Editor from '../components/Editor';
import { prop, remSize } from '../../../theme';
import { ExitIcon } from '../../../common/icons';
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
const background = prop('Button.default.background');
const textColor = prop('primaryTextColor');
const Header = styled.div`
position: fixed;
width: 100%;
background: ${background};
color: ${textColor};
padding: ${remSize(12)};
padding-left: ${remSize(32)};
padding-right: ${remSize(32)};
z-index: 1;
display: flex;
flex: 1;
flex-direction: row;
justify-content: flex-start;
align-items: center;
`;
const Footer = styled.div`
position: fixed;
width: 100%;
background: ${background};
color: ${textColor};
padding: ${remSize(12)};
padding-left: ${remSize(32)};
z-index: 1;
bottom: 0;
`;
const Content = styled.div`
z-index: 0;
margin-top: ${remSize(16)};
`;
const Icon = styled.a`
> svg {
fill: ${textColor};
color: ${textColor};
margin-left: ${remSize(16)};
}
`;
const IconLinkWrapper = styled(Link)`
width: 3rem;
margin-right: 1.25rem;
margin-left: none;
`;
const Screen = ({ children }) => (
<div className="fullscreen-preview">
{children}
</div>
);
Screen.propTypes = {
children: PropTypes.node.isRequired
};
import IconButton from '../../../components/mobile/IconButton';
import Header from '../../../components/mobile/Header';
import Screen from '../../../components/mobile/MobileScreen';
import Footer from '../../../components/mobile/Footer';
import IDEWrapper from '../../../components/mobile/IDEWrapper';
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
const IDEViewMobile = (props) => {
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
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);
const [tmController, setTmController] = useState(null); // eslint-disable-line
const [overlay, setOverlay] = useState(null); // eslint-disable-line
return (
<Screen>
<Header>
<IconLinkWrapper to="/" aria-label="Return to original editor">
<ExitIcon />
</IconLinkWrapper>
<div>
<h2>{project.name}</h2>
<h3>{selectedFile.name}</h3>
</div>
<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>
<Content>
<IDEWrapper>
<Editor
lintWarning={preferences.lintWarning}
linewrap={preferences.linewrap}
@ -141,14 +93,14 @@ const IDEViewMobile = (props) => {
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
provideController={setTmController}
/>
</Content>
</IDEWrapper>
<Footer><h2>Bottom Bar</h2></Footer>
</Screen>
);
};
IDEViewMobile.propTypes = {
MobileIDEView.propTypes = {
preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired,
@ -205,6 +157,8 @@ IDEViewMobile.propTypes = {
updatedAt: PropTypes.string
}).isRequired,
startSketch: PropTypes.func.isRequired,
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
@ -293,4 +247,4 @@ function mapDispatchToProps(dispatch) {
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEViewMobile));
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

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

View File

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

View File

@ -2,7 +2,9 @@ import { Route, IndexRoute } from 'react-router';
import React from 'react';
import App from './modules/App/App';
import IDEView from './modules/IDE/pages/IDEView';
import IDEViewMobile from './modules/IDE/pages/IDEViewMobile';
import MobileIDEView from './modules/IDE/pages/MobileIDEView';
import MobileSketchView from './modules/Mobile/MobileSketchView';
import MobilePreferences from './modules/Mobile/MobilePreferences';
import FullView from './modules/IDE/pages/FullView';
import LoginView from './modules/User/pages/LoginView';
import SignupView from './modules/User/pages/SignupView';
@ -21,7 +23,11 @@ const checkAuth = (store) => {
store.dispatch(getUser());
};
// TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called
const onRouteChange = (store) => {
const path = window.location.pathname;
if (path.includes('/mobile')) return;
store.dispatch(stopSketch());
};
@ -50,8 +56,10 @@ const routes = store => (
<Route path="/:username/collections/create" component={DashboardView} />
<Route path="/:username/collections/:collection_id" component={CollectionView} />
<Route path="/about" component={IDEView} />
<Route path="/mobile" component={IDEViewMobile} />
<Route path="/mobile" component={MobileIDEView} />
<Route path="/mobile/preview" component={MobileSketchView} />
<Route path="/mobile/preferences" component={MobilePreferences} />
</Route>
);

View File

@ -88,6 +88,13 @@ export default {
Icon: {
default: grays.middleGray,
hover: grays.darker
},
MobilePanel: {
default: {
foreground: colors.black,
background: grays.light,
border: grays.middleLight,
},
}
},
[Theme.dark]: {
@ -120,6 +127,13 @@ export default {
Icon: {
default: grays.middleLight,
hover: grays.lightest
},
MobilePanel: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
}
},
[Theme.contrast]: {
@ -152,6 +166,13 @@ export default {
Icon: {
default: grays.mediumLight,
hover: colors.yellow
},
MobilePanel: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
}
},
};

View File

@ -69,9 +69,8 @@ Note that this is optional, unless you are working on the part of the applicatio
If your S3 bucket is in the US East (N Virginia) region (us-east-1), you'll
need to set a custom URL base for it, because it does not follow the standard
naming pattern as the rest of the regions. Instead, add the following to your
environment/.env file:
`S3_BUCKET_URL_BASE=https://s3.amazonaws.com`
environment/.env file, changing `BUCKET_NAME` to your bucket name. This is necessary because this override is currently treated as the full path to the bucket rather than as a proper base url:
`S3_BUCKET_URL_BASE=https://s3.amazonaws.com/{BUCKET_NAME}/`
If you've configured your S3 bucket and DNS records to use a custom domain
name, you can also set it using this variable. I.e.:

408
package-lock.json generated
View File

@ -10974,8 +10974,7 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
"resolved": ""
}
}
},
@ -14779,9 +14778,9 @@
},
"dependencies": {
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
"integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
"dev": true
}
}
@ -17303,14 +17302,6 @@
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
}
}
},
"ftp": {
@ -17711,9 +17702,9 @@
}
},
"globule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.0.tgz",
"integrity": "sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz",
"integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==",
"dev": true,
"requires": {
"glob": "~7.1.1",
@ -18176,6 +18167,14 @@
}
}
},
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-tags": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
@ -18596,6 +18595,52 @@
}
}
},
"i18next": {
"version": "19.5.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.5.4.tgz",
"integrity": "sha512-fty+v8v0Unn8KufOajoZ6OR5aKme/lP+BdbT5rirTXE8wdj6LbsbG9DwtI9VtMeru1PEf+L530Cq3IRmovMJGQ==",
"requires": {
"@babel/runtime": "^7.10.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz",
"integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
}
}
},
"i18next-browser-languagedetector": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.3.1.tgz",
"integrity": "sha512-KIToAzf8zwWvacgnRwJp63ase26o24AuNUlfNVJ5YZAFmdGhsJpmFClxXPuk9rv1FMI4lnc8zLSqgZPEZMrW4g==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"i18next-http-backend": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.0.16.tgz",
"integrity": "sha512-IsRfJl2alhCxhu+8jWCz7ZsGqjOWm6gu9vJ6TKmSWIvTISKPn32SGWUM0Uk8ChtrIyW7bXNudKwCEDLzar84Dw==",
"requires": {
"node-fetch": "2.6.0"
},
"dependencies": {
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -18722,9 +18767,9 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"in-publish": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
"integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz",
"integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==",
"dev": true
},
"indent-string": {
@ -18894,12 +18939,6 @@
"loose-envify": "^1.0.0"
}
},
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
"dev": true
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@ -19089,13 +19128,10 @@
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
},
"is-finite": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
@ -23674,14 +23710,14 @@
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"jquery": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"js-base64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
"integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==",
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.2.tgz",
"integrity": "sha512-1hgLrLIrmCgZG+ID3VoLNLOSwjGnoZa8tyrUdEteMeIzsT6PH7PMLyUvbDwzNE56P3PNxyvuIOx4Uh2E5rzQIw==",
"dev": true
},
"js-beautify": {
@ -24384,15 +24420,6 @@
"readable-stream": "^2.0.5"
}
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
"dev": true,
"requires": {
"invert-kv": "^1.0.0"
}
},
"leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -25405,9 +25432,9 @@
"dev": true
},
"markdown-to-jsx": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.1.tgz",
"integrity": "sha512-FdtDAv8d9/tjyHxdCvWZxxOgK2icwzBkTq/dPk+XlQ2B+DYDcwE89FWGzT92erXQ0CQR/bQbpNK3loNYhYL70g==",
"version": "6.11.4",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz",
"integrity": "sha512-3lRCD5Sh+tfA52iGgfs/XZiw33f7fFX9Bn55aNnVNUd2GzLDkOWyKYYD8Yju2B1Vn+feiEdgJs8T6Tg0xNokPw==",
"dev": true,
"requires": {
"prop-types": "^15.6.2",
@ -25592,14 +25619,6 @@
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0",
"strip-bom": "^2.0.0"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
}
}
},
"parse-json": {
@ -25629,14 +25648,6 @@
"graceful-fs": "^4.1.2",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
}
}
},
"pify": {
@ -26986,12 +26997,6 @@
"which": "1"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
@ -27133,9 +27138,9 @@
}
},
"node-sass": {
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz",
"integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==",
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz",
"integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@ -27152,7 +27157,7 @@
"node-gyp": "^3.8.0",
"npmlog": "^4.0.0",
"request": "^2.88.0",
"sass-graph": "^2.2.4",
"sass-graph": "2.2.5",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
},
@ -28203,15 +28208,6 @@
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-locale": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
"lcid": "^1.0.0"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -31648,6 +31644,15 @@
"prop-types": "^15.6.1"
}
},
"react-i18next": {
"version": "11.7.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.7.0.tgz",
"integrity": "sha512-8tvVkpuxQlubcszZON+jmoCgiA9gCZ74OAYli9KChPhETtq8pJsANBTe9KRLRLmX3ubumgvidURWr0VvKz1tww==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
}
},
"react-input-autosize": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
@ -33762,220 +33767,59 @@
}
},
"sass-graph": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
"integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz",
"integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==",
"dev": true,
"requires": {
"glob": "^7.0.0",
"lodash": "^4.0.0",
"scss-tokenizer": "^0.2.3",
"yargs": "^7.0.0"
"yargs": "^13.3.2"
},
"dependencies": {
"camelcase": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
"dev": true
},
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wrap-ansi": "^2.0.0"
}
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
"dev": true,
"requires": {
"path-exists": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"parse-json": "^2.2.0",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0",
"strip-bom": "^2.0.0"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
}
}
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
"error-ex": "^1.2.0"
}
},
"path-exists": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
"dev": true,
"requires": {
"pinkie-promise": "^2.0.0"
}
},
"path-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true
}
}
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
"dev": true,
"requires": {
"load-json-file": "^1.0.0",
"normalize-package-data": "^2.3.2",
"path-type": "^1.0.0"
}
},
"read-pkg-up": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
"dev": true,
"requires": {
"find-up": "^1.0.0",
"read-pkg": "^1.0.0"
}
},
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-bom": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"is-utf8": "^0.2.0"
"ansi-regex": "^4.1.0"
}
},
"which-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
"integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
"dev": true
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
}
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yargs": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
"integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"camelcase": "^3.0.0",
"cliui": "^3.2.0",
"decamelize": "^1.1.1",
"get-caller-file": "^1.0.1",
"os-locale": "^1.4.0",
"read-pkg-up": "^1.0.1",
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^1.0.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^1.0.2",
"which-module": "^1.0.0",
"y18n": "^3.2.1",
"yargs-parser": "^5.0.0"
}
},
"yargs-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
"integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
"dev": true,
"requires": {
"camelcase": "^3.0.0"
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
}
}
@ -34582,8 +34426,7 @@
},
"kind-of": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
"resolved": ""
}
}
},
@ -36895,8 +36738,7 @@
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=",
"dev": true
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
},
"vue-docgen-api": {
"version": "4.20.0",
@ -38412,9 +38254,9 @@
}
},
"yargs-parser": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",

View File

@ -114,7 +114,7 @@
"jest": "^26.0.1",
"lint-staged": "^10.1.3",
"mini-css-extract-plugin": "^0.8.2",
"node-sass": "^4.13.1",
"node-sass": "^4.14.1",
"nodemon": "^1.19.4",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-cssnext": "^3.1.0",
@ -168,6 +168,9 @@
"express-session": "^1.17.0",
"friendly-words": "^1.1.10",
"htmlhint": "^0.10.1",
"i18next": "^19.4.5",
"i18next-browser-languagedetector": "^4.2.0",
"i18next-http-backend": "^1.0.15",
"is-url": "^1.2.4",
"jest-express": "^1.11.0",
"js-beautify": "^1.10.3",
@ -195,6 +198,7 @@
"react-dom": "^16.12.0",
"react-helmet": "^5.1.3",
"react-hot-loader": "^4.12.19",
"react-i18next": "^11.5.0",
"react-redux": "^5.1.2",
"react-router": "^3.2.5",
"react-split-pane": "^0.1.89",

View File

@ -34,6 +34,7 @@ export function serveProject(req, res) {
resolveScripts(sketchDoc, files);
resolveStyles(sketchDoc, files);
res.setHeader('Cache-Control', 'public, max-age=0');
res.send(serializeDocument(sketchDoc));
});
});

View File

@ -118,6 +118,14 @@ if (process.env.MOBILE_ENABLED) {
router.get('/mobile', (req, res) => {
res.send(renderIndex());
});
router.get('/mobile/preview', (req, res) => {
res.send(renderIndex());
});
router.get('/mobile/preferences', (req, res) => {
res.send(renderIndex());
});
}
router.get('/:username/collections/create', (req, res) => {

View File

@ -73,7 +73,7 @@ function getCategories() {
function getSketchesInCategories(categories) {
return Q.all(categories.map((category) => {
const options = {
url: `${category.url.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
url: `${category.url.replace('?ref=main', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers,
json: true
@ -107,7 +107,7 @@ function getSketchesInCategories(categories) {
function getSketchContent(projectsInAllCategories) {
return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => {
const options = {
url: `${project.sketchUrl.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
url: `${project.sketchUrl.replace('?ref=main', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET',
headers
};
@ -264,7 +264,7 @@ function createProjectsInP5user(projectsInAllCategories) {
const fileID = objectID().toHexString();
newProject.files.push({
name: assetName,
url: `https://cdn.jsdelivr.net/gh/processing/p5.js-website@master/src/data/examples/assets/${assetName}`,
url: `https://cdn.jsdelivr.net/gh/processing/p5.js-website@main/src/data/examples/assets/${assetName}`,
id: fileID,
_id: fileID,
children: [],

View File

@ -79,6 +79,7 @@ app.options('*', corsMiddleware);
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
}));
app.use('/translations', Express.static('translations/locales/'));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(cookieParser());

View File

@ -33,6 +33,8 @@ export function renderIndex() {
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
window.process.env.MOBILE_ENABLED = ${process.env.MOBILE_ENABLED ? `${process.env.MOBILE_ENABLED}` : undefined};
window.process.env.TRANSLATIONS_ENABLED = ${process.env.TRANSLATIONS_ENABLED === 'true' ? true :false};
</script>
</head>
<body>

View File

@ -0,0 +1,115 @@
{
"Contribute": "Contribute",
"NewP5": "New to p5.js?",
"Report": "Report a bug",
"Learn": "Learn",
"About": "About",
"Resources": "Resources",
"Libraries": "Libraries",
"Forum": "Forum",
"File": "File",
"New": "New",
"Save": "Save",
"Share": "Share",
"Duplicate": "Duplicate",
"Examples": "Examples",
"Edit": "Edit",
"TidyCode": "Tidy Code",
"Find": "Find",
"AddToCollection": "Add to Collection",
"FindNext": "Find Next",
"FindPrevious": "Find Previous",
"Sketch": "Sketch",
"AddFile": "Add File",
"AddFolder": "Add Folder",
"Run": "Run",
"Stop": "Stop",
"Help": "Help",
"KeyboardShortcuts": "Keyboard Shortcuts",
"Reference": "Reference",
"Tidy": "Tidy",
"Lang": "Language",
"FindNextMatch": "Find Next Match",
"FindPrevMatch": "Find Previous Match",
"IndentCodeLeft": "Indent Code Left",
"IndentCodeRight": "Indent Code Right",
"CommentLine": "Comment Line",
"StartSketch": "Start Sketch",
"StopSketch": "StopSketch",
"TurnOnAccessibleOutput": "Turn On Accessible Output",
"TurnOffAccessibleOutput": "Turn Off Accessible Output",
"ToogleSidebar": "Toogle Sidebar",
"ToogleConsole": "Toogle Console",
"Preview": "Preview",
"Auto-refresh": "Auto-refresh",
"Console": "Console",
"Settings": "Settings",
"GeneralSettings": "General settings",
"Theme": "Theme",
"Light": "Light",
"Dark": "Dark",
"HighContrast": "High Contrast",
"TextSize": "Text Size",
"Decrease": "Decrease",
"Increase": "Increase",
"IndentationAmount": "Indentation amount",
"Autosave": "Autosave",
"On": "On",
"Off": "Off",
"SketchSettings": "Sketch Settings",
"SecurityProtocol": "Security Protocol",
"ServeOverHTTPS": "Serve over HTTPS",
"Accessibility": "Accessibility",
"LintWarningSound": "Lint warning sound",
"PreviewSound": "Preview sound",
"AccessibleTextBasedCanvas": "Accessible text-based canvas",
"UsedScreenReader": "Used with screen reader",
"PlainText": "Plain-text",
"TableText": "Table-text",
"Sound": "Sound",
"WordWrap": "Word Wrap",
"LineNumbers": "Line numbers",
"LangChange": "Language changed",
"Welcome": "Welcome",
"Login": "Log in",
"LoginOr": "or",
"SignUp": "Sign up",
"Email": "email",
"Username": "username",
"LoginGithub": "Login with Github",
"LoginGoogle": "Login with Google",
"DontHaveAccount": "Don't have an account?",
"ForgotPassword": "Forgot your password?",
"ResetPassword": "Reset your password",
"BackEditor": "Back to Editor",
"UsernameSplit": "User Name",
"Password": "Password",
"ConfirmPassword": "Confirm Password",
"OpenedNewSketch": "Opened new sketch.",
"Hello": "Hello",
"MyAccount": "My Account",
"My":"My",
"Sketches": "My sketches",
"Collections": "My collections",
"Asset": "Asset",
"MyAssets": "My assets",
"TitleAbout": "p5.js Web Editor | About",
"CodeEditing": "Code Editing",
"Error": "Error",
"In order to save": "In order to save",
"you must be logged in": "you must be logged in",
"Please": "please",
"Find in files": "Find in files",
"Create": "Create",
"enter a name": "enter a name",
"Add": "Add",
"Folder": "Folder",
"FindText": "Find Text",
"FindNextTextMatch": "Find Next Text Match",
"FindPreviousTextMatch": "Find Previous Text Match",
"Code editing keyboard shortcuts follow": "Code editing keyboard shortcuts follow",
"Sublime Text shortcuts": "Sublime Text shortcuts",
"WarningUnsavedChanges": "Are you sure you want to leave this page? You have unsaved changes."
}

View File

@ -0,0 +1,113 @@
{
"Contribute": "Contribuir",
"NewP5": "¿Empezando con p5.js?",
"Report": "Reporta un error",
"Learn": "Aprende",
"About": "Acerca de",
"Resources": "Recursos",
"Libraries": "Bibliotecas",
"Forum": "Foro",
"File": "Archivo",
"New": "Nuevo",
"Save": "Guardar",
"Share": "Compartir",
"Duplicate": "Duplicar",
"Examples": "Ejemplos",
"Edit": "Editar",
"TidyCode": "Ordenar código",
"Find": "Buscar",
"AddToCollection": "Agregar a colección",
"FindNext": "Buscar siguiente",
"FindPrevious": "Buscar anterior",
"Sketch": "Bosquejo",
"AddFile": "Agregar archivo",
"AddFolder": "Agregar directorio",
"Run": "Ejecutar",
"Stop": "Detener",
"Help": "Ayuda",
"KeyboardShortcuts": "Atajos",
"Reference": "Referencia",
"Tidy": "Ordenar",
"Lang": "Lenguaje",
"FindNextMatch": "Encontrar siguiente ocurrencia",
"FindPrevMatch": "Encontrar ocurrencia previa",
"IndentCodeLeft": "Indentar código a la izquierda",
"IndentCodeRight": "Indentar código a la derecha",
"CommentLine": "Comentar línea de código",
"StartSketch": "Iniciar bosquejo",
"StopSketch": "Detener bosquejo",
"TurnOnAccessibleOutput": "Activar salida accesible",
"TurnOffAccessibleOutput": "Desactivar salida accesible",
"ToogleSidebar": "Alternar barra de deslizamiento",
"ToogleConsole": "Alternar consola",
"Preview": "Vista previa",
"Auto-refresh": "Auto-refrescar",
"Console": "Consola",
"Settings": "Configuración",
"GeneralSettings": "Configuración general",
"Theme": "Modo de visualización",
"Light": "Claro",
"Dark": "Oscuro",
"HighContrast": "Alto contraste",
"TextSize": "Tamaño del texto",
"Decrease": "Disminuir",
"Increase": "Aumentar",
"IndentationAmount": "Cantidad de indentación",
"Autosave": "Grabar automáticamente",
"On": "Activar",
"Off": "Desactivar",
"SketchSettings": "Configuración del bosquejo",
"SecurityProtocol": "Protocolo de seguridad",
"ServeOverHTTPS": "Usar HTTPS",
"Accessibility": "Accesibilidad",
"LintWarningSound": "Sonido de alarma Lint",
"PreviewSound": "Probar sonido",
"AccessibleTextBasedCanvas": "Lienzo accesible por texto",
"UsedScreenReader": "Uso con screen reader",
"PlainText": "Texto sin formato",
"TableText": "Tablero de texto",
"Sound": "Sonido",
"WordWrap": "Ajuste automático de línea",
"LineNumbers": "Número de línea",
"LangChange": "Lenguaje cambiado",
"Welcome": "Bienvenida",
"Login": "Ingresa",
"LoginOr": "o",
"SignUp": "registráte",
"email": "correo electrónico",
"username": "nombre de usuario",
"LoginGithub": "Ingresa con Github",
"LoginGoogle": "Ingresa con Google",
"DontHaveAccount": "No tienes cuenta?",
"ForgotPassword": "¿Olvidaste tu contraseña?",
"ResetPassword": "Regenera tu contraseña",
"BackEditor": "Regresa al editor",
"UsernameSplit": "Nombre de usuario",
"Password": "Contraseña",
"ConfirmPassword": "Confirma la contraseña",
"OpenedNewSketch": "Creaste nuevo bosquejo.",
"Hello": "Hola",
"MyAccount": "Mi Cuenta",
"My": "Mi",
"MySketches": "Mis bosquejos",
"MyCollections":"Mis colecciones",
"Asset": "Asset",
"MyAssets": "Mis assets",
"TitleAbout": "Editor Web p5.js | Acerca de",
"CodeEditing": "Editando Código",
"Error": "Error",
"In order to save": "Para guardar",
"you must be logged in": "debes ingresar a tu cuenta",
"Please": "Por favor",
"Find in files": "Encontrar en archivos",
"Create": "Create",
"enter a name": "enter a name",
"Add": "Add",
"Folder": "Directorio",
"FindText": "Encontrar texto",
"FindNextTextMatch": "Encontrar la siguiente ocurrencia de texto",
"FindPreviousTextMatch": "Encontrar la ocurrencia previa de texto",
"Code editing keyboard shortcuts follow": "Los atajos para edición son como",
"Sublime Text shortcuts": "los atajos de Sublime Text ",
"WarningUnsavedChanges": "¿Estás seguro de que quieres salir de la página? Tienes cambios sin guardar."
}