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 &&
+
+ {userIsOwner &&
+ -
+
+
}
+
+ }
+ |
+ );
+
+ 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 &&
+
+ {userIsOwner &&
+ -
+
+
}
+
+ }
+ |
+ );
+
+ 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 }) => (
);
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;