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
</Link>
</li>
<li>
<Link to={`/${this.props.user.username}/assets`}>
My assets
</Link>
</li>
<li>
<Link to={`/${this.props.user.username}/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 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 HIDE_HELP_MODAL = 'HIDE_HELP_MODAL';
export const SET_ASSETS = 'SET_ASSETS';

View file

@ -1,21 +1,70 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
function Overlay(props) {
return (
<div className="overlay">
<div className="overlay-content">
{props.children}
const exitUrl = require('../../../images/exit.svg');
class Overlay extends React.Component {
constructor(props) {
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>
);
);
}
}
Overlay.propTypes = {
children: PropTypes.element
children: PropTypes.element,
closeOverlay: PropTypes.func,
title: PropTypes.string,
ariaLabel: PropTypes.string,
previousPath: PropTypes.string.isRequired
};
Overlay.defaultProps = {
children: null
children: null,
title: 'Modal',
closeOverlay: null,
ariaLabel: 'modal'
};
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 { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
const playUrl = require('../../../images/play.svg');
const asteriskUrl = require('../../../images/p5-asterisk.svg');
class About extends React.Component {
constructor(props) {
super(props);
this.closeAboutModal = this.closeAboutModal.bind(this);
}
componentDidMount() {
this.aboutSection.focus();
}
closeAboutModal() {
browserHistory.push(this.props.previousPath);
}
render() {
return (
<section className="about" ref={(element) => { this.aboutSection = element; }} tabIndex="0">
<header className="about__header">
<h2 className="about__header-title">Welcome</h2>
<button className="about__exit-button" onClick={this.closeAboutModal}>
<InlineSVG src={exitUrl} alt="Close About Overlay" />
</button>
</header>
<div className="about__content">
<div className="about__content-column">
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
<p className="about__play-video">
<a
href="http://hello.p5js.org/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
Play hello! video</a>
</p>
</div>
<div className="about__content-column">
<h3 className="about__content-column-title">New to p5.js?</h3>
<p className="about__content-column-list">
<a
href="https://p5js.org/examples/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Examples</a>
</p>
<p className="about__content-column-list">
<a
href="https://p5js.org/tutorials/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Tutorials</a>
</p>
</div>
<div className="about__content-column">
<h3 className="about__content-column-title">Resources</h3>
<p className="about__content-column-list">
<a
href="https://p5js.org/libraries/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Libraries</a>
</p>
<p className="about__content-column-list">
<a
href="https://p5js.org/reference/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Reference</a>
</p>
<p className="about__content-column-list">
<a
href="https://forum.processing.org/two/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Forum</a>
</p>
</div>
</div>
<div className="about__footer">
<p className="about__footer-list">
<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>
);
}
function About(props) {
return (
<div className="about__content">
<div className="about__content-column">
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
<p className="about__play-video">
<a
href="http://hello.p5js.org/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
Play hello! video</a>
</p>
</div>
<div className="about__content-column">
<h3 className="about__content-column-title">New to p5.js?</h3>
<p className="about__content-column-list">
<a
href="https://p5js.org/examples/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Examples</a>
</p>
<p className="about__content-column-list">
<a
href="https://p5js.org/tutorials/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Tutorials</a>
</p>
</div>
<div className="about__content-column">
<h3 className="about__content-column-title">Resources</h3>
<p className="about__content-column-list">
<a
href="https://p5js.org/libraries/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Libraries</a>
</p>
<p className="about__content-column-list">
<a
href="https://p5js.org/reference/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Reference</a>
</p>
<p className="about__content-column-list">
<a
href="https://forum.processing.org/two/"
target="_blank"
rel="noopener noreferrer"
>
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Forum</a>
</p>
</div>
<div className="about__footer">
<p className="about__footer-list">
<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>
</div>
</div>
);
}
About.propTypes = {
previousPath: PropTypes.string.isRequired
};
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 InlineSVG from 'react-inlinesvg';
import { Link } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
class ErrorModal extends React.Component {
componentDidMount() {
this.errorModal.focus();
}
forceAuthentication() {
return (
<p>
@ -40,25 +32,17 @@ class ErrorModal extends React.Component {
render() {
return (
<section className="error-modal" ref={(element) => { this.errorModal = element; }} tabIndex="0">
<header className="error-modal__header">
<h2 className="error-modal__title">Error</h2>
<button className="error-modal__exit-button" onClick={this.props.closeModal}>
<InlineSVG src={exitUrl} alt="Close Error Modal" />
</button>
</header>
<div className="error-modal__content">
{(() => { // eslint-disable-line
if (this.props.type === 'forceAuthentication') {
return this.forceAuthentication();
} else if (this.props.type === 'staleSession') {
return this.staleSession();
} else if (this.props.type === 'staleProject') {
return this.staleProject();
}
})()}
</div>
</section>
<div className="error-modal__content">
{(() => { // eslint-disable-line
if (this.props.type === 'forceAuthentication') {
return this.forceAuthentication();
} else if (this.props.type === 'staleSession') {
return this.staleSession();
} else if (this.props.type === 'staleProject') {
return this.staleProject();
}
})()}
</div>
);
}
}

View file

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

View file

@ -1,57 +1,46 @@
import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
class ShareModal extends React.Component {
componentDidMount() {
this.shareModal.focus();
}
render() {
const hostname = window.location.origin;
return (
<section className="share-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0">
<header className="share-modal__header">
<h2>Share Sketch</h2>
<button className="about__exit-button" onClick={this.props.closeShareModal}>
<InlineSVG src={exitUrl} alt="Close Share Overlay" />
</button>
</header>
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
<input
type="text"
className="share-modal__input"
id="share-modal__embed"
value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label>
<input
type="text"
className="share-modal__input"
id="share-modal__fullscreen"
value={`${hostname}/full/${this.props.projectId}`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__edit">Edit</label>
<input
type="text"
className="share-modal__input"
id="share-modal__edit"
value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`}
/>
</div>
</section>
);
}
function ShareModal(props) {
const {
projectId,
ownerUsername
} = props;
const hostname = window.location.origin;
return (
<div className="share-modal">
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
<input
type="text"
className="share-modal__input"
id="share-modal__embed"
value={`<iframe src="${hostname}/embed/${projectId}"></iframe>`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label>
<input
type="text"
className="share-modal__input"
id="share-modal__fullscreen"
value={`${hostname}/full/${projectId}`}
/>
</div>
<div className="share-modal__section">
<label className="share-modal__label" htmlFor="share-modal__edit">Edit</label>
<input
type="text"
className="share-modal__input"
id="share-modal__edit"
value={`${hostname}/${ownerUsername}/sketches/${projectId}`}
/>
</div>
</div>
);
}
ShareModal.propTypes = {
projectId: PropTypes.string.isRequired,
closeShareModal: PropTypes.func.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 ToastActions from '../actions/toast';
const exitUrl = require('../../../images/exit.svg');
const trashCan = require('../../../images/trash-can.svg');
class SketchList extends React.Component {
constructor(props) {
super(props);
this.closeSketchList = this.closeSketchList.bind(this);
this.props.getProjects(this.props.username);
}
componentDidMount() {
document.getElementById('sketchlist').focus();
}
closeSketchList() {
browserHistory.push(this.props.previousPath);
}
render() {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return (
<section className="sketch-list" aria-label="project list" tabIndex="0" role="main" id="sketchlist">
<header className="sketch-list__header">
<h2 className="sketch-list__header-title">Open a Sketch</h2>
<button className="sketch-list__exit-button" onClick={this.closeSketchList}>
<InlineSVG src={exitUrl} alt="Close Sketch List Overlay" />
</button>
</header>
<div className="sketches-table-container">
<table className="sketches-table" summary="table containing all saved projects">
<thead>
<tr>
<th className="sketch-list__trash-column" scope="col"></th>
<th scope="col">Sketch</th>
<th scope="col">Date created</th>
<th scope="col">Date updated</th>
<div className="sketches-table-container">
<table className="sketches-table" summary="table containing all saved projects">
<thead>
<tr>
<th className="sketch-list__trash-column" scope="col"></th>
<th scope="col">Sketch</th>
<th scope="col">Date created</th>
<th scope="col">Date updated</th>
</tr>
</thead>
<tbody>
{this.props.sketches.map(sketch =>
// eslint-disable-next-line
<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>
</thead>
<tbody>
{this.props.sketches.map(sketch =>
// eslint-disable-next-line
<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>
)}
</tbody>
</table>
</div>
);
}
}
@ -98,8 +80,7 @@ SketchList.propTypes = {
updatedAt: PropTypes.string.isRequired
})).isRequired,
username: PropTypes.string,
deleteProject: PropTypes.func.isRequired,
previousPath: PropTypes.string.isRequired,
deleteProject: PropTypes.func.isRequired
};
SketchList.defaultProps = {

View file

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

View file

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

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 {
@include themify() {
& path {
@ -67,10 +37,15 @@
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
padding-top: #{17 / $base-font-size}rem;
padding-right: #{78 / $base-font-size}rem;
padding-bottom: #{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 {
@ -110,6 +85,7 @@
padding-right: #{20 / $base-font-size}rem;
padding-bottom: #{21 / $base-font-size}rem;
padding-left: #{291 / $base-font-size}rem;
width: 100%;
}
.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;
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;
}
.overlay-content {
.overlay__content {
height: 100%;
width: 100%;
display: flex;
justify-content: 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 {
flex: 1 0 0%;
flex: 1 1 0%;
overflow-y: scroll;
max-width: 100%;
width: #{1000 / $base-font-size}rem;
}
.sketches-table {
@ -67,11 +51,6 @@
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 {
@extend %hidden-element;
width:#{20 / $base-font-size}rem;

View file

@ -1,3 +1,9 @@
.dropzone {
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/preview-frame';
@import 'components/help-modal';
@import 'components/share';
@import 'components/asset-list';
@import 'components/keyboard-shortcuts';
@import 'layout/ide';
@import 'layout/fullscreen';

View file

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

View file

@ -1,6 +1,8 @@
import uuid from 'node-uuid';
import policy from 's3-policy';
import s3 from 's3';
import { getProjectsForUserId } from './project.controller';
import { findUserByUsername } from './user.controller';
const client = s3.createClient({
maxAsyncS3: 20,
@ -104,3 +106,45 @@ export function copyObjectInS3(req, res) {
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) {
if (req.user) {
Project.find({ user: req.user._id }) // eslint-disable-line no-underscore-dangle
export function getProjectsForUserId(userId) {
return new Promise((resolve, reject) => {
Project.find({ user: userId })
.sort('-createdAt')
.select('name files id createdAt updatedAt')
.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);
});
} else {
@ -142,7 +158,7 @@ export function getProjectsForUser(req, res) {
res.status(404).json({ message: 'User with that username does not exist.' });
return;
}
Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle
Project.find({ user: user._id })
.sort('-createdAt')
.select('name files id createdAt updatedAt')
.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
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/copy').post(AWSController.copyObjectInS3);
router.route('/S3/:object_key').delete(AWSController.deleteObjectFromS3);
router.route('/S3/:username/objects').get(AWSController.listObjectsInS3ForUser);
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) => {
userExists(req.params.username, exists => (
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))