Create Asset List View and refactor overlay code (#356)

* start to create asset list

* begin refactoring overlay component to remove duplicate code

* refactoring of overlays, asset list styles

* changes to add size to asset list

* fixes to asset list

* handle case in which a user hasn't uploaded any assets

* fix bug in which asset list only grabbed first asset

* remove console.log

* update overlay exit styling to use icon mixin
This commit is contained in:
Cassie Tarakajian 2017-07-11 17:37:43 +02:00 committed by GitHub
parent 080e9aa823
commit e140702784
30 changed files with 716 additions and 502 deletions

View file

@ -142,6 +142,11 @@ class Nav extends React.PureComponent {
My sketches My sketches
</Link> </Link>
</li> </li>
<li>
<Link to={`/${this.props.user.username}/assets`}>
My assets
</Link>
</li>
<li> <li>
<Link to={`/${this.props.user.username}/account`}> <Link to={`/${this.props.user.username}/account`}>
My account My account

View file

@ -1,3 +1,5 @@
// TODO Organize this file by reducer type, ot break this apart into
// multiple files
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const TOGGLE_SKETCH = 'TOGGLE_SKETCH'; export const TOGGLE_SKETCH = 'TOGGLE_SKETCH';
@ -124,3 +126,4 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
export const SHOW_HELP_MODAL = 'SHOW_HELP_MODAL'; export const SHOW_HELP_MODAL = 'SHOW_HELP_MODAL';
export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL'; export const HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';
export const SET_ASSETS = 'SET_ASSETS';

View file

@ -1,21 +1,70 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
function Overlay(props) { const exitUrl = require('../../../images/exit.svg');
return (
<div className="overlay"> class Overlay extends React.Component {
<div className="overlay-content"> constructor(props) {
{props.children} super(props);
this.close = this.close.bind(this);
}
componentDidMount() {
this.overlay.focus();
}
close() {
if (!this.props.closeOverlay) {
browserHistory.push(this.props.previousPath);
} else {
this.props.closeOverlay();
}
}
render() {
const {
ariaLabel,
title,
children
} = this.props;
return (
<div className="overlay">
<div className="overlay__content">
<section
tabIndex="0"
role="main"
aria-label={ariaLabel}
ref={(element) => { this.overlay = element; }}
className="overlay__body"
>
<header className="overlay__header">
<h2 className="overlay__title">{title}</h2>
<button className="overlay__close-button" onClick={this.close}>
<InlineSVG src={exitUrl} alt="close overlay" />
</button>
</header>
{children}
</section>
</div>
</div> </div>
</div> );
); }
} }
Overlay.propTypes = { Overlay.propTypes = {
children: PropTypes.element children: PropTypes.element,
closeOverlay: PropTypes.func,
title: PropTypes.string,
ariaLabel: PropTypes.string,
previousPath: PropTypes.string.isRequired
}; };
Overlay.defaultProps = { Overlay.defaultProps = {
children: null children: null,
title: 'Modal',
closeOverlay: null,
ariaLabel: 'modal'
}; };
export default Overlay; export default Overlay;

View file

@ -0,0 +1,30 @@
import axios from 'axios';
import * as ActionTypes from '../../../constants';
const ROOT_URL = process.env.API_URL;
function setAssets(assets) {
return {
type: ActionTypes.SET_ASSETS,
assets
};
}
export function getAssets(username) {
return (dispatch, getState) => {
axios.get(`${ROOT_URL}/S3/${username}/objects`, { withCredentials: true })
.then((response) => {
dispatch(setAssets(response.data.assets));
})
.catch(response => dispatch({
type: ActionTypes.ERROR
}));
};
}
export function deleteAsset(assetKey, userId) {
return {
type: 'PLACEHOLDER'
};
}

View file

@ -1,131 +1,101 @@
import React, { PropTypes } from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
const squareLogoUrl = require('../../../images/p5js-square-logo.svg'); const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
const playUrl = require('../../../images/play.svg'); const playUrl = require('../../../images/play.svg');
const asteriskUrl = require('../../../images/p5-asterisk.svg'); const asteriskUrl = require('../../../images/p5-asterisk.svg');
class About extends React.Component { function About(props) {
constructor(props) { return (
super(props); <div className="about__content">
this.closeAboutModal = this.closeAboutModal.bind(this); <div className="about__content-column">
} <InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
<p className="about__play-video">
componentDidMount() { <a
this.aboutSection.focus(); href="http://hello.p5js.org/"
} target="_blank"
rel="noopener noreferrer"
closeAboutModal() { >
browserHistory.push(this.props.previousPath); <InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
} Play hello! video</a>
</p>
render() { </div>
return ( <div className="about__content-column">
<section className="about" ref={(element) => { this.aboutSection = element; }} tabIndex="0"> <h3 className="about__content-column-title">New to p5.js?</h3>
<header className="about__header"> <p className="about__content-column-list">
<h2 className="about__header-title">Welcome</h2> <a
<button className="about__exit-button" onClick={this.closeAboutModal}> href="https://p5js.org/examples/"
<InlineSVG src={exitUrl} alt="Close About Overlay" /> target="_blank"
</button> rel="noopener noreferrer"
</header> >
<div className="about__content"> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
<div className="about__content-column"> Examples</a>
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" /> </p>
<p className="about__play-video"> <p className="about__content-column-list">
<a <a
href="http://hello.p5js.org/" href="https://p5js.org/tutorials/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Play hello! video</a> Tutorials</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">Resources</h3>
<p className="about__content-column-list"> <p className="about__content-column-list">
<a <a
href="https://p5js.org/examples/" 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" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Examples</a> Libraries</a>
</p> </p>
<p className="about__content-column-list"> <p className="about__content-column-list">
<a <a
href="https://p5js.org/tutorials/" href="https://p5js.org/reference/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Tutorials</a> Reference</a>
</p> </p>
</div> <p className="about__content-column-list">
<div className="about__content-column"> <a
<h3 className="about__content-column-title">Resources</h3> href="https://forum.processing.org/two/"
<p className="about__content-column-list"> target="_blank"
<a rel="noopener noreferrer"
href="https://p5js.org/libraries/" >
target="_blank" <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
rel="noopener noreferrer" Forum</a>
> </p>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> </div>
Libraries</a> <div className="about__footer">
</p> <p className="about__footer-list">
<p className="about__content-column-list"> <a
<a href="https://github.com/processing/p5.js-web-editor"
href="https://p5js.org/reference/" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" >Contribute</a>
> </p>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <p className="about__footer-list">
Reference</a> <a
</p> href="https://github.com/processing/p5.js-web-editor/issues/new"
<p className="about__content-column-list"> target="_blank"
<a rel="noopener noreferrer"
href="https://forum.processing.org/two/" >Report a bug</a>
target="_blank" </p>
rel="noopener noreferrer" <p className="about__footer-list">
> <a
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> href="https://twitter.com/p5xjs?lang=en"
Forum</a> target="_blank"
</p> rel="noopener noreferrer"
</div> >Twitter</a>
</div> </p>
<div className="about__footer"> </div>
<p className="about__footer-list"> </div>
<a );
href="https://github.com/processing/p5.js-web-editor"
target="_blank"
rel="noopener noreferrer"
>Contribute</a>
</p>
<p className="about__footer-list">
<a
href="https://github.com/processing/p5.js-web-editor/issues/new"
target="_blank"
rel="noopener noreferrer"
>Report a bug</a>
</p>
<p className="about__footer-list">
<a
href="https://twitter.com/p5xjs?lang=en"
target="_blank"
rel="noopener noreferrer"
>Twitter</a>
</p>
<button className="about__ok-button" onClick={this.closeAboutModal}>OK!</button>
</div>
</section>
);
}
} }
About.propTypes = {
previousPath: PropTypes.string.isRequired
};
export default About; export default About;

View file

@ -0,0 +1,71 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router';
import prettyBytes from 'pretty-bytes';
import * as AssetActions from '../actions/assets';
class AssetList extends React.Component {
constructor(props) {
super(props);
this.props.getAssets(this.props.username);
}
render() {
return (
<div className="asset-table-container">
{this.props.assets.length === 0 &&
<p className="asset-table__empty">No uploaded assets.</p>
}
{this.props.assets.length > 0 &&
<table className="asset-table">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>View</th>
<th>Sketch</th>
</tr>
</thead>
<tbody>
{this.props.assets.map(asset =>
<tr className="asset-table__row" key={asset.key}>
<td>{asset.name}</td>
<td>{prettyBytes(asset.size)}</td>
<td><Link to={asset.url} target="_blank">View</Link></td>
<td><Link to={`/${this.props.username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
</tr>
)}
</tbody>
</table>}
</div>
);
}
}
AssetList.propTypes = {
username: PropTypes.string.isRequired,
assets: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
sketchName: PropTypes.string.isRequired,
sketchId: PropTypes.string.isRequired
})).isRequired,
getAssets: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
user: state.user,
assets: state.assets
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, AssetActions), dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(AssetList);

View file

@ -1,15 +1,7 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
import { Link } from 'react-router'; import { Link } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
class ErrorModal extends React.Component { class ErrorModal extends React.Component {
componentDidMount() {
this.errorModal.focus();
}
forceAuthentication() { forceAuthentication() {
return ( return (
<p> <p>
@ -40,25 +32,17 @@ class ErrorModal extends React.Component {
render() { render() {
return ( return (
<section className="error-modal" ref={(element) => { this.errorModal = element; }} tabIndex="0"> <div className="error-modal__content">
<header className="error-modal__header"> {(() => { // eslint-disable-line
<h2 className="error-modal__title">Error</h2> if (this.props.type === 'forceAuthentication') {
<button className="error-modal__exit-button" onClick={this.props.closeModal}> return this.forceAuthentication();
<InlineSVG src={exitUrl} alt="Close Error Modal" /> } else if (this.props.type === 'staleSession') {
</button> return this.staleSession();
</header> } else if (this.props.type === 'staleProject') {
<div className="error-modal__content"> return this.staleProject();
{(() => { // eslint-disable-line }
if (this.props.type === 'forceAuthentication') { })()}
return this.forceAuthentication(); </div>
} else if (this.props.type === 'staleSession') {
return this.staleSession();
} else if (this.props.type === 'staleProject') {
return this.staleProject();
}
})()}
</div>
</section>
); );
} }
} }

View file

@ -1,105 +1,84 @@
import React, { PropTypes } from 'react'; import React from 'react';
import InlineSVG from 'react-inlinesvg';
import { import {
metaKeyName, metaKeyName,
} from '../../../utils/metaKey'; } from '../../../utils/metaKey';
const exitUrl = require('../../../images/exit.svg'); function KeyboardShortcutModal() {
return (
class KeyboardShortcutModal extends React.Component { <ul className="keyboard-shortcuts" title="keyboard shortcuts">
componentDidMount() { <li className="keyboard-shortcut-item">
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; <span className="keyboard-shortcut__command">Shift + Tab</span>
} <span>Tidy</span>
</li>
render() { <li className="keyboard-shortcut-item">
return ( <span className="keyboard-shortcut__command">
<section className="keyboard-shortcuts"> {metaKeyName} + S
<header className="keyboard-shortcuts__header"> </span>
<h2>Keyboard Shortcuts</h2> <span>Save</span>
<button className="keyboard-shortcuts__close" onClick={this.props.closeModal}> </li>
<InlineSVG src={exitUrl} alt="Close Keyboard Shortcuts Overlay" /> <li className="keyboard-shortcut-item">
</button> <span className="keyboard-shortcut__command">
</header> {metaKeyName} + F
<ul title="keyboard shortcuts"> </span>
<li className="keyboard-shortcut-item"> <span>Find Text</span>
<span className="keyboard-shortcut__command">Shift + Tab</span> </li>
<span>Tidy</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + G
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + S <span>Find Next Text Match</span>
</span> </li>
<span>Save</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + Shift + G
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + F <span>Find Previous Text Match</span>
</span> </li>
<span>Find Text</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + [
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + G <span>Indent Code Left</span>
</span> </li>
<span>Find Next Text Match</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + ]
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + Shift + G <span>Indent Code Right</span>
</span> </li>
<span>Find Previous Text Match</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + /
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + [ <span>Comment Line</span>
</span> </li>
<span>Indent Code Left</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + Enter
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + ] <span>Start Sketch</span>
</span> </li>
<span>Indent Code Right</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + Shift + Enter
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + / <span>Stop Sketch</span>
</span> </li>
<span>Comment Line</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + Shift + 1
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + Enter <span>Toggle Text-based Canvas</span>
</span> </li>
<span>Start Sketch</span> <li className="keyboard-shortcut-item">
</li> <span className="keyboard-shortcut__command">
<li className="keyboard-shortcut-item"> {metaKeyName} + Shift + 2
<span className="keyboard-shortcut__command"> </span>
{metaKeyName} + Shift + Enter <span>Turn Off Text-based Canvas</span>
</span> </li>
<span>Stop Sketch</span> </ul>
</li> );
<li className="keyboard-shortcut-item">
<span className="keyboard-shortcut__command">
{metaKeyName} + Shift + 1
</span>
<span>Toggle Text-based Canvas</span>
</li>
<li className="keyboard-shortcut-item">
<span className="keyboard-shortcut__command">
{metaKeyName} + Shift + 2
</span>
<span>Turn Off Text-based Canvas</span>
</li>
</ul>
</section>
);
}
} }
KeyboardShortcutModal.propTypes = {
closeModal: PropTypes.func.isRequired
};
export default KeyboardShortcutModal; export default KeyboardShortcutModal;

View file

@ -1,57 +1,46 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg'); function ShareModal(props) {
const {
class ShareModal extends React.Component { projectId,
componentDidMount() { ownerUsername
this.shareModal.focus(); } = props;
} const hostname = window.location.origin;
render() { return (
const hostname = window.location.origin; <div className="share-modal">
return ( <div className="share-modal__section">
<section className="share-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0"> <label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
<header className="share-modal__header"> <input
<h2>Share Sketch</h2> type="text"
<button className="about__exit-button" onClick={this.props.closeShareModal}> className="share-modal__input"
<InlineSVG src={exitUrl} alt="Close Share Overlay" /> id="share-modal__embed"
</button> value={`<iframe src="${hostname}/embed/${projectId}"></iframe>`}
</header> />
<div className="share-modal__section"> </div>
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label> <div className="share-modal__section">
<input <label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label>
type="text" <input
className="share-modal__input" type="text"
id="share-modal__embed" className="share-modal__input"
value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`} id="share-modal__fullscreen"
/> value={`${hostname}/full/${projectId}`}
</div> />
<div className="share-modal__section"> </div>
<label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label> <div className="share-modal__section">
<input <label className="share-modal__label" htmlFor="share-modal__edit">Edit</label>
type="text" <input
className="share-modal__input" type="text"
id="share-modal__fullscreen" className="share-modal__input"
value={`${hostname}/full/${this.props.projectId}`} id="share-modal__edit"
/> value={`${hostname}/${ownerUsername}/sketches/${projectId}`}
</div> />
<div className="share-modal__section"> </div>
<label className="share-modal__label" htmlFor="share-modal__edit">Edit</label> </div>
<input );
type="text"
className="share-modal__input"
id="share-modal__edit"
value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`}
/>
</div>
</section>
);
}
} }
ShareModal.propTypes = { ShareModal.propTypes = {
projectId: PropTypes.string.isRequired, projectId: PropTypes.string.isRequired,
closeShareModal: PropTypes.func.isRequired,
ownerUsername: PropTypes.string.isRequired ownerUsername: PropTypes.string.isRequired
}; };

View file

@ -8,80 +8,62 @@ import * as SketchActions from '../actions/projects';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
const exitUrl = require('../../../images/exit.svg');
const trashCan = require('../../../images/trash-can.svg'); const trashCan = require('../../../images/trash-can.svg');
class SketchList extends React.Component { class SketchList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.closeSketchList = this.closeSketchList.bind(this);
this.props.getProjects(this.props.username); this.props.getProjects(this.props.username);
} }
componentDidMount() {
document.getElementById('sketchlist').focus();
}
closeSketchList() {
browserHistory.push(this.props.previousPath);
}
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 (
<section className="sketch-list" aria-label="project list" tabIndex="0" role="main" id="sketchlist"> <div className="sketches-table-container">
<header className="sketch-list__header"> <table className="sketches-table" summary="table containing all saved projects">
<h2 className="sketch-list__header-title">Open a Sketch</h2> <thead>
<button className="sketch-list__exit-button" onClick={this.closeSketchList}> <tr>
<InlineSVG src={exitUrl} alt="Close Sketch List Overlay" /> <th className="sketch-list__trash-column" scope="col"></th>
</button> <th scope="col">Sketch</th>
</header> <th scope="col">Date created</th>
<div className="sketches-table-container"> <th scope="col">Date updated</th>
<table className="sketches-table" summary="table containing all saved projects"> </tr>
<thead> </thead>
<tr> <tbody>
<th className="sketch-list__trash-column" scope="col"></th> {this.props.sketches.map(sketch =>
<th scope="col">Sketch</th> // eslint-disable-next-line
<th scope="col">Date created</th> <tr
<th scope="col">Date updated</th> className="sketches-table__row visibility-toggle"
key={sketch.id}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
>
<td className="sketch-list__trash-column">
{(() => { // eslint-disable-line
if (this.props.username === this.props.user.username || this.props.username === undefined) {
return (
<button
className="sketch-list__trash-button"
onClick={(e) => {
e.stopPropagation();
if (window.confirm(`Are you sure you want to delete "${sketch.name}"?`)) {
this.props.deleteProject(sketch.id);
}
}}
>
<InlineSVG src={trashCan} alt="Delete Project" />
</button>
);
}
})()}
</td>
<th scope="row"><Link to={`/${username}/sketches/${sketch.id}`}>{sketch.name}</Link></th>
<td>{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}</td>
<td>{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}</td>
</tr> </tr>
</thead> )}
<tbody> </tbody>
{this.props.sketches.map(sketch => </table>
// eslint-disable-next-line </div>
<tr
className="sketches-table__row visibility-toggle"
key={sketch.id}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
>
<td className="sketch-list__trash-column">
{(() => { // eslint-disable-line
if (this.props.username === this.props.user.username || this.props.username === undefined) {
return (
<button
className="sketch-list__trash-button"
onClick={(e) => {
e.stopPropagation();
if (window.confirm(`Are you sure you want to delete "${sketch.name}"?`)) {
this.props.deleteProject(sketch.id);
}
}}
>
<InlineSVG src={trashCan} alt="Delete Project" />
</button>
);
}
})()}
</td>
<th scope="row"><Link to={`/${username}/sketches/${sketch.id}`}>{sketch.name}</Link></th>
<td>{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}</td>
<td>{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
); );
} }
} }
@ -98,8 +80,7 @@ SketchList.propTypes = {
updatedAt: PropTypes.string.isRequired updatedAt: PropTypes.string.isRequired
})).isRequired, })).isRequired,
username: PropTypes.string, username: PropTypes.string,
deleteProject: PropTypes.func.isRequired, deleteProject: PropTypes.func.isRequired
previousPath: PropTypes.string.isRequired,
}; };
SketchList.defaultProps = { SketchList.defaultProps = {

View file

@ -30,6 +30,7 @@ import * as ConsoleActions from '../actions/console';
import { getHTMLFile } from '../reducers/files'; import { getHTMLFile } from '../reducers/files';
import Overlay from '../../App/components/Overlay'; import Overlay from '../../App/components/Overlay';
import SketchList from '../components/SketchList'; import SketchList from '../components/SketchList';
import AssetList from '../components/AssetList';
import About from '../components/About'; import About from '../components/About';
class IDEView extends React.Component { class IDEView extends React.Component {
@ -425,10 +426,30 @@ class IDEView extends React.Component {
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.location.pathname.match(/sketches$/)) { if (this.props.location.pathname.match(/sketches$/)) {
return ( return (
<Overlay> <Overlay
ariaLabel="project list"
title="Open a Sketch"
previousPath={this.props.ide.previousPath}
>
<SketchList <SketchList
username={this.props.params.username} username={this.props.params.username}
previousPath={this.props.ide.previousPath} user={this.props.user}
/>
</Overlay>
);
}
})()}
{(() => { // eslint-disable-line
if (this.props.location.pathname.match(/assets$/)) {
return (
<Overlay
title="Assets"
ariaLabel="asset list"
previousPath={this.props.ide.previousPath}
>
<AssetList
username={this.props.params.username}
user={this.props.user}
/> />
</Overlay> </Overlay>
); );
@ -437,7 +458,11 @@ class IDEView extends React.Component {
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.location.pathname === '/about') { if (this.props.location.pathname === '/about') {
return ( return (
<Overlay> <Overlay
previousPath={this.props.ide.previousPath}
title="Welcome"
ariaLabel="about"
>
<About previousPath={this.props.ide.previousPath} /> <About previousPath={this.props.ide.previousPath} />
</Overlay> </Overlay>
); );
@ -446,10 +471,13 @@ class IDEView extends React.Component {
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.ide.shareModalVisible) { if (this.props.ide.shareModalVisible) {
return ( return (
<Overlay> <Overlay
title="Share Sketch"
ariaLabel="share"
closeOverlay={this.props.closeShareModal}
>
<ShareModal <ShareModal
projectId={this.props.project.id} projectId={this.props.project.id}
closeShareModal={this.props.closeShareModal}
ownerUsername={this.props.project.owner.username} ownerUsername={this.props.project.owner.username}
/> />
</Overlay> </Overlay>
@ -459,10 +487,12 @@ class IDEView extends React.Component {
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.ide.keyboardShortcutVisible) { if (this.props.ide.keyboardShortcutVisible) {
return ( return (
<Overlay> <Overlay
<KeyboardShortcutModal title="Keyboard Shortcuts"
closeModal={this.props.closeKeyboardShortcutModal} ariaLabel="keyboard shortcuts"
/> closeOverlay={this.props.closeKeyboardShortcutModal}
>
<KeyboardShortcutModal />
</Overlay> </Overlay>
); );
} }
@ -470,10 +500,13 @@ class IDEView extends React.Component {
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.ide.errorType) { if (this.props.ide.errorType) {
return ( return (
<Overlay> <Overlay
title="Error"
ariaLabel="error"
closeOverlay={this.props.hideErrorModal}
>
<ErrorModal <ErrorModal
type={this.props.ide.errorType} type={this.props.ide.errorType}
closeModal={this.props.hideErrorModal}
/> />
</Overlay> </Overlay>
); );

View file

@ -0,0 +1,12 @@
import * as ActionTypes from '../../../constants';
const assets = (state = [], action) => {
switch (action.type) {
case ActionTypes.SET_ASSETS:
return action.assets;
default:
return state;
}
};
export default assets;

View file

@ -9,6 +9,7 @@ import user from './modules/User/reducers';
import sketches from './modules/IDE/reducers/projects'; import sketches from './modules/IDE/reducers/projects';
import toast from './modules/IDE/reducers/toast'; import toast from './modules/IDE/reducers/toast';
import console from './modules/IDE/reducers/console'; import console from './modules/IDE/reducers/console';
import assets from './modules/IDE/reducers/assets';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
form, form,
@ -20,7 +21,8 @@ const rootReducer = combineReducers({
sketches, sketches,
editorAccessibility, editorAccessibility,
toast, toast,
console console,
assets
}); });
export default rootReducer; export default rootReducer;

View file

@ -49,6 +49,7 @@ const routes = (store) => {
<Route path="/sketches" component={IDEView} /> <Route path="/sketches" component={IDEView} />
<Route path="/:username/sketches/:project_id" component={IDEView} /> <Route path="/:username/sketches/:project_id" component={IDEView} />
<Route path="/:username/sketches" component={IDEView} /> <Route path="/:username/sketches" component={IDEView} />
<Route path="/:username/assets" component={IDEView} />
<Route path="/:username/account" component={forceToHttps(AccountView)} /> <Route path="/:username/account" component={forceToHttps(AccountView)} />
<Route path="/about" component={IDEView} /> <Route path="/about" component={IDEView} />
</Route> </Route>

View file

@ -186,4 +186,4 @@
color: getThemifyVariable('primary-text-color'); color: getThemifyVariable('primary-text-color');
} }
} }
} }

View file

@ -1,33 +1,3 @@
.about {
@extend %modal;
display: flex;
flex-wrap: wrap;
flex-flow: column;
width: #{720 / $base-font-size}rem;
outline: none;
& a {
color: $form-navigation-options-color;
}
}
.about__header {
display: flex;
justify-content: space-between;
padding-top: #{12 / $base-font-size}rem;
padding-right: #{14 / $base-font-size}rem;
padding-bottom: #{20 / $base-font-size}rem;
padding-left: #{21 / $base-font-size}rem;
}
.about__header-title {
font-size: #{38 / $base-font-size}rem;
font-weight: normal;
}
.about__exit-button {
@include icon();
}
.about__logo { .about__logo {
@include themify() { @include themify() {
& path { & path {
@ -67,10 +37,15 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
padding-top: #{17 / $base-font-size}rem; padding-top: #{17 / $base-font-size}rem;
padding-right: #{78 / $base-font-size}rem; padding-right: #{78 / $base-font-size}rem;
padding-bottom: #{20 / $base-font-size}rem; padding-bottom: #{20 / $base-font-size}rem;
padding-left: #{20 / $base-font-size}rem; padding-left: #{20 / $base-font-size}rem;
width: #{720 / $base-font-size}rem;
& a {
color: $form-navigation-options-color;
}
} }
.about__content-column { .about__content-column {
@ -110,6 +85,7 @@
padding-right: #{20 / $base-font-size}rem; padding-right: #{20 / $base-font-size}rem;
padding-bottom: #{21 / $base-font-size}rem; padding-bottom: #{21 / $base-font-size}rem;
padding-left: #{291 / $base-font-size}rem; padding-left: #{291 / $base-font-size}rem;
width: 100%;
} }
.about__footer-list { .about__footer-list {

View file

@ -0,0 +1,56 @@
.asset-table-container {
flex: 1 1 0%;
overflow-y: scroll;
max-width: 100%;
width: #{1000 / $base-font-size}rem;
min-height: #{400 / $base-font-size}rem;
}
.asset-table {
width: 100%;
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
max-height: 100%;
border-spacing: 0;
& .asset-list__delete-column {
width: #{23 / $base-font-size}rem;
}
& thead {
font-size: #{12 / $base-font-size}rem;
@include themify() {
color: getThemifyVariable('inactive-text-color')
}
}
& th {
height: #{32 / $base-font-size}rem;
font-weight: normal;
}
}
.asset-table__row {
margin: #{10 / $base-font-size}rem;
height: #{72 / $base-font-size}rem;
font-size: #{16 / $base-font-size}rem;
&:nth-child(odd) {
@include themify() {
background: getThemifyVariable('console-header-background-color');
}
}
& a {
@include themify() {
color: getThemifyVariable('primary-text-color');
}
}
& td:first-child {
padding-left: #{10 / $base-font-size}rem;
}
}
.asset-table__empty {
text-align: center;
font-size: #{16 / $base-font-size}rem;
}

View file

@ -0,0 +1,20 @@
.keyboard-shortcuts {
padding: #{20 / $base-font-size}rem;
padding-bottom: #{40 / $base-font-size}rem;
width: #{450 / $base-font-size}rem;
}
.keyboard-shortcut-item {
display: flex;
& + & {
margin-top: #{10 / $base-font-size}rem;
}
align-items: baseline;
}
.keyboard-shortcut__command {
width: 50%;
font-weight: bold;
text-align: right;
padding-right: #{10 / $base-font-size}rem;
}

View file

@ -48,66 +48,3 @@
text-align: center; text-align: center;
margin: #{20 / $base-font-size}rem 0; margin: #{20 / $base-font-size}rem 0;
} }
.uploader {
min-height: #{200 / $base-font-size}rem;
width: 100%;
text-align: center;
}
.share-modal {
@extend %modal;
padding: #{20 / $base-font-size}rem;
width: #{500 / $base-font-size}rem;
}
.share-modal__header {
display: flex;
justify-content: space-between;
}
.share-modal__section {
width: 100%;
display: flex;
align-items: center;
padding: #{10 / $base-font-size}rem 0;
}
.share-modal__label {
width: #{86 / $base-font-size}rem;
}
.share-modal__input {
flex: 1;
}
.keyboard-shortcuts {
@extend %modal;
padding: #{20 / $base-font-size}rem;
width: #{450 / $base-font-size}rem;
}
.keyboard-shortcuts__header {
display: flex;
justify-content: space-between;
margin-bottom: #{20 / $base-font-size}rem;
}
.keyboard-shortcuts__close {
@include icon();
}
.keyboard-shortcut-item {
display: flex;
& + & {
margin-top: #{10 / $base-font-size}rem;
}
align-items: baseline;
}
.keyboard-shortcut__command {
width: 50%;
font-weight: bold;
text-align: right;
padding-right: #{10 / $base-font-size}rem;
}

View file

@ -9,10 +9,31 @@
overflow-y: hidden; overflow-y: hidden;
} }
.overlay-content { .overlay__content {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
}
.overlay__body {
@extend %modal;
display: flex;
flex-wrap: wrap;
flex-flow: column;
max-height: 80%;
max-width: 65%;
}
.overlay__header {
display: flex;
justify-content: space-between;
padding: #{40 / $base-font-size}rem #{20 / $base-font-size}rem #{30 / $base-font-size}rem #{20 / $base-font-size}rem;
flex: 1 0 auto;
}
.overlay__close-button {
@include icon();
padding: #{12 / $base-font-size}rem #{16 / $base-font-size}rem;
} }

View file

@ -0,0 +1,24 @@
.share-modal {
padding: #{20 / $base-font-size}rem;
width: #{500 / $base-font-size}rem;
}
.share-modal__header {
display: flex;
justify-content: space-between;
}
.share-modal__section {
width: 100%;
display: flex;
align-items: center;
padding: #{10 / $base-font-size}rem 0;
}
.share-modal__label {
width: #{86 / $base-font-size}rem;
}
.share-modal__input {
flex: 1;
}

View file

@ -1,25 +1,9 @@
.sketch-list {
@extend %modal;
display: flex;
flex-wrap: wrap;
flex-flow: column;
width: #{1000 / $base-font-size}rem;
height: 80%;
outline: none;
}
.sketch-list__header {
display: flex;
justify-content: space-between;
}
.sketch-list__header-title {
padding: #{40 / $base-font-size}rem #{16 / $base-font-size}rem #{12 / $base-font-size}rem #{21 / $base-font-size}rem;
}
.sketches-table-container { .sketches-table-container {
flex: 1 0 0%; flex: 1 1 0%;
overflow-y: scroll; overflow-y: scroll;
max-width: 100%;
width: #{1000 / $base-font-size}rem;
} }
.sketches-table { .sketches-table {
@ -67,11 +51,6 @@
font-weight: normal; font-weight: normal;
} }
.sketch-list__exit-button {
@include icon();
margin: #{12 / $base-font-size}rem #{16 / $base-font-size}rem;
}
.visibility-toggle .sketch-list__trash-button { .visibility-toggle .sketch-list__trash-button {
@extend %hidden-element; @extend %hidden-element;
width:#{20 / $base-font-size}rem; width:#{20 / $base-font-size}rem;

View file

@ -1,3 +1,9 @@
.dropzone { .dropzone {
color: $primary-text-color; color: $primary-text-color;
}
.uploader {
min-height: #{200 / $base-font-size}rem;
width: 100%;
text-align: center;
} }

View file

@ -34,6 +34,9 @@
@import 'components/error-modal'; @import 'components/error-modal';
@import 'components/preview-frame'; @import 'components/preview-frame';
@import 'components/help-modal'; @import 'components/help-modal';
@import 'components/share';
@import 'components/asset-list';
@import 'components/keyboard-shortcuts';
@import 'layout/ide'; @import 'layout/ide';
@import 'layout/fullscreen'; @import 'layout/fullscreen';

View file

@ -97,6 +97,7 @@
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pretty-bytes": "^4.0.2",
"project-name-generator": "^2.1.3", "project-name-generator": "^2.1.3",
"pug": "^2.0.0-beta6", "pug": "^2.0.0-beta6",
"q": "^1.4.1", "q": "^1.4.1",

View file

@ -1,6 +1,8 @@
import uuid from 'node-uuid'; import uuid from 'node-uuid';
import policy from 's3-policy'; import policy from 's3-policy';
import s3 from 's3'; import s3 from 's3';
import { getProjectsForUserId } from './project.controller';
import { findUserByUsername } from './user.controller';
const client = s3.createClient({ const client = s3.createClient({
maxAsyncS3: 20, maxAsyncS3: 20,
@ -104,3 +106,45 @@ export function copyObjectInS3(req, res) {
res.json({ url: `${s3Bucket}${userId}/${newFilename}` }); res.json({ url: `${s3Bucket}${userId}/${newFilename}` });
}); });
} }
export function listObjectsInS3ForUser(req, res) {
const username = req.params.username;
findUserByUsername(username, (user) => {
const userId = user.id;
const params = {
s3Params: {
Bucket: `${process.env.S3_BUCKET}`,
Prefix: `${userId}/`
}
};
let assets = [];
const list = client.listObjects(params)
.on('data', (data) => {
assets = assets.concat(data["Contents"].map((object) => {
return { key: object["Key"], size: object["Size"] };
}));
})
.on('end', () => {
const projectAssets = [];
getProjectsForUserId(userId).then((projects) => {
projects.forEach((project) => {
project.files.forEach((file) => {
if (!file.url) return;
const foundAsset = assets.find((asset) => file.url.includes(asset.key));
if (!foundAsset) return;
projectAssets.push({
name: file.name,
sketchName: project.name,
sketchId: project.id,
url: file.url,
key: foundAsset.key,
size: foundAsset.size
});
});
});
res.json({assets: projectAssets});
});
});
});
}

View file

@ -121,12 +121,28 @@ export function deleteProject(req, res) {
}); });
} }
export function getProjects(req, res) { export function getProjectsForUserId(userId) {
if (req.user) { return new Promise((resolve, reject) => {
Project.find({ user: req.user._id }) // eslint-disable-line no-underscore-dangle Project.find({ user: userId })
.sort('-createdAt') .sort('-createdAt')
.select('name files id createdAt updatedAt') .select('name files id createdAt updatedAt')
.exec((err, projects) => { .exec((err, projects) => {
if (err) {
console.log(err);
}
resolve(projects);
});
});
}
export function getProjectsForUserName(username) {
}
export function getProjects(req, res) {
if (req.user) {
getProjectsForUserId(req.user._id)
.then((projects) => {
res.json(projects); res.json(projects);
}); });
} else { } else {
@ -142,7 +158,7 @@ export function getProjectsForUser(req, res) {
res.status(404).json({ message: 'User with that username does not exist.' }); res.status(404).json({ message: 'User with that username does not exist.' });
return; return;
} }
Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle Project.find({ user: user._id })
.sort('-createdAt') .sort('-createdAt')
.select('name files id createdAt updatedAt') .select('name files id createdAt updatedAt')
.exec((innerErr, projects) => res.json(projects)); .exec((innerErr, projects) => res.json(projects));

View file

@ -15,6 +15,21 @@ const random = (done) => {
}); });
}; };
export function findUserByUsername(username, cb) {
User.findOne({ username },
(err, user) => {
cb(user);
});
}
export function createUser(req, res, next) {
const user = new User({
username: req.body.username,
email: req.body.email,
password: req.body.password
});
};
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
export function createUser(req, res, next) { export function createUser(req, res, next) {

View file

@ -6,5 +6,6 @@ const router = new Router();
router.route('/S3/sign').post(AWSController.signS3); router.route('/S3/sign').post(AWSController.signS3);
router.route('/S3/copy').post(AWSController.copyObjectInS3); router.route('/S3/copy').post(AWSController.copyObjectInS3);
router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3); router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3);
router.route('/S3/:username/objects').get(AWSController.listObjectsInS3ForUser);
export default router; export default router;

View file

@ -58,6 +58,12 @@ router.route('/:username/sketches').get((req, res) => {
)); ));
}); });
router.route('/:username/assets').get((req, res) => {
userExists(req.params.username, exists => (
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
));
});
router.route('/:username/account').get((req, res) => { router.route('/:username/account').get((req, res) => {
userExists(req.params.username, exists => ( userExists(req.params.username, exists => (
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))