From 6ca6e78a28523739917d0fffc1a20e95688ccc33 Mon Sep 17 00:00:00 2001 From: Andrew Nicolaou Date: Tue, 9 Jul 2019 10:35:24 +0200 Subject: [PATCH] Displays existing collection - List all collections for a given user - View an individual collection - Link to a sketch from a collection --- client/constants.js | 2 + client/modules/IDE/actions/collections.js | 34 ++ client/modules/IDE/components/Collection.jsx | 375 +++++++++++++++++ .../modules/IDE/components/CollectionList.jsx | 381 ++++++++++++++++++ client/modules/IDE/pages/IDEView.jsx | 22 +- client/modules/IDE/reducers/collections.js | 12 + client/modules/IDE/selectors/collections.js | 37 ++ .../User/components/DashboardTabSwitcher.jsx | 6 +- client/modules/User/pages/DashboardView.jsx | 23 +- client/reducers.js | 4 +- client/routes.jsx | 2 + .../collectionForUserExists.js | 29 ++ .../collection.controller/index.js | 1 + server/routes/server.routes.js | 13 + 14 files changed, 920 insertions(+), 21 deletions(-) create mode 100644 client/modules/IDE/actions/collections.js create mode 100644 client/modules/IDE/components/Collection.jsx create mode 100644 client/modules/IDE/components/CollectionList.jsx create mode 100644 client/modules/IDE/reducers/collections.js create mode 100644 client/modules/IDE/selectors/collections.js create mode 100644 server/controllers/collection.controller/collectionForUserExists.js diff --git a/client/constants.js b/client/constants.js index 75c81b2f..ceffbf12 100644 --- a/client/constants.js +++ b/client/constants.js @@ -36,6 +36,8 @@ export const HIDE_EDIT_PROJECT_NAME = 'HIDE_EDIT_PROJECT_NAME'; export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; +export const SET_COLLECTIONS = 'SET_COLLECTIONS'; + export const DELETE_PROJECT = 'DELETE_PROJECT'; export const SET_SELECTED_FILE = 'SET_SELECTED_FILE'; diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js new file mode 100644 index 00000000..d3fffe6f --- /dev/null +++ b/client/modules/IDE/actions/collections.js @@ -0,0 +1,34 @@ +import axios from 'axios'; +import * as ActionTypes from '../../../constants'; +import { startLoader, stopLoader } from './loader'; + +const __process = (typeof global !== 'undefined' ? global : window).process; +const ROOT_URL = __process.env.API_URL; + +// eslint-disable-next-line +export function getCollections(username) { + return (dispatch) => { + dispatch(startLoader()); + let url; + if (username) { + url = `${ROOT_URL}/${username}/collections`; + } else { + url = `${ROOT_URL}/collections`; + } + axios.get(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.SET_COLLECTIONS, + collections: response.data + }); + dispatch(stopLoader()); + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + }); + }; +} diff --git a/client/modules/IDE/components/Collection.jsx b/client/modules/IDE/components/Collection.jsx new file mode 100644 index 00000000..22ed85b9 --- /dev/null +++ b/client/modules/IDE/components/Collection.jsx @@ -0,0 +1,375 @@ +import format from 'date-fns/format'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import InlineSVG from 'react-inlinesvg'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; +import * as ProjectActions from '../actions/project'; +import * as ProjectsActions from '../actions/projects'; +import * as CollectionsActions from '../actions/collections'; +import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import * as IdeActions from '../actions/ide'; +import { getCollection } from '../selectors/collections'; +import Loader from '../../App/components/loader'; +import Overlay from '../../App/components/Overlay'; + +const arrowUp = require('../../../images/sort-arrow-up.svg'); +const arrowDown = require('../../../images/sort-arrow-down.svg'); +const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); + +class CollectionItemRowBase extends React.Component { + constructor(props) { + super(props); + this.state = { + optionsOpen: false, + renameOpen: false, + renameValue: props.item.project.name, + isFocused: false + }; + } + + onFocusComponent = () => { + this.setState({ isFocused: true }); + } + + onBlurComponent = () => { + this.setState({ isFocused: false }); + setTimeout(() => { + if (!this.state.isFocused) { + this.closeAll(); + } + }, 200); + } + + openOptions = () => { + this.setState({ + optionsOpen: true + }); + } + + closeOptions = () => { + this.setState({ + optionsOpen: false + }); + } + + toggleOptions = () => { + if (this.state.optionsOpen) { + this.closeOptions(); + } else { + this.openOptions(); + } + } + + openRename = () => { + this.setState({ + renameOpen: true + }); + } + + closeRename = () => { + this.setState({ + renameOpen: false + }); + } + + closeAll = () => { + this.setState({ + renameOpen: false, + optionsOpen: false + }); + } + + handleRenameChange = (e) => { + this.setState({ + renameValue: e.target.value + }); + } + + handleRenameEnter = (e) => { + if (e.key === 'Enter') { + // TODO pass this func + this.props.changeProjectName(this.props.collection.id, this.state.renameValue); + this.closeAll(); + } + } + + resetSketchName = () => { + this.setState({ + renameValue: this.props.collection.name + }); + } + + handleDropdownOpen = () => { + this.closeAll(); + this.openOptions(); + } + + handleRenameOpen = () => { + this.closeAll(); + this.openRename(); + } + + handleSketchDownload = () => { + this.props.exportProjectAsZip(this.props.collection.id); + } + + handleSketchDuplicate = () => { + this.closeAll(); + this.props.cloneProject(this.props.collection.id); + } + + handleSketchShare = () => { + this.closeAll(); + this.props.showShareModal(this.props.collection.id, this.props.collection.name, this.props.username); + } + + handleSketchDelete = () => { + this.closeAll(); + if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) { + this.props.deleteProject(this.props.collection.id); + } + } + + render() { + const { item, username } = this.props; + const { renameOpen, optionsOpen, renameValue } = this.state; + const sketchOwnerUsername = item.project.user.username; + const userIsOwner = this.props.user.username === sketchOwnerUsername; + const sketchUrl = `/${item.project.user.username}/sketches/${item.project.id}`; + + const dropdown = ( + + + {optionsOpen && + + } + + ); + + return ( + + + + {renameOpen ? '' : item.project.name} + + {renameOpen + && + e.stopPropagation()} + /> + } + + {format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')} + {sketchOwnerUsername} + {/* + {format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')} + {format(new Date(itm.updatedAt), 'MMM D, YYYY h:mm A')} + {(collection.items || []).length} + {dropdown} + */} + ); + } +} + +CollectionItemRowBase.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteProject: PropTypes.func.isRequired, + showShareModal: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + changeProjectName: PropTypes.func.isRequired +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); +} + +const CollectionItemRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionItemRowBase); + +class Collection extends React.Component { + constructor(props) { + super(props); + this.props.getCollections(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); + } + + getTitle() { + if (this.props.username === this.props.user.username) { + return 'p5.js Web Editor | My collections'; + } + return `p5.js Web Editor | ${this.props.username}'s collections`; + } + + getCollectionName() { + return this.props.collection.name; + } + + hasCollection() { + return !this.props.loading && this.props.collection != null; + } + + hasCollectionItems() { + return this.hasCollection() && this.props.collection.items.length > 0; + } + + _renderLoader() { + if (this.props.loading) return ; + return null; + } + + _renderCollectionMetadata() { + return ( +
+

{this.props.collection.description}

+
+ ); + } + + _renderEmptyTable() { + if (!this.hasCollectionItems()) { + return (

No sketches in collection.

); + } + return null; + } + + _renderFieldHeader(fieldName, displayName) { + const { field, direction } = this.props.sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + return ( + + + + ); + } + + render() { + const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + const title = this.hasCollection() ? this.getCollectionName() : null; + + return ( + +
+ + {this.getTitle()} + + {this._renderLoader()} + {this.hasCollection() && this._renderCollectionMetadata()} + {this._renderEmptyTable()} + {this.hasCollectionItems() && + + + + {this._renderFieldHeader('name', 'Name')} + {this._renderFieldHeader('createdAt', 'Date Added')} + {this._renderFieldHeader('user', 'Owner')} + + + + + {this.props.collection.items.map(item => + ())} + +
} +
+
+ ); + } +} + +Collection.propTypes = { + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + getCollections: PropTypes.func.isRequired, + collection: PropTypes.shape({}).isRequired, // TODO + username: PropTypes.string, + loading: PropTypes.bool.isRequired, + toggleDirectionForField: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired +}; + +Collection.defaultProps = { + username: undefined +}; + +function mapStateToProps(state, ownProps) { + return { + user: state.user, + collection: getCollection(state, ownProps.collectionId), + sorting: state.sorting, + loading: state.loading, + project: state.project + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions), dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(Collection); diff --git a/client/modules/IDE/components/CollectionList.jsx b/client/modules/IDE/components/CollectionList.jsx new file mode 100644 index 00000000..2b071847 --- /dev/null +++ b/client/modules/IDE/components/CollectionList.jsx @@ -0,0 +1,381 @@ +import format from 'date-fns/format'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import InlineSVG from 'react-inlinesvg'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; +import * as ProjectActions from '../actions/project'; +import * as ProjectsActions from '../actions/projects'; +import * as CollectionsActions from '../actions/collections'; +import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import * as IdeActions from '../actions/ide'; +import getSortedCollections from '../selectors/collections'; +import Loader from '../../App/components/loader'; + +const arrowUp = require('../../../images/sort-arrow-up.svg'); +const arrowDown = require('../../../images/sort-arrow-down.svg'); +const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); + +class CollectionListRowBase extends React.Component { + constructor(props) { + super(props); + this.state = { + optionsOpen: false, + renameOpen: false, + renameValue: props.collection.name, + isFocused: false + }; + } + + onFocusComponent = () => { + this.setState({ isFocused: true }); + } + + onBlurComponent = () => { + this.setState({ isFocused: false }); + setTimeout(() => { + if (!this.state.isFocused) { + this.closeAll(); + } + }, 200); + } + + openOptions = () => { + this.setState({ + optionsOpen: true + }); + } + + closeOptions = () => { + this.setState({ + optionsOpen: false + }); + } + + toggleOptions = () => { + if (this.state.optionsOpen) { + this.closeOptions(); + } else { + this.openOptions(); + } + } + + openRename = () => { + this.setState({ + renameOpen: true + }); + } + + closeRename = () => { + this.setState({ + renameOpen: false + }); + } + + closeAll = () => { + this.setState({ + renameOpen: false, + optionsOpen: false + }); + } + + handleRenameChange = (e) => { + this.setState({ + renameValue: e.target.value + }); + } + + handleRenameEnter = (e) => { + if (e.key === 'Enter') { + // TODO pass this func + this.props.changeProjectName(this.props.collection.id, this.state.renameValue); + this.closeAll(); + } + } + + resetSketchName = () => { + this.setState({ + renameValue: this.props.collection.name + }); + } + + handleDropdownOpen = () => { + this.closeAll(); + this.openOptions(); + } + + handleRenameOpen = () => { + this.closeAll(); + this.openRename(); + } + + handleSketchDownload = () => { + this.props.exportProjectAsZip(this.props.collection.id); + } + + handleSketchDuplicate = () => { + this.closeAll(); + this.props.cloneProject(this.props.collection.id); + } + + handleSketchShare = () => { + this.closeAll(); + this.props.showShareModal(this.props.collection.id, this.props.collection.name, this.props.username); + } + + handleSketchDelete = () => { + this.closeAll(); + if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) { + this.props.deleteProject(this.props.collection.id); + } + } + + render() { + const { collection, username } = this.props; + const { renameOpen, optionsOpen, renameValue } = this.state; + const userIsOwner = this.props.user.username === this.props.username; + + const dropdown = ( + + + {optionsOpen && + + } + + ); + + return ( + + + + {renameOpen ? '' : collection.name} + + {renameOpen + && + e.stopPropagation()} + /> + } + + {format(new Date(collection.createdAt), 'MMM D, YYYY h:mm A')} + {format(new Date(collection.updatedAt), 'MMM D, YYYY h:mm A')} + {(collection.items || []).length} + {dropdown} + ); + } +} + +CollectionListRowBase.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + username: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + deleteProject: PropTypes.func.isRequired, + showShareModal: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + changeProjectName: PropTypes.func.isRequired +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); +} + +const CollectionListRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); + +class CollectionList extends React.Component { + constructor(props) { + super(props); + this.props.getCollections(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); + } + + getTitle() { + if (this.props.username === this.props.user.username) { + return 'p5.js Web Editor | My collections'; + } + return `p5.js Web Editor | ${this.props.username}'s collections`; + } + + hasCollections() { + return !this.props.loading && this.props.collections.length > 0; + } + + _renderLoader() { + if (this.props.loading) return ; + return null; + } + + _renderEmptyTable() { + if (!this.props.loading && this.props.collections.length === 0) { + return (

No collections.

); + } + return null; + } + + _renderFieldHeader(fieldName, displayName) { + const { field, direction } = this.props.sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + return ( + + + + ); + } + + render() { + const username = this.props.username !== undefined ? this.props.username : this.props.user.username; + return ( +
+ + {this.getTitle()} + + {this._renderLoader()} + {this._renderEmptyTable()} + {this.hasCollections() && + + + + {this._renderFieldHeader('name', 'Name')} + {this._renderFieldHeader('createdAt', 'Date Created')} + {this._renderFieldHeader('updatedAt', 'Date Updated')} + {this._renderFieldHeader('numItems', '# sketches')} + + + + + {this.props.collections.map(collection => + ())} + +
} +
+ ); + } +} + +const ProjectShape = PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string.isRequired + }).isRequired, +}); + +const ItemsShape = PropTypes.shape({ + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + project: ProjectShape +}); + +CollectionList.propTypes = { + user: PropTypes.shape({ + username: PropTypes.string, + authenticated: PropTypes.bool.isRequired + }).isRequired, + getCollections: PropTypes.func.isRequired, + collections: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired, + items: PropTypes.arrayOf(ItemsShape), + })).isRequired, + username: PropTypes.string, + loading: PropTypes.bool.isRequired, + toggleDirectionForField: PropTypes.func.isRequired, + resetSorting: PropTypes.func.isRequired, + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired, + project: PropTypes.shape({ + id: PropTypes.string, + owner: PropTypes.shape({ + id: PropTypes.string + }) + }) +}; + +CollectionList.defaultProps = { + project: { + id: undefined, + owner: undefined + }, + username: undefined +}; + +function mapStateToProps(state) { + return { + user: state.user, + collections: getSortedCollections(state), + sorting: state.sorting, + loading: state.loading, + project: state.project + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions), + dispatch + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 072360ae..3cb3095a 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -320,12 +320,12 @@ class IDEView extends React.Component { {( ( (this.props.preferences.textOutput || - this.props.preferences.gridOutput || - this.props.preferences.soundOutput + this.props.preferences.gridOutput || + this.props.preferences.soundOutput ) && - this.props.ide.isPlaying + this.props.ide.isPlaying ) || - this.props.ide.isAccessibleOutputPlaying + this.props.ide.isAccessibleOutputPlaying ) } @@ -356,14 +356,14 @@ class IDEView extends React.Component { - { this.props.ide.modalIsVisible && + {this.props.ide.modalIsVisible && } - { this.props.ide.newFolderModalVisible && + {this.props.ide.newFolderModalVisible && } - { this.props.location.pathname === '/feedback' && + {this.props.location.pathname === '/feedback' && } - { this.props.ide.shareModalVisible && + {this.props.ide.shareModalVisible && } - { this.props.ide.keyboardShortcutVisible && + {this.props.ide.keyboardShortcutVisible && } - { this.props.ide.errorType && + {this.props.ide.errorType && } - { this.props.ide.helpType && + {this.props.ide.helpType && { + switch (action.type) { + case ActionTypes.SET_COLLECTIONS: + return action.collections; + default: + return state; + } +}; + +export default sketches; diff --git a/client/modules/IDE/selectors/collections.js b/client/modules/IDE/selectors/collections.js new file mode 100644 index 00000000..3ba90241 --- /dev/null +++ b/client/modules/IDE/selectors/collections.js @@ -0,0 +1,37 @@ +import { createSelector } from 'reselect'; +import differenceInMilliseconds from 'date-fns/difference_in_milliseconds'; +import find from 'lodash/find'; +import orderBy from 'lodash/orderBy'; +import { DIRECTION } from '../actions/sorting'; + +const getCollections = state => state.collections; +const getField = state => state.sorting.field; +const getDirection = state => state.sorting.direction; + +const getSortedCollections = createSelector( + getCollections, + getField, + getDirection, + (collections, field, direction) => { + if (field === 'name') { + if (direction === DIRECTION.DESC) { + return orderBy(collections, 'name', 'desc'); + } + return orderBy(collections, 'name', 'asc'); + } + const sortedCollections = [...collections].sort((a, b) => { + const result = + direction === DIRECTION.ASC + ? differenceInMilliseconds(new Date(a[field]), new Date(b[field])) + : differenceInMilliseconds(new Date(b[field]), new Date(a[field])); + return result; + }); + return sortedCollections; + } +); + +export function getCollection(state, id) { + return find(getCollections(state), { id }); +} + +export default getSortedCollections; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 8143759f..4b657b01 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -4,6 +4,7 @@ import { Link } from 'react-router'; const TabKey = { assets: 'assets', + collections: 'collections', sketches: 'sketches', }; @@ -30,8 +31,9 @@ Tab.propTypes = { const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => (
    - Sketches - {isOwner && Assets} + Sketches + Collections + {isOwner && Assets}
); diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index d6c02bfd..0993696f 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -8,6 +8,7 @@ import { updateSettings, initiateVerification, createApiKey, removeApiKey } from import Nav from '../../../components/Nav'; import AssetList from '../../IDE/components/AssetList'; +import CollectionList from '../../IDE/components/CollectionList'; import SketchList from '../../IDE/components/SketchList'; import Searchbar from '../../IDE/components/Searchbar'; @@ -36,11 +37,13 @@ class DashboardView extends React.Component { browserHistory.push('/'); } - selectedTabName() { + selectedTabKey() { const path = this.props.location.pathname; if (/assets/.test(path)) { return TabKey.assets; + } else if (/collections/.test(path)) { + return TabKey.collections; } return TabKey.sketches; @@ -58,12 +61,20 @@ class DashboardView extends React.Component { return this.props.user.username === this.props.params.username; } - navigationItem() { - + renderContent(tabKey, username) { + switch (tabKey) { + case TabKey.assets: + return ; + case TabKey.collections: + return ; + case TabKey.sketches: + default: + return ; + } } render() { - const currentTab = this.selectedTabName(); + const currentTab = this.selectedTabKey(); const isOwner = this.isOwner(); const { username } = this.props.params; @@ -80,9 +91,7 @@ class DashboardView extends React.Component {
- { - currentTab === TabKey.sketches ? : - } + {this.renderContent(currentTab, username)}
diff --git a/client/reducers.js b/client/reducers.js index 4a173a8f..121c6cd6 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -13,6 +13,7 @@ import assets from './modules/IDE/reducers/assets'; import search from './modules/IDE/reducers/search'; import sorting from './modules/IDE/reducers/sorting'; import loading from './modules/IDE/reducers/loading'; +import collections from './modules/IDE/reducers/collections'; const rootReducer = combineReducers({ form, @@ -28,7 +29,8 @@ const rootReducer = combineReducers({ toast, console, assets, - loading + loading, + collections }); export default rootReducer; diff --git a/client/routes.jsx b/client/routes.jsx index 18f21384..fc1eaf60 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -43,6 +43,8 @@ const routes = store => ( + + diff --git a/server/controllers/collection.controller/collectionForUserExists.js b/server/controllers/collection.controller/collectionForUserExists.js new file mode 100644 index 00000000..e2881fd4 --- /dev/null +++ b/server/controllers/collection.controller/collectionForUserExists.js @@ -0,0 +1,29 @@ +import Collection from '../../models/collection'; +import User from '../../models/user'; + +export default function collectionForUserExists(username, collectionId, callback) { + function sendFailure() { + callback(false); + } + + function sendSuccess(collection) { + callback(collection != null); + } + + function findUser() { + return User.findOne({ username }); + } + + function findCollection(owner) { + if (owner == null) { + throw new Error('User not found'); + } + + return Collection.findOne({ _id: collectionId, owner }); + } + + return findUser() + .then(findCollection) + .then(sendSuccess) + .catch(sendFailure); +} diff --git a/server/controllers/collection.controller/index.js b/server/controllers/collection.controller/index.js index b09db3f0..8cb9e368 100644 --- a/server/controllers/collection.controller/index.js +++ b/server/controllers/collection.controller/index.js @@ -1,4 +1,5 @@ export { default as addProjectToCollection } from './addProjectToCollection'; +export { default as collectionForUserExists } from './collectionForUserExists'; export { default as createCollection } from './createCollection'; export { default as listCollections } from './listCollections'; export { default as removeCollection } from './removeCollection'; diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 41d7dc60..29877809 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -3,6 +3,7 @@ import { renderIndex } from '../views/index'; import { get404Sketch } from '../views/404Page'; import { userExists } from '../controllers/user.controller'; import { projectExists, projectForUserExists } from '../controllers/project.controller'; +import { collectionForUserExists } from '../controllers/collection.controller'; const router = new Router(); @@ -111,4 +112,16 @@ router.get('/:username/sketches', (req, res) => { )); }); +router.get('/:username/collections', (req, res) => { + userExists(req.params.username, exists => ( + exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) + )); +}); + +router.get('/:username/collections/:id', (req, res) => { + collectionForUserExists(req.params.username, req.params.id, exists => ( + exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) + )); +}); + export default router;