Merge branch 'release-1.0.2' into release

This commit is contained in:
Cassie Tarakajian 2020-06-01 14:16:37 -04:00
commit 435f40406c
56 changed files with 9201 additions and 385 deletions

View file

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

View file

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

View file

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

2
.gitignore vendored
View file

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

29
.storybook/main.js Normal file
View file

@ -0,0 +1,29 @@
const path = require('path');
module.exports = {
stories: ['../client/**/*.stories.(jsx|mdx)'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-docs',
'@storybook/addon-knobs',
'@storybook/addon-links',
'storybook-addon-theme-playground/dist/register'
],
webpackFinal: async config => {
// do mutation to the config
const rules = config.module.rules;
// modify storybook's file-loader rule to avoid conflicts with svgr
const fileLoaderRule = rules.find(rule => rule.test.test('.svg'));
fileLoaderRule.exclude = path.resolve(__dirname, '../client');
// use svgr for svg files
rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
})
return config;
},
};

31
.storybook/preview.js Normal file
View file

@ -0,0 +1,31 @@
import React from 'react';
import { addDecorator, addParameters } from '@storybook/react';
import { withKnobs } from "@storybook/addon-knobs";
import { withThemePlayground } from 'storybook-addon-theme-playground';
import { ThemeProvider } from "styled-components";
import theme, { Theme } from '../client/theme';
addDecorator(withKnobs);
const themeConfigs = Object.values(Theme).map(
name => {
return { name, theme: theme[name] };
}
);
addDecorator(withThemePlayground({
theme: themeConfigs,
provider: ThemeProvider
}));
addParameters({
options: {
/**
* display the top-level grouping as a "root" in the sidebar
*/
showRoots: true,
},
})
// addDecorator(storyFn => <ThemeProvider theme={theme}>{storyFn()}</ThemeProvider>);

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

@ -0,0 +1,240 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Link } from 'react-router';
import { remSize, prop } from '../theme';
const kinds = {
block: 'block',
icon: 'icon',
inline: 'inline',
};
// The '&&&' will increase the specificity of the
// component's CSS so that it overrides the more
// general global styles
const StyledButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
width: max-content;
text-decoration: none;
color: ${prop('Button.default.foreground')};
background-color: ${prop('Button.default.background')};
cursor: pointer;
border: 2px solid ${prop('Button.default.border')};
border-radius: 2px;
padding: ${remSize(8)} ${remSize(25)};
line-height: 1;
svg * {
fill: ${prop('Button.default.foreground')};
}
&:hover:not(:disabled) {
color: ${prop('Button.hover.foreground')};
background-color: ${prop('Button.hover.background')};
border-color: ${prop('Button.hover.border')};
svg * {
fill: ${prop('Button.hover.foreground')};
}
}
&:active:not(:disabled) {
color: ${prop('Button.active.foreground')};
background-color: ${prop('Button.active.background')};
svg * {
fill: ${prop('Button.active.foreground')};
}
}
&:disabled {
color: ${prop('Button.disabled.foreground')};
background-color: ${prop('Button.disabled.background')};
border-color: ${prop('Button.disabled.border')};
cursor: not-allowed;
svg * {
fill: ${prop('Button.disabled.foreground')};
}
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
const StyledInlineButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: ${prop('primaryTextColor')};
cursor: pointer;
border: none;
line-height: 1;
svg * {
fill: ${prop('primaryTextColor')};
}
&:disabled {
cursor: not-allowed;
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
const StyledIconButton = styled.button`
&&& {
display: flex;
justify-content: center;
align-items: center;
width: ${remSize(32)}px;
height: ${remSize(32)}px;
text-decoration: none;
color: ${prop('Button.default.foreground')};
background-color: ${prop('Button.hover.background')};
cursor: pointer;
border: 1px solid transparent;
border-radius: 50%;
padding: ${remSize(8)} ${remSize(25)};
line-height: 1;
&:hover:not(:disabled) {
color: ${prop('Button.hover.foreground')};
background-color: ${prop('Button.hover.background')};
svg * {
fill: ${prop('Button.hover.foreground')};
}
}
&:active:not(:disabled) {
color: ${prop('Button.active.foreground')};
background-color: ${prop('Button.active.background')};
svg * {
fill: ${prop('Button.active.foreground')};
}
}
&:disabled {
color: ${prop('Button.disabled.foreground')};
background-color: ${prop('Button.disabled.background')};
cursor: not-allowed;
}
> * + * {
margin-left: ${remSize(8)};
}
}
`;
/**
* A Button performs an primary action
*/
const Button = ({
children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props
}) => {
const hasChildren = React.Children.count(children) > 0;
const content = <>{iconBefore}{hasChildren && <span>{children}</span>}{iconAfter}</>;
let StyledComponent = StyledButton;
if (kind === kinds.inline) {
StyledComponent = StyledInlineButton;
} else if (kind === kinds.icon) {
StyledComponent = StyledIconButton;
}
if (href) {
return (
<StyledComponent
kind={kind}
as="a"
aria-label={ariaLabel}
href={href}
{...props}
>
{content}
</StyledComponent>
);
}
if (to) {
return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>;
}
return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>;
};
Button.defaultProps = {
'children': null,
'disabled': false,
'iconAfter': null,
'iconBefore': null,
'kind': kinds.block,
'href': null,
'aria-label': null,
'to': null,
'type': 'button',
};
Button.kinds = kinds;
Button.propTypes = {
/**
* The visible part of the button, telling the user what
* the action is
*/
'children': PropTypes.element,
/**
If the button can be activated or not
*/
'disabled': PropTypes.bool,
/**
* SVG icon to place after child content
*/
'iconAfter': PropTypes.element,
/**
* SVG icon to place before child content
*/
'iconBefore': PropTypes.element,
/**
* The kind of button - determines how it appears visually
*/
'kind': PropTypes.oneOf(Object.values(kinds)),
/**
* Specifying an href will use an <a> to link to the URL
*/
'href': PropTypes.string,
/*
* An ARIA Label used for accessibility
*/
'aria-label': PropTypes.string,
/**
* Specifying a to URL will use a react-router Link
*/
'to': PropTypes.string,
/**
* If using a button, then type is defines the type of button
*/
'type': PropTypes.oneOf(['button', 'submit']),
};
export default Button;

View file

@ -0,0 +1,70 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import Button from './Button';
import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons';
export default {
title: 'Common/Button',
component: Button
};
export const AllFeatures = () => (
<Button
disabled={boolean('disabled', false)}
type="submit"
label={text('label', 'submit')}
>
{text('children', 'this is the button')}
</Button>
);
export const SubmitButton = () => (
<Button type="submit" label="submit">This is a submit button</Button>
);
export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>;
export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>;
export const AnchorButton = () => (
<Button href="http://p5js.org" label="submit">Actually an anchor</Button>
);
export const ReactRouterLink = () => (
<Button to="./somewhere" label="submit">Actually a Link</Button>
);
export const ButtonWithIconBefore = () => (
<Button
iconBefore={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const ButtonWithIconAfter = () => (
<Button
iconAfter={<GithubIcon aria-label="Github logo" />}
>
Create
</Button>
);
export const InlineButtonWithIconAfter = () => (
<Button
iconAfter={<DropdownArrowIcon />}
kind={Button.kinds.inline}
>
File name
</Button>
);
export const InlineIconOnlyButton = () => (
<Button
aria-label="Add to collection"
iconBefore={<PlusIcon />}
kind={Button.kinds.inline}
/>
);

51
client/common/Icons.jsx Normal file
View file

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
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 DropdownArrow from '../images/down-filled-triangle.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 { 'aria-label': ariaLabel } = props;
if (ariaLabel) {
return (<SvgComponent
{...props}
aria-label={ariaLabel}
role="img"
focusable="false"
/>);
}
return (<SvgComponent
{...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 DropdownArrowIcon = withLabel(DropdownArrow);

View file

@ -0,0 +1,18 @@
import React from 'react';
import { select } from '@storybook/addon-knobs';
import * as icons from './icons';
export default {
title: 'Common/Icons',
component: icons
};
export const AllIcons = () => {
const names = Object.keys(icons);
const SelectedIcon = icons[select('name', names, names[0])];
return (
<SelectedIcon />
);
};

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

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
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 DropdownArrow from '../images/down-filled-triangle.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 { 'aria-label': ariaLabel } = props;
if (ariaLabel) {
return (<SvgComponent
{...props}
aria-label={ariaLabel}
role="img"
focusable="false"
/>);
}
return (<SvgComponent
{...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 DropdownArrowIcon = withLabel(DropdownArrow);

View file

@ -0,0 +1,18 @@
import React from 'react';
import { select } from '@storybook/addon-knobs';
import * as icons from './icons';
export default {
title: 'Common/Icons',
component: icons
};
export const AllIcons = () => {
const names = Object.keys(icons);
const SelectedIcon = icons[select('name', names, names[0])];
return (
<SelectedIcon />
);
};

View file

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

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

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 539 B

View file

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

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 543 B

View file

@ -3,8 +3,10 @@ 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';
require('./styles/main.scss'); require('./styles/main.scss');
@ -18,7 +20,9 @@ const store = configureStore(initialState);
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
<Router history={history} routes={routes(store)} /> <ThemeProvider>
<Router history={history} routes={routes(store)} />
</ThemeProvider>
</Provider> </Provider>
); );

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

@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title=" |Intro" />
# Welcome to the P5.js Web Editor Style Guide
This guide will contain all the components in the project, with examples of how they can be reused.

View file

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import theme, { Theme } from '../../../theme';
const Provider = ({ children, currentTheme }) => (
<ThemeProvider theme={{ ...theme[currentTheme] }}>
{children}
</ThemeProvider>
);
Provider.propTypes = {
children: PropTypes.node.isRequired,
currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired,
};
function mapStateToProps(state) {
return {
currentTheme: state.preferences.theme,
};
}
export default connect(mapStateToProps)(Provider);

View file

@ -49,10 +49,10 @@ 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 }) axios.get(`${ROOT_URL}/${username}/projects/${id}`, { withCredentials: true })
.then((response) => { .then((response) => {
dispatch(setProject(response.data)); dispatch(setProject(response.data));
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));

View file

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

View file

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

View file

@ -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,12 +36,10 @@ class NewFolderForm extends React.Component {
ref={(element) => { this.fileName = element; }} ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)} {...domOnlyProps(name)}
/> />
<input <Button
type="submit" type="submit"
value="Add Folder" >Add Folder
aria-label="add folder" </Button>
className="new-folder-form__submit"
/>
</div> </div>
{name.touched && name.error && <span className="form-error">{name.error}</span>} {name.touched && name.error && <span className="form-error">{name.error}</span>}
</form> </form>

View file

@ -53,9 +53,9 @@ 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);
} }
} }
@ -123,6 +123,11 @@ class IDEView extends React.Component {
this.autosaveInterval = null; this.autosaveInterval = null;
} }
getTitle = () => {
const { id } = this.props.project;
return id ? `p5.js Web Editor | ${this.props.project.name}` : 'p5.js Web Editor';
}
isUserOwner() { isUserOwner() {
return this.props.project.owner && this.props.project.owner.id === this.props.user.id; return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
} }
@ -203,7 +208,7 @@ class IDEView extends React.Component {
return ( return (
<div className="ide"> <div className="ide">
<Helmet> <Helmet>
<title>p5.js Web Editor | {this.props.project.name}</title> <title>{this.getTitle()}</title>
</Helmet> </Helmet>
{this.props.toast.isVisible && <Toast />} {this.props.toast.isVisible && <Toast />}
<Nav <Nav

View file

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

View file

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

View file

@ -6,6 +6,9 @@ import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Button from '../../../common/Button';
import { DropdownArrowIcon } from '../../../common/icons';
import * as ProjectActions from '../../IDE/actions/project'; import * as ProjectActions from '../../IDE/actions/project';
import * as ProjectsActions from '../../IDE/actions/projects'; import * as ProjectsActions from '../../IDE/actions/projects';
import * as CollectionsActions from '../../IDE/actions/collections'; import * as CollectionsActions from '../../IDE/actions/collections';
@ -20,7 +23,6 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc
import CopyableInput from '../../IDE/components/CopyableInput'; import CopyableInput from '../../IDE/components/CopyableInput';
import { SketchSearchbar } from '../../IDE/components/Searchbar'; import { SketchSearchbar } from '../../IDE/components/Searchbar';
import DropdownArrowIcon from '../../../images/down-arrow.svg';
import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import RemoveIcon from '../../../images/close.svg'; import RemoveIcon from '../../../images/close.svg';
@ -50,14 +52,12 @@ const ShareURL = ({ value }) => {
return ( return (
<div className="collection-share" ref={node}> <div className="collection-share" ref={node}>
<button <Button
className="collection-share__button"
onClick={() => setShowURL(!showURL)} onClick={() => setShowURL(!showURL)}
aria-label="Show collection share URL" iconAfter={<DropdownArrowIcon />}
> >
<span>Share</span> Share
<DropdownArrowIcon className="collection-share__arrow" focusable="false" aria-hidden="true" /> </Button>
</button>
{ showURL && { showURL &&
<div className="collection__share-dropdown"> <div className="collection__share-dropdown">
<CopyableInput value={value} label="Link to Collection" /> <CopyableInput value={value} label="Link to Collection" />
@ -264,9 +264,9 @@ class Collection extends React.Component {
</p> </p>
{ {
this.isOwner() && this.isOwner() &&
<button className="collection-metadata__add-button" onClick={this.showAddSketches}> <Button onClick={this.showAddSketches}>
Add Sketch Add Sketch
</button> </Button>
} }
</div> </div>
</div> </div>
@ -317,7 +317,7 @@ class Collection extends React.Component {
_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, 'arrowDown': true,
'sketches-table__header--selected': field === fieldName 'sketches-table__header--selected': field === fieldName
}); });
const buttonLabel = this._getButtonLabel(fieldName, displayName); const buttonLabel = this._getButtonLabel(fieldName, displayName);

View file

@ -6,6 +6,7 @@ import { bindActionCreators } from 'redux';
import * as CollectionsActions from '../../IDE/actions/collections'; import * as CollectionsActions from '../../IDE/actions/collections';
import { generateCollectionName } from '../../../utils/generateRandomName'; import { generateCollectionName } from '../../../utils/generateRandomName';
import Button from '../../../common/Button';
class CollectionCreate extends React.Component { class CollectionCreate extends React.Component {
constructor() { constructor() {
@ -81,7 +82,7 @@ class CollectionCreate extends React.Component {
rows="4" rows="4"
/> />
</p> </p>
<input type="submit" disabled={invalid} value="Create collection" aria-label="create collection" /> <Button type="submit" disabled={invalid}>Create collection</Button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GithubIcon from '../../../images/github.svg';
function GithubButton(props) {
return (
<a
className="github-button"
href="/auth/github"
>
<GithubIcon className="github-icon" role="img" aria-label="GitHub Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GithubButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GithubButton;

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GoogleIcon from '../../../images/google.svg';
function GoogleButton(props) {
return (
<a
className="google-button"
href="/auth/google/"
>
<GoogleIcon className="google-icon" role="img" aria-label="Google Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GoogleButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GoogleButton;

View file

@ -1,5 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from '../../../common/Button';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
function LoginForm(props) { function LoginForm(props) {
@ -30,7 +33,11 @@ function LoginForm(props) {
/> />
{password.touched && password.error && <span className="form-error">{password.error}</span>} {password.touched && password.error && <span className="form-error">{password.error}</span>}
</p> </p>
<input type="submit" disabled={submitting || pristine} value="Log In" aria-label="login" /> <Button
type="submit"
disabled={submitting || pristine}
>Log In
</Button>
</form> </form>
); );
} }

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function NewPasswordForm(props) { function NewPasswordForm(props) {
const { const {
@ -34,7 +36,7 @@ function NewPasswordForm(props) {
<span className="form-error">{confirmPassword.error}</span> <span className="form-error">{confirmPassword.error}</span>
} }
</p> </p>
<input type="submit" disabled={submitting || invalid || pristine} value="Set New Password" aria-label="sign up" /> <Button type="submit" disabled={submitting || invalid || pristine}>Set New Password</Button>
</form> </form>
); );
} }

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function ResetPasswordForm(props) { function ResetPasswordForm(props) {
const { const {
@ -19,12 +21,11 @@ function ResetPasswordForm(props) {
/> />
{email.touched && email.error && <span className="form-error">{email.error}</span>} {email.touched && email.error && <span className="form-error">{email.error}</span>}
</p> </p>
<input <Button
type="submit" type="submit"
disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate} disabled={submitting || invalid || pristine || props.user.resetPasswordInitiate}
value="Send Password Reset Email" >Send Password Reset Email
aria-label="Send email to reset password" </Button>
/>
</form> </form>
); );
} }

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button';
function SignupForm(props) { function SignupForm(props) {
const { const {
@ -58,7 +60,11 @@ function SignupForm(props) {
<span className="form-error">{confirmPassword.error}</span> <span className="form-error">{confirmPassword.error}</span>
} }
</p> </p>
<input type="submit" disabled={submitting || invalid || pristine} value="Sign Up" aria-label="sign up" /> <Button
type="submit"
disabled={submitting || invalid || pristine}
>Sign Up
</Button>
</form> </form>
); );
} }

View file

@ -0,0 +1,52 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import { remSize } from '../../../theme';
import { GithubIcon, GoogleIcon } from '../../../common/icons';
import Button from '../../../common/Button';
const authUrls = {
github: '/auth/github',
google: '/auth/google'
};
const labels = {
github: 'Login with GitHub',
google: 'Login with Google'
};
const icons = {
github: GithubIcon,
google: GoogleIcon
};
const services = {
github: 'github',
google: 'google'
};
const StyledButton = styled(Button)`
width: ${remSize(300)};
`;
function SocialAuthButton({ service }) {
const ServiceIcon = icons[service];
return (
<StyledButton
iconBefore={<ServiceIcon aria-label={`${service} logo`} />}
href={authUrls[service]}
>
{labels[service]}
</StyledButton>
);
}
SocialAuthButton.services = services;
SocialAuthButton.propTypes = {
service: PropTypes.oneOf(['github', 'google']).isRequired
};
export default SocialAuthButton;

View file

@ -0,0 +1,16 @@
import React from 'react';
import SocialAuthButton from './SocialAuthButton';
export default {
title: 'User/components/SocialAuthButton',
component: SocialAuthButton
};
export const Github = () => (
<SocialAuthButton service={SocialAuthButton.services.github}>Log in with Github</SocialAuthButton>
);
export const Google = () => (
<SocialAuthButton service={SocialAuthButton.services.google}>Sign up with Google</SocialAuthButton>
);

View file

@ -8,8 +8,7 @@ import { Helmet } from 'react-helmet';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import AccountForm from '../components/AccountForm'; import AccountForm from '../components/AccountForm';
import { validateSettings } from '../../../utils/reduxFormUtils'; import { validateSettings } from '../../../utils/reduxFormUtils';
import GithubButton from '../components/GithubButton'; import SocialAuthButton from '../components/SocialAuthButton';
import GoogleButton from '../components/GoogleButton';
import APIKeyForm from '../components/APIKeyForm'; import APIKeyForm from '../components/APIKeyForm';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
@ -24,8 +23,10 @@ function SocialLoginPanel(props) {
<p className="account__social-text"> <p className="account__social-text">
Use your GitHub or Google account to log into the p5.js Web Editor. Use your GitHub or Google account to log into the p5.js Web Editor.
</p> </p>
<GithubButton buttonText="Login with GitHub" /> <div className="account__social-stack">
<GoogleButton buttonText="Login with Google" /> <SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} />
</div>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -2,8 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { browserHistory, Link } from 'react-router'; import { browserHistory } from 'react-router';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import Button from '../../../common/Button';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import Overlay from '../../App/components/Overlay'; import Overlay from '../../App/components/Overlay';
@ -79,16 +82,16 @@ class DashboardView extends React.Component {
case TabKey.collections: case TabKey.collections:
return this.isOwner() && ( return this.isOwner() && (
<React.Fragment> <React.Fragment>
<Link className="dashboard__action-button" to={`/${username}/collections/create`}> <Button to={`/${username}/collections/create`}>
Create collection Create collection
</Link> </Button>
<CollectionSearchbar /> <CollectionSearchbar />
</React.Fragment>); </React.Fragment>);
case TabKey.sketches: case TabKey.sketches:
default: default:
return ( return (
<React.Fragment> <React.Fragment>
{this.isOwner() && <Link className="dashboard__action-button" to="/">New sketch</Link>} {this.isOwner() && <Button to="/">New sketch</Button>}
<SketchSearchbar /> <SketchSearchbar />
</React.Fragment> </React.Fragment>
); );

View file

@ -6,8 +6,7 @@ import { Helmet } from 'react-helmet';
import { validateAndLoginUser } from '../actions'; import { validateAndLoginUser } from '../actions';
import LoginForm from '../components/LoginForm'; import LoginForm from '../components/LoginForm';
import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateLogin } from '../../../utils/reduxFormUtils';
import GithubButton from '../components/GithubButton'; import SocialAuthButton from '../components/SocialAuthButton';
import GoogleButton from '../components/GoogleButton';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
class LoginView extends React.Component { class LoginView extends React.Component {
@ -41,8 +40,10 @@ class LoginView extends React.Component {
<h2 className="form-container__title">Log In</h2> <h2 className="form-container__title">Log In</h2>
<LoginForm {...this.props} /> <LoginForm {...this.props} />
<h2 className="form-container__divider">Or</h2> <h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" /> <div className="form-container__stack">
<GoogleButton buttonText="Login with Google" /> <SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} />
</div>
<p className="form__navigation-options"> <p className="form__navigation-options">
Don&apos;t have an account?&nbsp; Don&apos;t have an account?&nbsp;
<Link className="form__signup-button" to="/signup">Sign Up</Link> <Link className="form__signup-button" to="/signup">Sign Up</Link>

View file

@ -8,6 +8,7 @@ import { reduxForm } from 'redux-form';
import * as UserActions from '../actions'; import * as UserActions from '../actions';
import SignupForm from '../components/SignupForm'; import SignupForm from '../components/SignupForm';
import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateSignup } from '../../../utils/reduxFormUtils';
import SocialAuthButton from '../components/SocialAuthButton';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
const __process = (typeof global !== 'undefined' ? global : window).process; const __process = (typeof global !== 'undefined' ? global : window).process;
@ -33,6 +34,11 @@ class SignupView extends React.Component {
<div className="form-container__content"> <div className="form-container__content">
<h2 className="form-container__title">Sign Up</h2> <h2 className="form-container__title">Sign Up</h2>
<SignupForm {...this.props} /> <SignupForm {...this.props} />
<h2 className="form-container__divider">Or</h2>
<div className="form-container__stack">
<SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} />
</div>
<p className="form__navigation-options"> <p className="form__navigation-options">
Already have an account?&nbsp; Already have an account?&nbsp;
<Link className="form__login-button" to="/login">Log In</Link> <Link className="form__login-button" to="/login">Log In</Link>

View file

@ -74,6 +74,7 @@ h2 {
h3 { h3 {
font-weight: normal; font-weight: normal;
font-size: #{16 / $base-font-size}rem;
} }
h4 { h4 {
font-weight: normal; font-weight: normal;

View file

@ -20,3 +20,12 @@
.account__social-text { .account__social-text {
padding-bottom: #{15 / $base-font-size}rem; padding-bottom: #{15 / $base-font-size}rem;
} }
.account__social-stack {
display: flex;
}
.account__social-stack > * {
margin-right: #{15 / $base-font-size}rem;
}

View file

@ -12,22 +12,6 @@
font-weight: bold; font-weight: bold;
} }
.api-key-form__create-button {
display: flex;
justify-content: center;
align-items: center;
}
.api-key-form__create-icon {
display: flex;
height: #{12 / $base-font-size}rem;
margin-right: #{3 / $base-font-size}rem;
}
.api-key-form__create-button .isvg {
padding-right: 10px;
}
.api-key-list { .api-key-list {
display: block; display: block;
max-width: 900px; max-width: 900px;

View file

@ -94,16 +94,6 @@
position: relative; position: relative;
} }
.collection-share__button {
@extend %button;
display: flex;
align-items: center;
}
.collection-share__arrow {
margin-left: #{5 / $base-font-size}rem;
}
.collection-share .copyable-input { .collection-share .copyable-input {
padding-bottom: 0; padding-bottom: 0;
} }
@ -114,11 +104,6 @@
width: #{350 / $base-font-size}rem; width: #{350 / $base-font-size}rem;
} }
.collection-metadata__add-button {
@extend %button;
flex-grow: 0;
}
.collection-content { .collection-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -83,8 +83,3 @@
.dashboard-header__actions > *:not(:first-child) { .dashboard-header__actions > *:not(:first-child) {
margin-left: #{15 / $base-font-size}rem; margin-left: #{15 / $base-font-size}rem;
} }
.dashboard__action-button {
flex-grow: 0;
@extend %button;
}

View file

@ -57,3 +57,7 @@
.form-container__exit-button { .form-container__exit-button {
@include icon(); @include icon();
} }
.form-container__stack > * + * {
margin-top: #{10 / $base-font-size}rem;
}

View file

@ -9,6 +9,10 @@
} }
} }
.form > * + * {
margin-top: #{12 / $base-font-size}rem;
}
.form--inline { .form--inline {
display: flex; display: flex;
align-items: center; align-items: center;
@ -79,20 +83,25 @@
} }
.form [type="submit"] { .form [type="submit"] {
@extend %button; margin-left: auto;
padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem; margin-right: auto;
margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
} }
.form [type="submit"][disabled] { // .form [type="submit"] {
cursor: not-allowed; // @extend %button;
} // padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem;
// margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
// }
.form--inline [type="submit"] { // .form [type="submit"][disabled] {
margin: 0 0 0 #{24 / $base-font-size}rem; // cursor: not-allowed;
} // }
.form [type="submit"][disabled], // .form--inline [type="submit"] {
.form--inline [type="submit"][disabled] { // margin: 0 0 0 #{24 / $base-font-size}rem;
cursor: not-allowed; // }
}
// .form [type="submit"][disabled],
// .form--inline [type="submit"][disabled] {
// cursor: not-allowed;
// }

View file

@ -1,35 +0,0 @@
.github-button,
.google-button {
@include themify() {
@extend %button;
& path {
color: getThemifyVariable('primary-text-color');
}
&:hover path, &:active path {
fill: $white;
}
&:hover, &:active {
color: getThemifyVariable('button-hover-color');
background-color: getThemifyVariable('button-background-hover-color');
border-color: getThemifyVariable('button-background-hover-color');
}
}
width: #{300 / $base-font-size}rem;
display: flex;
justify-content: center;
align-items: center;
& + & {
margin-top: #{10 / $base-font-size}rem;
}
}
.github-icon {
margin-right: #{10 / $base-font-size}rem;
}
.google-icon {
width: #{32 / $base-font-size}rem;
height: #{32 / $base-font-size}rem;
margin-right: #{10 / $base-font-size}rem;
}

View file

@ -4,6 +4,11 @@
width: #{450 / $base-font-size}rem; width: #{450 / $base-font-size}rem;
} }
.keyboard-shortcuts-note {
text-align: center;
margin-bottom: 24px;
}
.keyboard-shortcut-item { .keyboard-shortcut-item {
display: flex; display: flex;
& + & { & + & {
@ -13,8 +18,31 @@
} }
.keyboard-shortcut__command { .keyboard-shortcut__command {
width: 50%;
font-weight: bold; font-weight: bold;
text-align: right; text-align: right;
padding-right: #{10 / $base-font-size}rem; margin-right: #{10 / $base-font-size}rem;
padding: #{3 / $base-font-size}rem;
@include themify {
border: 1px solid getThemifyVariable("button-border-color");
border-radius: 3px;
}
}
.keyboard-shortcuts__title {
padding-bottom: #{10 / $base-font-size}rem;
}
.keyboard-shortcuts__description {
padding-bottom: #{10 / $base-font-size}rem;
}
.keyboard-shortcuts__list:not(:last-of-type) {
padding-bottom: #{10 / $base-font-size}rem;
}
.keyboard-shortcuts__title:not(:first-of-type) {
@include themify() {
border-top: 1px dashed getThemifyVariable("button-border-color");
padding-top: #{10 / $base-font-size}rem;
}
} }

View file

@ -29,6 +29,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
height: #{35 / $base-font-size}rem; height: #{35 / $base-font-size}rem;
& .isvg {
margin-left: #{8 / $base-font-size}rem;
}
& svg { & svg {
@include themify() { @include themify() {
fill: getThemifyVariable('inactive-text-color') fill: getThemifyVariable('inactive-text-color')

View file

@ -31,7 +31,6 @@
@import 'components/resizer'; @import 'components/resizer';
@import 'components/overlay'; @import 'components/overlay';
@import 'components/about'; @import 'components/about';
@import 'components/github-button';
@import 'components/forms'; @import 'components/forms';
@import 'components/toast'; @import 'components/toast';
@import 'components/timer'; @import 'components/timer';

145
client/theme.js Normal file
View file

@ -0,0 +1,145 @@
import lodash from 'lodash';
export const Theme = {
contrast: 'contrast',
dark: 'dark',
light: 'light',
};
export const colors = {
p5jsPink: '#ed225d',
processingBlue: '#007BBB',
p5jsActivePink: '#f10046',
white: '#fff',
black: '#000',
yellow: '#f5dc23',
orange: '#ffa500',
red: '#ff0000',
lightsteelblue: '#B0C4DE',
dodgerblue: '#1E90FF',
p5ContrastPink: ' #FFA9D9',
borderColor: ' #B5B5B5',
outlineColor: '#0F9DD7',
};
export const grays = {
lightest: '#FFF', // primary
lighter: '#FBFBFB',
light: '#F0F0F0', // primary
mediumLight: '#D9D9D9',
middleLight: '#A6A6A6',
middleGray: '#747474', // primary
middleDark: '#666',
mediumDark: '#4D4D4D',
dark: '#333', // primary
darker: '#1C1C1C',
darkest: '#000',
};
export const common = {
baseFontSize: 12
};
export const remSize = size => `${size / common.baseFontSize}rem`;
export const prop = key => (props) => {
const keypath = `theme.${key}`;
const value = lodash.get(props, keypath);
if (value == null) {
throw new Error(`themed prop ${key} not found`);
}
return value;
};
export default {
[Theme.light]: {
colors,
...common,
primaryTextColor: grays.dark,
Button: {
default: {
foreground: colors.black,
background: grays.light,
border: grays.middleLight,
},
hover: {
foreground: grays.lightest,
background: colors.p5jsPink,
border: colors.p5jsPink,
},
active: {
foreground: grays.lightest,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: colors.black,
background: grays.light,
border: grays.middleLight,
},
},
},
[Theme.dark]: {
colors,
...common,
primaryTextColor: grays.lightest,
Button: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
hover: {
foreground: grays.lightest,
background: colors.p5jsPink,
border: colors.p5jsPink,
},
active: {
foreground: grays.lightest,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
},
},
[Theme.contrast]: {
colors,
...common,
primaryTextColor: grays.lightest,
Button: {
default: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
hover: {
foreground: grays.dark,
background: colors.yellow,
border: colors.yellow,
},
active: {
foreground: grays.dark,
background: colors.p5jsActivePink,
border: colors.p5jsActivePink,
},
disabled: {
foreground: grays.light,
background: grays.dark,
border: grays.middleDark,
},
},
},
};

8153
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,9 @@
"fetch-examples-gg:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-gg.bundle.js", "fetch-examples-gg:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-gg.bundle.js",
"fetch-examples-ml5:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-ml5.bundle.js", "fetch-examples-ml5:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-ml5.bundle.js",
"update-syntax-highlighting": "node ./server/scripts/update-syntax-highlighting.js", "update-syntax-highlighting": "node ./server/scripts/update-syntax-highlighting.js",
"heroku-postbuild": "touch .env; npm run build" "heroku-postbuild": "touch .env; npm run build",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -69,6 +71,12 @@
"@babel/plugin-transform-react-inline-elements": "^7.8.3", "@babel/plugin-transform-react-inline-elements": "^7.8.3",
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.8.3",
"@storybook/addon-actions": "^5.3.6",
"@storybook/addon-docs": "^5.3.6",
"@storybook/addon-knobs": "^5.3.6",
"@storybook/addon-links": "^5.3.6",
"@storybook/addons": "^5.3.6",
"@storybook/react": "^5.3.6",
"@svgr/webpack": "^5.4.0", "@svgr/webpack": "^5.4.0",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^9.0.0", "babel-eslint": "^9.0.0",
@ -81,7 +89,7 @@
"enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-react-16": "^1.15.2",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0", "eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.18.3",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
@ -99,6 +107,7 @@
"react-test-renderer": "^16.12.0", "react-test-renderer": "^16.12.0",
"rimraf": "^2.7.1", "rimraf": "^2.7.1",
"sass-loader": "^6.0.7", "sass-loader": "^6.0.7",
"storybook-addon-theme-playground": "^1.2.0",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"terser-webpack-plugin": "^1.4.3", "terser-webpack-plugin": "^1.4.3",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
@ -117,6 +126,7 @@
"archiver": "^1.1.0", "archiver": "^1.1.0",
"async": "^2.6.3", "async": "^2.6.3",
"axios": "^0.18.1", "axios": "^0.18.1",
"babel-plugin-styled-components": "^1.10.6",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"blob-util": "^1.2.1", "blob-util": "^1.2.1",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
@ -191,6 +201,8 @@
"sinon-mongoose": "^2.3.0", "sinon-mongoose": "^2.3.0",
"slugify": "^1.3.6", "slugify": "^1.3.6",
"srcdoc-polyfill": "^0.2.0", "srcdoc-polyfill": "^0.2.0",
"styled-components": "^5.0.0",
"styled-theming": "^2.2.0",
"url": "^0.11.0", "url": "^0.11.0",
"webpack": "^4.41.6", "webpack": "^4.41.6",
"webpack-dev-middleware": "^2.0.6", "webpack-dev-middleware": "^2.0.6",

View file

@ -63,25 +63,21 @@ export function updateProject(req, res) {
} }
export function getProject(req, res) { export function getProject(req, res) {
const projectId = req.params.project_id; const { project_id: projectId, username } = req.params;
Project.findById(projectId) User.findOne({ username }, (err, user) => { // eslint-disable-line
.populate('user', 'username') if (!user) {
.exec((err, project) => { // eslint-disable-line return res.status(404).send({ message: 'Project with that username does not exist' });
if (err) { }
return res.status(404).send({ message: 'Project with that id does not exist' }); Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] })
} else if (!project) { .populate('user', 'username')
Project.findOne({ slug: projectId }) .exec((err, project) => { // eslint-disable-line
.populate('user', 'username') if (err) {
.exec((innerErr, projectBySlug) => { console.log(err);
if (innerErr || !projectBySlug) { return res.status(404).send({ message: 'Project with that id does not exist' });
return res.status(404).send({ message: 'Project with that id does not exist' }); }
}
return res.json(projectBySlug);
});
} else {
return res.json(project); return res.json(project);
} });
}); });
} }
export function getProjectsForUserId(userId) { export function getProjectsForUserId(userId) {
@ -150,18 +146,10 @@ export function projectForUserExists(username, projectId, callback) {
callback(false); callback(false);
return; return;
} }
Project.findOne({ _id: projectId, user: user._id }, (innerErr, project) => { Project.findOne({ user: user._id, $or: [{ _id: projectId }, { slug: projectId }] }, (innerErr, project) => {
if (project) { if (project) {
callback(true); callback(true);
return;
} }
Project.findOne({ slug: projectId, user: user._id }, (slugError, projectBySlug) => {
if (projectBySlug) {
callback(true);
return;
}
callback(false);
});
}); });
}); });
} }

View file

@ -8,7 +8,7 @@ router.post('/projects', isAuthenticated, ProjectController.createProject);
router.put('/projects/:project_id', isAuthenticated, ProjectController.updateProject); router.put('/projects/:project_id', isAuthenticated, ProjectController.updateProject);
router.get('/projects/:project_id', ProjectController.getProject); router.get('/:username/projects/:project_id', ProjectController.getProject);
router.delete('/projects/:project_id', isAuthenticated, ProjectController.deleteProject); router.delete('/projects/:project_id', isAuthenticated, ProjectController.deleteProject);

View file

@ -46,17 +46,20 @@ if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) {
})); }));
} }
const corsOriginsWhitelist = [ const allowedCorsOrigins = [
/p5js\.org$/, /p5js\.org$/,
]; ];
// to allow client-only development
if (process.env.CORS_ALLOW_LOCALHOST === 'true') {
allowedCorsOrigins.push(/localhost/);
}
// Run Webpack dev server in development mode // Run Webpack dev server in development mode
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
const compiler = webpack(config); const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler)); app.use(webpackHotMiddleware(compiler));
corsOriginsWhitelist.push(/localhost/);
} }
const mongoConnectionString = process.env.MONGO_URL; const mongoConnectionString = process.env.MONGO_URL;
@ -65,7 +68,7 @@ app.set('trust proxy', true);
// Enable Cross-Origin Resource Sharing (CORS) for all origins // Enable Cross-Origin Resource Sharing (CORS) for all origins
const corsMiddleware = cors({ const corsMiddleware = cors({
credentials: true, credentials: true,
origin: corsOriginsWhitelist, origin: allowedCorsOrigins,
}); });
app.use(corsMiddleware); app.use(corsMiddleware);
// Enable pre-flight OPTIONS route for all end-points // Enable pre-flight OPTIONS route for all end-points