Merge branch 'develop' into p5js-to-1.0.0
2
.babelrc
|
@ -6,6 +6,7 @@
|
||||||
"env": {
|
"env": {
|
||||||
"production": {
|
"production": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"babel-plugin-styled-components",
|
||||||
"transform-react-remove-prop-types",
|
"transform-react-remove-prop-types",
|
||||||
"@babel/plugin-transform-react-constant-elements",
|
"@babel/plugin-transform-react-constant-elements",
|
||||||
"@babel/plugin-transform-react-inline-elements",
|
"@babel/plugin-transform-react-inline-elements",
|
||||||
|
@ -48,6 +49,7 @@
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"babel-plugin-styled-components",
|
||||||
"react-hot-loader/babel"
|
"react-hot-loader/babel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ API_URL=/editor
|
||||||
AWS_ACCESS_KEY=<your-aws-access-key>
|
AWS_ACCESS_KEY=<your-aws-access-key>
|
||||||
AWS_REGION=<your-aws-region>
|
AWS_REGION=<your-aws-region>
|
||||||
AWS_SECRET_KEY=<your-aws-secret-key>
|
AWS_SECRET_KEY=<your-aws-secret-key>
|
||||||
|
CORS_ALLOW_LOCALHOST=true
|
||||||
EMAIL_SENDER=<transactional-email-sender>
|
EMAIL_SENDER=<transactional-email-sender>
|
||||||
EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production
|
EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production
|
||||||
EXAMPLE_USER_EMAIL=examples@p5js.org
|
EXAMPLE_USER_EMAIL=examples@p5js.org
|
||||||
|
|
|
@ -77,5 +77,13 @@
|
||||||
"__SERVER__": true,
|
"__SERVER__": true,
|
||||||
"__DISABLE_SSR__": true,
|
"__DISABLE_SSR__": true,
|
||||||
"__DEVTOOLS__": true
|
"__DEVTOOLS__": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.stories.jsx"],
|
||||||
|
"rules": {
|
||||||
|
"import/no-extraneous-dependencies": "off"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
40
.github/CONTRIBUTING.md
vendored
|
@ -15,8 +15,6 @@ Hello! We welcome community contributions to the p5.js Web Editor. Contributing
|
||||||
- [Issue Search and Tagging](#issue-search-and-tagging)
|
- [Issue Search and Tagging](#issue-search-and-tagging)
|
||||||
- [Beginning Work](#beginning-work)
|
- [Beginning Work](#beginning-work)
|
||||||
- [Contribution Guides](#contribution-guides)
|
- [Contribution Guides](#contribution-guides)
|
||||||
- [Writing Commit Messages](#writing-commit-messages)
|
|
||||||
- [Tips](#tips)
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
|
@ -62,45 +60,9 @@ If you feel like an issue is tagged incorrectly (e.g. it's low priority and you
|
||||||
|
|
||||||
If you'd like to work on an issue, please comment on it to let the maintainers know, so that they can assign it to you. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
If you'd like to work on an issue, please comment on it to let the maintainers know, so that they can assign it to you. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
||||||
|
|
||||||
Then, follow the [installation guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md) to get the project building and working on your computer.
|
Then, look at the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md) for instructions on how to install the project locally and follow the right development workflow.
|
||||||
|
|
||||||
### Contribution Guides
|
### Contribution Guides
|
||||||
|
|
||||||
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/)
|
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/)
|
||||||
* [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/)
|
* [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/)
|
||||||
|
|
||||||
## Writing Commit Messages
|
|
||||||
|
|
||||||
Good commit messages serve at least three important purposes:
|
|
||||||
|
|
||||||
* They speed up the reviewing process.
|
|
||||||
* They help us write good release notes.
|
|
||||||
* They help future maintainers understand your change and the reasons behind it.
|
|
||||||
|
|
||||||
Structure your commit message like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
Short (50 chars or less) summary of changes ( involving Fixes #Issue-number keyword )
|
|
||||||
|
|
||||||
More detailed explanatory text, if necessary. Wrap it to about 72
|
|
||||||
characters or so. In some contexts, the first line is treated as the
|
|
||||||
subject of an email and the rest of the text as the body. The blank
|
|
||||||
line separating the summary from the body is critical (unless you omit
|
|
||||||
the body entirely); tools like rebase can get confused if you run the
|
|
||||||
two together.
|
|
||||||
|
|
||||||
Further paragraphs come after blank lines.
|
|
||||||
|
|
||||||
- Bullet points are okay, too
|
|
||||||
|
|
||||||
- Typically a hyphen or asterisk is used for the bullet, preceded by a
|
|
||||||
single space, with blank lines in between, but conventions vary here
|
|
||||||
```
|
|
||||||
|
|
||||||
* Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed".
|
|
||||||
* Always leave the second line blank.
|
|
||||||
* Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened.
|
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
* If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`.
|
|
3
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
||||||
custom: https://processingfoundation.org/support
|
github: processing
|
||||||
|
custom: https://processingfoundation.org/
|
||||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
Fixes #issue-number
|
||||||
|
|
||||||
I have verified that this pull request:
|
I have verified that this pull request:
|
||||||
|
|
||||||
* [ ] has no linting errors (`npm run lint`)
|
* [ ] has no linting errors (`npm run lint`)
|
||||||
|
|
3
.gitignore
vendored
|
@ -17,3 +17,6 @@ cert_chain.crt
|
||||||
localhost.crt
|
localhost.crt
|
||||||
localhost.key
|
localhost.key
|
||||||
privkey.pem
|
privkey.pem
|
||||||
|
|
||||||
|
storybook-static
|
||||||
|
duplicates.json
|
||||||
|
|
29
.storybook/main.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
stories: ['../client/**/*.stories.(jsx|mdx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-actions',
|
||||||
|
'@storybook/addon-docs',
|
||||||
|
'@storybook/addon-knobs',
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'storybook-addon-theme-playground/dist/register'
|
||||||
|
],
|
||||||
|
webpackFinal: async config => {
|
||||||
|
// do mutation to the config
|
||||||
|
|
||||||
|
const rules = config.module.rules;
|
||||||
|
|
||||||
|
// modify storybook's file-loader rule to avoid conflicts with svgr
|
||||||
|
const fileLoaderRule = rules.find(rule => rule.test.test('.svg'));
|
||||||
|
fileLoaderRule.exclude = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
|
// use svgr for svg files
|
||||||
|
rules.push({
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: ["@svgr/webpack"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
31
.storybook/preview.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { addDecorator, addParameters } from '@storybook/react';
|
||||||
|
import { withKnobs } from "@storybook/addon-knobs";
|
||||||
|
import { withThemePlayground } from 'storybook-addon-theme-playground';
|
||||||
|
import { ThemeProvider } from "styled-components";
|
||||||
|
|
||||||
|
import theme, { Theme } from '../client/theme';
|
||||||
|
|
||||||
|
addDecorator(withKnobs);
|
||||||
|
|
||||||
|
const themeConfigs = Object.values(Theme).map(
|
||||||
|
name => {
|
||||||
|
return { name, theme: theme[name] };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
addDecorator(withThemePlayground({
|
||||||
|
theme: themeConfigs,
|
||||||
|
provider: ThemeProvider
|
||||||
|
}));
|
||||||
|
|
||||||
|
addParameters({
|
||||||
|
options: {
|
||||||
|
/**
|
||||||
|
* display the top-level grouping as a "root" in the sidebar
|
||||||
|
*/
|
||||||
|
showRoots: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// addDecorator(storyFn => <ThemeProvider theme={theme}>{storyFn()}</ThemeProvider>);
|
|
@ -37,12 +37,13 @@ deploy:
|
||||||
script: ./deploy.sh
|
script: ./deploy.sh
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
branch: master
|
branch: release
|
||||||
|
tags: true
|
||||||
- provider: script
|
- provider: script
|
||||||
script: ./deploy_staging.sh
|
script: ./deploy_staging.sh
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
branch: feature/public-api
|
branch: develop
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
|
|
|
@ -14,6 +14,7 @@ COPY .babelrc index.js nodemon.json ./
|
||||||
COPY ./webpack ./webpack
|
COPY ./webpack ./webpack
|
||||||
COPY client ./client
|
COPY client ./client
|
||||||
COPY server ./server
|
COPY server ./server
|
||||||
|
COPY translations/locales ./translations/locales
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
FROM development as build
|
FROM development as build
|
||||||
|
|
240
client/common/Button.jsx
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
import { remSize, prop } from '../theme';
|
||||||
|
|
||||||
|
const kinds = {
|
||||||
|
block: 'block',
|
||||||
|
icon: 'icon',
|
||||||
|
inline: 'inline',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The '&&&' will increase the specificity of the
|
||||||
|
// component's CSS so that it overrides the more
|
||||||
|
// general global styles
|
||||||
|
const StyledButton = styled.button`
|
||||||
|
&&& {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: max-content;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
color: ${prop('Button.default.foreground')};
|
||||||
|
background-color: ${prop('Button.default.background')};
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid ${prop('Button.default.border')};
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: ${remSize(8)} ${remSize(25)};
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.default.foreground')};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: ${prop('Button.hover.foreground')};
|
||||||
|
background-color: ${prop('Button.hover.background')};
|
||||||
|
border-color: ${prop('Button.hover.border')};
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.hover.foreground')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
color: ${prop('Button.active.foreground')};
|
||||||
|
background-color: ${prop('Button.active.background')};
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.active.foreground')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: ${prop('Button.disabled.foreground')};
|
||||||
|
background-color: ${prop('Button.disabled.background')};
|
||||||
|
border-color: ${prop('Button.disabled.border')};
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.disabled.foreground')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: ${remSize(8)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInlineButton = styled.button`
|
||||||
|
&&& {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('primaryTextColor')};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: ${remSize(8)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconButton = styled.button`
|
||||||
|
&&& {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: ${remSize(32)}px;
|
||||||
|
height: ${remSize(32)}px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
color: ${prop('Button.default.foreground')};
|
||||||
|
background-color: ${prop('Button.hover.background')};
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: ${remSize(8)} ${remSize(25)};
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: ${prop('Button.hover.foreground')};
|
||||||
|
background-color: ${prop('Button.hover.background')};
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.hover.foreground')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
color: ${prop('Button.active.foreground')};
|
||||||
|
background-color: ${prop('Button.active.background')};
|
||||||
|
|
||||||
|
svg * {
|
||||||
|
fill: ${prop('Button.active.foreground')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: ${prop('Button.disabled.foreground')};
|
||||||
|
background-color: ${prop('Button.disabled.background')};
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: ${remSize(8)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Button performs an primary action
|
||||||
|
*/
|
||||||
|
const Button = ({
|
||||||
|
children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props
|
||||||
|
}) => {
|
||||||
|
const hasChildren = React.Children.count(children) > 0;
|
||||||
|
const content = <>{iconBefore}{hasChildren && <span>{children}</span>}{iconAfter}</>;
|
||||||
|
let StyledComponent = StyledButton;
|
||||||
|
|
||||||
|
if (kind === kinds.inline) {
|
||||||
|
StyledComponent = StyledInlineButton;
|
||||||
|
} else if (kind === kinds.icon) {
|
||||||
|
StyledComponent = StyledIconButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<StyledComponent
|
||||||
|
kind={kind}
|
||||||
|
as="a"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</StyledComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to) {
|
||||||
|
return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.defaultProps = {
|
||||||
|
'children': null,
|
||||||
|
'disabled': false,
|
||||||
|
'iconAfter': null,
|
||||||
|
'iconBefore': null,
|
||||||
|
'kind': kinds.block,
|
||||||
|
'href': null,
|
||||||
|
'aria-label': null,
|
||||||
|
'to': null,
|
||||||
|
'type': 'button',
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.kinds = kinds;
|
||||||
|
|
||||||
|
Button.propTypes = {
|
||||||
|
/**
|
||||||
|
* The visible part of the button, telling the user what
|
||||||
|
* the action is
|
||||||
|
*/
|
||||||
|
'children': PropTypes.element,
|
||||||
|
/**
|
||||||
|
If the button can be activated or not
|
||||||
|
*/
|
||||||
|
'disabled': PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* SVG icon to place after child content
|
||||||
|
*/
|
||||||
|
'iconAfter': PropTypes.element,
|
||||||
|
/**
|
||||||
|
* SVG icon to place before child content
|
||||||
|
*/
|
||||||
|
'iconBefore': PropTypes.element,
|
||||||
|
/**
|
||||||
|
* The kind of button - determines how it appears visually
|
||||||
|
*/
|
||||||
|
'kind': PropTypes.oneOf(Object.values(kinds)),
|
||||||
|
/**
|
||||||
|
* Specifying an href will use an <a> to link to the URL
|
||||||
|
*/
|
||||||
|
'href': PropTypes.string,
|
||||||
|
/*
|
||||||
|
* An ARIA Label used for accessibility
|
||||||
|
*/
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Specifying a to URL will use a react-router Link
|
||||||
|
*/
|
||||||
|
'to': PropTypes.string,
|
||||||
|
/**
|
||||||
|
* If using a button, then type is defines the type of button
|
||||||
|
*/
|
||||||
|
'type': PropTypes.oneOf(['button', 'submit']),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
70
client/common/Button.stories.jsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { boolean, text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import Button from './Button';
|
||||||
|
import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Common/Button',
|
||||||
|
component: Button
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllFeatures = () => (
|
||||||
|
<Button
|
||||||
|
disabled={boolean('disabled', false)}
|
||||||
|
type="submit"
|
||||||
|
label={text('label', 'submit')}
|
||||||
|
>
|
||||||
|
{text('children', 'this is the button')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SubmitButton = () => (
|
||||||
|
<Button type="submit" label="submit">This is a submit button</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>;
|
||||||
|
|
||||||
|
export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>;
|
||||||
|
|
||||||
|
export const AnchorButton = () => (
|
||||||
|
<Button href="http://p5js.org" label="submit">Actually an anchor</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ReactRouterLink = () => (
|
||||||
|
<Button to="./somewhere" label="submit">Actually a Link</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ButtonWithIconBefore = () => (
|
||||||
|
<Button
|
||||||
|
iconBefore={<GithubIcon aria-label="Github logo" />}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ButtonWithIconAfter = () => (
|
||||||
|
<Button
|
||||||
|
iconAfter={<GithubIcon aria-label="Github logo" />}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InlineButtonWithIconAfter = () => (
|
||||||
|
<Button
|
||||||
|
iconAfter={<DropdownArrowIcon />}
|
||||||
|
kind={Button.kinds.inline}
|
||||||
|
>
|
||||||
|
File name
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InlineIconOnlyButton = () => (
|
||||||
|
<Button
|
||||||
|
aria-label="Add to collection"
|
||||||
|
iconBefore={<PlusIcon />}
|
||||||
|
kind={Button.kinds.inline}
|
||||||
|
/>
|
||||||
|
);
|
80
client/common/icons.jsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { prop } from '../theme';
|
||||||
|
import SortArrowUp from '../images/sort-arrow-up.svg';
|
||||||
|
import SortArrowDown from '../images/sort-arrow-down.svg';
|
||||||
|
import Github from '../images/github.svg';
|
||||||
|
import Google from '../images/google.svg';
|
||||||
|
import Plus from '../images/plus-icon.svg';
|
||||||
|
import Close from '../images/close.svg';
|
||||||
|
import Exit from '../images/exit.svg';
|
||||||
|
import DropdownArrow from '../images/down-filled-triangle.svg';
|
||||||
|
import Preferences from '../images/preferences.svg';
|
||||||
|
import Play from '../images/triangle-arrow-right.svg';
|
||||||
|
import Code from '../images/code.svg';
|
||||||
|
import Terminal from '../images/terminal.svg';
|
||||||
|
|
||||||
|
|
||||||
|
// HOC that adds the right web accessibility props
|
||||||
|
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
||||||
|
|
||||||
|
// could also give these a default size, color, etc. based on the theme
|
||||||
|
// Need to add size to these - like small icon, medium icon, large icon. etc.
|
||||||
|
function withLabel(SvgComponent) {
|
||||||
|
const Icon = (props) => {
|
||||||
|
const StyledIcon = styled(SvgComponent)`
|
||||||
|
&&& {
|
||||||
|
color: ${prop('Icon.default')};
|
||||||
|
& g, & path, & polygon {
|
||||||
|
opacity: 1;
|
||||||
|
fill: ${prop('Icon.default')};
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: ${prop('Icon.hover')};
|
||||||
|
& g, & path, & polygon {
|
||||||
|
opacity: 1;
|
||||||
|
fill: ${prop('Icon.hover')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { 'aria-label': ariaLabel } = props;
|
||||||
|
if (ariaLabel) {
|
||||||
|
return (<StyledIcon
|
||||||
|
{...props}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
role="img"
|
||||||
|
focusable="false"
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
return (<StyledIcon
|
||||||
|
{...props}
|
||||||
|
aria-hidden
|
||||||
|
focusable="false"
|
||||||
|
/>);
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon.propTypes = {
|
||||||
|
'aria-label': PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
Icon.defaultProps = {
|
||||||
|
'aria-label': null
|
||||||
|
};
|
||||||
|
|
||||||
|
return Icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SortArrowUpIcon = withLabel(SortArrowUp);
|
||||||
|
export const SortArrowDownIcon = withLabel(SortArrowDown);
|
||||||
|
export const GithubIcon = withLabel(Github);
|
||||||
|
export const GoogleIcon = withLabel(Google);
|
||||||
|
export const PlusIcon = withLabel(Plus);
|
||||||
|
export const CloseIcon = withLabel(Close);
|
||||||
|
export const ExitIcon = withLabel(Exit);
|
||||||
|
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
||||||
|
export const PreferencesIcon = withLabel(Preferences);
|
||||||
|
export const PlayIcon = withLabel(Play);
|
||||||
|
export const TerminalIcon = withLabel(Terminal);
|
18
client/common/icons.stories.jsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { select } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import * as icons from './icons';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Common/Icons',
|
||||||
|
component: icons
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllIcons = () => {
|
||||||
|
const names = Object.keys(icons);
|
||||||
|
|
||||||
|
const SelectedIcon = icons[select('name', names, names[0])];
|
||||||
|
return (
|
||||||
|
<SelectedIcon />
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,17 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const addIcon = require('../images/plus.svg');
|
import AddIcon from '../images/plus.svg';
|
||||||
const removeIcon = require('../images/minus.svg');
|
import RemoveIcon from '../images/minus.svg';
|
||||||
|
|
||||||
const AddRemoveButton = ({ type, onClick }) => {
|
const AddRemoveButton = ({ type, onClick }) => {
|
||||||
const alt = type === 'add' ? 'add to collection' : 'remove from collection';
|
const alt = type === 'add' ? 'Add to collection' : 'Remove from collection';
|
||||||
const icon = type === 'add' ? addIcon : removeIcon;
|
const Icon = type === 'add' ? AddIcon : RemoveIcon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="overlay__close-button" onClick={onClick}>
|
<button
|
||||||
<InlineSVG src={icon} alt={alt} />
|
className="overlay__close-button"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label={alt}
|
||||||
|
>
|
||||||
|
<Icon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,21 +3,21 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
import * as toastActions from '../modules/IDE/actions/toast';
|
import * as toastActions from '../modules/IDE/actions/toast';
|
||||||
import * as projectActions from '../modules/IDE/actions/project';
|
import * as projectActions from '../modules/IDE/actions/project';
|
||||||
import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
|
import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
|
||||||
import { logoutUser } from '../modules/User/actions';
|
import { logoutUser } from '../modules/User/actions';
|
||||||
|
|
||||||
|
import getConfig from '../utils/getConfig';
|
||||||
import { metaKeyName, } from '../utils/metaKey';
|
import { metaKeyName, } from '../utils/metaKey';
|
||||||
import caretLeft from '../images/left-arrow.svg';
|
|
||||||
|
|
||||||
const triangleUrl = require('../images/down-filled-triangle.svg');
|
import CaretLeftIcon from '../images/left-arrow.svg';
|
||||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
import TriangleIcon from '../images/down-filled-triangle.svg';
|
||||||
|
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
|
|
||||||
class Nav extends React.PureComponent {
|
class Nav extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -57,6 +57,10 @@ class Nav extends React.PureComponent {
|
||||||
this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
|
this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
|
||||||
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
|
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
|
||||||
this.handleFocusForAccount = this.handleFocus.bind(this, 'account');
|
this.handleFocusForAccount = this.handleFocus.bind(this, 'account');
|
||||||
|
this.toggleDropdownForLang = this.toggleDropdown.bind(this, 'lang');
|
||||||
|
this.handleFocusForLang = this.handleFocus.bind(this, 'lang');
|
||||||
|
this.handleLangSelection = this.handleLangSelection.bind(this);
|
||||||
|
|
||||||
this.closeDropDown = this.closeDropDown.bind(this);
|
this.closeDropDown = this.closeDropDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +169,13 @@ class Nav extends React.PureComponent {
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLangSelection(event) {
|
||||||
|
i18next.changeLanguage(event.target.value);
|
||||||
|
this.props.showToast(1500);
|
||||||
|
this.props.setToastText('LangChange');
|
||||||
|
this.setDropdown('none');
|
||||||
|
}
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
this.props.logoutUser();
|
this.props.logoutUser();
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
|
@ -229,13 +240,13 @@ class Nav extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-left">
|
<ul className="nav__items-left">
|
||||||
<li className="nav__item-logo">
|
<li className="nav__item-logo">
|
||||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__item nav__item--no-icon">
|
<li className="nav__item nav__item--no-icon">
|
||||||
<Link to="/" className="nav__back-link">
|
<Link to="/" className="nav__back-link">
|
||||||
<InlineSVG src={caretLeft} className="nav__back-icon" />
|
<CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" />
|
||||||
<span className="nav__item-header">
|
<span className="nav__item-header">
|
||||||
Back to Editor
|
{this.props.t('BackEditor')}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -247,7 +258,7 @@ class Nav extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-left">
|
<ul className="nav__items-left">
|
||||||
<li className="nav__item-logo">
|
<li className="nav__item-logo">
|
||||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||||
</li>
|
</li>
|
||||||
<li className={navDropdownState.file}>
|
<li className={navDropdownState.file}>
|
||||||
<button
|
<button
|
||||||
|
@ -260,8 +271,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">File</span>
|
<span className="nav__item-header">{this.props.t('File')}</span>
|
||||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -270,18 +281,18 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
New
|
{this.props.t('New')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{ __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) &&
|
{ getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
<button
|
<button
|
||||||
onClick={this.handleSave}
|
onClick={this.handleSave}
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Save
|
{this.props.t('Save')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+s</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.project.id && this.props.user.authenticated &&
|
{ this.props.project.id && this.props.user.authenticated &&
|
||||||
|
@ -291,7 +302,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Duplicate
|
{this.props.t('Duplicate')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.project.id &&
|
{ this.props.project.id &&
|
||||||
|
@ -301,7 +312,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Share
|
{this.props.t('Share')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.project.id &&
|
{ this.props.project.id &&
|
||||||
|
@ -311,7 +322,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Download
|
{this.props.t('Download')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.user.authenticated &&
|
{ this.props.user.authenticated &&
|
||||||
|
@ -322,10 +333,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Open
|
{this.props.t('Open')}
|
||||||
</Link>
|
</Link>
|
||||||
</li> }
|
</li> }
|
||||||
{__process.env.UI_COLLECTIONS_ENABLED &&
|
{getConfig('UI_COLLECTIONS_ENABLED') &&
|
||||||
this.props.user.authenticated &&
|
this.props.user.authenticated &&
|
||||||
this.props.project.id &&
|
this.props.project.id &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -335,10 +346,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Add to Collection
|
{this.props.t('AddToCollection')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>}
|
</li>}
|
||||||
{ __process.env.EXAMPLES_ENABLED &&
|
{ getConfig('EXAMPLES_ENABLED') &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
<Link
|
<Link
|
||||||
to="/p5/sketches"
|
to="/p5/sketches"
|
||||||
|
@ -346,7 +357,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Examples
|
{this.props.t('Examples')}
|
||||||
</Link>
|
</Link>
|
||||||
</li> }
|
</li> }
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -362,8 +373,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Edit</span>
|
<span className="nav__item-header">{this.props.t('Edit')}</span>
|
||||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown" >
|
<ul className="nav__dropdown" >
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -375,7 +386,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Tidy Code
|
{this.props.t('TidyCode')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -385,7 +396,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find
|
{this.props.t('Find')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -395,7 +406,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find Next
|
{this.props.t('FindNext')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -405,7 +416,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find Previous
|
{this.props.t('FindPrevious')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -422,8 +433,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Sketch</span>
|
<span className="nav__item-header">{this.props.t('Sketch')}</span>
|
||||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -432,7 +443,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Add File
|
{this.props.t('AddFile')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -441,7 +452,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Add Folder
|
{this.props.t('AddFolder')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -450,7 +461,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Run
|
{this.props.t('Run')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -460,7 +471,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Stop
|
{this.props.t('Stop')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -497,8 +508,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Help</span>
|
<span className="nav__item-header">{this.props.t('Help')}</span>
|
||||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -507,7 +518,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.handleKeyboardShortcuts}
|
onClick={this.handleKeyboardShortcuts}
|
||||||
>
|
>
|
||||||
Keyboard Shortcuts
|
{this.props.t('KeyboardShortcuts')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -518,7 +529,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForHelp}
|
onFocus={this.handleFocusForHelp}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>Reference
|
>{this.props.t('Reference')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -528,7 +539,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
About
|
{this.props.t('About')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -537,18 +548,73 @@ class Nav extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderLanguageMenu(navDropdownState) {
|
||||||
|
return (
|
||||||
|
<ul className="nav__items-right" title="user-menu">
|
||||||
|
<li className={navDropdownState.lang}>
|
||||||
|
<button
|
||||||
|
onClick={this.toggleDropdownForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onFocus={this.clearHideTimeout}
|
||||||
|
onMouseOver={() => {
|
||||||
|
if (this.state.dropdownOpen !== 'none') {
|
||||||
|
this.setDropdown('lang');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="nav__item-header"> {this.props.t('Lang')}</span>
|
||||||
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<ul className="nav__dropdown">
|
||||||
|
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="it"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>
|
||||||
|
Italian (Test Fallback)
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="en-US"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>English
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="es-419"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>
|
||||||
|
Español
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
renderUnauthenticatedUserMenu(navDropdownState) {
|
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<Link to="/login">
|
<Link to="/login" className="nav__auth-button">
|
||||||
<span className="nav__item-header">Log in</span>
|
<span className="nav__item-header">{this.props.t('Login')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-spacer">or</span>
|
<span className="nav__item-or">{this.props.t('LoginOr')}</span>
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<Link to="/signup">
|
<Link to="/signup" className="nav__auth-button">
|
||||||
<span className="nav__item-header">Sign up</span>
|
<span className="nav__item-header">{this.props.t('SignUp')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -559,7 +625,7 @@ class Nav extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<span>Hello, {this.props.user.username}!</span>
|
<span>{this.props.t('Hello')}, {this.props.user.username}!</span>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-spacer">|</span>
|
<span className="nav__item-spacer">|</span>
|
||||||
<li className={navDropdownState.account}>
|
<li className={navDropdownState.account}>
|
||||||
|
@ -574,8 +640,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
My Account
|
{this.props.t('MyAccount')}
|
||||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -585,10 +651,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My sketches
|
{this.props.t('MySketches')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{__process.env.UI_COLLECTIONS_ENABLED &&
|
{getConfig('UI_COLLECTIONS_ENABLED') &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
<Link
|
<Link
|
||||||
to={`/${this.props.user.username}/collections`}
|
to={`/${this.props.user.username}/collections`}
|
||||||
|
@ -596,7 +662,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My collections
|
{this.props.t('MyCollections')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
@ -607,7 +673,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My assets
|
{this.props.t('MyAssets')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -617,7 +683,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Settings
|
{this.props.t('Settings')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -626,7 +692,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForAccount}
|
onFocus={this.handleFocusForAccount}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Log out
|
{this.props.t('LogOut')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -636,7 +702,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserMenu(navDropdownState) {
|
renderUserMenu(navDropdownState) {
|
||||||
const isLoginEnabled = __process.env.LOGIN_ENABLED;
|
const isLoginEnabled = getConfig('LOGIN_ENABLED');
|
||||||
const isAuthenticated = this.props.user.authenticated;
|
const isAuthenticated = this.props.user.authenticated;
|
||||||
|
|
||||||
if (isLoginEnabled && isAuthenticated) {
|
if (isLoginEnabled && isAuthenticated) {
|
||||||
|
@ -679,25 +745,21 @@ class Nav extends React.PureComponent {
|
||||||
account: classNames({
|
account: classNames({
|
||||||
'nav__item': true,
|
'nav__item': true,
|
||||||
'nav__item--open': this.state.dropdownOpen === 'account'
|
'nav__item--open': this.state.dropdownOpen === 'account'
|
||||||
|
}),
|
||||||
|
lang: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'lang'
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<header>
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
{this.renderLeftLayout(navDropdownState)}
|
{this.renderLeftLayout(navDropdownState)}
|
||||||
|
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
||||||
{this.renderUserMenu(navDropdownState)}
|
{this.renderUserMenu(navDropdownState)}
|
||||||
{/*
|
|
||||||
<div className="nav__announce">
|
|
||||||
This is a preview version of the editor, that has not yet been officially released.
|
|
||||||
It is in development, you can report bugs <a
|
|
||||||
href="https://github.com/processing/p5.js-web-editor/issues"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>here</a>.
|
|
||||||
Please use with caution.
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -745,7 +807,9 @@ Nav.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
})
|
}),
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Nav.defaultProps = {
|
Nav.defaultProps = {
|
||||||
|
@ -778,5 +842,5 @@ const mapDispatchToProps = {
|
||||||
setAllAccessibleOutput
|
setAllAccessibleOutput
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav));
|
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
|
||||||
export { Nav as NavComponent };
|
export { Nav as NavComponent };
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||||
const arrowUrl = require('../images/triangle-arrow-left.svg');
|
import ArrowIcon from '../images/triangle-arrow-left.svg';
|
||||||
|
|
||||||
class NavBasic extends React.PureComponent {
|
class NavBasic extends React.PureComponent {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -15,13 +14,13 @@ class NavBasic extends React.PureComponent {
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
<ul className="nav__items-left">
|
<ul className="nav__items-left">
|
||||||
<li className="nav__item-logo">
|
<li className="nav__item-logo">
|
||||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||||
</li>
|
</li>
|
||||||
{ this.props.onBack && (
|
{ this.props.onBack && (
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<button onClick={this.props.onBack}>
|
<button onClick={this.props.onBack}>
|
||||||
<span className="nav__item-header">
|
<span className="nav__item-header">
|
||||||
<InlineSVG src={arrowUrl} alt="Left arrow" />
|
<ArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
Back to the editor
|
Back to the editor
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||||
const editorUrl = require('../images/code.svg');
|
import CodeIcon from '../images/code.svg';
|
||||||
|
|
||||||
const PreviewNav = ({ owner, project }) => (
|
const PreviewNav = ({ owner, project }) => (
|
||||||
<nav className="nav preview-nav">
|
<nav className="nav preview-nav">
|
||||||
<div className="nav__items-left">
|
<div className="nav__items-left">
|
||||||
<div className="nav__item-logo">
|
<div className="nav__item-logo">
|
||||||
<InlineSVG src={logoUrl} alt="p5.js logo" />
|
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||||
</div>
|
</div>
|
||||||
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
|
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
|
||||||
<p className="toolbar__project-owner">by</p>
|
<p className="toolbar__project-owner">by</p>
|
||||||
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
|
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="nav__items-right">
|
<div className="nav__items-right">
|
||||||
<Link to={`/${owner.username}/sketches/${project.id}`}>
|
<Link to={`/${owner.username}/sketches/${project.id}`} aria-label="Edit Sketch" >
|
||||||
<InlineSVG className="preview-nav__editor-svg" src={editorUrl} />
|
<CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { FileNode } from '../../modules/IDE/components/FileNode';
|
|
||||||
|
|
||||||
beforeAll(() => {});
|
|
||||||
describe('<FileNode />', () => {
|
|
||||||
let component;
|
|
||||||
let props = {};
|
|
||||||
|
|
||||||
describe('with valid props', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
id: '0',
|
|
||||||
children: [],
|
|
||||||
name: 'test.jsx',
|
|
||||||
fileType: 'dunno',
|
|
||||||
setSelectedFile: jest.fn(),
|
|
||||||
deleteFile: jest.fn(),
|
|
||||||
updateFileName: jest.fn(),
|
|
||||||
resetSelectedFile: jest.fn(),
|
|
||||||
newFile: jest.fn(),
|
|
||||||
newFolder: jest.fn(),
|
|
||||||
showFolderChildren: jest.fn(),
|
|
||||||
hideFolderChildren: jest.fn(),
|
|
||||||
canEdit: true,
|
|
||||||
authenticated: false
|
|
||||||
};
|
|
||||||
component = shallow(<FileNode {...props} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when changing name', () => {
|
|
||||||
let input;
|
|
||||||
let renameTriggerButton;
|
|
||||||
const changeName = (newFileName) => {
|
|
||||||
renameTriggerButton.simulate('click');
|
|
||||||
input.simulate('change', { target: { value: newFileName } });
|
|
||||||
input.simulate('blur');
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
input = component.find('.sidebar__file-item-input');
|
|
||||||
renameTriggerButton = component
|
|
||||||
.find('.sidebar__file-item-option')
|
|
||||||
.first();
|
|
||||||
component.setState({ isEditing: true });
|
|
||||||
});
|
|
||||||
it('should render', () => expect(component).toBeDefined());
|
|
||||||
|
|
||||||
// it('should debug', () => console.log(component.debug()));
|
|
||||||
|
|
||||||
describe('to a valid filename', () => {
|
|
||||||
const newName = 'newname.jsx';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should save the name', () => {
|
|
||||||
expect(props.updateFileName).toBeCalledWith(props.id, newName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to an empty filename', () => {
|
|
||||||
const newName = '';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should not save the name', () => {
|
|
||||||
expect(props.updateFileName).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { render } from '@testing-library/react';
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { NavComponent } from './../Nav';
|
|
||||||
|
import { NavComponent } from '../Nav';
|
||||||
|
|
||||||
describe('Nav', () => {
|
describe('Nav', () => {
|
||||||
const props = {
|
const props = {
|
||||||
|
@ -44,19 +44,12 @@ describe('Nav', () => {
|
||||||
setToastText: jest.fn(),
|
setToastText: jest.fn(),
|
||||||
rootFile: {
|
rootFile: {
|
||||||
id: 'root-file'
|
id: 'root-file'
|
||||||
}
|
},
|
||||||
|
t: jest.fn()
|
||||||
};
|
};
|
||||||
const getWrapper = () => shallow(<NavComponent {...props} />);
|
|
||||||
|
|
||||||
test('it renders main navigation', () => {
|
|
||||||
const nav = getWrapper();
|
|
||||||
expect(nav.exists('.nav')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer
|
const { asFragment } = render(<NavComponent {...props} />);
|
||||||
.create(<NavComponent {...props} />)
|
expect(asFragment()).toMatchSnapshot();
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,333 +1,219 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Nav renders correctly 1`] = `
|
exports[`Nav renders correctly 1`] = `
|
||||||
<nav
|
<DocumentFragment>
|
||||||
className="nav"
|
<header>
|
||||||
|
<nav
|
||||||
|
class="nav"
|
||||||
title="main-navigation"
|
title="main-navigation"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="nav__items-left"
|
class="nav__items-left"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__item-logo"
|
class="nav__item-logo"
|
||||||
>
|
>
|
||||||
<span
|
<test-file-stub
|
||||||
className="isvg loading svg__logo"
|
aria-label="p5.js Logo"
|
||||||
|
classname="svg__logo"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
File
|
<test-file-stub
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<span
|
classname="nav__item-header-triangle"
|
||||||
className="isvg loading nav__item-header-triangle"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Duplicate
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={Object {}}
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Edit
|
<test-file-stub
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<span
|
classname="nav__item-header-triangle"
|
||||||
className="isvg loading nav__item-header-triangle"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Tidy Code
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+Tab
|
||||||
+Tab
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
Ctrl
|
⌃+F
|
||||||
+F
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find Next
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
Ctrl
|
⌃+G
|
||||||
+G
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find Previous
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+⌃+G
|
||||||
+
|
|
||||||
Ctrl
|
|
||||||
+G
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Sketch
|
<test-file-stub
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<span
|
classname="nav__item-header-triangle"
|
||||||
className="isvg loading nav__item-header-triangle"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Add File
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Add Folder
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Run
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
Ctrl
|
⌃+Enter
|
||||||
+Enter
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+⌃+Enter
|
||||||
+
|
|
||||||
Ctrl
|
|
||||||
+Enter
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Help
|
<test-file-stub
|
||||||
</span>
|
aria-hidden="true"
|
||||||
<span
|
classname="nav__item-header-triangle"
|
||||||
className="isvg loading nav__item-header-triangle"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Keyboard Shortcuts
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/reference/"
|
href="https://p5js.org/reference/"
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
/>
|
||||||
Reference
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={Object {}}
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
</header>
|
||||||
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
35
client/components/mobile/ActionStrip.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { remSize } from '../../theme';
|
||||||
|
import IconButton from './IconButton';
|
||||||
|
import { TerminalIcon } from '../../common/icons';
|
||||||
|
import * as IDEActions from '../../modules/IDE/actions/ide';
|
||||||
|
|
||||||
|
const BottomBarContent = styled.h2`
|
||||||
|
padding: ${remSize(8)};
|
||||||
|
|
||||||
|
svg {
|
||||||
|
max-height: ${remSize(32)};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch());
|
||||||
|
const { consoleIsExpanded } = useSelector(state => state.ide);
|
||||||
|
|
||||||
|
const actions = [{ icon: TerminalIcon, aria: 'Say Something', action: consoleIsExpanded ? collapseConsole : expandConsole }];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomBarContent>
|
||||||
|
{actions.map(({ icon, aria, action }) =>
|
||||||
|
(<IconButton
|
||||||
|
icon={icon}
|
||||||
|
aria-label={aria}
|
||||||
|
key={`bottom-bar-${aria}`}
|
||||||
|
onClick={() => action()}
|
||||||
|
/>))}
|
||||||
|
</BottomBarContent>
|
||||||
|
);
|
||||||
|
};
|
17
client/components/mobile/Footer.jsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { prop, grays } from '../../theme';
|
||||||
|
|
||||||
|
|
||||||
|
const background = prop('MobilePanel.default.background');
|
||||||
|
const textColor = prop('primaryTextColor');
|
||||||
|
|
||||||
|
export default styled.div`
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
background: ${background};
|
||||||
|
color: ${textColor};
|
||||||
|
|
||||||
|
& > * + * { border-top: dashed 1px ${prop('Separator')} }
|
||||||
|
`;
|
79
client/components/mobile/Header.jsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { prop, remSize } from '../../theme';
|
||||||
|
|
||||||
|
const background = transparent => prop(transparent ? 'backgroundColor' : 'MobilePanel.default.background');
|
||||||
|
const textColor = prop('primaryTextColor');
|
||||||
|
|
||||||
|
|
||||||
|
const HeaderDiv = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
background: ${props => background(props.transparent === true)};
|
||||||
|
color: ${textColor};
|
||||||
|
padding: ${remSize(12)};
|
||||||
|
padding-left: ${remSize(16)};
|
||||||
|
padding-right: ${remSize(16)};
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
max-height: ${remSize(32)};
|
||||||
|
padding: ${remSize(4)}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const TitleContainer = styled.div`
|
||||||
|
margin-left: ${remSize(4)};
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
${props => props.padded && `h2{
|
||||||
|
padding-top: ${remSize(10)};
|
||||||
|
padding-bottom: ${remSize(10)};
|
||||||
|
}`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
title, subtitle, leftButton, children, transparent
|
||||||
|
}) => (
|
||||||
|
<HeaderDiv transparent={transparent}>
|
||||||
|
{leftButton}
|
||||||
|
<TitleContainer padded={subtitle === null}>
|
||||||
|
{title && <h2>{title}</h2>}
|
||||||
|
{subtitle && <h3>{subtitle}</h3>}
|
||||||
|
</TitleContainer>
|
||||||
|
<IconContainer>
|
||||||
|
{children}
|
||||||
|
</IconContainer>
|
||||||
|
</HeaderDiv>
|
||||||
|
);
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
subtitle: PropTypes.string,
|
||||||
|
leftButton: PropTypes.element,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
|
||||||
|
transparent: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.defaultProps = {
|
||||||
|
title: null,
|
||||||
|
subtitle: null,
|
||||||
|
leftButton: null,
|
||||||
|
children: [],
|
||||||
|
transparent: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
8
client/components/mobile/IDEWrapper.jsx
Normal 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)};
|
||||||
|
`;
|
31
client/components/mobile/IconButton.jsx
Normal 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;
|
19
client/components/mobile/MobileScreen.jsx
Normal 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;
|
67
client/components/mobile/PreferencePicker.jsx
Normal 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
|
@ -0,0 +1,38 @@
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import Backend from 'i18next-http-backend';
|
||||||
|
|
||||||
|
const fallbackLng = ['en-US'];
|
||||||
|
const availableLanguages = ['en-US', 'es-419'];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
loadPath: '/locales/{{lng}}/translations.json',
|
||||||
|
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
||||||
|
mode: 'no-cors'
|
||||||
|
},
|
||||||
|
allowMultiLoading: false, // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next) // pass the i18n instance to react-i18next.
|
||||||
|
.use(LanguageDetector)// to detect the language from currentBrowser
|
||||||
|
.use(Backend) // to fetch the data from server
|
||||||
|
.init({
|
||||||
|
lng: 'en-US',
|
||||||
|
defaultNS: 'WebEditor',
|
||||||
|
fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier
|
||||||
|
debug: false,
|
||||||
|
backend: options,
|
||||||
|
getAsync: false,
|
||||||
|
initImmediate: false,
|
||||||
|
useSuspense: true,
|
||||||
|
whitelist: availableLanguages,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // react already safes from xss
|
||||||
|
},
|
||||||
|
saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler
|
||||||
|
missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
54
client/images/console-debug-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="console-debug-contrast.svg"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="640"
|
||||||
|
inkscape:window-height="480"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="23.6"
|
||||||
|
inkscape:cx="5"
|
||||||
|
inkscape:cy="5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
|
||||||
|
id="path2"
|
||||||
|
fill="#38B6F5" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -50,5 +50,5 @@
|
||||||
<path
|
<path
|
||||||
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
|
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
|
||||||
id="path2"
|
id="path2"
|
||||||
fill="#007BBB" />
|
fill="#0071AD" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
54
client/images/console-error-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="console-error-contrast.svg"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="640"
|
||||||
|
inkscape:window-height="480"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="23.6"
|
||||||
|
inkscape:cx="5"
|
||||||
|
inkscape:cy="5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||||
|
id="path2"
|
||||||
|
fill="#EA7B7D" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
client/images/console-info-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="console-info-contrast.svg"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="640"
|
||||||
|
inkscape:window-height="480"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="23.6"
|
||||||
|
inkscape:cx="5"
|
||||||
|
inkscape:cy="5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||||
|
id="path2"
|
||||||
|
fill="#D9D9D9" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -50,5 +50,5 @@
|
||||||
<path
|
<path
|
||||||
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||||
id="path2"
|
id="path2"
|
||||||
fill="#a3a3a3" />
|
fill="#D9D9D9" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -50,5 +50,5 @@
|
||||||
<path
|
<path
|
||||||
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||||
id="path2"
|
id="path2"
|
||||||
fill="#7D7D7D" />
|
fill="#4D4D4D" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
54
client/images/console-warn-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.1"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="console-warn-contrast.svg"
|
||||||
|
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="640"
|
||||||
|
inkscape:window-height="480"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="23.6"
|
||||||
|
inkscape:cx="5"
|
||||||
|
inkscape:cy="5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<path
|
||||||
|
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
|
||||||
|
id="path2"
|
||||||
|
fill="#f5bc38" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -50,5 +50,5 @@
|
||||||
<path
|
<path
|
||||||
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
|
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
|
||||||
id="path2"
|
id="path2"
|
||||||
fill="#FAAF00" />
|
fill="#996B00" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -3,8 +3,8 @@
|
||||||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1.0">
|
||||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#333333">
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#AAA">
|
||||||
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||||
<polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon>
|
<polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 979 B After Width: | Height: | Size: 968 B |
|
@ -5,7 +5,7 @@
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="exit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="exit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="Artboard-1" fill="#D8D8D8">
|
<g id="Artboard-1" fill="#AAA">
|
||||||
<path d="M8,5.87867966 L2.69669914,0.575378798 L0.575378798,2.69669914 L5.87867966,8 L0.575378798,13.3033009 L2.69669914,15.4246212 L8,10.1213203 L13.3033009,15.4246212 L15.4246212,13.3033009 L10.1213203,8 L15.4246212,2.69669914 L13.3033009,0.575378798 L8,5.87867966 Z" id="exit"></path>
|
<path d="M8,5.87867966 L2.69669914,0.575378798 L0.575378798,2.69669914 L5.87867966,8 L0.575378798,13.3033009 L2.69669914,15.4246212 L8,10.1213203 L13.3033009,15.4246212 L15.4246212,13.3033009 L10.1213203,8 L15.4246212,2.69669914 L13.3033009,0.575378798 L8,5.87867966 Z" id="exit"></path>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 826 B After Width: | Height: | Size: 823 B |
|
@ -1,47 +1,9 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<g>
|
||||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
<path d="M7.091875,19.338 L5.978,23.49625 L1.9068125,23.582375 C0.690125,21.3256875 0,18.74375 0,16 C0,13.3468125 0.64525,10.8448125 1.789,8.64175 L1.789875,8.64175 L5.414375,9.30625 L7.002125,12.909 C6.6698125,13.8778125 6.4886875,14.9178125 6.4886875,16 C6.4888125,17.1745 6.7015625,18.2998125 7.091875,19.338 Z" fill="#FBBB00"></path>
|
||||||
<path style="fill:#FBBB00;" d="M113.47,309.408L95.648,375.94l-65.139,1.378C11.042,341.211,0,299.9,0,256
|
<path d="M31.7204375,13.011 C31.9041875,13.978875 32,14.9784375 32,16 C32,17.1455 31.8795625,18.262875 31.650125,19.3406875 C30.87125,23.008375 28.8360625,26.211 26.01675,28.477375 L26.015875,28.4765 L21.450625,28.2435625 L20.8045,24.210125 C22.67525,23.113 24.13725,21.3960625 24.907375,19.3406875 L16.35175,19.3406875 L16.35175,13.011 L25.0321875,13.011 L31.7204375,13.011 Z" fill="#518EF8"></path>
|
||||||
c0-42.451,10.324-82.483,28.624-117.732h0.014l57.992,10.632l25.404,57.644c-5.317,15.501-8.215,32.141-8.215,49.456
|
<path d="M26.0158125,28.4765 L26.0166875,28.477375 C23.27475,30.6813125 19.791625,32 16,32 C9.9068125,32 4.60925,28.5943125 1.9068125,23.5824375 L7.091875,19.3380625 C8.4430625,22.9441875 11.92175,25.51125 16,25.51125 C17.7529375,25.51125 19.3951875,25.037375 20.804375,24.210125 L26.0158125,28.4765 Z" fill="#28B446"></path>
|
||||||
C103.821,274.792,107.225,292.797,113.47,309.408z"/>
|
<path d="M26.21275,3.6835 L21.0294375,7.927 C19.571,7.015375 17.847,6.48875 16,6.48875 C11.8294375,6.48875 8.2856875,9.1735625 7.0021875,12.909 L1.789875,8.64175 L1.789,8.64175 C4.451875,3.5076875 9.81625,0 16,0 C19.8821875,0 23.44175,1.382875 26.21275,3.6835 Z" fill="#F14336"></path>
|
||||||
<path style="fill:#518EF8;" d="M507.527,208.176C510.467,223.662,512,239.655,512,256c0,18.328-1.927,36.206-5.598,53.451
|
</g>
|
||||||
c-12.462,58.683-45.025,109.925-90.134,146.187l-0.014-0.014l-73.044-3.727l-10.338-64.535
|
|
||||||
c29.932-17.554,53.324-45.025,65.646-77.911h-136.89V208.176h138.887L507.527,208.176L507.527,208.176z"/>
|
|
||||||
<path style="fill:#28B446;" d="M416.253,455.624l0.014,0.014C372.396,490.901,316.666,512,256,512
|
|
||||||
c-97.491,0-182.252-54.491-225.491-134.681l82.961-67.91c21.619,57.698,77.278,98.771,142.53,98.771
|
|
||||||
c28.047,0,54.323-7.582,76.87-20.818L416.253,455.624z"/>
|
|
||||||
<path style="fill:#F14336;" d="M419.404,58.936l-82.933,67.896c-23.335-14.586-50.919-23.012-80.471-23.012
|
|
||||||
c-66.729,0-123.429,42.957-143.965,102.724l-83.397-68.276h-0.014C71.23,56.123,157.06,0,256,0
|
|
||||||
C318.115,0,375.068,22.126,419.404,58.936z"/>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
<g transform="translate(0.666667, 0.333333)">
|
||||||
<g>
|
<polygon points="4.56711429 7.96345714 0.103114286 0.1872 8.99022857 0.1872"></polygon>
|
||||||
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/>
|
</g>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 539 B |
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
<g transform="translate(-0.200000, -0.200000)" >
|
||||||
<g>
|
<polygon points="4.93361111 0.202222222 9.75583333 8.6025 0.155833333 8.6025"></polygon>
|
||||||
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/>
|
</g>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 543 B |
5
client/images/terminal.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="5" y="8" width="22" height="16" rx="2" fill="#333333"/>
|
||||||
|
<path d="M24 21H14V20H24V21Z" fill="#F0F0F0"/>
|
||||||
|
<path d="M10.4081 16.0231L8.3676 18.0637C8.27757 18.1537 8.15754 18.1537 8.06752 18.0637C7.97749 17.9736 7.97749 17.8536 8.06752 17.7636L9.95802 15.8731L8.06752 13.9826C7.97749 13.8926 7.97749 13.7725 8.06752 13.6675C8.15754 13.5775 8.27757 13.5775 8.3676 13.6675L10.4081 15.723C10.4532 15.753 10.4832 15.8131 10.4832 15.8731C10.4832 15.9181 10.4532 15.9781 10.4081 16.0231Z" fill="#F0F0F0"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 608 B |
|
@ -3,7 +3,7 @@
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5">
|
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#FFF">
|
||||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 695 B |
|
@ -3,7 +3,7 @@
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5">
|
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
|
||||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
@ -3,7 +3,7 @@
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5">
|
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
|
||||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(90.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(90.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
@ -3,8 +3,8 @@
|
||||||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1">
|
||||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#333333">
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#AAA">
|
||||||
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||||
<polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon>
|
<polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon>
|
||||||
</g>
|
</g>
|
||||||
|
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 860 B |
|
@ -1,10 +1,14 @@
|
||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Router, browserHistory } from 'react-router';
|
import { Router, browserHistory } from 'react-router';
|
||||||
|
|
||||||
import configureStore from './store';
|
import configureStore from './store';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
import ThemeProvider from './modules/App/components/ThemeProvider';
|
||||||
|
import Loader from './modules/App/components/loader';
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
require('./styles/main.scss');
|
require('./styles/main.scss');
|
||||||
|
|
||||||
|
@ -18,13 +22,17 @@ const store = configureStore(initialState);
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<ThemeProvider>
|
||||||
<Router history={history} routes={routes(store)} />
|
<Router history={history} routes={routes(store)} />
|
||||||
|
</ThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const HotApp = hot(App);
|
const HotApp = hot(App);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<HotApp />,
|
<Suspense fallback={(<Loader />)}>
|
||||||
|
<HotApp />
|
||||||
|
</Suspense>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|
7
client/index.stories.mdx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Meta } from '@storybook/addon-docs/blocks';
|
||||||
|
|
||||||
|
<Meta title=" |Intro" />
|
||||||
|
|
||||||
|
# Welcome to the P5.js Web Editor Style Guide
|
||||||
|
|
||||||
|
This guide will contain all the components in the project, with examples of how they can be reused.
|
5
client/jest.setup.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import '@babel/polyfill';
|
||||||
|
|
||||||
|
// See: https://github.com/testing-library/jest-dom
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import '@testing-library/jest-dom';
|
|
@ -1,11 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import getConfig from '../../utils/getConfig';
|
||||||
import DevTools from './components/DevTools';
|
import DevTools from './components/DevTools';
|
||||||
import { setPreviousPath } from '../IDE/actions/ide';
|
import { setPreviousPath } from '../IDE/actions/ide';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -35,7 +34,7 @@ class App extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
{this.state.isMounted && !window.devToolsExtension && __process.env.NODE_ENV === 'development' && <DevTools />}
|
{this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />}
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
class Overlay extends React.Component {
|
class Overlay extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -81,8 +80,8 @@ class Overlay extends React.Component {
|
||||||
<h2 className="overlay__title">{title}</h2>
|
<h2 className="overlay__title">{title}</h2>
|
||||||
<div className="overlay__actions">
|
<div className="overlay__actions">
|
||||||
{actions}
|
{actions}
|
||||||
<button className="overlay__close-button" onClick={this.close} >
|
<button className="overlay__close-button" onClick={this.close} aria-label={`Close ${title} overlay`} >
|
||||||
<InlineSVG src={exitUrl} alt="close overlay" />
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
26
client/modules/App/components/ThemeProvider.jsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { ThemeProvider } from 'styled-components';
|
||||||
|
|
||||||
|
import theme, { Theme } from '../../../theme';
|
||||||
|
|
||||||
|
const Provider = ({ children, currentTheme }) => (
|
||||||
|
<ThemeProvider theme={{ ...theme[currentTheme] }}>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
Provider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
currentTheme: state.preferences.theme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Provider);
|
|
@ -1,10 +1,7 @@
|
||||||
import axios from 'axios';
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { startLoader, stopLoader } from './loader';
|
import { startLoader, stopLoader } from './loader';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
function setAssets(assets, totalSize) {
|
function setAssets(assets, totalSize) {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.SET_ASSETS,
|
type: ActionTypes.SET_ASSETS,
|
||||||
|
@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
|
||||||
export function getAssets() {
|
export function getAssets() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true })
|
apiClient.get('/S3/objects')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(setAssets(response.data.assets, response.data.totalSize));
|
dispatch(setAssets(response.data.assets, response.data.totalSize));
|
||||||
dispatch(stopLoader());
|
dispatch(stopLoader());
|
||||||
|
@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
|
||||||
|
|
||||||
export function deleteAssetRequest(assetKey) {
|
export function deleteAssetRequest(assetKey) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
|
apiClient.delete(`/S3/${assetKey}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(deleteAsset(assetKey));
|
dispatch(deleteAsset(assetKey));
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import axios from 'axios';
|
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { startLoader, stopLoader } from './loader';
|
import { startLoader, stopLoader } from './loader';
|
||||||
import { setToastText, showToast } from './toast';
|
import { setToastText, showToast } from './toast';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
const TOAST_DISPLAY_TIME_MS = 1500;
|
const TOAST_DISPLAY_TIME_MS = 1500;
|
||||||
|
|
||||||
|
@ -15,11 +13,11 @@ export function getCollections(username) {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
let url;
|
let url;
|
||||||
if (username) {
|
if (username) {
|
||||||
url = `${ROOT_URL}/${username}/collections`;
|
url = `/${username}/collections`;
|
||||||
} else {
|
} else {
|
||||||
url = `${ROOT_URL}/collections`;
|
url = '/collections';
|
||||||
}
|
}
|
||||||
axios.get(url, { withCredentials: true })
|
apiClient.get(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.SET_COLLECTIONS,
|
type: ActionTypes.SET_COLLECTIONS,
|
||||||
|
@ -27,7 +25,8 @@ export function getCollections(username) {
|
||||||
});
|
});
|
||||||
dispatch(stopLoader());
|
dispatch(stopLoader());
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
@ -40,8 +39,8 @@ export function getCollections(username) {
|
||||||
export function createCollection(collection) {
|
export function createCollection(collection) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
const url = `${ROOT_URL}/collections`;
|
const url = '/collections';
|
||||||
return axios.post(url, collection, { withCredentials: true })
|
return apiClient.post(url, collection)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.CREATE_COLLECTION
|
type: ActionTypes.CREATE_COLLECTION
|
||||||
|
@ -57,7 +56,8 @@ export function createCollection(collection) {
|
||||||
|
|
||||||
browserHistory.push(location);
|
browserHistory.push(location);
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
console.error('Error creating collection', response.data);
|
console.error('Error creating collection', response.data);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
|
@ -71,8 +71,8 @@ export function createCollection(collection) {
|
||||||
export function addToCollection(collectionId, projectId) {
|
export function addToCollection(collectionId, projectId) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
const url = `/collections/${collectionId}/${projectId}`;
|
||||||
return axios.post(url, { withCredentials: true })
|
return apiClient.post(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ADD_TO_COLLECTION,
|
type: ActionTypes.ADD_TO_COLLECTION,
|
||||||
|
@ -87,7 +87,8 @@ export function addToCollection(collectionId, projectId) {
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
@ -102,8 +103,8 @@ export function addToCollection(collectionId, projectId) {
|
||||||
export function removeFromCollection(collectionId, projectId) {
|
export function removeFromCollection(collectionId, projectId) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
const url = `/collections/${collectionId}/${projectId}`;
|
||||||
return axios.delete(url, { withCredentials: true })
|
return apiClient.delete(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.REMOVE_FROM_COLLECTION,
|
type: ActionTypes.REMOVE_FROM_COLLECTION,
|
||||||
|
@ -118,7 +119,8 @@ export function removeFromCollection(collectionId, projectId) {
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
@ -132,8 +134,8 @@ export function removeFromCollection(collectionId, projectId) {
|
||||||
|
|
||||||
export function editCollection(collectionId, { name, description }) {
|
export function editCollection(collectionId, { name, description }) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
const url = `${ROOT_URL}/collections/${collectionId}`;
|
const url = `/collections/${collectionId}`;
|
||||||
return axios.patch(url, { name, description }, { withCredentials: true })
|
return apiClient.patch(url, { name, description })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.EDIT_COLLECTION,
|
type: ActionTypes.EDIT_COLLECTION,
|
||||||
|
@ -141,7 +143,8 @@ export function editCollection(collectionId, { name, description }) {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
@ -154,8 +157,8 @@ export function editCollection(collectionId, { name, description }) {
|
||||||
|
|
||||||
export function deleteCollection(collectionId) {
|
export function deleteCollection(collectionId) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
const url = `${ROOT_URL}/collections/${collectionId}`;
|
const url = `/collections/${collectionId}`;
|
||||||
return axios.delete(url, { withCredentials: true })
|
return apiClient.delete(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.DELETE_COLLECTION,
|
type: ActionTypes.DELETE_COLLECTION,
|
||||||
|
@ -164,7 +167,8 @@ export function deleteCollection(collectionId) {
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import axios from 'axios';
|
|
||||||
import objectID from 'bson-objectid';
|
import objectID from 'bson-objectid';
|
||||||
import blobUtil from 'blob-util';
|
import blobUtil from 'blob-util';
|
||||||
import { reset } from 'redux-form';
|
import { reset } from 'redux-form';
|
||||||
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
|
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
|
||||||
import { setProjectSavedTime } from './project';
|
import { setProjectSavedTime } from './project';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
function appendToFilename(filename, string) {
|
function appendToFilename(filename, string) {
|
||||||
const dotIndex = filename.lastIndexOf('.');
|
const dotIndex = filename.lastIndexOf('.');
|
||||||
|
@ -50,7 +48,7 @@ export function createFile(formProps) {
|
||||||
parentId,
|
parentId,
|
||||||
children: []
|
children: []
|
||||||
};
|
};
|
||||||
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
|
apiClient.post(`/projects/${state.project.id}/files`, postParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.CREATE_FILE,
|
type: ActionTypes.CREATE_FILE,
|
||||||
|
@ -65,10 +63,13 @@ export function createFile(formProps) {
|
||||||
// });
|
// });
|
||||||
dispatch(setUnsavedChanges(true));
|
dispatch(setUnsavedChanges(true));
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const id = objectID().toHexString();
|
const id = objectID().toHexString();
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -103,7 +104,7 @@ export function createFolder(formProps) {
|
||||||
parentId,
|
parentId,
|
||||||
fileType: 'folder'
|
fileType: 'folder'
|
||||||
};
|
};
|
||||||
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
|
apiClient.post(`/projects/${state.project.id}/files`, postParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.CREATE_FILE,
|
type: ActionTypes.CREATE_FILE,
|
||||||
|
@ -113,10 +114,13 @@ export function createFolder(formProps) {
|
||||||
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
||||||
dispatch(closeNewFolderModal());
|
dispatch(closeNewFolderModal());
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const id = objectID().toHexString();
|
const id = objectID().toHexString();
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -155,7 +159,7 @@ export function deleteFile(id, parentId) {
|
||||||
parentId
|
parentId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
axios.delete(`${ROOT_URL}/projects/${state.project.id}/files/${id}`, deleteConfig, { withCredentials: true })
|
apiClient.delete(`/projects/${state.project.id}/files/${id}`, deleteConfig)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.DELETE_FILE,
|
type: ActionTypes.DELETE_FILE,
|
||||||
|
@ -163,7 +167,8 @@ export function deleteFile(id, parentId) {
|
||||||
parentId
|
parentId
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import axios from 'axios';
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
function updatePreferences(formParams, dispatch) {
|
function updatePreferences(formParams, dispatch) {
|
||||||
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true })
|
apiClient.put('/preferences', formParams)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFontSize(value) {
|
export function setFontSize(value) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import axios from 'axios';
|
|
||||||
import objectID from 'bson-objectid';
|
import objectID from 'bson-objectid';
|
||||||
import each from 'async/each';
|
import each from 'async/each';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import apiClient from '../../../utils/apiClient';
|
||||||
|
import getConfig from '../../../utils/getConfig';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { showToast, setToastText } from './toast';
|
import { showToast, setToastText } from './toast';
|
||||||
import {
|
import {
|
||||||
|
@ -14,8 +15,9 @@ import {
|
||||||
} from './ide';
|
} from './ide';
|
||||||
import { clearState, saveState } from '../../../persistState';
|
import { clearState, saveState } from '../../../persistState';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const ROOT_URL = getConfig('API_URL');
|
||||||
const ROOT_URL = __process.env.API_URL;
|
const S3_BUCKET_URL_BASE = getConfig('S3_BUCKET_URL_BASE');
|
||||||
|
const S3_BUCKET = getConfig('S3_BUCKET');
|
||||||
|
|
||||||
export function setProject(project) {
|
export function setProject(project) {
|
||||||
return {
|
return {
|
||||||
|
@ -49,18 +51,21 @@ export function setNewProject(project) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProject(id) {
|
export function getProject(id, username) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(justOpenedProject());
|
dispatch(justOpenedProject());
|
||||||
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
apiClient.get(`/${username}/projects/${id}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(setProject(response.data));
|
dispatch(setProject(response.data));
|
||||||
dispatch(setUnsavedChanges(false));
|
dispatch(setUnsavedChanges(false));
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +144,7 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
fileToUpdate.content = selectedFile.content;
|
fileToUpdate.content = selectedFile.content;
|
||||||
}
|
}
|
||||||
if (state.project.id) {
|
if (state.project.id) {
|
||||||
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
|
return apiClient.put(`/projects/${state.project.id}`, formParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
dispatch(setUnsavedChanges(false));
|
dispatch(setUnsavedChanges(false));
|
||||||
|
@ -152,17 +157,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||||
dispatch(resetJustOpenedProject());
|
dispatch(resetJustOpenedProject());
|
||||||
} else {
|
} else {
|
||||||
dispatch(showToast(1500));
|
dispatch(showToast(1500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
|
dispatch(setToastText('Failed to save sketch.'));
|
||||||
|
dispatch(showToast(1500));
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else if (response.status === 409) {
|
} else if (response.status === 409) {
|
||||||
|
@ -173,7 +181,7 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
return apiClient.post('/projects', formParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||||
|
@ -191,17 +199,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.preferences.autosave) {
|
if (state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||||
dispatch(resetJustOpenedProject());
|
dispatch(resetJustOpenedProject());
|
||||||
} else {
|
} else {
|
||||||
dispatch(showToast(1500));
|
dispatch(showToast(1500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
|
dispatch(setToastText('Failed to save sketch.'));
|
||||||
|
dispatch(showToast(1500));
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -255,7 +266,7 @@ export function cloneProject(id) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
resolve(getState());
|
resolve(getState());
|
||||||
} else {
|
} else {
|
||||||
fetch(`${ROOT_URL}/projects/${id}`)
|
apiClient.get(`/projects/${id}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => resolve({
|
.then(data => resolve({
|
||||||
files: data.files,
|
files: data.files,
|
||||||
|
@ -278,11 +289,11 @@ export function cloneProject(id) {
|
||||||
|
|
||||||
// duplicate all files hosted on S3
|
// duplicate all files hosted on S3
|
||||||
each(newFiles, (file, callback) => {
|
each(newFiles, (file, callback) => {
|
||||||
if (file.url && file.url.includes('amazonaws')) {
|
if (file.url && (file.url.includes(S3_BUCKET_URL_BASE) || file.url.includes(S3_BUCKET))) {
|
||||||
const formParams = {
|
const formParams = {
|
||||||
url: file.url
|
url: file.url
|
||||||
};
|
};
|
||||||
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true })
|
apiClient.post('/S3/copy', formParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
file.url = response.data.url;
|
file.url = response.data.url;
|
||||||
callback(null);
|
callback(null);
|
||||||
|
@ -293,15 +304,18 @@ export function cloneProject(id) {
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
// if not errors in duplicating the files on S3, then duplicate it
|
// if not errors in duplicating the files on S3, then duplicate it
|
||||||
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
|
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
|
||||||
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
apiClient.post('/projects', formParams)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||||
dispatch(setNewProject(response.data));
|
dispatch(setNewProject(response.data));
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||||
error: response.data
|
error: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -329,7 +343,7 @@ export function setProjectSavedTime(updatedAt) {
|
||||||
export function changeProjectName(id, newName) {
|
export function changeProjectName(id, newName) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true })
|
apiClient.put(`/projects/${id}`, { name: newName })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -344,8 +358,8 @@ export function changeProjectName(id, newName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
console.log(response);
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
@ -356,7 +370,7 @@ export function changeProjectName(id, newName) {
|
||||||
|
|
||||||
export function deleteProject(id) {
|
export function deleteProject(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
apiClient.delete(`/projects/${id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (id === state.project.id) {
|
if (id === state.project.id) {
|
||||||
|
@ -368,7 +382,8 @@ export function deleteProject(id) {
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import axios from 'axios';
|
import apiClient from '../../../utils/apiClient';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { startLoader, stopLoader } from './loader';
|
import { startLoader, stopLoader } from './loader';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export function getProjects(username) {
|
export function getProjects(username) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
let url;
|
let url;
|
||||||
if (username) {
|
if (username) {
|
||||||
url = `${ROOT_URL}/${username}/projects`;
|
url = `/${username}/projects`;
|
||||||
} else {
|
} else {
|
||||||
url = `${ROOT_URL}/projects`;
|
url = '/projects';
|
||||||
}
|
}
|
||||||
axios.get(url, { withCredentials: true })
|
apiClient.get(url)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.SET_PROJECTS,
|
type: ActionTypes.SET_PROJECTS,
|
||||||
|
@ -23,7 +20,8 @@ export function getProjects(username) {
|
||||||
});
|
});
|
||||||
dispatch(stopLoader());
|
dispatch(stopLoader());
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
error: response.data
|
error: response.data
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import axios from 'axios';
|
import apiClient from '../../../utils/apiClient';
|
||||||
|
import getConfig from '../../../utils/getConfig';
|
||||||
import { createFile } from './files';
|
import { createFile } from './files';
|
||||||
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') ||
|
||||||
const s3BucketHttps = __process.env.S3_BUCKET_URL_BASE ||
|
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
|
||||||
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
|
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
|
||||||
|
|
||||||
function localIntercept(file, options = {}) {
|
function localIntercept(file, options = {}) {
|
||||||
|
@ -46,18 +45,13 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
file.postData = []; // eslint-disable-line
|
file.postData = []; // eslint-disable-line
|
||||||
axios.post(
|
apiClient.post('/S3/sign', {
|
||||||
`${ROOT_URL}/S3/sign`, {
|
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
userId
|
userId
|
||||||
// _csrf: document.getElementById('__createPostToken').value
|
// _csrf: document.getElementById('__createPostToken').value
|
||||||
},
|
})
|
||||||
{
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
file.custom_status = 'ready'; // eslint-disable-line
|
file.custom_status = 'ready'; // eslint-disable-line
|
||||||
file.postData = response.data; // eslint-disable-line
|
file.postData = response.data; // eslint-disable-line
|
||||||
|
@ -65,7 +59,8 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
||||||
file.previewTemplate.className += ' uploading'; // eslint-disable-line
|
file.previewTemplate.className += ' uploading'; // eslint-disable-line
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
file.custom_status = 'rejected'; // eslint-disable-line
|
file.custom_status = 'rejected'; // eslint-disable-line
|
||||||
if (response.data && response.data.responseText && response.data.responseText.message) {
|
if (response.data && response.data.responseText && response.data.responseText.message) {
|
||||||
done(response.data.responseText.message);
|
done(response.data.responseText.message);
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
|
import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
|
||||||
// const playUrl = require('../../../images/play.svg');
|
// import PlayIcon from '../../../images/play.svg';
|
||||||
const asteriskUrl = require('../../../images/p5-asterisk.svg');
|
import AsteriskIcon from '../../../images/p5-asterisk.svg';
|
||||||
|
|
||||||
function About(props) {
|
function About(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="about__content">
|
<div className="about__content">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | About</title>
|
<title>p5.js Web Editor | About </title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
|
<SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
|
||||||
{/* Video button to hello p5 video page */}
|
{/* Video button to hello p5 video page */}
|
||||||
{/* <p className="about__play-video">
|
{/* <p className="about__play-video">
|
||||||
<a
|
<a
|
||||||
|
@ -21,20 +21,20 @@ function About(props) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
|
<PlayIcon className="about__play-video-button" title="Play Hello Video" />
|
||||||
Play hello! video</a>
|
Play hello! video</a>
|
||||||
</p> */}
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<h3 className="about__content-column-title">New to p5.js?</h3>
|
<h3 className="about__content-column-title">{t('NewP5')}</h3>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/examples/"
|
href="https://p5js.org/examples/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Examples
|
{t('Examples')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -43,21 +43,21 @@ function About(props) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Learn
|
{t('Learn')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<h3 className="about__content-column-title">Resources</h3>
|
<h3 className="about__content-column-title">{t('Resources')}</h3>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/libraries/"
|
href="https://p5js.org/libraries/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Libraries
|
{t('Libraries')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -66,8 +66,8 @@ function About(props) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Reference
|
{t('Reference')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -76,8 +76,8 @@ function About(props) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Forum
|
{t('Forum')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +87,7 @@ function About(props) {
|
||||||
href="https://github.com/processing/p5.js-web-editor"
|
href="https://github.com/processing/p5.js-web-editor"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>Contribute
|
>{t('Contribute')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__footer-list">
|
<p className="about__footer-list">
|
||||||
|
@ -95,7 +95,7 @@ function About(props) {
|
||||||
href="https://github.com/processing/p5.js-web-editor/issues/new"
|
href="https://github.com/processing/p5.js-web-editor/issues/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>Report a bug
|
>{t('Report')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__footer-list">
|
<p className="about__footer-list">
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { bindActionCreators } from 'redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
import * as AssetActions from '../actions/assets';
|
import * as AssetActions from '../actions/assets';
|
||||||
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
|
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
class AssetListRowBase extends React.Component {
|
class AssetListRowBase extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -86,8 +85,9 @@ class AssetListRowBase extends React.Component {
|
||||||
onClick={this.toggleOptions}
|
onClick={this.toggleOptions}
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
|
aria-label="Toggle Open/Close Asset Options"
|
||||||
>
|
>
|
||||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{optionsOpen &&
|
{optionsOpen &&
|
||||||
<ul
|
<ul
|
||||||
|
@ -175,7 +175,7 @@ class AssetList extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { assetList } = this.props;
|
const { assetList } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="asset-table-container">
|
<article className="asset-table-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{this.getAssetsTitle()}</title>
|
<title>{this.getAssetsTitle()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -195,7 +195,7 @@ class AssetList extends React.Component {
|
||||||
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
</div>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
import getConfig from '../../../utils/getConfig';
|
||||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
|
||||||
|
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||||
const MAX_SIZE_B = limit;
|
const MAX_SIZE_B = limit;
|
||||||
|
|
||||||
const formatPercent = (percent) => {
|
const formatPercent = (percent) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -19,8 +18,8 @@ import { SketchSearchbar } from '../Searchbar';
|
||||||
|
|
||||||
import CollectionListRow from './CollectionListRow';
|
import CollectionListRow from './CollectionListRow';
|
||||||
|
|
||||||
const arrowUp = require('../../../../images/sort-arrow-up.svg');
|
import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
|
||||||
const arrowDown = require('../../../../images/sort-arrow-down.svg');
|
import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
|
||||||
|
|
||||||
class CollectionList extends React.Component {
|
class CollectionList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -83,21 +82,43 @@ class CollectionList extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getButtonLabel = (fieldName, displayName) => {
|
||||||
|
const { field, direction } = this.props.sorting;
|
||||||
|
let buttonLabel;
|
||||||
|
if (field !== fieldName) {
|
||||||
|
if (field === 'name') {
|
||||||
|
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||||
|
} else {
|
||||||
|
buttonLabel = `Sort by ${displayName} descending.`;
|
||||||
|
}
|
||||||
|
} else if (direction === SortingActions.DIRECTION.ASC) {
|
||||||
|
buttonLabel = `Sort by ${displayName} descending.`;
|
||||||
|
} else {
|
||||||
|
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||||
|
}
|
||||||
|
return buttonLabel;
|
||||||
|
}
|
||||||
|
|
||||||
_renderFieldHeader = (fieldName, displayName) => {
|
_renderFieldHeader = (fieldName, displayName) => {
|
||||||
const { field, direction } = this.props.sorting;
|
const { field, direction } = this.props.sorting;
|
||||||
const headerClass = classNames({
|
const headerClass = classNames({
|
||||||
'sketches-table__header': true,
|
'sketches-table__header': true,
|
||||||
'sketches-table__header--selected': field === fieldName
|
'sketches-table__header--selected': field === fieldName
|
||||||
});
|
});
|
||||||
|
const buttonLabel = this._getButtonLabel(fieldName, displayName);
|
||||||
return (
|
return (
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
<button
|
||||||
|
className="sketch-list__sort-button"
|
||||||
|
onClick={() => this.props.toggleDirectionForField(fieldName)}
|
||||||
|
aria-label={buttonLabel}
|
||||||
|
>
|
||||||
<span className={headerClass}>{displayName}</span>
|
<span className={headerClass}>{displayName}</span>
|
||||||
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||||
<InlineSVG src={arrowUp} />
|
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
|
||||||
}
|
}
|
||||||
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||||
<InlineSVG src={arrowDown} />
|
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
|
@ -108,7 +129,7 @@ class CollectionList extends React.Component {
|
||||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sketches-table-container">
|
<article className="sketches-table-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{this.getTitle()}</title>
|
<title>{this.getTitle()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -155,7 +176,7 @@ class CollectionList extends React.Component {
|
||||||
</Overlay>
|
</Overlay>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
@ -10,7 +9,7 @@ import * as CollectionsActions from '../../actions/collections';
|
||||||
import * as IdeActions from '../../actions/ide';
|
import * as IdeActions from '../../actions/ide';
|
||||||
import * as ToastActions from '../../actions/toast';
|
import * as ToastActions from '../../actions/toast';
|
||||||
|
|
||||||
const downFilledTriangle = require('../../../../images/down-filled-triangle.svg');
|
import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
class CollectionListRowBase extends React.Component {
|
class CollectionListRowBase extends React.Component {
|
||||||
static projectInCollection(project, collection) {
|
static projectInCollection(project, collection) {
|
||||||
|
@ -129,8 +128,9 @@ class CollectionListRowBase extends React.Component {
|
||||||
onClick={this.toggleOptions}
|
onClick={this.toggleOptions}
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
|
aria-label="Toggle Open/Close collection options"
|
||||||
>
|
>
|
||||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
<DownFilledTriangleIcon title="Menu" />
|
||||||
</button>
|
</button>
|
||||||
{optionsOpen &&
|
{optionsOpen &&
|
||||||
<ul
|
<ul
|
||||||
|
|
|
@ -1,39 +1,35 @@
|
||||||
import PropTypes from 'prop-types';
|
import React, { useRef } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Console as ConsoleFeed } from 'console-feed';
|
import { Console as ConsoleFeed } from 'console-feed';
|
||||||
import {
|
import {
|
||||||
CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES,
|
CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES,
|
||||||
CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES
|
CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES
|
||||||
} from '../../../styles/components/_console-feed.scss';
|
} from '../../../styles/components/_console-feed.scss';
|
||||||
import warnLightUrl from '../../../images/console-warn-light.svg';
|
import warnLightUrl from '../../../images/console-warn-light.svg?byUrl';
|
||||||
import warnDarkUrl from '../../../images/console-warn-dark.svg';
|
import warnDarkUrl from '../../../images/console-warn-dark.svg?byUrl';
|
||||||
import errorLightUrl from '../../../images/console-error-light.svg';
|
import warnContrastUrl from '../../../images/console-warn-contrast.svg?byUrl';
|
||||||
import errorDarkUrl from '../../../images/console-error-dark.svg';
|
import errorLightUrl from '../../../images/console-error-light.svg?byUrl';
|
||||||
import debugLightUrl from '../../../images/console-debug-light.svg';
|
import errorDarkUrl from '../../../images/console-error-dark.svg?byUrl';
|
||||||
import debugDarkUrl from '../../../images/console-debug-dark.svg';
|
import errorContrastUrl from '../../../images/console-error-contrast.svg?byUrl';
|
||||||
import infoLightUrl from '../../../images/console-info-light.svg';
|
import debugLightUrl from '../../../images/console-debug-light.svg?byUrl';
|
||||||
import infoDarkUrl from '../../../images/console-info-dark.svg';
|
import debugDarkUrl from '../../../images/console-debug-dark.svg?byUrl';
|
||||||
|
import debugContrastUrl from '../../../images/console-debug-contrast.svg?byUrl';
|
||||||
|
import infoLightUrl from '../../../images/console-info-light.svg?byUrl';
|
||||||
|
import infoDarkUrl from '../../../images/console-info-dark.svg?byUrl';
|
||||||
|
import infoContrastUrl from '../../../images/console-info-contrast.svg?byUrl';
|
||||||
|
|
||||||
const upArrowUrl = require('../../../images/up-arrow.svg');
|
import UpArrowIcon from '../../../images/up-arrow.svg';
|
||||||
const downArrowUrl = require('../../../images/down-arrow.svg');
|
import DownArrowIcon from '../../../images/down-arrow.svg';
|
||||||
|
|
||||||
class Console extends React.Component {
|
import * as IDEActions from '../../IDE/actions/ide';
|
||||||
componentDidUpdate(prevProps) {
|
import * as ConsoleActions from '../../IDE/actions/console';
|
||||||
this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight;
|
import { useDidUpdate } from '../../../utils/custom-hooks';
|
||||||
if (this.props.theme !== prevProps.theme) {
|
|
||||||
this.props.clearConsole();
|
|
||||||
this.props.dispatchConsoleEvent(this.props.consoleEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.fontSize !== prevProps.fontSize) {
|
const getConsoleFeedStyle = (theme, times, fontSize) => {
|
||||||
this.props.clearConsole();
|
|
||||||
this.props.dispatchConsoleEvent(this.props.consoleEvents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getConsoleFeedStyle(theme, times) {
|
|
||||||
const style = {};
|
const style = {};
|
||||||
const CONSOLE_FEED_LIGHT_ICONS = {
|
const CONSOLE_FEED_LIGHT_ICONS = {
|
||||||
LOG_WARN_ICON: `url(${warnLightUrl})`,
|
LOG_WARN_ICON: `url(${warnLightUrl})`,
|
||||||
|
@ -47,12 +43,18 @@ class Console extends React.Component {
|
||||||
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
|
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
|
||||||
LOG_INFO_ICON: `url(${infoDarkUrl})`
|
LOG_INFO_ICON: `url(${infoDarkUrl})`
|
||||||
};
|
};
|
||||||
|
const CONSOLE_FEED_CONTRAST_ICONS = {
|
||||||
|
LOG_WARN_ICON: `url(${warnContrastUrl})`,
|
||||||
|
LOG_ERROR_ICON: `url(${errorContrastUrl})`,
|
||||||
|
LOG_DEBUG_ICON: `url(${debugContrastUrl})`,
|
||||||
|
LOG_INFO_ICON: `url(${infoContrastUrl})`
|
||||||
|
};
|
||||||
const CONSOLE_FEED_SIZES = {
|
const CONSOLE_FEED_SIZES = {
|
||||||
TREENODE_LINE_HEIGHT: 1.2,
|
TREENODE_LINE_HEIGHT: 1.2,
|
||||||
BASE_FONT_SIZE: this.props.fontSize,
|
BASE_FONT_SIZE: fontSize,
|
||||||
ARROW_FONT_SIZE: this.props.fontSize,
|
ARROW_FONT_SIZE: fontSize,
|
||||||
LOG_ICON_WIDTH: this.props.fontSize,
|
LOG_ICON_WIDTH: fontSize,
|
||||||
LOG_ICON_HEIGHT: 1.45 * this.props.fontSize,
|
LOG_ICON_HEIGHT: 1.45 * fontSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (times > 1) {
|
if (times > 1) {
|
||||||
|
@ -64,81 +66,79 @@ class Console extends React.Component {
|
||||||
case 'dark':
|
case 'dark':
|
||||||
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
||||||
case 'contrast':
|
case 'contrast':
|
||||||
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style);
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const Console = () => {
|
||||||
|
const consoleEvents = useSelector(state => state.console);
|
||||||
|
const isExpanded = useSelector(state => state.ide.consoleIsExpanded);
|
||||||
|
const { theme, fontSize } = useSelector(state => state.preferences);
|
||||||
|
|
||||||
|
const {
|
||||||
|
collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent
|
||||||
|
} = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch());
|
||||||
|
|
||||||
|
useDidUpdate(() => {
|
||||||
|
clearConsole();
|
||||||
|
dispatchConsoleEvent(consoleEvents);
|
||||||
|
}, [theme, fontSize]);
|
||||||
|
|
||||||
|
const cm = useRef({});
|
||||||
|
|
||||||
|
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
|
||||||
|
|
||||||
render() {
|
|
||||||
const consoleClass = classNames({
|
const consoleClass = classNames({
|
||||||
'preview-console': true,
|
'preview-console': true,
|
||||||
'preview-console--collapsed': !this.props.isExpanded
|
'preview-console--collapsed': !isExpanded
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={consoleClass} role="main">
|
<section className={consoleClass} >
|
||||||
<div className="preview-console__header">
|
<header className="preview-console__header">
|
||||||
<h2 className="preview-console__header-title">Console</h2>
|
<h2 className="preview-console__header-title">Console</h2>
|
||||||
<div className="preview-console__header-buttons">
|
<div className="preview-console__header-buttons">
|
||||||
<button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
|
<button className="preview-console__clear" onClick={clearConsole} aria-label="Clear console">
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="preview-console__collapse"
|
className="preview-console__collapse"
|
||||||
onClick={this.props.collapseConsole}
|
onClick={collapseConsole}
|
||||||
aria-label="collapse console"
|
aria-label="Close console"
|
||||||
>
|
>
|
||||||
<InlineSVG src={downArrowUrl} />
|
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console">
|
<button className="preview-console__expand" onClick={expandConsole} aria-label="Open console" >
|
||||||
<InlineSVG src={upArrowUrl} />
|
<UpArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
<div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
|
<div ref={cm} className="preview-console__messages">
|
||||||
{this.props.consoleEvents.map((consoleEvent) => {
|
{consoleEvents.map((consoleEvent) => {
|
||||||
const { method, times } = consoleEvent;
|
const { method, times } = consoleEvent;
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
|
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
|
||||||
{ times > 1 &&
|
{ times > 1 &&
|
||||||
<div
|
<div
|
||||||
className="preview-console__logged-times"
|
className="preview-console__logged-times"
|
||||||
style={{ fontSize: this.props.fontSize, borderRadius: this.props.fontSize / 2 }}
|
style={{ fontSize, borderRadius: fontSize / 2 }}
|
||||||
>
|
>
|
||||||
{times}
|
{times}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<ConsoleFeed
|
<ConsoleFeed
|
||||||
styles={this.getConsoleFeedStyle(theme, times)}
|
styles={getConsoleFeedStyle(theme, times, fontSize)}
|
||||||
logs={[consoleEvent]}
|
logs={[consoleEvent]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.propTypes = {
|
|
||||||
consoleEvents: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
method: PropTypes.string.isRequired,
|
|
||||||
args: PropTypes.arrayOf(PropTypes.string)
|
|
||||||
})),
|
|
||||||
isExpanded: PropTypes.bool.isRequired,
|
|
||||||
collapseConsole: PropTypes.func.isRequired,
|
|
||||||
expandConsole: PropTypes.func.isRequired,
|
|
||||||
clearConsole: PropTypes.func.isRequired,
|
|
||||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
fontSize: PropTypes.number.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Console.defaultProps = {
|
|
||||||
consoleEvents: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Console;
|
export default Console;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Clipboard from 'clipboard';
|
import Clipboard from 'clipboard';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import shareUrl from '../../../images/share.svg';
|
import ShareIcon from '../../../images/share.svg';
|
||||||
|
|
||||||
class CopyableInput extends React.Component {
|
class CopyableInput extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -70,8 +69,9 @@ class CopyableInput extends React.Component {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={value}
|
href={value}
|
||||||
className="copyable-input__preview"
|
className="copyable-input__preview"
|
||||||
|
aria-label={`Open ${label} view in new tab`}
|
||||||
>
|
>
|
||||||
<InlineSVG src={shareUrl} alt={`open ${label} view in new tab`} />
|
<ShareIcon focusable="false" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const editIconUrl = require('../../../images/pencil.svg');
|
import EditIcon from '../../../images/pencil.svg';
|
||||||
|
|
||||||
function EditIcon() {
|
|
||||||
return <InlineSVG className="editable-input__icon" src={editIconUrl} alt="Edit" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO I think this needs a description prop so that it's accessible
|
||||||
function EditableInput({
|
function EditableInput({
|
||||||
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
|
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
|
||||||
}) {
|
}) {
|
||||||
|
@ -52,9 +48,13 @@ function EditableInput({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classes}>
|
<span className={classes}>
|
||||||
<button className="editable-input__label" onClick={beginEditing}>
|
<button
|
||||||
|
className="editable-input__label"
|
||||||
|
onClick={beginEditing}
|
||||||
|
aria-label={`Edit ${displayValue} value`}
|
||||||
|
>
|
||||||
<span>{displayValue}</span>
|
<span>{displayValue}</span>
|
||||||
<EditIcon />
|
<EditIcon className="editable-input__icon" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<InputComponent
|
<InputComponent
|
||||||
|
|
|
@ -24,7 +24,6 @@ import 'codemirror/addon/edit/matchbrackets';
|
||||||
import { JSHINT } from 'jshint';
|
import { JSHINT } from 'jshint';
|
||||||
import { CSSLint } from 'csslint';
|
import { CSSLint } from 'csslint';
|
||||||
import { HTMLHint } from 'htmlhint';
|
import { HTMLHint } from 'htmlhint';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import '../../../utils/htmlmixed';
|
import '../../../utils/htmlmixed';
|
||||||
|
@ -36,6 +35,11 @@ import { metaKey, } from '../../../utils/metaKey';
|
||||||
|
|
||||||
import search from '../../../utils/codemirror-search';
|
import search from '../../../utils/codemirror-search';
|
||||||
|
|
||||||
|
import beepUrl from '../../../sounds/audioAlert.mp3';
|
||||||
|
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
|
||||||
|
import RightArrowIcon from '../../../images/right-arrow.svg';
|
||||||
|
import LeftArrowIcon from '../../../images/left-arrow.svg';
|
||||||
|
|
||||||
search(CodeMirror);
|
search(CodeMirror);
|
||||||
|
|
||||||
const beautifyCSS = beautifyJS.css;
|
const beautifyCSS = beautifyJS.css;
|
||||||
|
@ -45,11 +49,6 @@ window.JSHINT = JSHINT;
|
||||||
window.CSSLint = CSSLint;
|
window.CSSLint = CSSLint;
|
||||||
window.HTMLHint = HTMLHint;
|
window.HTMLHint = HTMLHint;
|
||||||
|
|
||||||
const beepUrl = require('../../../sounds/audioAlert.mp3');
|
|
||||||
const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg');
|
|
||||||
const rightArrowUrl = require('../../../images/right-arrow.svg');
|
|
||||||
const leftArrowUrl = require('../../../images/left-arrow.svg');
|
|
||||||
|
|
||||||
const IS_TAB_INDENT = false;
|
const IS_TAB_INDENT = false;
|
||||||
const INDENTATION_AMOUNT = 2;
|
const INDENTATION_AMOUNT = 2;
|
||||||
|
|
||||||
|
@ -315,29 +314,30 @@ class Editor extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section className={editorSectionClass} >
|
||||||
role="main"
|
|
||||||
className={editorSectionClass}
|
|
||||||
>
|
|
||||||
<header className="editor__header">
|
<header className="editor__header">
|
||||||
<button
|
<button
|
||||||
aria-label="collapse file navigation"
|
aria-label="Open Sketch files navigation"
|
||||||
className="sidebar__contract"
|
className="sidebar__contract"
|
||||||
onClick={this.props.collapseSidebar}
|
onClick={this.props.collapseSidebar}
|
||||||
>
|
>
|
||||||
<InlineSVG src={leftArrowUrl} />
|
<LeftArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="expand file navigation"
|
aria-label="Close sketch files navigation"
|
||||||
className="sidebar__expand"
|
className="sidebar__expand"
|
||||||
onClick={this.props.expandSidebar}
|
onClick={this.props.expandSidebar}
|
||||||
>
|
>
|
||||||
<InlineSVG src={rightArrowUrl} />
|
<RightArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<div className="editor__file-name">
|
<div className="editor__file-name">
|
||||||
<span>
|
<span>
|
||||||
{this.props.file.name}
|
{this.props.file.name}
|
||||||
{this.props.unsavedChanges ? <InlineSVG src={unsavedChangesDotUrl} /> : null}
|
<span className="editor__unsaved-changes">
|
||||||
|
{this.props.unsavedChanges ?
|
||||||
|
<UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" /> :
|
||||||
|
null}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Timer
|
<Timer
|
||||||
projectSavedTime={this.props.projectSavedTime}
|
projectSavedTime={this.props.projectSavedTime}
|
||||||
|
@ -345,8 +345,8 @@ class Editor extends React.Component {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
||||||
</div>
|
</article>
|
||||||
<EditorAccessibility
|
<EditorAccessibility
|
||||||
lintMessages={this.props.lintMessages}
|
lintMessages={this.props.lintMessages}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import githubLogoUrl from '../../../images/github.svg';
|
import GitHubLogo from '../../../images/github.svg';
|
||||||
|
|
||||||
function Feedback(props) {
|
function Feedback(props) {
|
||||||
return (
|
return (
|
||||||
|
@ -24,7 +23,7 @@ function Feedback(props) {
|
||||||
className="feedback__github-link"
|
className="feedback__github-link"
|
||||||
>
|
>
|
||||||
Go to Github
|
Go to Github
|
||||||
<InlineSVG className="feedback__github-logo" src={githubLogoUrl} />
|
<GitHubLogo className="feedback__github-logo" focusable="false" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as IDEActions from '../actions/ide';
|
import * as IDEActions from '../actions/ide';
|
||||||
import * as FileActions from '../actions/files';
|
import * as FileActions from '../actions/files';
|
||||||
import downArrowUrl from '../../../images/down-filled-triangle.svg';
|
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
|
||||||
import folderRightUrl from '../../../images/triangle-arrow-right.svg';
|
import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
|
||||||
import folderDownUrl from '../../../images/triangle-arrow-down.svg';
|
import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
|
||||||
import fileUrl from '../../../images/file.svg';
|
import FileIcon from '../../../images/file.svg';
|
||||||
|
|
||||||
export class FileNode extends React.Component {
|
export class FileNode extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -185,7 +184,7 @@ export class FileNode extends React.Component {
|
||||||
<span className="file-item__spacer"></span>
|
<span className="file-item__spacer"></span>
|
||||||
{ isFile &&
|
{ isFile &&
|
||||||
<span className="sidebar__file-item-icon">
|
<span className="sidebar__file-item-icon">
|
||||||
<InlineSVG src={fileUrl} />
|
<FileIcon focusable="false" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{ isFolder &&
|
{ isFolder &&
|
||||||
|
@ -193,24 +192,28 @@ export class FileNode extends React.Component {
|
||||||
<button
|
<button
|
||||||
className="sidebar__file-item-closed"
|
className="sidebar__file-item-closed"
|
||||||
onClick={this.showFolderChildren}
|
onClick={this.showFolderChildren}
|
||||||
|
aria-label="Open folder contents"
|
||||||
>
|
>
|
||||||
<InlineSVG className="folder-right" src={folderRightUrl} />
|
<FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="sidebar__file-item-open"
|
className="sidebar__file-item-open"
|
||||||
onClick={this.hideFolderChildren}
|
onClick={this.hideFolderChildren}
|
||||||
|
aria-label="Close file contents"
|
||||||
>
|
>
|
||||||
<InlineSVG className="folder-down" src={folderDownUrl} />
|
<FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
|
aria-label="Name"
|
||||||
className="sidebar__file-item-name"
|
className="sidebar__file-item-name"
|
||||||
onClick={this.handleFileClick}
|
onClick={this.handleFileClick}
|
||||||
>
|
>
|
||||||
{this.state.updatedName}
|
{this.state.updatedName}
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
|
data-testid="input"
|
||||||
type="text"
|
type="text"
|
||||||
className="sidebar__file-item-input"
|
className="sidebar__file-item-input"
|
||||||
value={this.state.updatedName}
|
value={this.state.updatedName}
|
||||||
|
@ -222,14 +225,14 @@ export class FileNode extends React.Component {
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="sidebar__file-item-show-options"
|
className="sidebar__file-item-show-options"
|
||||||
aria-label="view file options"
|
aria-label="Toggle open/close file options"
|
||||||
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
|
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onClick={this.toggleFileOptions}
|
onClick={this.toggleFileOptions}
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
<InlineSVG src={downArrowUrl} />
|
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<div className="sidebar__file-item-options">
|
<div className="sidebar__file-item-options">
|
||||||
<ul title="file options">
|
<ul title="file options">
|
||||||
|
|
31
client/modules/IDE/components/FileNode.stories.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { FileNode } from './FileNode';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'IDE/FileNode',
|
||||||
|
component: FileNode
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Show = () => (
|
||||||
|
<FileNode
|
||||||
|
id="nodeId"
|
||||||
|
parantId="parentId"
|
||||||
|
name="File name"
|
||||||
|
fileType="jpeg"
|
||||||
|
isSelectedFile
|
||||||
|
isFolderClosed={false}
|
||||||
|
setSelectedFile={action('setSelectedFile')}
|
||||||
|
deleteFile={action('deleteFile')}
|
||||||
|
updateFileName={action('updateFileName')}
|
||||||
|
resetSelectedFile={action('resetSelectedFile')}
|
||||||
|
newFile={action('newFile')}
|
||||||
|
newFolder={action('newFolder')}
|
||||||
|
showFolderChildren={action('showFolderChildren')}
|
||||||
|
hideFolderChildren={action('hideFolderChildren')}
|
||||||
|
openUploadFileModal={action('openUploadFileModal')}
|
||||||
|
canEdit
|
||||||
|
authenticated
|
||||||
|
/>
|
||||||
|
);
|
127
client/modules/IDE/components/FileNode.test.jsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
|
import { FileNode } from './FileNode';
|
||||||
|
|
||||||
|
describe('<FileNode />', () => {
|
||||||
|
const changeName = (newFileName) => {
|
||||||
|
const renameButton = screen.getByText(/Rename/i);
|
||||||
|
fireEvent.click(renameButton);
|
||||||
|
|
||||||
|
const input = screen.getByTestId('input');
|
||||||
|
fireEvent.change(input, { target: { value: newFileName } });
|
||||||
|
fireEvent.blur(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectFileNameToBe = async (expectedName) => {
|
||||||
|
const name = screen.getByLabelText(/Name/i);
|
||||||
|
await waitFor(() => within(name).queryByText(expectedName));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileNode = (fileType, extraProps = {}) => {
|
||||||
|
const props = {
|
||||||
|
...extraProps,
|
||||||
|
id: '0',
|
||||||
|
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
|
||||||
|
fileType,
|
||||||
|
canEdit: true,
|
||||||
|
children: [],
|
||||||
|
authenticated: false,
|
||||||
|
setSelectedFile: jest.fn(),
|
||||||
|
deleteFile: jest.fn(),
|
||||||
|
updateFileName: jest.fn(),
|
||||||
|
resetSelectedFile: jest.fn(),
|
||||||
|
newFile: jest.fn(),
|
||||||
|
newFolder: jest.fn(),
|
||||||
|
showFolderChildren: jest.fn(),
|
||||||
|
hideFolderChildren: jest.fn(),
|
||||||
|
openUploadFileModal: jest.fn(),
|
||||||
|
setProjectName: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<FileNode {...props} />);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('fileType: file', () => {
|
||||||
|
it('cannot change to an empty name', async () => {
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName('');
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to a valid filename', async () => {
|
||||||
|
const newName = 'newname.jsx';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||||
|
await expectFileNameToBe(newName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must have an extension', async () => {
|
||||||
|
const newName = 'newname';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to a different extension', async () => {
|
||||||
|
const newName = 'newname.gif';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be just an extension', async () => {
|
||||||
|
const newName = '.jsx';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fileType: folder', () => {
|
||||||
|
it('cannot change to an empty name', async () => {
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName('');
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to another name', async () => {
|
||||||
|
const newName = 'foldername';
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||||
|
await expectFileNameToBe(newName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot have a file extension', async () => {
|
||||||
|
const newName = 'foldername.jsx';
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,11 +4,11 @@ import Dropzone from 'dropzone';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as UploaderActions from '../actions/uploader';
|
import * as UploaderActions from '../actions/uploader';
|
||||||
|
import getConfig from '../../../utils/getConfig';
|
||||||
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
|
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const s3Bucket = getConfig('S3_BUCKET_URL_BASE') ||
|
||||||
const s3Bucket = __process.env.S3_BUCKET_URL_BASE ||
|
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
|
||||||
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
|
|
||||||
|
|
||||||
class FileUploader extends React.Component {
|
class FileUploader extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
|
@ -1,92 +1,91 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { metaKeyName, } from '../../../utils/metaKey';
|
import { metaKeyName, } from '../../../utils/metaKey';
|
||||||
|
|
||||||
function KeyboardShortcutModal() {
|
function KeyboardShortcutModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<ul className="keyboard-shortcuts" title="keyboard shortcuts">
|
<div className="keyboard-shortcuts">
|
||||||
|
<h3 className="keyboard-shortcuts__title">{t('CodeEditing')}</h3>
|
||||||
|
<p className="keyboard-shortcuts__description">
|
||||||
|
{t('Code editing keyboard shortcuts follow')} <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">{t('Sublime Text shortcuts')}</a>.
|
||||||
|
</p>
|
||||||
|
<ul className="keyboard-shortcuts__list">
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
|
<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} + S
|
|
||||||
</span>
|
|
||||||
<span>Save</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + F
|
{metaKeyName} + F
|
||||||
</span>
|
</span>
|
||||||
<span>Find Text</span>
|
<span>{t('FindText')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + G
|
{metaKeyName} + G
|
||||||
</span>
|
</span>
|
||||||
<span>Find Next Text Match</span>
|
<span>{t('FindNextTextMatch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + G
|
{metaKeyName} + {'\u21E7'} + G
|
||||||
</span>
|
</span>
|
||||||
<span>Find Previous Text Match</span>
|
<span>{t('FindPreviousTextMatch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + [
|
{metaKeyName} + [
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Left</span>
|
<span>{t('IndentCodeLeft')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + ]
|
{metaKeyName} + ]
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Right</span>
|
<span>{t('IndentCodeRight')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + /
|
{metaKeyName} + /
|
||||||
</span>
|
</span>
|
||||||
<span>Comment Line</span>
|
<span>{t('CommentLine')}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3 className="keyboard-shortcuts__title">General</h3>
|
||||||
|
<ul className="keyboard-shortcuts__list">
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
{metaKeyName} + S
|
||||||
|
</span>
|
||||||
|
<span>{t('Save')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + Enter
|
{metaKeyName} + Enter
|
||||||
</span>
|
</span>
|
||||||
<span>Start Sketch</span>
|
<span>{t('StartSketch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + Enter
|
{metaKeyName} + {'\u21E7'} + Enter
|
||||||
</span>
|
</span>
|
||||||
<span>Stop Sketch</span>
|
<span>{t('StopSketch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + 1
|
{metaKeyName} + {'\u21E7'} + 1
|
||||||
</span>
|
</span>
|
||||||
<span>Turn on Accessible Output</span>
|
<span>{t('TurnOnAccessibleOutput')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + 2
|
{metaKeyName} + {'\u21E7'} + 2
|
||||||
</span>
|
</span>
|
||||||
<span>Turn off Accessible Output</span>
|
<span>{t('TurnOffAccessibleOutput')}</span>
|
||||||
</li>
|
|
||||||
<li className="keyboard-shortcut-item">
|
|
||||||
<span className="keyboard-shortcut__command">
|
|
||||||
{metaKeyName} + B
|
|
||||||
</span>
|
|
||||||
<span>Toggle Sidebar</span>
|
|
||||||
</li>
|
|
||||||
<li className="keyboard-shortcut-item">
|
|
||||||
<span className="keyboard-shortcut__command">
|
|
||||||
Ctrl + `
|
|
||||||
</span>
|
|
||||||
<span>Toggle Console</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
|
|
||||||
|
import Button from '../../../common/Button';
|
||||||
|
|
||||||
class NewFileForm extends React.Component {
|
class NewFileForm extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -33,7 +35,10 @@ class NewFileForm extends React.Component {
|
||||||
{...domOnlyProps(name)}
|
{...domOnlyProps(name)}
|
||||||
ref={(element) => { this.fileName = element; }}
|
ref={(element) => { this.fileName = element; }}
|
||||||
/>
|
/>
|
||||||
<input type="submit" value="Add File" aria-label="add file" />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
>Add File
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -3,13 +3,12 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators, compose } from 'redux';
|
import { bindActionCreators, compose } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import NewFileForm from './NewFileForm';
|
import NewFileForm from './NewFileForm';
|
||||||
import { closeNewFileModal } from '../actions/ide';
|
import { closeNewFileModal } from '../actions/ide';
|
||||||
import { createFile } from '../actions/files';
|
import { createFile } from '../actions/files';
|
||||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
|
|
||||||
// At some point this will probably be generalized to a generic modal
|
// At some point this will probably be generalized to a generic modal
|
||||||
|
@ -35,8 +34,12 @@ class NewFileModal extends React.Component {
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Create File</h2>
|
<h2 className="modal__title">Create File</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
|
<button
|
||||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
className="modal__exit-button"
|
||||||
|
onClick={this.props.closeNewFileModal}
|
||||||
|
aria-label="Close New File Modal"
|
||||||
|
>
|
||||||
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<NewFileForm
|
<NewFileForm
|
||||||
|
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||||
|
|
||||||
|
import Button from '../../../common/Button';
|
||||||
|
|
||||||
class NewFolderForm extends React.Component {
|
class NewFolderForm extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -34,7 +36,10 @@ class NewFolderForm extends React.Component {
|
||||||
ref={(element) => { this.fileName = element; }}
|
ref={(element) => { this.fileName = element; }}
|
||||||
{...domOnlyProps(name)}
|
{...domOnlyProps(name)}
|
||||||
/>
|
/>
|
||||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
>Add Folder
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import NewFolderForm from './NewFolderForm';
|
import NewFolderForm from './NewFolderForm';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
class NewFolderModal extends React.Component {
|
class NewFolderModal extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -17,8 +16,12 @@ class NewFolderModal extends React.Component {
|
||||||
<div className="modal-content-folder">
|
<div className="modal-content-folder">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Create Folder</h2>
|
<h2 className="modal__title">Create Folder</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button
|
||||||
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
className="modal__exit-button"
|
||||||
|
onClick={this.props.closeModal}
|
||||||
|
aria-label="Close New Folder Modal"
|
||||||
|
>
|
||||||
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<NewFolderForm {...this.props} />
|
<NewFolderForm {...this.props} />
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const optionsOnOff = (name, onLabel = 'On', offLabel = 'Off') => [
|
||||||
|
{
|
||||||
|
value: true, label: onLabel, ariaLabel: `${name} on`, name: `${name}`, id: `${name}-on`.replace(' ', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: offLabel, ariaLabel: `${name} off`, name: `${name}`, id: `${name}-off`.replace(' ', '-')
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const optionsPickOne = (name, ...options) => options.map(option => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
ariaLabel: `${option} ${name} on`,
|
||||||
|
name: `${option} ${name}`,
|
||||||
|
id: `${option}-${name}-on`.replace(' ', '-')
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nameToValueName = x => (x && x.toLowerCase().replace(/#|_|-/g, ' '));
|
||||||
|
|
||||||
|
// preferenceOnOff: name, value and onSelect are mandatory. propname is optional
|
||||||
|
export const preferenceOnOff = (name, value, onSelect, propname) => ({
|
||||||
|
title: name,
|
||||||
|
value,
|
||||||
|
options: optionsOnOff(propname || nameToValueName(name)),
|
||||||
|
onSelect
|
||||||
|
});
|
|
@ -1,15 +1,15 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
// import { bindActionCreators } from 'redux';
|
// import { bindActionCreators } from 'redux';
|
||||||
// import { connect } from 'react-redux';
|
// import { connect } from 'react-redux';
|
||||||
// import * as PreferencesActions from '../actions/preferences';
|
// import * as PreferencesActions from '../actions/preferences';
|
||||||
|
|
||||||
const plusUrl = require('../../../images/plus.svg');
|
import PlusIcon from '../../../../images/plus.svg';
|
||||||
const minusUrl = require('../../../images/minus.svg');
|
import MinusIcon from '../../../../images/minus.svg';
|
||||||
const beepUrl = require('../../../sounds/audioAlert.mp3');
|
import beepUrl from '../../../../sounds/audioAlert.mp3';
|
||||||
|
|
||||||
class Preferences extends React.Component {
|
class Preferences extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -99,13 +99,13 @@ class Preferences extends React.Component {
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="tabs__titles">
|
<div className="tabs__titles">
|
||||||
<Tab><h4 className="tabs__title">General Settings</h4></Tab>
|
<Tab><h4 className="tabs__title">{this.props.t('GeneralSettings')}</h4></Tab>
|
||||||
<Tab><h4 className="tabs__title">Accessibility</h4></Tab>
|
<Tab><h4 className="tabs__title">{this.props.t('Accessibility')}</h4></Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Theme</h4>
|
<h4 className="preference__title">{this.props.t('Theme')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -117,7 +117,7 @@ class Preferences extends React.Component {
|
||||||
value="light"
|
value="light"
|
||||||
checked={this.props.theme === 'light'}
|
checked={this.props.theme === 'light'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="light-theme-on" className="preference__option">Light</label>
|
<label htmlFor="light-theme-on" className="preference__option">{this.props.t('Light')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setTheme('dark')}
|
onChange={() => this.props.setTheme('dark')}
|
||||||
|
@ -128,7 +128,7 @@ class Preferences extends React.Component {
|
||||||
value="dark"
|
value="dark"
|
||||||
checked={this.props.theme === 'dark'}
|
checked={this.props.theme === 'dark'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="dark-theme-on" className="preference__option">Dark</label>
|
<label htmlFor="dark-theme-on" className="preference__option">{this.props.t('Dark')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setTheme('contrast')}
|
onChange={() => this.props.setTheme('contrast')}
|
||||||
|
@ -139,19 +139,19 @@ class Preferences extends React.Component {
|
||||||
value="contrast"
|
value="contrast"
|
||||||
checked={this.props.theme === 'contrast'}
|
checked={this.props.theme === 'contrast'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="high-contrast-theme-on" className="preference__option">High Contrast</label>
|
<label htmlFor="high-contrast-theme-on" className="preference__option">{this.props.t('HighContrast')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Text size</h4>
|
<h4 className="preference__title">{this.props.t('TextSize')}</h4>
|
||||||
<button
|
<button
|
||||||
className="preference__minus-button"
|
className="preference__minus-button"
|
||||||
onClick={this.decreaseFontSize}
|
onClick={this.decreaseFontSize}
|
||||||
aria-label="decrease font size"
|
aria-label="decrease font size"
|
||||||
disabled={this.state.fontSize <= 8}
|
disabled={this.state.fontSize <= 8}
|
||||||
>
|
>
|
||||||
<InlineSVG src={minusUrl} alt="Decrease Font Size" />
|
<MinusIcon focusable="false" aria-hidden="true" />
|
||||||
<h6 className="preference__label">Decrease</h6>
|
<h6 className="preference__label">{this.props.t('Decrease')}</h6>
|
||||||
</button>
|
</button>
|
||||||
<form onSubmit={this.onFontInputSubmit}>
|
<form onSubmit={this.onFontInputSubmit}>
|
||||||
<input
|
<input
|
||||||
|
@ -171,12 +171,12 @@ class Preferences extends React.Component {
|
||||||
aria-label="increase font size"
|
aria-label="increase font size"
|
||||||
disabled={this.state.fontSize >= 36}
|
disabled={this.state.fontSize >= 36}
|
||||||
>
|
>
|
||||||
<InlineSVG src={plusUrl} alt="Increase Font Size" />
|
<PlusIcon focusable="false" aria-hidden="true" />
|
||||||
<h6 className="preference__label">Increase</h6>
|
<h6 className="preference__label">{this.props.t('Increase')}</h6>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Autosave</h4>
|
<h4 className="preference__title">{this.props.t('Autosave')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -188,7 +188,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.autosave}
|
checked={this.props.autosave}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autosave-on" className="preference__option">On</label>
|
<label htmlFor="autosave-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setAutosave(false)}
|
onChange={() => this.props.setAutosave(false)}
|
||||||
|
@ -199,11 +199,11 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.autosave}
|
checked={!this.props.autosave}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autosave-off" className="preference__option">Off</label>
|
<label htmlFor="autosave-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Word Wrap</h4>
|
<h4 className="preference__title">{this.props.t('WordWrap')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -215,7 +215,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.linewrap}
|
checked={this.props.linewrap}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="linewrap-on" className="preference__option">On</label>
|
<label htmlFor="linewrap-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLinewrap(false)}
|
onChange={() => this.props.setLinewrap(false)}
|
||||||
|
@ -226,13 +226,13 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.linewrap}
|
checked={!this.props.linewrap}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="linewrap-off" className="preference__option">Off</label>
|
<label htmlFor="linewrap-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Line numbers</h4>
|
<h4 className="preference__title">{this.props.t('LineNumbers')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -244,7 +244,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.lineNumbers}
|
checked={this.props.lineNumbers}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="line-numbers-on" className="preference__option">On</label>
|
<label htmlFor="line-numbers-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLineNumbers(false)}
|
onChange={() => this.props.setLineNumbers(false)}
|
||||||
|
@ -255,11 +255,11 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.lineNumbers}
|
checked={!this.props.lineNumbers}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="line-numbers-off" className="preference__option">Off</label>
|
<label htmlFor="line-numbers-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Lint warning sound</h4>
|
<h4 className="preference__title">{this.props.t('LintWarningSound')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -271,7 +271,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.lintWarning}
|
checked={this.props.lintWarning}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="lint-warning-on" className="preference__option">On</label>
|
<label htmlFor="lint-warning-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLintWarning(false)}
|
onChange={() => this.props.setLintWarning(false)}
|
||||||
|
@ -282,19 +282,19 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.lintWarning}
|
checked={!this.props.lintWarning}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="lint-warning-off" className="preference__option">Off</label>
|
<label htmlFor="lint-warning-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
<button
|
<button
|
||||||
className="preference__preview-button"
|
className="preference__preview-button"
|
||||||
onClick={() => beep.play()}
|
onClick={() => beep.play()}
|
||||||
aria-label="preview sound"
|
aria-label="preview sound"
|
||||||
>
|
>
|
||||||
Preview sound
|
{this.props.t('PreviewSound')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Accessible text-based canvas</h4>
|
<h4 className="preference__title">{this.props.t('AccessibleTextBasedCanvas')}</h4>
|
||||||
<h6 className="preference__subtitle">Used with screen reader</h6>
|
<h6 className="preference__subtitle">{this.props.t('UsedScreenReader')}</h6>
|
||||||
|
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
|
@ -308,7 +308,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.textOutput)}
|
checked={(this.props.textOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label>
|
<label htmlFor="text-output-on" className="preference__option preference__canvas">{this.props.t('PlainText')}</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
@ -320,7 +320,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.gridOutput)}
|
checked={(this.props.gridOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="table-output-on" className="preference__option preference__canvas">Table-text</label>
|
<label htmlFor="table-output-on" className="preference__option preference__canvas">{this.props.t('TableText')}</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
@ -332,7 +332,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.soundOutput)}
|
checked={(this.props.soundOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label>
|
<label htmlFor="sound-output-on" className="preference__option preference__canvas">{this.props.t('Sound')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -361,6 +361,7 @@ Preferences.propTypes = {
|
||||||
setLintWarning: PropTypes.func.isRequired,
|
setLintWarning: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
setTheme: PropTypes.func.isRequired,
|
setTheme: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Preferences;
|
export default withTranslation('WebEditor')(Preferences);
|
|
@ -22,6 +22,23 @@ import {
|
||||||
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
|
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
|
||||||
from '../../../utils/consoleUtils';
|
from '../../../utils/consoleUtils';
|
||||||
|
|
||||||
|
|
||||||
|
const shouldRenderSketch = (props, prevProps = undefined) => {
|
||||||
|
const { isPlaying, previewIsRefreshing, fullView } = props;
|
||||||
|
|
||||||
|
// if the user explicitly clicks on the play button
|
||||||
|
if (isPlaying && previewIsRefreshing) return true;
|
||||||
|
|
||||||
|
if (!prevProps) return false;
|
||||||
|
|
||||||
|
return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender
|
||||||
|
|| props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences
|
||||||
|
|| props.textOutput !== prevProps.textOutput
|
||||||
|
|| props.gridOutput !== prevProps.gridOutput
|
||||||
|
|| props.soundOutput !== prevProps.soundOutput
|
||||||
|
|| (fullView && props.files[0].id !== prevProps.files[0].id));
|
||||||
|
};
|
||||||
|
|
||||||
class PreviewFrame extends React.Component {
|
class PreviewFrame extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('message', this.handleConsoleEvent);
|
window.addEventListener('message', this.handleConsoleEvent);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...this.props,
|
||||||
|
previewIsRefreshing: this.props.previewIsRefreshing,
|
||||||
|
isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
|
||||||
|
};
|
||||||
|
if (shouldRenderSketch(props)) this.renderSketch();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
// if sketch starts or stops playing, want to rerender
|
if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
|
||||||
if (this.props.isPlaying !== prevProps.isPlaying) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the user explicitly clicks on the play button
|
|
||||||
if (this.props.isPlaying && this.props.previewIsRefreshing) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user switches textoutput preferences
|
|
||||||
if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.textOutput !== prevProps.textOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.gridOutput !== prevProps.gridOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.soundOutput !== prevProps.soundOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
|
|
||||||
this.renderSketch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// small bug - if autorefresh is on, and the usr changes files
|
// small bug - if autorefresh is on, and the usr changes files
|
||||||
// in the sketch, preview will reload
|
// in the sketch, preview will reload
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('message', this.handleConsoleEvent);
|
window.removeEventListener('message', this.handleConsoleEvent);
|
||||||
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
|
const iframeBody = this.iframeElement.contentDocument.body;
|
||||||
|
if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConsoleEvent(messageEvent) {
|
handleConsoleEvent(messageEvent) {
|
||||||
|
@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
|
||||||
jsFileStrings.forEach((jsFileString) => {
|
jsFileStrings.forEach((jsFileString) => {
|
||||||
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||||
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
||||||
|
const quoteCharacter = jsFileString.substr(0, 1);
|
||||||
const resolvedFile = resolvePathToFile(filePath, files);
|
const resolvedFile = resolvePathToFile(filePath, files);
|
||||||
|
|
||||||
if (resolvedFile) {
|
if (resolvedFile) {
|
||||||
if (resolvedFile.url) {
|
if (resolvedFile.url) {
|
||||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||||
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
|
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
|
||||||
// could also pull file from API instead of using bloburl
|
// could also pull file from API instead of using bloburl
|
||||||
const blobURL = getBlobUrl(resolvedFile);
|
const blobURL = getBlobUrl(resolvedFile);
|
||||||
this.props.setBlobUrl(resolvedFile, blobURL);
|
this.props.setBlobUrl(resolvedFile, blobURL);
|
||||||
const filePathRegex = new RegExp(filePath, 'gi');
|
|
||||||
newContent = newContent.replace(filePathRegex, blobURL);
|
newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,10 +265,11 @@ class PreviewFrame extends React.Component {
|
||||||
cssFileStrings.forEach((cssFileString) => {
|
cssFileStrings.forEach((cssFileString) => {
|
||||||
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||||
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
||||||
|
const quoteCharacter = cssFileString.substr(0, 1);
|
||||||
const resolvedFile = resolvePathToFile(filePath, files);
|
const resolvedFile = resolvePathToFile(filePath, files);
|
||||||
if (resolvedFile) {
|
if (resolvedFile) {
|
||||||
if (resolvedFile.url) {
|
if (resolvedFile.url) {
|
||||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,6 +345,8 @@ class PreviewFrame extends React.Component {
|
||||||
'preview-frame': true,
|
'preview-frame': true,
|
||||||
'preview-frame--full-view': this.props.fullView
|
'preview-frame--full-view': this.props.fullView
|
||||||
});
|
});
|
||||||
|
const sandboxAttributes =
|
||||||
|
'allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals allow-downloads';
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
id="canvas_frame"
|
id="canvas_frame"
|
||||||
|
@ -362,7 +356,7 @@ class PreviewFrame extends React.Component {
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
title="sketch preview"
|
title="sketch preview"
|
||||||
ref={(element) => { this.iframeElement = element; }}
|
ref={(element) => { this.iframeElement = element; }}
|
||||||
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals"
|
sandbox={sandboxAttributes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -393,7 +387,7 @@ PreviewFrame.propTypes = {
|
||||||
clearConsole: PropTypes.func.isRequired,
|
clearConsole: PropTypes.func.isRequired,
|
||||||
cmController: PropTypes.shape({
|
cmController: PropTypes.shape({
|
||||||
getContent: PropTypes.func
|
getContent: PropTypes.func
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
PreviewFrame.defaultProps = {
|
PreviewFrame.defaultProps = {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
const check = require('../../../../images/check_encircled.svg');
|
import CheckIcon from '../../../../images/check_encircled.svg';
|
||||||
const close = require('../../../../images/close.svg');
|
import CloseIcon from '../../../../images/close.svg';
|
||||||
|
|
||||||
const Icons = ({ isAdded }) => {
|
const Icons = ({ isAdded }) => {
|
||||||
const classes = [
|
const classes = [
|
||||||
|
@ -13,9 +12,9 @@ const Icons = ({ isAdded }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<InlineSVG className="quick-add__remove-icon" src={close} alt="Remove from collection" />
|
<CloseIcon className="quick-add__remove-icon" role="img" aria-label="Descending" focusable="false" />
|
||||||
<InlineSVG className="quick-add__in-icon" src={check} alt="In collection" />
|
<CheckIcon className="quick-add__in-icon" role="img" aria-label="Descending" focusable="false" />
|
||||||
<InlineSVG className="quick-add__add-icon" src={close} alt="Add to collection" />
|
<CloseIcon className="quick-add__add-icon" role="img" aria-label="Descending" focusable="false" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,9 +6,11 @@ import Icons from './Icons';
|
||||||
|
|
||||||
const Item = ({
|
const Item = ({
|
||||||
isAdded, onSelect, name, url
|
isAdded, onSelect, name, url
|
||||||
}) => (
|
}) => {
|
||||||
|
const buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection';
|
||||||
|
return (
|
||||||
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
||||||
<button className="quick-add__item-toggle" onClick={onSelect}>
|
<button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}>
|
||||||
<Icons isAdded={isAdded} />
|
<Icons isAdded={isAdded} />
|
||||||
</button>
|
</button>
|
||||||
<span className="quick-add__item-name">{name}</span>
|
<span className="quick-add__item-name">{name}</span>
|
||||||
|
@ -21,7 +23,8 @@ const Item = ({
|
||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ItemType = PropTypes.shape({
|
const ItemType = PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
const searchIcon = require('../../../../images/magnifyingglass.svg');
|
import SearchIcon from '../../../../images/magnifyingglass.svg';
|
||||||
|
|
||||||
class Searchbar extends React.Component {
|
class Searchbar extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -24,20 +23,13 @@ class Searchbar extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchEnter = (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.searchChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchChange = () => {
|
searchChange = () => {
|
||||||
if (this.state.searchValue.trim().length === 0) return;
|
|
||||||
this.props.setSearchTerm(this.state.searchValue.trim());
|
this.props.setSearchTerm(this.state.searchValue.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSearchChange = (e) => {
|
handleSearchChange = (e) => {
|
||||||
this.setState({ searchValue: e.target.value }, () => {
|
this.setState({ searchValue: e.target.value }, () => {
|
||||||
this.throttledSearchChange(this.state.searchValue);
|
this.throttledSearchChange(this.state.searchValue.trim());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +38,7 @@ class Searchbar extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
|
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
|
||||||
<div className="searchbar__button">
|
<div className="searchbar__button">
|
||||||
<InlineSVG className="searchbar__icon" src={searchIcon} />
|
<SearchIcon className="searchbar__icon" focusable="false" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className="searchbar__input"
|
className="searchbar__input"
|
||||||
|
@ -54,7 +46,6 @@ class Searchbar extends React.Component {
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
placeholder={this.props.searchLabel}
|
placeholder={this.props.searchLabel}
|
||||||
onChange={this.handleSearchChange}
|
onChange={this.handleSearchChange}
|
||||||
onKeyUp={this.handleSearchEnter}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="searchbar__clear-button"
|
className="searchbar__clear-button"
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import ConnectedFileNode from './FileNode';
|
import ConnectedFileNode from './FileNode';
|
||||||
|
|
||||||
const downArrowUrl = require('../../../images/down-filled-triangle.svg');
|
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
class Sidebar extends React.Component {
|
class Sidebar extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -69,14 +68,14 @@ class Sidebar extends React.Component {
|
||||||
const rootFile = this.props.files.filter(file => file.name === 'root')[0];
|
const rootFile = this.props.files.filter(file => file.name === 'root')[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={sidebarClass} title="file-navigation" >
|
<section className={sidebarClass}>
|
||||||
<div className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
|
<header className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
|
||||||
<h3 className="sidebar__title">
|
<h3 className="sidebar__title">
|
||||||
<span>Sketch Files</span>
|
<span>Sketch Files</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div className="sidebar__icons">
|
<div className="sidebar__icons">
|
||||||
<button
|
<button
|
||||||
aria-label="add file or folder"
|
aria-label="Toggle open/close sketch file options"
|
||||||
className="sidebar__add"
|
className="sidebar__add"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
ref={(element) => { this.sidebarOptions = element; }}
|
ref={(element) => { this.sidebarOptions = element; }}
|
||||||
|
@ -84,7 +83,7 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
<InlineSVG src={downArrowUrl} />
|
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="sidebar__project-options">
|
<ul className="sidebar__project-options">
|
||||||
<li>
|
<li>
|
||||||
|
@ -131,12 +130,12 @@ class Sidebar extends React.Component {
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
<ConnectedFileNode
|
<ConnectedFileNode
|
||||||
id={rootFile.id}
|
id={rootFile.id}
|
||||||
canEdit={canEditProject}
|
canEdit={canEditProject}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import format from 'date-fns/format';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
@ -19,9 +18,9 @@ import Loader from '../../App/components/loader';
|
||||||
import Overlay from '../../App/components/Overlay';
|
import Overlay from '../../App/components/Overlay';
|
||||||
import AddToCollectionList from './AddToCollectionList';
|
import AddToCollectionList from './AddToCollectionList';
|
||||||
|
|
||||||
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
|
||||||
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
|
||||||
const downFilledTriangle = require('../../../images/down-filled-triangle.svg');
|
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
class SketchListRowBase extends React.Component {
|
class SketchListRowBase extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -168,8 +167,9 @@ class SketchListRowBase extends React.Component {
|
||||||
onClick={this.toggleOptions}
|
onClick={this.toggleOptions}
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
|
aria-label="Toggle Open/Close Sketch Options"
|
||||||
>
|
>
|
||||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{optionsOpen &&
|
{optionsOpen &&
|
||||||
<ul
|
<ul
|
||||||
|
@ -326,15 +326,15 @@ class SketchList extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.props.getProjects(this.props.username);
|
this.props.getProjects(this.props.username);
|
||||||
this.props.resetSorting();
|
this.props.resetSorting();
|
||||||
this._renderFieldHeader = this._renderFieldHeader.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isInitialDataLoad: true,
|
isInitialDataLoad: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) {
|
if (this.props.sketches !== prevProps.sketches && Array.isArray(this.props.sketches)) {
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({
|
this.setState({
|
||||||
isInitialDataLoad: false,
|
isInitialDataLoad: false,
|
||||||
});
|
});
|
||||||
|
@ -368,21 +368,43 @@ class SketchList extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderFieldHeader(fieldName, displayName) {
|
_getButtonLabel = (fieldName, displayName) => {
|
||||||
|
const { field, direction } = this.props.sorting;
|
||||||
|
let buttonLabel;
|
||||||
|
if (field !== fieldName) {
|
||||||
|
if (field === 'name') {
|
||||||
|
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||||
|
} else {
|
||||||
|
buttonLabel = `Sort by ${displayName} descending.`;
|
||||||
|
}
|
||||||
|
} else if (direction === SortingActions.DIRECTION.ASC) {
|
||||||
|
buttonLabel = `Sort by ${displayName} descending.`;
|
||||||
|
} else {
|
||||||
|
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||||
|
}
|
||||||
|
return buttonLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFieldHeader = (fieldName, displayName) => {
|
||||||
const { field, direction } = this.props.sorting;
|
const { field, direction } = this.props.sorting;
|
||||||
const headerClass = classNames({
|
const headerClass = classNames({
|
||||||
'sketches-table__header': true,
|
'sketches-table__header': true,
|
||||||
'sketches-table__header--selected': field === fieldName
|
'sketches-table__header--selected': field === fieldName
|
||||||
});
|
});
|
||||||
|
const buttonLabel = this._getButtonLabel(fieldName, displayName);
|
||||||
return (
|
return (
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
<button
|
||||||
|
className="sketch-list__sort-button"
|
||||||
|
onClick={() => this.props.toggleDirectionForField(fieldName)}
|
||||||
|
aria-label={buttonLabel}
|
||||||
|
>
|
||||||
<span className={headerClass}>{displayName}</span>
|
<span className={headerClass}>{displayName}</span>
|
||||||
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||||
<InlineSVG src={arrowUp} />
|
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
|
||||||
}
|
}
|
||||||
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||||
<InlineSVG src={arrowDown} />
|
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
|
@ -392,7 +414,7 @@ class SketchList extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
return (
|
return (
|
||||||
<div className="sketches-table-container">
|
<article className="sketches-table-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{this.getSketchesTitle()}</title>
|
<title>{this.getSketchesTitle()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -435,7 +457,7 @@ class SketchList extends React.Component {
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
</div>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,20 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as ToastActions from '../actions/toast';
|
import * as ToastActions from '../actions/toast';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
function Toast(props) {
|
function Toast(props) {
|
||||||
|
const { t } = useTranslation('WebEditor');
|
||||||
return (
|
return (
|
||||||
<section className="toast">
|
<section className="toast">
|
||||||
<p>
|
<p>
|
||||||
{props.text}
|
{t(props.text)}
|
||||||
</p>
|
</p>
|
||||||
<button className="toast__close" onClick={props.hideToast}>
|
<button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" >
|
||||||
<InlineSVG src={exitUrl} alt="Close Keyboard Shortcuts Overlay" />
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,37 +3,50 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
import * as IDEActions from '../actions/ide';
|
import * as IDEActions from '../actions/ide';
|
||||||
import * as preferenceActions from '../actions/preferences';
|
import * as preferenceActions from '../actions/preferences';
|
||||||
import * as projectActions from '../actions/project';
|
import * as projectActions from '../actions/project';
|
||||||
|
|
||||||
const playUrl = require('../../../images/play.svg');
|
import PlayIcon from '../../../images/play.svg';
|
||||||
const stopUrl = require('../../../images/stop.svg');
|
import StopIcon from '../../../images/stop.svg';
|
||||||
const preferencesUrl = require('../../../images/preferences.svg');
|
import PreferencesIcon from '../../../images/preferences.svg';
|
||||||
const editProjectNameUrl = require('../../../images/pencil.svg');
|
import EditProjectNameIcon from '../../../images/pencil.svg';
|
||||||
|
|
||||||
class Toolbar extends React.Component {
|
class Toolbar extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
|
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
|
||||||
|
this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
projectNameInputValue: props.project.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(event) {
|
handleKeyPress(event) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this.props.hideEditProjectName();
|
this.props.hideEditProjectName();
|
||||||
|
this.projectNameInput.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProjectNameChange(event) {
|
handleProjectNameChange(event) {
|
||||||
this.props.setProjectName(event.target.value);
|
this.setState({ projectNameInputValue: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateProjectName() {
|
handleProjectNameSave() {
|
||||||
if ((this.props.project.name.trim()).length === 0) {
|
const newProjectName = this.state.projectNameInputValue.trim();
|
||||||
this.props.setProjectName(this.originalProjectName);
|
if (newProjectName.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
projectNameInputValue: this.props.project.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.props.setProjectName(newProjectName);
|
||||||
|
this.props.hideEditProjectName();
|
||||||
|
if (this.props.project.id) {
|
||||||
|
this.props.saveProject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +74,8 @@ class Toolbar extends React.Component {
|
||||||
'toolbar__project-name-container--editing': this.props.project.isEditingName
|
'toolbar__project-name-container--editing': this.props.project.isEditingName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canEditProjectName = this.canEditProjectName();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<button
|
<button
|
||||||
|
@ -70,25 +85,25 @@ class Toolbar extends React.Component {
|
||||||
this.props.setTextOutput(true);
|
this.props.setTextOutput(true);
|
||||||
this.props.setGridOutput(true);
|
this.props.setGridOutput(true);
|
||||||
}}
|
}}
|
||||||
aria-label="play sketch"
|
aria-label="Play sketch"
|
||||||
disabled={this.props.infiniteLoop}
|
disabled={this.props.infiniteLoop}
|
||||||
>
|
>
|
||||||
<InlineSVG src={playUrl} alt="Play Sketch" />
|
<PlayIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={playButtonClass}
|
className={playButtonClass}
|
||||||
onClick={this.props.startSketch}
|
onClick={this.props.startSketch}
|
||||||
aria-label="play only visual sketch"
|
aria-label="Play only visual sketch"
|
||||||
disabled={this.props.infiniteLoop}
|
disabled={this.props.infiniteLoop}
|
||||||
>
|
>
|
||||||
<InlineSVG src={playUrl} alt="Play only visual Sketch" />
|
<PlayIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={stopButtonClass}
|
className={stopButtonClass}
|
||||||
onClick={this.props.stopSketch}
|
onClick={this.props.stopSketch}
|
||||||
aria-label="stop sketch"
|
aria-label="Stop sketch"
|
||||||
>
|
>
|
||||||
<InlineSVG src={stopUrl} alt="Stop Sketch" />
|
<StopIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<div className="toolbar__autorefresh">
|
<div className="toolbar__autorefresh">
|
||||||
<input
|
<input
|
||||||
|
@ -104,38 +119,36 @@ class Toolbar extends React.Component {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className={nameContainerClass}>
|
<div className={nameContainerClass}>
|
||||||
<a
|
<button
|
||||||
className="toolbar__project-name"
|
className="toolbar__project-name"
|
||||||
href={this.props.owner ? `/${this.props.owner.username}/sketches/${this.props.project.id}` : ''}
|
onClick={() => {
|
||||||
onClick={(e) => {
|
if (canEditProjectName) {
|
||||||
if (this.canEditProjectName()) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.originalProjectName = this.props.project.name;
|
|
||||||
this.props.showEditProjectName();
|
this.props.showEditProjectName();
|
||||||
setTimeout(() => this.projectNameInput.focus(), 0);
|
setTimeout(() => this.projectNameInput.focus(), 0);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={!canEditProjectName}
|
||||||
|
aria-label="Edit sketch name"
|
||||||
>
|
>
|
||||||
<span>{this.props.project.name}</span>
|
<span>{this.props.project.name}</span>
|
||||||
{
|
{
|
||||||
this.canEditProjectName() &&
|
canEditProjectName &&
|
||||||
<InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" />
|
<EditProjectNameIcon
|
||||||
|
className="toolbar__edit-name-button"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</a>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
maxLength="128"
|
maxLength="128"
|
||||||
className="toolbar__project-name-input"
|
className="toolbar__project-name-input"
|
||||||
value={this.props.project.name}
|
aria-label="New sketch name"
|
||||||
|
value={this.state.projectNameInputValue}
|
||||||
onChange={this.handleProjectNameChange}
|
onChange={this.handleProjectNameChange}
|
||||||
ref={(element) => { this.projectNameInput = element; }}
|
ref={(element) => { this.projectNameInput = element; }}
|
||||||
onBlur={() => {
|
onBlur={this.handleProjectNameSave}
|
||||||
this.validateProjectName();
|
|
||||||
this.props.hideEditProjectName();
|
|
||||||
if (this.props.project.id) {
|
|
||||||
this.props.saveProject();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
|
@ -151,9 +164,9 @@ class Toolbar extends React.Component {
|
||||||
<button
|
<button
|
||||||
className={preferencesButtonClass}
|
className={preferencesButtonClass}
|
||||||
onClick={this.props.openPreferences}
|
onClick={this.props.openPreferences}
|
||||||
aria-label="preferences"
|
aria-label="Open Preferences"
|
||||||
>
|
>
|
||||||
<InlineSVG src={preferencesUrl} alt="Preferences" />
|
<PreferencesIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -210,4 +223,5 @@ const mapDispatchToProps = {
|
||||||
...projectActions,
|
...projectActions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ToolbarComponent = Toolbar;
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);
|
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);
|
||||||
|
|
84
client/modules/IDE/components/Toolbar.test.jsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
|
import { ToolbarComponent } from './Toolbar';
|
||||||
|
|
||||||
|
const renderComponent = (extraProps = {}) => {
|
||||||
|
const props = lodash.merge({
|
||||||
|
isPlaying: false,
|
||||||
|
preferencesIsVisible: false,
|
||||||
|
stopSketch: jest.fn(),
|
||||||
|
setProjectName: jest.fn(),
|
||||||
|
openPreferences: jest.fn(),
|
||||||
|
showEditProjectName: jest.fn(),
|
||||||
|
hideEditProjectName: jest.fn(),
|
||||||
|
infiniteLoop: false,
|
||||||
|
autorefresh: false,
|
||||||
|
setAutorefresh: jest.fn(),
|
||||||
|
setTextOutput: jest.fn(),
|
||||||
|
setGridOutput: jest.fn(),
|
||||||
|
startSketch: jest.fn(),
|
||||||
|
startAccessibleSketch: jest.fn(),
|
||||||
|
saveProject: jest.fn(),
|
||||||
|
currentUser: 'me',
|
||||||
|
originalProjectName: 'testname',
|
||||||
|
|
||||||
|
owner: {
|
||||||
|
username: 'me'
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: 'testname',
|
||||||
|
isEditingName: false,
|
||||||
|
id: 'id',
|
||||||
|
},
|
||||||
|
}, extraProps);
|
||||||
|
|
||||||
|
render(<ToolbarComponent {...props} />);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<ToolbarComponent />', () => {
|
||||||
|
it('sketch owner can switch to sketch name editing mode', async () => {
|
||||||
|
const props = renderComponent();
|
||||||
|
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||||
|
|
||||||
|
fireEvent.click(sketchName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-owner can\t switch to sketch editing mode', async () => {
|
||||||
|
const props = renderComponent({ currentUser: 'not-me' });
|
||||||
|
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||||
|
|
||||||
|
fireEvent.click(sketchName);
|
||||||
|
|
||||||
|
expect(sketchName).toBeDisabled();
|
||||||
|
await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sketch owner can change name', async () => {
|
||||||
|
const props = renderComponent({ project: { isEditingName: true } });
|
||||||
|
|
||||||
|
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||||
|
fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } });
|
||||||
|
fireEvent.blur(sketchNameInput);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name'));
|
||||||
|
await waitFor(() => expect(props.saveProject).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sketch owner can\'t change to empty name', async () => {
|
||||||
|
const props = renderComponent({ project: { isEditingName: true } });
|
||||||
|
|
||||||
|
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||||
|
fireEvent.change(sketchNameInput, { target: { value: '' } });
|
||||||
|
fireEvent.blur(sketchNameInput);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled());
|
||||||
|
await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,14 +2,13 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import getConfig from '../../../utils/getConfig';
|
||||||
import FileUploader from './FileUploader';
|
import FileUploader from './FileUploader';
|
||||||
import { getreachedTotalSizeLimit } from '../selectors/users';
|
import { getreachedTotalSizeLimit } from '../selectors/users';
|
||||||
import exitUrl from '../../../images/exit.svg';
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
|
||||||
const limitText = prettyBytes(limit);
|
const limitText = prettyBytes(limit);
|
||||||
|
|
||||||
class UploadFileModal extends React.Component {
|
class UploadFileModal extends React.Component {
|
||||||
|
@ -33,8 +32,12 @@ class UploadFileModal extends React.Component {
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Upload File</h2>
|
<h2 className="modal__title">Upload File</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button
|
||||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
className="modal__exit-button"
|
||||||
|
onClick={this.props.closeModal}
|
||||||
|
aria-label="Close upload file modal"
|
||||||
|
>
|
||||||
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{ this.props.reachedTotalSizeLimit &&
|
{ this.props.reachedTotalSizeLimit &&
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as ProjectActions from '../actions/project';
|
||||||
|
|
||||||
class FullView extends React.Component {
|
class FullView extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getProject(this.props.params.project_id);
|
this.props.getProject(this.props.params.project_id, this.props.params.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
ident = () => {}
|
ident = () => {}
|
||||||
|
@ -25,7 +25,7 @@ class FullView extends React.Component {
|
||||||
owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }}
|
owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }}
|
||||||
project={{ name: this.props.project.name, id: this.props.params.project_id }}
|
project={{ name: this.props.project.name, id: this.props.params.project_id }}
|
||||||
/>
|
/>
|
||||||
<div className="preview-frame-holder">
|
<main className="preview-frame-holder">
|
||||||
<PreviewFrame
|
<PreviewFrame
|
||||||
htmlFile={this.props.htmlFile}
|
htmlFile={this.props.htmlFile}
|
||||||
jsFiles={this.props.jsFiles}
|
jsFiles={this.props.jsFiles}
|
||||||
|
@ -48,7 +48,7 @@ class FullView extends React.Component {
|
||||||
expandConsole={this.ident}
|
expandConsole={this.ident}
|
||||||
clearConsole={this.ident}
|
clearConsole={this.ident}
|
||||||
/>
|
/>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,8 @@ class FullView extends React.Component {
|
||||||
|
|
||||||
FullView.propTypes = {
|
FullView.propTypes = {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
project_id: PropTypes.string
|
project_id: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
|
|
@ -3,13 +3,14 @@ import React from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import Editor from '../components/Editor';
|
import Editor from '../components/Editor';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import PreviewFrame from '../components/PreviewFrame';
|
import PreviewFrame from '../components/PreviewFrame';
|
||||||
import Toolbar from '../components/Toolbar';
|
import Toolbar from '../components/Toolbar';
|
||||||
import Preferences from '../components/Preferences';
|
import Preferences from '../components/Preferences/index';
|
||||||
import NewFileModal from '../components/NewFileModal';
|
import NewFileModal from '../components/NewFileModal';
|
||||||
import NewFolderModal from '../components/NewFolderModal';
|
import NewFolderModal from '../components/NewFolderModal';
|
||||||
import UploadFileModal from '../components/UploadFileModal';
|
import UploadFileModal from '../components/UploadFileModal';
|
||||||
|
@ -34,11 +35,38 @@ import AddToCollectionList from '../components/AddToCollectionList';
|
||||||
import Feedback from '../components/Feedback';
|
import Feedback from '../components/Feedback';
|
||||||
import { CollectionSearchbar } from '../components/Searchbar';
|
import { CollectionSearchbar } from '../components/Searchbar';
|
||||||
|
|
||||||
|
function getTitle(props) {
|
||||||
|
const { id } = props.project;
|
||||||
|
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUserOwner(props) {
|
||||||
|
return props.project.owner && props.project.owner.id === props.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnIfUnsavedChanges(props) { // eslint-disable-line
|
||||||
|
const { route } = props.route;
|
||||||
|
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
||||||
|
// don't warn
|
||||||
|
props.persistState();
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
} else if (route && (props.location.pathname === '/login' || props.location.pathname === '/signup')) {
|
||||||
|
// don't warn
|
||||||
|
props.persistState();
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
} else if (props.ide.unsavedChanges) {
|
||||||
|
if (!window.confirm(props.t('WarningUnsavedChanges'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
props.setUnsavedChanges(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IDEView extends React.Component {
|
class IDEView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
|
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
|
||||||
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
|
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
|
||||||
|
@ -53,18 +81,18 @@ class IDEView extends React.Component {
|
||||||
|
|
||||||
this.props.stopSketch();
|
this.props.stopSketch();
|
||||||
if (this.props.params.project_id) {
|
if (this.props.params.project_id) {
|
||||||
const id = this.props.params.project_id;
|
const { project_id: id, username } = this.props.params;
|
||||||
if (id !== this.props.project.id) {
|
if (id !== this.props.project.id) {
|
||||||
this.props.getProject(id);
|
this.props.getProject(id, username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
||||||
document.addEventListener('keydown', this.handleGlobalKeydown, false);
|
document.addEventListener('keydown', this.handleGlobalKeydown, false);
|
||||||
|
|
||||||
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
|
this.props.router.setRouteLeaveHook(this.props.route, this.handleUnsavedChanges);
|
||||||
|
|
||||||
window.onbeforeunload = () => this.warnIfUnsavedChanges();
|
window.onbeforeunload = this.handleUnsavedChanges;
|
||||||
|
|
||||||
this.autosaveInterval = null;
|
this.autosaveInterval = null;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +120,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.isUserOwner() && this.props.project.id) {
|
if (isUserOwner(this.props) && this.props.project.id) {
|
||||||
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
||||||
if (
|
if (
|
||||||
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
||||||
|
@ -113,7 +141,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.route.path !== prevProps.route.path) {
|
if (this.props.route.path !== prevProps.route.path) {
|
||||||
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
|
this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,16 +151,12 @@ class IDEView extends React.Component {
|
||||||
this.autosaveInterval = null;
|
this.autosaveInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isUserOwner() {
|
|
||||||
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGlobalKeydown(e) {
|
handleGlobalKeydown(e) {
|
||||||
// 83 === s
|
// 83 === s
|
||||||
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) {
|
if (isUserOwner(this.props) || (this.props.user.authenticated && !this.props.project.owner)) {
|
||||||
this.props.saveProject(this.cmController.getContent());
|
this.props.saveProject(this.cmController.getContent());
|
||||||
} else if (this.props.user.authenticated) {
|
} else if (this.props.user.authenticated) {
|
||||||
this.props.cloneProject();
|
this.props.cloneProject();
|
||||||
|
@ -170,42 +194,34 @@ class IDEView extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this.props.expandConsole();
|
this.props.expandConsole();
|
||||||
}
|
}
|
||||||
|
} else if (e.keyCode === 27) {
|
||||||
|
if (this.props.ide.newFolderModalVisible) {
|
||||||
|
this.props.closeNewFolderModal();
|
||||||
|
} else if (this.props.ide.uploadFileModalVisible) {
|
||||||
|
this.props.closeUploadFileModal();
|
||||||
|
} else if (this.props.ide.modalIsVisible) {
|
||||||
|
this.props.closeNewFileModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnIfUnsavedChanges(route) { // eslint-disable-line
|
handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
|
||||||
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
|
||||||
// don't warn
|
|
||||||
this.props.persistState();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
|
|
||||||
// don't warn
|
|
||||||
this.props.persistState();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
} else if (this.props.ide.unsavedChanges) {
|
|
||||||
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.props.setUnsavedChanges(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ide">
|
<div className="ide">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | {this.props.project.name}</title>
|
<title>{getTitle(this.props)}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{this.props.toast.isVisible && <Toast />}
|
{this.props.toast.isVisible && <Toast />}
|
||||||
<Nav
|
<Nav
|
||||||
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
|
warnIfUnsavedChanges={this.handleUnsavedChanges}
|
||||||
cmController={this.cmController}
|
cmController={this.cmController}
|
||||||
/>
|
/>
|
||||||
<Toolbar />
|
<Toolbar key={this.props.project.id} />
|
||||||
{this.props.ide.preferencesIsVisible &&
|
{this.props.ide.preferencesIsVisible &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="Settings"
|
title={this.props.t('Settings')}
|
||||||
ariaLabel="settings"
|
ariaLabel="settings"
|
||||||
closeOverlay={this.props.closePreferences}
|
closeOverlay={this.props.closePreferences}
|
||||||
>
|
>
|
||||||
|
@ -231,7 +247,7 @@ class IDEView extends React.Component {
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
<div className="editor-preview-container">
|
<main className="editor-preview-container">
|
||||||
<SplitPane
|
<SplitPane
|
||||||
split="vertical"
|
split="vertical"
|
||||||
size={this.state.sidebarSize}
|
size={this.state.sidebarSize}
|
||||||
|
@ -300,7 +316,7 @@ class IDEView extends React.Component {
|
||||||
isExpanded={this.props.ide.sidebarIsExpanded}
|
isExpanded={this.props.ide.sidebarIsExpanded}
|
||||||
expandSidebar={this.props.expandSidebar}
|
expandSidebar={this.props.expandSidebar}
|
||||||
collapseSidebar={this.props.collapseSidebar}
|
collapseSidebar={this.props.collapseSidebar}
|
||||||
isUserOwner={this.isUserOwner()}
|
isUserOwner={isUserOwner(this.props)}
|
||||||
clearConsole={this.props.clearConsole}
|
clearConsole={this.props.clearConsole}
|
||||||
consoleEvents={this.props.console}
|
consoleEvents={this.props.console}
|
||||||
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
||||||
|
@ -308,20 +324,11 @@ class IDEView extends React.Component {
|
||||||
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible}
|
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible}
|
||||||
provideController={(ctl) => { this.cmController = ctl; }}
|
provideController={(ctl) => { this.cmController = ctl; }}
|
||||||
/>
|
/>
|
||||||
<Console
|
<Console />
|
||||||
fontSize={this.props.preferences.fontSize}
|
|
||||||
consoleEvents={this.props.console}
|
|
||||||
isExpanded={this.props.ide.consoleIsExpanded}
|
|
||||||
expandConsole={this.props.expandConsole}
|
|
||||||
collapseConsole={this.props.collapseConsole}
|
|
||||||
clearConsole={this.props.clearConsole}
|
|
||||||
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
|
|
||||||
theme={this.props.preferences.theme}
|
|
||||||
/>
|
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
<div className="preview-frame-holder">
|
<section className="preview-frame-holder">
|
||||||
<header className="preview-frame__header">
|
<header className="preview-frame__header">
|
||||||
<h2 className="preview-frame__title">Preview</h2>
|
<h2 className="preview-frame__title">{this.props.t('Preview')}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div className="preview-frame__content">
|
<div className="preview-frame__content">
|
||||||
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
|
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
|
||||||
|
@ -362,10 +369,10 @@ class IDEView extends React.Component {
|
||||||
cmController={this.cmController}
|
cmController={this.cmController}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</main>
|
||||||
{ this.props.ide.modalIsVisible &&
|
{ this.props.ide.modalIsVisible &&
|
||||||
<NewFileModal />
|
<NewFileModal />
|
||||||
}
|
}
|
||||||
|
@ -382,7 +389,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
{ this.props.location.pathname === '/about' &&
|
{ this.props.location.pathname === '/about' &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="About"
|
title={this.props.t('About')}
|
||||||
previousPath={this.props.ide.previousPath}
|
previousPath={this.props.ide.previousPath}
|
||||||
ariaLabel="about"
|
ariaLabel="about"
|
||||||
>
|
>
|
||||||
|
@ -428,7 +435,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
{this.props.ide.keyboardShortcutVisible &&
|
{this.props.ide.keyboardShortcutVisible &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="Keyboard Shortcuts"
|
title={this.props.t('KeyboardShortcuts')}
|
||||||
ariaLabel="keyboard shortcuts"
|
ariaLabel="keyboard shortcuts"
|
||||||
closeOverlay={this.props.closeKeyboardShortcutModal}
|
closeOverlay={this.props.closeKeyboardShortcutModal}
|
||||||
>
|
>
|
||||||
|
@ -562,6 +569,7 @@ IDEView.propTypes = {
|
||||||
closeProjectOptions: PropTypes.func.isRequired,
|
closeProjectOptions: PropTypes.func.isRequired,
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
closeNewFolderModal: PropTypes.func.isRequired,
|
closeNewFolderModal: PropTypes.func.isRequired,
|
||||||
|
closeNewFileModal: PropTypes.func.isRequired,
|
||||||
createFolder: PropTypes.func.isRequired,
|
createFolder: PropTypes.func.isRequired,
|
||||||
closeShareModal: PropTypes.func.isRequired,
|
closeShareModal: PropTypes.func.isRequired,
|
||||||
showEditorOptions: PropTypes.func.isRequired,
|
showEditorOptions: PropTypes.func.isRequired,
|
||||||
|
@ -590,12 +598,12 @@ IDEView.propTypes = {
|
||||||
showErrorModal: PropTypes.func.isRequired,
|
showErrorModal: PropTypes.func.isRequired,
|
||||||
hideErrorModal: PropTypes.func.isRequired,
|
hideErrorModal: PropTypes.func.isRequired,
|
||||||
clearPersistedState: PropTypes.func.isRequired,
|
clearPersistedState: PropTypes.func.isRequired,
|
||||||
persistState: PropTypes.func.isRequired,
|
|
||||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
startSketch: PropTypes.func.isRequired,
|
startSketch: PropTypes.func.isRequired,
|
||||||
openUploadFileModal: PropTypes.func.isRequired,
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
closeUploadFileModal: PropTypes.func.isRequired
|
closeUploadFileModal: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
@ -632,4 +640,4 @@ function mapDispatchToProps(dispatch) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
|
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));
|
||||||
|
|
262
client/modules/IDE/pages/MobileIDEView.jsx
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
// Imports to be Refactored
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import * as FileActions from '../actions/files';
|
||||||
|
import * as IDEActions from '../actions/ide';
|
||||||
|
import * as ProjectActions from '../actions/project';
|
||||||
|
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||||
|
import * as PreferencesActions from '../actions/preferences';
|
||||||
|
import * as UserActions from '../../User/actions';
|
||||||
|
import * as ToastActions from '../actions/toast';
|
||||||
|
import * as ConsoleActions from '../actions/console';
|
||||||
|
import { getHTMLFile } from '../reducers/files';
|
||||||
|
|
||||||
|
// Local Imports
|
||||||
|
import Editor from '../components/Editor';
|
||||||
|
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
|
||||||
|
|
||||||
|
import IconButton from '../../../components/mobile/IconButton';
|
||||||
|
import Header from '../../../components/mobile/Header';
|
||||||
|
import Screen from '../../../components/mobile/MobileScreen';
|
||||||
|
import Footer from '../../../components/mobile/Footer';
|
||||||
|
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||||
|
import Console from '../components/Console';
|
||||||
|
import { remSize } from '../../../theme';
|
||||||
|
import ActionStrip from '../../../components/mobile/ActionStrip';
|
||||||
|
|
||||||
|
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
|
||||||
|
|
||||||
|
|
||||||
|
const Expander = styled.div`
|
||||||
|
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MobileIDEView = (props) => {
|
||||||
|
const {
|
||||||
|
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||||
|
selectedFile, updateFileContent, files,
|
||||||
|
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges,
|
||||||
|
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||||
|
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||||
|
const [overlay, setOverlay] = useState(null); // eslint-disable-line
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<Header
|
||||||
|
title={project.name}
|
||||||
|
subtitle={selectedFile.name}
|
||||||
|
leftButton={
|
||||||
|
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
to="/mobile/preferences"
|
||||||
|
onClick={() => setOverlay('preferences')}
|
||||||
|
icon={PreferencesIcon}
|
||||||
|
aria-label="Open preferences menu"
|
||||||
|
/>
|
||||||
|
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<IDEWrapper>
|
||||||
|
<Editor
|
||||||
|
lintWarning={preferences.lintWarning}
|
||||||
|
linewrap={preferences.linewrap}
|
||||||
|
lintMessages={editorAccessibility.lintMessages}
|
||||||
|
updateLintMessage={updateLintMessage}
|
||||||
|
clearLintMessage={clearLintMessage}
|
||||||
|
file={selectedFile}
|
||||||
|
updateFileContent={updateFileContent}
|
||||||
|
fontSize={preferences.fontSize}
|
||||||
|
lineNumbers={preferences.lineNumbers}
|
||||||
|
files={files}
|
||||||
|
editorOptionsVisible={ide.editorOptionsVisible}
|
||||||
|
showEditorOptions={showEditorOptions}
|
||||||
|
closeEditorOptions={closeEditorOptions}
|
||||||
|
showKeyboardShortcutModal={showKeyboardShortcutModal}
|
||||||
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
|
isPlaying={ide.isPlaying}
|
||||||
|
theme={preferences.theme}
|
||||||
|
startRefreshSketch={startRefreshSketch}
|
||||||
|
stopSketch={stopSketch}
|
||||||
|
autorefresh={preferences.autorefresh}
|
||||||
|
unsavedChanges={ide.unsavedChanges}
|
||||||
|
projectSavedTime={project.updatedAt}
|
||||||
|
isExpanded={ide.sidebarIsExpanded}
|
||||||
|
expandSidebar={expandSidebar}
|
||||||
|
collapseSidebar={collapseSidebar}
|
||||||
|
isUserOwner={isUserOwner(props)}
|
||||||
|
clearConsole={clearConsole}
|
||||||
|
consoleEvents={console}
|
||||||
|
showRuntimeErrorWarning={showRuntimeErrorWarning}
|
||||||
|
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
||||||
|
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
||||||
|
provideController={setTmController}
|
||||||
|
/>
|
||||||
|
</IDEWrapper>
|
||||||
|
<Footer>
|
||||||
|
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
|
||||||
|
<ActionStrip />
|
||||||
|
</Footer>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
MobileIDEView.propTypes = {
|
||||||
|
|
||||||
|
preferences: PropTypes.shape({
|
||||||
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
autosave: PropTypes.bool.isRequired,
|
||||||
|
linewrap: PropTypes.bool.isRequired,
|
||||||
|
lineNumbers: PropTypes.bool.isRequired,
|
||||||
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
autorefresh: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
ide: PropTypes.shape({
|
||||||
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||||
|
consoleEvent: PropTypes.array,
|
||||||
|
modalIsVisible: PropTypes.bool.isRequired,
|
||||||
|
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||||
|
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalProjectId: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectName: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectUsername: PropTypes.string.isRequired,
|
||||||
|
editorOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
||||||
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoop: PropTypes.bool.isRequired,
|
||||||
|
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoopMessage: PropTypes.string.isRequired,
|
||||||
|
projectSavedTime: PropTypes.string,
|
||||||
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
|
errorType: PropTypes.string,
|
||||||
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
|
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
editorAccessibility: PropTypes.shape({
|
||||||
|
lintMessages: PropTypes.array.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
id: PropTypes.string
|
||||||
|
}),
|
||||||
|
updatedAt: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
startSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
updateLintMessage: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
clearLintMessage: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
selectedFile: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
updateFileContent: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
files: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
closeEditorOptions: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
showEditorOptions: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
setUnsavedChanges: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
startRefreshSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
stopSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
expandSidebar: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
collapseSidebar: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
clearConsole: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
console: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
method: PropTypes.string.isRequired,
|
||||||
|
args: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
user: PropTypes.shape({
|
||||||
|
authenticated: PropTypes.bool.isRequired,
|
||||||
|
id: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
files: state.files,
|
||||||
|
selectedFile: state.files.find(file => file.isSelectedFile) ||
|
||||||
|
state.files.find(file => file.name === 'sketch.js') ||
|
||||||
|
state.files.find(file => file.name !== 'root'),
|
||||||
|
htmlFile: getHTMLFile(state.files),
|
||||||
|
ide: state.ide,
|
||||||
|
preferences: state.preferences,
|
||||||
|
editorAccessibility: state.editorAccessibility,
|
||||||
|
user: state.user,
|
||||||
|
project: state.project,
|
||||||
|
toast: state.toast,
|
||||||
|
console: state.console
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
EditorAccessibilityActions,
|
||||||
|
FileActions,
|
||||||
|
ProjectActions,
|
||||||
|
IDEActions,
|
||||||
|
PreferencesActions,
|
||||||
|
UserActions,
|
||||||
|
ToastActions,
|
||||||
|
ConsoleActions
|
||||||
|
),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
|
@ -37,6 +37,11 @@ const getSortedCollections = createSelector(
|
||||||
return orderBy(collections, 'name', 'desc');
|
return orderBy(collections, 'name', 'desc');
|
||||||
}
|
}
|
||||||
return orderBy(collections, 'name', 'asc');
|
return orderBy(collections, 'name', 'asc');
|
||||||
|
} else if (field === 'numItems') {
|
||||||
|
if (direction === DIRECTION.DESC) {
|
||||||
|
return orderBy(collections, 'items.length', 'desc');
|
||||||
|
}
|
||||||
|
return orderBy(collections, 'items.length', 'asc');
|
||||||
}
|
}
|
||||||
const sortedCollections = [...collections].sort((a, b) => {
|
const sortedCollections = [...collections].sort((a, b) => {
|
||||||
const result =
|
const result =
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import getConfig from '../../../utils/getConfig';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const getAuthenticated = state => state.user.authenticated;
|
const getAuthenticated = state => state.user.authenticated;
|
||||||
const getTotalSize = state => state.user.totalSize;
|
const getTotalSize = state => state.user.totalSize;
|
||||||
const getAssetsTotalSize = state => state.assets.totalSize;
|
const getAssetsTotalSize = state => state.assets.totalSize;
|
||||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||||
|
|
||||||
export const getCanUploadMedia = createSelector(
|
export const getCanUploadMedia = createSelector(
|
||||||
getAuthenticated,
|
getAuthenticated,
|
||||||
|
|
92
client/modules/Mobile/MobilePreferences.jsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect, useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||||
|
import * as IdeActions from '../IDE/actions/ide';
|
||||||
|
|
||||||
|
import IconButton from '../../components/mobile/IconButton';
|
||||||
|
import Screen from '../../components/mobile/MobileScreen';
|
||||||
|
import Header from '../../components/mobile/Header';
|
||||||
|
import PreferencePicker from '../../components/mobile/PreferencePicker';
|
||||||
|
import { ExitIcon } from '../../common/icons';
|
||||||
|
import { remSize, prop } from '../../theme';
|
||||||
|
import { optionsOnOff, optionsPickOne, preferenceOnOff } from '../IDE/components/Preferences/PreferenceCreators';
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: ${remSize(68)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionHeader = styled.h2`
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
padding-top: ${remSize(32)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionSubeader = styled.h3`
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const MobilePreferences = () => {
|
||||||
|
// Props
|
||||||
|
const {
|
||||||
|
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, lineNumbers, lintWarning
|
||||||
|
} = useSelector(state => state.preferences);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const {
|
||||||
|
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, setLineNumbers, setLintWarning,
|
||||||
|
} = bindActionCreators({ ...PreferencesActions, ...IdeActions }, useDispatch());
|
||||||
|
|
||||||
|
|
||||||
|
const generalSettings = [
|
||||||
|
{
|
||||||
|
title: 'Theme',
|
||||||
|
value: theme,
|
||||||
|
options: optionsPickOne('theme', 'light', 'dark', 'contrast'),
|
||||||
|
onSelect: x => setTheme(x) // setTheme
|
||||||
|
},
|
||||||
|
preferenceOnOff('Autosave', autosave, setAutosave, 'autosave'),
|
||||||
|
preferenceOnOff('Word Wrap', linewrap, setLinewrap, 'linewrap')
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputSettings = [
|
||||||
|
preferenceOnOff('Plain-text', textOutput, setTextOutput, 'text output'),
|
||||||
|
preferenceOnOff('Table-text', gridOutput, setGridOutput, 'table output'),
|
||||||
|
preferenceOnOff('Lint Warning Sound', soundOutput, setSoundOutput, 'sound output')
|
||||||
|
];
|
||||||
|
|
||||||
|
const accessibilitySettings = [
|
||||||
|
preferenceOnOff('Line Numbers', lineNumbers, setLineNumbers),
|
||||||
|
preferenceOnOff('Lint Warning Sound', lintWarning, setLintWarning)
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<section>
|
||||||
|
<Header transparent title="Preferences">
|
||||||
|
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
|
||||||
|
</Header>
|
||||||
|
<section className="preferences">
|
||||||
|
<Content>
|
||||||
|
<SectionHeader>General Settings</SectionHeader>
|
||||||
|
{ generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
<SectionHeader>Accessibility</SectionHeader>
|
||||||
|
{ accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
<SectionHeader>Accessible Output</SectionHeader>
|
||||||
|
<SectionSubeader>Used with screen reader</SectionSubeader>
|
||||||
|
{ outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</Screen>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(MobilePreferences);
|
84
client/modules/Mobile/MobileSketchView.jsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect, useSelector, useDispatch } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Header from '../../components/mobile/Header';
|
||||||
|
import IconButton from '../../components/mobile/IconButton';
|
||||||
|
import PreviewFrame from '../IDE/components/PreviewFrame';
|
||||||
|
import Screen from '../../components/mobile/MobileScreen';
|
||||||
|
import Console from '../IDE/components/Console';
|
||||||
|
import * as ProjectActions from '../IDE/actions/project';
|
||||||
|
import * as IDEActions from '../IDE/actions/ide';
|
||||||
|
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||||
|
import * as ConsoleActions from '../IDE/actions/console';
|
||||||
|
import * as FilesActions from '../IDE/actions/files';
|
||||||
|
|
||||||
|
import { getHTMLFile } from '../IDE/reducers/files';
|
||||||
|
|
||||||
|
import { ExitIcon } from '../../common/icons';
|
||||||
|
import { remSize } from '../../theme';
|
||||||
|
import Footer from '../../components/mobile/Footer';
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: ${remSize(68)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MobileSketchView = (props) => {
|
||||||
|
const { files, ide, preferences } = useSelector(state => state);
|
||||||
|
|
||||||
|
const htmlFile = useSelector(state => getHTMLFile(state.files));
|
||||||
|
const projectName = useSelector(state => state.project.name);
|
||||||
|
const selectedFile = useSelector(state => state.files.find(file => file.isSelectedFile) ||
|
||||||
|
state.files.find(file => file.name === 'sketch.js') ||
|
||||||
|
state.files.find(file => file.name !== 'root'));
|
||||||
|
|
||||||
|
const {
|
||||||
|
setTextOutput, setGridOutput, setSoundOutput, dispatchConsoleEvent,
|
||||||
|
endSketchRefresh, stopSketch, setBlobUrl, expandConsole, clearConsole
|
||||||
|
} = bindActionCreators({
|
||||||
|
...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions
|
||||||
|
}, useDispatch());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<Header
|
||||||
|
leftButton={<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />}
|
||||||
|
title={projectName}
|
||||||
|
/>
|
||||||
|
<Content>
|
||||||
|
<PreviewFrame
|
||||||
|
htmlFile={htmlFile}
|
||||||
|
files={files}
|
||||||
|
head={<link type="text/css" rel="stylesheet" href="/preview-styles.css" />}
|
||||||
|
|
||||||
|
content={selectedFile.content}
|
||||||
|
|
||||||
|
isPlaying
|
||||||
|
isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying}
|
||||||
|
previewIsRefreshing={ide.previewIsRefreshing}
|
||||||
|
|
||||||
|
textOutput={preferences.textOutput}
|
||||||
|
gridOutput={preferences.gridOutput}
|
||||||
|
soundOutput={preferences.soundOutput}
|
||||||
|
autorefresh={preferences.autorefresh}
|
||||||
|
|
||||||
|
setTextOutput={setTextOutput}
|
||||||
|
setGridOutput={setGridOutput}
|
||||||
|
setSoundOutput={setSoundOutput}
|
||||||
|
dispatchConsoleEvent={dispatchConsoleEvent}
|
||||||
|
endSketchRefresh={endSketchRefresh}
|
||||||
|
stopSketch={stopSketch}
|
||||||
|
setBlobUrl={setBlobUrl}
|
||||||
|
expandConsole={expandConsole}
|
||||||
|
clearConsole={clearConsole}
|
||||||
|
/>
|
||||||
|
</Content>
|
||||||
|
<Footer>
|
||||||
|
<Console />
|
||||||
|
</Footer>
|
||||||
|
</Screen>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileSketchView;
|
|
@ -1,13 +1,9 @@
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import axios from 'axios';
|
|
||||||
import * as ActionTypes from '../../constants';
|
import * as ActionTypes from '../../constants';
|
||||||
|
import apiClient from '../../utils/apiClient';
|
||||||
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
|
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
|
||||||
import { showToast, setToastText } from '../IDE/actions/toast';
|
import { showToast, setToastText } from '../IDE/actions/toast';
|
||||||
|
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
|
||||||
const ROOT_URL = __process.env.API_URL;
|
|
||||||
|
|
||||||
export function authError(error) {
|
export function authError(error) {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.AUTH_ERROR,
|
type: ActionTypes.AUTH_ERROR,
|
||||||
|
@ -17,7 +13,7 @@ export function authError(error) {
|
||||||
|
|
||||||
export function signUpUser(previousPath, formValues) {
|
export function signUpUser(previousPath, formValues) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true })
|
apiClient.post('/signup', formValues)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.AUTH_USER,
|
type: ActionTypes.AUTH_USER,
|
||||||
|
@ -26,12 +22,15 @@ export function signUpUser(previousPath, formValues) {
|
||||||
dispatch(justOpenedProject());
|
dispatch(justOpenedProject());
|
||||||
browserHistory.push(previousPath);
|
browserHistory.push(previousPath);
|
||||||
})
|
})
|
||||||
.catch(response => dispatch(authError(response.data.error)));
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch(authError(response.data.error));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginUser(formValues) {
|
export function loginUser(formValues) {
|
||||||
return axios.post(`${ROOT_URL}/login`, formValues, { withCredentials: true });
|
return apiClient.post('/login', formValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginUserSuccess(user) {
|
export function loginUserSuccess(user) {
|
||||||
|
@ -71,7 +70,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
|
||||||
|
|
||||||
export function getUser() {
|
export function getUser() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
|
apiClient.get('/session')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.AUTH_USER,
|
type: ActionTypes.AUTH_USER,
|
||||||
|
@ -82,7 +81,8 @@ export function getUser() {
|
||||||
preferences: response.data.preferences
|
preferences: response.data.preferences
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
const message = response.message || response.data.error;
|
const message = response.message || response.data.error;
|
||||||
dispatch(authError(message));
|
dispatch(authError(message));
|
||||||
});
|
});
|
||||||
|
@ -91,14 +91,15 @@ export function getUser() {
|
||||||
|
|
||||||
export function validateSession() {
|
export function validateSession() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
|
apiClient.get('/session')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (state.user.username !== response.data.username) {
|
if (state.user.username !== response.data.username) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
}
|
}
|
||||||
|
@ -108,13 +109,16 @@ export function validateSession() {
|
||||||
|
|
||||||
export function logoutUser() {
|
export function logoutUser() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.get(`${ROOT_URL}/logout`, { withCredentials: true })
|
apiClient.get('/logout')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.UNAUTH_USER
|
type: ActionTypes.UNAUTH_USER
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(response => dispatch(authError(response.data.error)));
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch(authError(response.data.error));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,14 +127,17 @@ export function initiateResetPassword(formValues) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.RESET_PASSWORD_INITIATE
|
type: ActionTypes.RESET_PASSWORD_INITIATE
|
||||||
});
|
});
|
||||||
axios.post(`${ROOT_URL}/reset-password`, formValues, { withCredentials: true })
|
apiClient.post('/reset-password', formValues)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// do nothing
|
// do nothing
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
message: response.data
|
message: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,14 +146,17 @@ export function initiateVerification() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.EMAIL_VERIFICATION_INITIATE
|
type: ActionTypes.EMAIL_VERIFICATION_INITIATE
|
||||||
});
|
});
|
||||||
axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true })
|
apiClient.post('/verify/send', {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// do nothing
|
// do nothing
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
message: response.data
|
message: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,15 +166,18 @@ export function verifyEmailConfirmation(token) {
|
||||||
type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
|
type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
|
||||||
state: 'checking',
|
state: 'checking',
|
||||||
});
|
});
|
||||||
return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true })
|
return apiClient.get(`/verify?t=${token}`, {})
|
||||||
.then(response => dispatch({
|
.then(response => dispatch({
|
||||||
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
|
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
|
||||||
message: response.data,
|
message: response.data,
|
||||||
}))
|
}))
|
||||||
.catch(response => dispatch({
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.EMAIL_VERIFICATION_INVALID,
|
type: ActionTypes.EMAIL_VERIFICATION_INVALID,
|
||||||
message: response.data
|
message: response.data
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +190,7 @@ export function resetPasswordReset() {
|
||||||
|
|
||||||
export function validateResetPasswordToken(token) {
|
export function validateResetPasswordToken(token) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.get(`${ROOT_URL}/reset-password/${token}`)
|
apiClient.get(`/reset-password/${token}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// do nothing if the token is valid
|
// do nothing if the token is valid
|
||||||
})
|
})
|
||||||
|
@ -189,7 +202,7 @@ export function validateResetPasswordToken(token) {
|
||||||
|
|
||||||
export function updatePassword(token, formValues) {
|
export function updatePassword(token, formValues) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
axios.post(`${ROOT_URL}/reset-password/${token}`, formValues)
|
apiClient.post(`/reset-password/${token}`, formValues)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(loginUserSuccess(response.data));
|
dispatch(loginUserSuccess(response.data));
|
||||||
browserHistory.push('/');
|
browserHistory.push('/');
|
||||||
|
@ -209,14 +222,17 @@ export function updateSettingsSuccess(user) {
|
||||||
|
|
||||||
export function updateSettings(formValues) {
|
export function updateSettings(formValues) {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true })
|
apiClient.put('/account', formValues)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(updateSettingsSuccess(response.data));
|
dispatch(updateSettingsSuccess(response.data));
|
||||||
browserHistory.push('/');
|
browserHistory.push('/');
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
dispatch(setToastText('Settings saved.'));
|
dispatch(setToastText('Settings saved.'));
|
||||||
})
|
})
|
||||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
Promise.reject(new Error(response.data.error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApiKeySuccess(user) {
|
export function createApiKeySuccess(user) {
|
||||||
|
@ -228,21 +244,27 @@ export function createApiKeySuccess(user) {
|
||||||
|
|
||||||
export function createApiKey(label) {
|
export function createApiKey(label) {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true })
|
apiClient.post('/account/api-keys', { label })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch(createApiKeySuccess(response.data));
|
dispatch(createApiKeySuccess(response.data));
|
||||||
})
|
})
|
||||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
Promise.reject(new Error(response.data.error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeApiKey(keyId) {
|
export function removeApiKey(keyId) {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true })
|
apiClient.delete(`/account/api-keys/${keyId}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionTypes.API_KEY_REMOVED,
|
type: ActionTypes.API_KEY_REMOVED,
|
||||||
user: response.data
|
user: response.data
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
Promise.reject(new Error(response.data.error));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|