From e140702784b4e7a096865a47abe342f24543fd2b Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 11 Jul 2017 17:37:43 +0200 Subject: [PATCH] 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 --- client/components/Nav.jsx | 5 + client/constants.js | 3 + client/modules/App/components/Overlay.jsx | 67 +++++- client/modules/IDE/actions/assets.js | 30 +++ client/modules/IDE/components/About.jsx | 214 ++++++++---------- client/modules/IDE/components/AssetList.jsx | 71 ++++++ client/modules/IDE/components/ErrorModal.jsx | 38 +--- .../IDE/components/KeyboardShortcutModal.jsx | 173 +++++++------- client/modules/IDE/components/ShareModal.jsx | 85 +++---- client/modules/IDE/components/SketchList.jsx | 109 ++++----- client/modules/IDE/pages/IDEView.jsx | 55 ++++- client/modules/IDE/reducers/assets.js | 12 + client/reducers.js | 4 +- client/routes.jsx | 1 + client/styles/abstracts/_placeholders.scss | 2 +- client/styles/components/_about.scss | 36 +-- client/styles/components/_asset-list.scss | 56 +++++ .../components/_keyboard-shortcuts.scss | 20 ++ client/styles/components/_modal.scss | 63 ------ client/styles/components/_overlay.scss | 23 +- client/styles/components/_share.scss | 24 ++ client/styles/components/_sketch-list.scss | 27 +-- client/styles/components/_uploader.scss | 6 + client/styles/main.scss | 3 + package.json | 1 + server/controllers/aws.controller.js | 44 ++++ server/controllers/project.controller.js | 24 +- server/controllers/user.controller.js | 15 ++ server/routes/aws.routes.js | 1 + server/routes/server.routes.js | 6 + 30 files changed, 716 insertions(+), 502 deletions(-) create mode 100644 client/modules/IDE/actions/assets.js create mode 100644 client/modules/IDE/components/AssetList.jsx create mode 100644 client/modules/IDE/reducers/assets.js create mode 100644 client/styles/components/_asset-list.scss create mode 100644 client/styles/components/_keyboard-shortcuts.scss create mode 100644 client/styles/components/_share.scss diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index a767539b..0a861e97 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -142,6 +142,11 @@ class Nav extends React.PureComponent { My sketches +
  • + + My assets + +
  • My account diff --git a/client/constants.js b/client/constants.js index 3d4d1e8b..81548d52 100644 --- a/client/constants.js +++ b/client/constants.js @@ -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'; diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 6331d017..8951df64 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -1,21 +1,70 @@ import React, { PropTypes } from 'react'; +import InlineSVG from 'react-inlinesvg'; +import { browserHistory } from 'react-router'; -function Overlay(props) { - return ( -
    -
    - {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 ( +
    +
    +
    { this.overlay = element; }} + className="overlay__body" + > +
    +

    {title}

    + +
    + {children} +
    +
    -
    - ); + ); + } } 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; diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js new file mode 100644 index 00000000..8cceb91e --- /dev/null +++ b/client/modules/IDE/actions/assets.js @@ -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' + }; +} diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index 7fa2bd39..aa734c7c 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -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 ( -
    { this.aboutSection = element; }} tabIndex="0"> -
    -

    Welcome

    - -
    -
    - -
    -

    New to p5.js?

    -

    - - - Examples -

    -

    - - - Tutorials -

    -
    -
    -

    Resources

    -

    - - - Libraries -

    -

    - - - Reference -

    -

    - - - Forum -

    -
    -
    -
    -

    - Contribute -

    -

    - Report a bug -

    -

    - Twitter -

    - -
    -
    - ); - } +function About(props) { + return ( +
    +
    + +

    + + + Play hello! video +

    +
    +
    +

    New to p5.js?

    +

    + + + Examples +

    +

    + + + Tutorials +

    +
    +
    +

    Resources

    +

    + + + Libraries +

    +

    + + + Reference +

    +

    + + + Forum +

    +
    +
    +

    + Contribute +

    +

    + Report a bug +

    +

    + Twitter +

    +
    +
    + ); } -About.propTypes = { - previousPath: PropTypes.string.isRequired -}; - export default About; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx new file mode 100644 index 00000000..e78de28c --- /dev/null +++ b/client/modules/IDE/components/AssetList.jsx @@ -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 ( +
    + {this.props.assets.length === 0 && +

    No uploaded assets.

    + } + {this.props.assets.length > 0 && + + + + + + + + + + + {this.props.assets.map(asset => + + + + + + + )} + +
    NameSizeViewSketch
    {asset.name}{prettyBytes(asset.size)}View{asset.sketchName}
    } +
    + ); + } +} + +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); diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index bf62721a..b7fd0851 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -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 (

    @@ -40,25 +32,17 @@ class ErrorModal extends React.Component { render() { return ( -

    { this.errorModal = element; }} tabIndex="0"> -
    -

    Error

    - -
    -
    - {(() => { // 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(); - } - })()} -
    -
    +
    + {(() => { // 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(); + } + })()} +
    ); } } diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx index 4101a6f4..0eb399ca 100644 --- a/client/modules/IDE/components/KeyboardShortcutModal.jsx +++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx @@ -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 ( -
    -
    -

    Keyboard Shortcuts

    - -
    -
      -
    • - Shift + Tab - Tidy -
    • -
    • - - {metaKeyName} + S - - Save -
    • -
    • - - {metaKeyName} + F - - Find Text -
    • -
    • - - {metaKeyName} + G - - Find Next Text Match -
    • -
    • - - {metaKeyName} + Shift + G - - Find Previous Text Match -
    • -
    • - - {metaKeyName} + [ - - Indent Code Left -
    • -
    • - - {metaKeyName} + ] - - Indent Code Right -
    • -
    • - - {metaKeyName} + / - - Comment Line -
    • -
    • - - {metaKeyName} + Enter - - Start Sketch -
    • -
    • - - {metaKeyName} + Shift + Enter - - Stop Sketch -
    • -
    • - - {metaKeyName} + Shift + 1 - - Toggle Text-based Canvas -
    • -
    • - - {metaKeyName} + Shift + 2 - - Turn Off Text-based Canvas -
    • -
    -
    - ); - } +function KeyboardShortcutModal() { + return ( +
      +
    • + Shift + Tab + Tidy +
    • +
    • + + {metaKeyName} + S + + Save +
    • +
    • + + {metaKeyName} + F + + Find Text +
    • +
    • + + {metaKeyName} + G + + Find Next Text Match +
    • +
    • + + {metaKeyName} + Shift + G + + Find Previous Text Match +
    • +
    • + + {metaKeyName} + [ + + Indent Code Left +
    • +
    • + + {metaKeyName} + ] + + Indent Code Right +
    • +
    • + + {metaKeyName} + / + + Comment Line +
    • +
    • + + {metaKeyName} + Enter + + Start Sketch +
    • +
    • + + {metaKeyName} + Shift + Enter + + Stop Sketch +
    • +
    • + + {metaKeyName} + Shift + 1 + + Toggle Text-based Canvas +
    • +
    • + + {metaKeyName} + Shift + 2 + + Turn Off Text-based Canvas +
    • +
    + ); } -KeyboardShortcutModal.propTypes = { - closeModal: PropTypes.func.isRequired -}; - export default KeyboardShortcutModal; diff --git a/client/modules/IDE/components/ShareModal.jsx b/client/modules/IDE/components/ShareModal.jsx index ad8c030e..3b398e1b 100644 --- a/client/modules/IDE/components/ShareModal.jsx +++ b/client/modules/IDE/components/ShareModal.jsx @@ -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 ( -
    { this.shareModal = element; }} tabIndex="0"> -
    -

    Share Sketch

    - -
    -
    - - `} - /> -
    -
    - - -
    -
    - - -
    -
    - ); - } +function ShareModal(props) { + const { + projectId, + ownerUsername + } = props; + const hostname = window.location.origin; + return ( +
    +
    + + `} + /> +
    +
    + + +
    +
    + + +
    +
    + ); } ShareModal.propTypes = { projectId: PropTypes.string.isRequired, - closeShareModal: PropTypes.func.isRequired, ownerUsername: PropTypes.string.isRequired }; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index a60a1add..7094d72b 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -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 ( -
    -
    -

    Open a Sketch

    - -
    -
    - - - - - - - +
    +
    SketchDate createdDate updated
    + + + + + + + + + + {this.props.sketches.map(sketch => + // eslint-disable-next-line + browserHistory.push(`/${username}/sketches/${sketch.id}`)} + > + + + + - - - {this.props.sketches.map(sketch => - // eslint-disable-next-line - browserHistory.push(`/${username}/sketches/${sketch.id}`)} - > - - - - - - )} - -
    SketchDate createdDate updated
    + {(() => { // eslint-disable-line + if (this.props.username === this.props.user.username || this.props.username === undefined) { + return ( + + ); + } + })()} + {sketch.name}{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}
    - {(() => { // eslint-disable-line - if (this.props.username === this.props.user.username || this.props.username === undefined) { - return ( - - ); - } - })()} - {sketch.name}{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}
    -
    -
    + )} + + +
    ); } } @@ -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 = { diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 1968c341..1d503291 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -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 ( - + + + ); + } + })()} + {(() => { // eslint-disable-line + if (this.props.location.pathname.match(/assets$/)) { + return ( + + ); @@ -437,7 +458,11 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.location.pathname === '/about') { return ( - + ); @@ -446,10 +471,13 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.shareModalVisible) { return ( - + @@ -459,10 +487,12 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.keyboardShortcutVisible) { return ( - - + + ); } @@ -470,10 +500,13 @@ class IDEView extends React.Component { {(() => { // eslint-disable-line if (this.props.ide.errorType) { return ( - + ); diff --git a/client/modules/IDE/reducers/assets.js b/client/modules/IDE/reducers/assets.js new file mode 100644 index 00000000..260660f2 --- /dev/null +++ b/client/modules/IDE/reducers/assets.js @@ -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; diff --git a/client/reducers.js b/client/reducers.js index 4505623a..c508c8ea 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -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; diff --git a/client/routes.jsx b/client/routes.jsx index f5f7ce6b..556d9970 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -49,6 +49,7 @@ const routes = (store) => { + diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index ec837235..9f3a2038 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -186,4 +186,4 @@ color: getThemifyVariable('primary-text-color'); } } -} \ No newline at end of file +} diff --git a/client/styles/components/_about.scss b/client/styles/components/_about.scss index 1ca80ea2..25e916ea 100644 --- a/client/styles/components/_about.scss +++ b/client/styles/components/_about.scss @@ -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 { diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss new file mode 100644 index 00000000..5b716170 --- /dev/null +++ b/client/styles/components/_asset-list.scss @@ -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; +} diff --git a/client/styles/components/_keyboard-shortcuts.scss b/client/styles/components/_keyboard-shortcuts.scss new file mode 100644 index 00000000..66963e91 --- /dev/null +++ b/client/styles/components/_keyboard-shortcuts.scss @@ -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; +} diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index 470509af..06830684 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -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; -} diff --git a/client/styles/components/_overlay.scss b/client/styles/components/_overlay.scss index 9d952969..690d1c7e 100644 --- a/client/styles/components/_overlay.scss +++ b/client/styles/components/_overlay.scss @@ -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; } \ No newline at end of file diff --git a/client/styles/components/_share.scss b/client/styles/components/_share.scss new file mode 100644 index 00000000..a9b72c7e --- /dev/null +++ b/client/styles/components/_share.scss @@ -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; +} \ No newline at end of file diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 9775b04c..034ab59b 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -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; diff --git a/client/styles/components/_uploader.scss b/client/styles/components/_uploader.scss index e6d8bcee..695e84da 100644 --- a/client/styles/components/_uploader.scss +++ b/client/styles/components/_uploader.scss @@ -1,3 +1,9 @@ .dropzone { color: $primary-text-color; +} + +.uploader { + min-height: #{200 / $base-font-size}rem; + width: 100%; + text-align: center; } \ No newline at end of file diff --git a/client/styles/main.scss b/client/styles/main.scss index fe97261e..abe5a93b 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -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'; diff --git a/package.json b/package.json index 544f1764..eaeec4a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index a22be355..2b23e21f 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -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}); + }); + }); + }); +} diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index cdfb0ac1..8814740a 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -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)); diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index ac273d34..2e002399 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -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) { diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index 85daddf7..e31457a6 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -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; diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 7bd944a2..f4ec6bf2 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -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))