diff --git a/client/constants.js b/client/constants.js index ceffbf12..76a3bfe5 100644 --- a/client/constants.js +++ b/client/constants.js @@ -37,6 +37,10 @@ export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; export const SET_COLLECTIONS = 'SET_COLLECTIONS'; +export const CREATE_COLLECTION = 'CREATED_COLLECTION'; + +export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION'; +export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION'; export const DELETE_PROJECT = 'DELETE_PROJECT'; diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index d3fffe6f..17f0b872 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -32,3 +32,80 @@ export function getCollections(username) { }); }; } + +export function createCollection(collection) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections`; + return axios.post(url, collection, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.CREATE_COLLECTION + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} + +export function addToCollection(collectionId, projectId) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; + return axios.post(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.ADD_TO_COLLECTION, + payload: response.data + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} + +export function removeFromCollection(collectionId, projectId) { + return (dispatch) => { + dispatch(startLoader()); + const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; + return axios.delete(url, { withCredentials: true }) + .then((response) => { + dispatch({ + type: ActionTypes.REMOVE_FROM_COLLECTION, + payload: response.data + }); + dispatch(stopLoader()); + + return response.data; + }) + .catch((response) => { + dispatch({ + type: ActionTypes.ERROR, + error: response.data + }); + dispatch(stopLoader()); + + return response.data; + }); + }; +} diff --git a/client/modules/IDE/components/CollectionList.jsx b/client/modules/IDE/components/CollectionList.jsx index 2b071847..9da68917 100644 --- a/client/modules/IDE/components/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList.jsx @@ -21,6 +21,10 @@ const arrowDown = require('../../../images/sort-arrow-down.svg'); const downFilledTriangle = require('../../../images/down-filled-triangle.svg'); class CollectionListRowBase extends React.Component { + static projectInCollection(project, collection) { + return collection.items.find(item => item.project.id === project.id) != null; + } + constructor(props) { super(props); this.state = { @@ -134,6 +138,14 @@ class CollectionListRowBase extends React.Component { } } + handleCollectionAdd = () => { + this.props.addToCollection(this.props.collection.id, this.props.project.id); + } + + handleCollectionRemove = () => { + this.props.removeFromCollection(this.props.collection.id, this.props.project.id); + } + render() { const { collection, username } = this.props; const { renameOpen, optionsOpen, renameValue } = this.state; @@ -198,6 +210,8 @@ class CollectionListRowBase extends React.Component { } CollectionListRowBase.propTypes = { + addToCollection: PropTypes.func.isRequired, + removeFromCollection: PropTypes.func.isRequired, collection: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired @@ -215,7 +229,7 @@ CollectionListRowBase.propTypes = { }; function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch); + return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions), dispatch); } const CollectionListRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase); @@ -279,6 +293,9 @@ class CollectionList extends React.Component { {this.getTitle()} + + New collection + {this._renderLoader()} {this._renderEmptyTable()} {this.hasCollections() && diff --git a/client/modules/IDE/reducers/collections.js b/client/modules/IDE/reducers/collections.js index fbbaefcc..4c4027b1 100644 --- a/client/modules/IDE/reducers/collections.js +++ b/client/modules/IDE/reducers/collections.js @@ -4,6 +4,18 @@ const sketches = (state = [], action) => { switch (action.type) { case ActionTypes.SET_COLLECTIONS: return action.collections; + + // The API returns the complete new collection + // with the items added or removed + case ActionTypes.ADD_TO_COLLECTION: + case ActionTypes.REMOVE_FROM_COLLECTION: + return state.map((collection) => { + if (collection.id === action.payload.id) { + return action.payload; + } + + return collection; + }); default: return state; } diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 0779c0f5..2eb19d4d 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -1,14 +1,8 @@ -import friendlyWords from 'friendly-words'; import * as ActionTypes from '../../../constants'; - -const generateRandomName = () => { - const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)]; - const obj = friendlyWords.objects[Math.floor(Math.random() * friendlyWords.objects.length)]; - return `${adj} ${obj}`; -}; +import { generateProjectName } from '../../../utils/generateRandomName'; const initialState = () => { - const generatedString = generateRandomName(); + const generatedString = generateProjectName(); const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1); return { name: generatedName, diff --git a/client/modules/IDE/components/Collection.jsx b/client/modules/User/components/Collection.jsx similarity index 100% rename from client/modules/IDE/components/Collection.jsx rename to client/modules/User/components/Collection.jsx diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx new file mode 100644 index 00000000..3542e6a3 --- /dev/null +++ b/client/modules/User/components/CollectionCreate.jsx @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { connect } from 'react-redux'; +import { browserHistory } from 'react-router'; +import { bindActionCreators } from 'redux'; +import * as CollectionsActions from '../../IDE/actions/collections'; + +import { generateCollectionName } from '../../../utils/generateRandomName'; + +class CollectionCreate extends React.Component { + constructor() { + super(); + + const name = generateCollectionName(); + + this.state = { + generatedCollectionName: name, + collection: { + name, + description: '' + } + }; + } + + getTitle() { + return 'p5.js Web Editor | Create collection'; + } + + handleTextChange = field => (evt) => { + this.setState({ + collection: { + ...this.state.collection, + [field]: evt.target.value, + } + }); + } + + handleCreateCollection = (event) => { + event.preventDefault(); + + this.props.createCollection(this.state.collection) + .then(({ id, owner }) => { + browserHistory.replace(`/${owner.username}/collections/${id}`); + }) + .catch((error) => { + console.error('Error creating collection', error); + this.setState({ + creationError: error, + }); + }); + } + + render() { + const { generatedCollectionName, creationError } = this.state; + const { name, description } = this.state.collection; + + const invalid = name === '' || name == null; + + return ( +
+ + {this.getTitle()} + + +
+ {creationError && Couldn't create collection} +

+ + + {invalid && Collection name is required} +

+

+ +