├── .npmrc ├── src ├── config │ ├── config.dev.js │ ├── config.prod.js │ └── config.js ├── assets │ ├── images │ │ ├── b.jpg │ │ ├── b.png │ │ ├── del.png │ │ ├── gal.png │ │ ├── dots.png │ │ └── noavatar.png │ └── temp.styl ├── containers │ ├── Board │ │ ├── snapToGrid.js │ │ ├── CardDragPreview.js │ │ ├── Cards │ │ │ ├── Card.js │ │ │ ├── DraggableCard.js │ │ │ ├── CardsContainer.js │ │ │ └── Cards.js │ │ ├── CustomDragLayer.js │ │ └── Board.js │ └── Base │ │ └── Base.js ├── constants.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ └── configureStore.dev.js ├── reducers │ ├── index.js │ └── lists.js ├── routes.js ├── helper.js ├── index.js └── actions │ └── lists.js ├── .editorconfig ├── .babelrc ├── .eslintrc ├── server.js ├── index.html ├── .gitignore ├── README.md ├── webpack.config.development.js └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | 3 | -------------------------------------------------------------------------------- /src/config/config.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | baseUrl: 'http://127.0.0.1:5000/api' 3 | }; 4 | -------------------------------------------------------------------------------- /src/config/config.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | baseUrl: 'http://127.0.0.1:5000' 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/b.jpg -------------------------------------------------------------------------------- /src/assets/images/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/b.png -------------------------------------------------------------------------------- /src/assets/images/del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/del.png -------------------------------------------------------------------------------- /src/assets/images/gal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/gal.png -------------------------------------------------------------------------------- /src/assets/images/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/dots.png -------------------------------------------------------------------------------- /src/assets/images/noavatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-pal/react-trello-board/HEAD/src/assets/images/noavatar.png -------------------------------------------------------------------------------- /src/containers/Board/snapToGrid.js: -------------------------------------------------------------------------------- 1 | export default function snapToGrid(x, y) { 2 | const snappedX = Math.round(x / 32) * 32; 3 | const snappedY = Math.round(y / 32) * 32; 4 | 5 | return [snappedX, snappedY]; 6 | } 7 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | if (process.env.NODE_ENV === 'production') { 3 | module.exports = require('./config.prod'); 4 | } else { 5 | module.exports = require('./config.dev'); 6 | } 7 | /* eslint-enable global-require */ 8 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const CARD_HEIGHT = 161; // height of a single card(excluding marginBottom/paddingBottom) 2 | export const CARD_MARGIN = 10; // height of a marginBottom+paddingBottom 3 | export const OFFSET_HEIGHT = 84; // height offset from the top of the page 4 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | if (process.env.NODE_ENV === 'production') { 3 | module.exports = require('./configureStore.prod'); 4 | } else { 5 | module.exports = require('./configureStore.dev'); 6 | } 7 | /* eslint-enable global-require */ 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"], 3 | "plugins": ["add-module-exports", "react-hot-loader/babel", "transform-decorators-legacy"], 4 | "env": { 5 | "development": { 6 | "presets": [ 7 | "react-hmre" 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { 3 | routerReducer 4 | } from 'react-router-redux'; 5 | 6 | import lists from './lists'; 7 | 8 | 9 | const rootReducer = combineReducers({ 10 | routing: routerReducer, 11 | lists, 12 | }); 13 | 14 | export default rootReducer; 15 | -------------------------------------------------------------------------------- /src/containers/Base/Base.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | children: PropTypes.element.isRequired 5 | }; 6 | 7 | 8 | const BaseContainer = (props) => ( 9 |
10 | {props.children} 11 |
12 | ); 13 | 14 | BaseContainer.propTypes = propTypes; 15 | 16 | export default BaseContainer; 17 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | import Base from './containers/Base/Base'; 5 | import Board from './containers/Board/Board'; 6 | 7 | export const urls = { 8 | index: '/', 9 | }; 10 | 11 | export const routes = ( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "react/jsx-first-prop-new-line": 0, 11 | "react/require-render-return": 0, 12 | "comma-dangle": 0, 13 | "no-use-before-define": 0, 14 | "new-cap": 0 15 | }, 16 | "plugins": [ 17 | "react" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | export function getHeaders(jsonContentType = true) { 2 | const headers = { 3 | Accept: 'application/json', 4 | }; 5 | if (jsonContentType) { 6 | headers['Content-Type'] = 'application/json'; 7 | } else { 8 | headers['Content-Type'] = 'multipart/form-data; boundary="--"'; 9 | } 10 | if (window.localStorage.jwt) { 11 | headers.Authorization = `JWT ${window.localStorage.jwt}`; 12 | } 13 | return headers; 14 | } 15 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackDevServer = require('webpack-dev-server'); 3 | const config = require('./webpack.config.development'); 4 | 5 | /* eslint-disable no-console */ 6 | /* eslint-disable consistent-return */ 7 | new WebpackDevServer(webpack(config), { 8 | publicPath: config.output.publicPath, 9 | hot: true, 10 | historyApiFallback: true 11 | }).listen(3000, 'localhost', (err) => { 12 | if (err) { 13 | return console.log(err); 14 | } 15 | console.log('Listening at http://localhost:3000/'); 16 | }); 17 | -------------------------------------------------------------------------------- /src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | import { browserHistory } from 'react-router'; 5 | 6 | import rootReducer from '../reducers'; 7 | 8 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 9 | const middleware = [ 10 | reduxRouterMiddleware, 11 | thunk, 12 | ].filter(Boolean); 13 | 14 | 15 | function configureStore(initialState) { 16 | const store = createStore(rootReducer, initialState, compose( 17 | applyMiddleware(...middleware), 18 | )); 19 | 20 | return store; 21 | } 22 | 23 | export default configureStore; 24 | -------------------------------------------------------------------------------- /src/containers/Board/CardDragPreview.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Card from './Cards/Card'; 3 | 4 | const styles = { 5 | display: 'inline-block', 6 | transform: 'rotate(-7deg)', 7 | WebkitTransform: 'rotate(-7deg)' 8 | }; 9 | 10 | const propTypes = { 11 | card: PropTypes.object 12 | }; 13 | 14 | const CardDragPreview = (props) => { 15 | styles.width = `${props.card.clientWidth || 243}px`; 16 | styles.height = `${props.card.clientHeight || 243}px`; 17 | 18 | return ( 19 |
20 | 21 |
22 | ); 23 | }; 24 | 25 | CardDragPreview.propTypes = propTypes; 26 | 27 | export default CardDragPreview; 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React-Trello-Board 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | require('babel-polyfill'); 3 | /* eslint-enable global-require */ 4 | import React from 'react'; 5 | import { render } from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import { Router, browserHistory } from 'react-router'; 8 | import { syncHistoryWithStore } from 'react-router-redux'; 9 | 10 | import { routes } from './routes'; 11 | 12 | import configureStore from './store/configureStore'; 13 | 14 | import './assets/temp.styl'; 15 | 16 | const store = configureStore(); 17 | const history = syncHistoryWithStore(browserHistory, store); 18 | 19 | render( 20 | 21 | 22 | , document.getElementById('app') 23 | ); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | #dist folder 30 | dist 31 | 32 | #Webstorm metadata 33 | .idea 34 | 35 | # Mac files 36 | .DS_Store 37 | 38 | webpack.config.production.babel.js 39 | -------------------------------------------------------------------------------- /src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-first-prop-new-line */ 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | /* eslint-enable global-require, react/jsx-first-prop-new-line */ 4 | import thunk from 'redux-thunk'; 5 | import { routerMiddleware } from 'react-router-redux'; 6 | import { browserHistory } from 'react-router'; 7 | 8 | import rootReducer from '../reducers'; 9 | 10 | const reduxRouterMiddleware = routerMiddleware(browserHistory); 11 | const middleware = [ 12 | reduxRouterMiddleware, 13 | thunk 14 | ].filter(Boolean); 15 | 16 | 17 | function configureStore(initialState) { 18 | const store = createStore(rootReducer, initialState, compose( 19 | applyMiddleware(...middleware), 20 | window.devToolsExtension ? window.devToolsExtension() : f => f 21 | )); 22 | 23 | if (module.hot) { 24 | // Enable Webpack hot module replacement for reducers 25 | module.hot.accept('../reducers', () => { 26 | /* eslint-disable global-require */ 27 | const nextReducer = require('../reducers').default; 28 | /* eslint-enable global-require */ 29 | store.replaceReducer(nextReducer); 30 | }); 31 | } 32 | 33 | return store; 34 | } 35 | 36 | export default configureStore; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Trello Board 2 | Welcome to the React Trello board. A [Trello](http://trello.com) like board based on [React](https://facebook.github.io/react/), [Redux](https://github.com/reactjs/redux), [React-dnd](https://github.com/gaearon/react-dnd). At the moment it has only Drag-and-drop functionality. 3 | 4 | ![`board`](https://s3.amazonaws.com/react-trello/board_screen.png) 5 | 6 | ## Live demo 7 | For a live demo of the project have a look at http://react-trello-board.web-pal.com 8 | 9 | 10 | ## Installation 11 | Firstly make sure that you have [Node](https://nodejs.org/en/download/) and [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed. 12 | Next clone this repo https://github.com/web-pal/react-trello-board.git. You can do this by going into your shell of choice and entering 13 | ``` 14 | git clone https://github.com/web-pal/react-trello-board.git 15 | ``` 16 | then change into that folder 17 | ``` 18 | cd react-trello-board 19 | ``` 20 | install the necessary packages locally 21 | ``` 22 | npm install 23 | ``` 24 | and start up a local server 25 | ``` 26 | npm start 27 | ``` 28 | Now visit [`localhost:3000`](http://localhost:3000) from your browser. Now your app should be up and running. 29 | 30 | ## Contribute 31 | We are very happy for any input and potential contributions for this project. 32 | 33 | Firstly raise an issue. Then if you are assigned to that issue fork the repo, make your edits and make a pull request. 34 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, react/jsx-first-prop-new-line */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | devtool: 'cheap-module-eval-source-map', 7 | entry: [ 8 | 'webpack-dev-server/client?http://localhost:3000', 9 | 'webpack/hot/only-dev-server', 10 | './src/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | new webpack.DefinePlugin({ 19 | __DEV__: true, 20 | 'process.env.NODE_ENV': JSON.stringify('development'), 21 | 'process.env.BROWSER': true 22 | }), 23 | new webpack.HotModuleReplacementPlugin() 24 | ], 25 | module: { 26 | loaders: [{ 27 | test: /\.js$/, 28 | loaders: ['babel'], 29 | exclude: /node_modules/, 30 | include: path.join(__dirname, 'src') 31 | }, 32 | { 33 | test: /\.styl$/, 34 | loader: 'style-loader!css-loader!stylus-loader' 35 | }, 36 | { 37 | test: /\.(jpg|jpeg|gif|png|ico|ttf|otf|eot|svg|woff|woff2)(\?[a-z0-9]+)?$/, 38 | loader: 'file-loader?name=[path][name].[ext]' 39 | }] 40 | }, 41 | stylus: { 42 | use: [require('nib')()], 43 | import: ['~nib/lib/nib/index.styl'] 44 | }, 45 | node: { 46 | net: 'empty', 47 | tls: 'empty', 48 | dns: 'empty' 49 | } 50 | }; 51 | /* eslint-enable global-require, react/jsx-first-prop-new-line */ 52 | -------------------------------------------------------------------------------- /src/actions/lists.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | 3 | export const GET_LISTS_START = 'GET_LISTS_START'; 4 | export const GET_LISTS = 'GET_LISTS'; 5 | export const MOVE_CARD = 'MOVE_CARD'; 6 | export const MOVE_LIST = 'MOVE_LIST'; 7 | export const TOGGLE_DRAGGING = 'TOGGLE_DRAGGING'; 8 | 9 | export function getLists(quantity) { 10 | return dispatch => { 11 | dispatch({ type: GET_LISTS_START, quantity }); 12 | setTimeout(() => { 13 | const lists = []; 14 | let count = 0; 15 | for (let i = 0; i < quantity; i++) { 16 | const cards = []; 17 | const randomQuantity = Math.floor(Math.random() * (9 - 1 + 1)) + 1; 18 | for (let ic = 0; ic < randomQuantity; ic++) { 19 | cards.push({ 20 | id: count, 21 | firstName: faker.name.firstName(), 22 | lastName: faker.name.lastName(), 23 | title: faker.name.jobTitle() 24 | }); 25 | count = count + 1; 26 | } 27 | lists.push({ 28 | id: i, 29 | name: faker.commerce.productName(), 30 | cards 31 | }); 32 | } 33 | dispatch({ type: GET_LISTS, lists, isFetching: true }); 34 | }, 1000); // fake delay 35 | dispatch({ type: GET_LISTS_START, isFetching: false }); 36 | }; 37 | } 38 | 39 | export function moveList(lastX, nextX) { 40 | return (dispatch) => { 41 | dispatch({ type: MOVE_LIST, lastX, nextX }); 42 | }; 43 | } 44 | 45 | export function moveCard(lastX, lastY, nextX, nextY) { 46 | return (dispatch) => { 47 | dispatch({ type: MOVE_CARD, lastX, lastY, nextX, nextY }); 48 | }; 49 | } 50 | 51 | export function toggleDragging(isDragging) { 52 | return (dispatch) => { 53 | dispatch({ type: TOGGLE_DRAGGING, isDragging }); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/reducers/lists.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | import { 4 | GET_LISTS, 5 | GET_LISTS_START, 6 | MOVE_CARD, 7 | MOVE_LIST, 8 | TOGGLE_DRAGGING 9 | } from '../actions/lists'; 10 | 11 | /* eslint-disable new-cap */ 12 | const InitialState = Record({ 13 | isFetching: false, 14 | lists: [], 15 | isDragging: false 16 | }); 17 | /* eslint-enable new-cap */ 18 | const initialState = new InitialState; 19 | 20 | 21 | export default function lists(state = initialState, action) { 22 | switch (action.type) { 23 | case GET_LISTS_START: 24 | return state.set('isFetching', true); 25 | case GET_LISTS: 26 | return state.withMutations((ctx) => { 27 | ctx.set('isFetching', false) 28 | .set('lists', action.lists); 29 | }); 30 | case MOVE_CARD: { 31 | const newLists = [...state.lists]; 32 | const { lastX, lastY, nextX, nextY } = action; 33 | if (lastX === nextX) { 34 | newLists[lastX].cards.splice(nextY, 0, newLists[lastX].cards.splice(lastY, 1)[0]); 35 | } else { 36 | // move element to new place 37 | newLists[nextX].cards.splice(nextY, 0, newLists[lastX].cards[lastY]); 38 | // delete element from old place 39 | newLists[lastX].cards.splice(lastY, 1); 40 | } 41 | return state.withMutations((ctx) => { 42 | ctx.set('lists', newLists); 43 | }); 44 | } 45 | case MOVE_LIST: { 46 | const newLists = [...state.lists]; 47 | const { lastX, nextX } = action; 48 | const t = newLists.splice(lastX, 1)[0]; 49 | 50 | newLists.splice(nextX, 0, t); 51 | 52 | return state.withMutations((ctx) => { 53 | ctx.set('lists', newLists); 54 | }); 55 | } 56 | case TOGGLE_DRAGGING: { 57 | return state.set('isDragging', action.isDragging); 58 | } 59 | default: 60 | return state; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React_trello_board", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "React trello board", 6 | "scripts": { 7 | "lint": "eslint src *.js", 8 | "start": "node server.js", 9 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.production.babel.js --progress --profile --colors" 10 | }, 11 | "dependencies": { 12 | "faker": "3.1.0", 13 | "immutable": "3.8.1", 14 | "lodash": "4.14.0", 15 | "react": "15.2.1", 16 | "react-dnd": "2.1.4", 17 | "react-dnd-html5-backend": "2.1.2", 18 | "react-dom": "15.2.1", 19 | "react-redux": "4.4.5", 20 | "react-router": "2.5.2", 21 | "react-router-redux": "4.0.5", 22 | "redux": "3.5.2" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "6.10.1", 26 | "babel-core": "6.10.4", 27 | "babel-eslint": "6.1.2", 28 | "babel-loader": "6.2.4", 29 | "babel-plugin-add-module-exports": "0.2.1", 30 | "babel-plugin-react-display-name": "2.0.0", 31 | "babel-plugin-transform-decorators-legacy": "1.3.4", 32 | "babel-preset-es2015": "6.9.0", 33 | "babel-preset-react": "6.11.1", 34 | "babel-preset-react-hmre": "1.1.1", 35 | "babel-preset-stage-0": "6.5.0", 36 | "classnames": "2.2.5", 37 | "cross-env": "1.0.8", 38 | "css-loader": "0.23.1", 39 | "eslint": "3.0.1", 40 | "eslint-config-airbnb": "9.0.1", 41 | "eslint-plugin-import": "1.10.2", 42 | "eslint-plugin-jsx-a11y": "1.5.5", 43 | "eslint-plugin-react": "5.2.2", 44 | "extract-text-webpack-plugin": "1.0.1", 45 | "file-loader": "0.9.0", 46 | "html-webpack-plugin": "2.22.0", 47 | "less": "2.7.1", 48 | "less-loader": "2.2.3", 49 | "nib": "1.1.0", 50 | "react-hot-loader": "3.0.0-beta.2", 51 | "redux-thunk": "2.1.0", 52 | "style-loader": "0.13.1", 53 | "stylus": "0.54.5", 54 | "stylus-loader": "2.1.1", 55 | "url-loader": "0.5.7", 56 | "webpack": "1.13.1", 57 | "webpack-dev-server": "1.14.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/containers/Board/Cards/Card.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | item: PropTypes.object.isRequired, 5 | style: PropTypes.object 6 | }; 7 | 8 | const galPng = require('../../../assets/images/gal.png'); 9 | const delPng = require('../../../assets/images/del.png'); 10 | 11 | 12 | const Card = (props) => { 13 | const { style, item } = props; 14 | 15 | return ( 16 |
17 |
{item.title}
18 |
19 |
20 | 21 |
22 |
23 |
{`${item.firstName} ${item.lastName}`}
24 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Libero, banditos.

25 |
26 |
27 |
28 |
29 | Add perfomers 30 |
31 | Perfomer 35 |
36 |
37 | Perfomer 41 |
42 |
43 | Perfomer 47 |
48 |
49 |
50 | Delete perfomers 51 |
52 | Perfomer 56 |
57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | Card.propTypes = propTypes; 64 | 65 | export default Card; 66 | -------------------------------------------------------------------------------- /src/containers/Board/CustomDragLayer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { DragLayer } from 'react-dnd'; 3 | 4 | import CardDragPreview from './CardDragPreview'; 5 | import snapToGrid from './snapToGrid'; 6 | 7 | 8 | const layerStyles = { 9 | position: 'fixed', 10 | pointerEvents: 'none', 11 | zIndex: 100000 12 | }; 13 | 14 | function getItemStyles(props) { 15 | const { initialOffset, currentOffset } = props; 16 | if (!initialOffset || !currentOffset) { 17 | return { 18 | display: 'none' 19 | }; 20 | } 21 | 22 | let { x, y } = currentOffset; 23 | 24 | if (props.snapToGrid) { 25 | x -= initialOffset.x; 26 | y -= initialOffset.y; 27 | [x, y] = snapToGrid(x, y); 28 | x += initialOffset.x; 29 | y += initialOffset.y; 30 | } 31 | 32 | const transform = `translate(${x}px, ${y}px)`; 33 | return { 34 | WebkitTransform: transform, 35 | transform 36 | }; 37 | } 38 | 39 | @DragLayer((monitor) => ({ 40 | item: monitor.getItem(), 41 | itemType: monitor.getItemType(), 42 | initialOffset: monitor.getInitialSourceClientOffset(), 43 | currentOffset: monitor.getSourceClientOffset(), 44 | isDragging: monitor.isDragging() 45 | })) 46 | export default class CustomDragLayer extends Component { 47 | static propTypes = { 48 | item: PropTypes.object, 49 | itemType: PropTypes.string, 50 | initialOffset: PropTypes.shape({ 51 | x: PropTypes.number.isRequired, 52 | y: PropTypes.number.isRequired 53 | }), 54 | currentOffset: PropTypes.shape({ 55 | x: PropTypes.number.isRequired, 56 | y: PropTypes.number.isRequired 57 | }), 58 | isDragging: PropTypes.bool.isRequired, 59 | snapToGrid: PropTypes.bool.isRequired 60 | }; 61 | 62 | renderItem(type, item) { 63 | switch (type) { 64 | case 'card': 65 | return ( 66 | 67 | ); 68 | default: 69 | return null; 70 | } 71 | } 72 | 73 | render() { 74 | const { item, itemType, isDragging } = this.props; 75 | 76 | if (!isDragging) { 77 | return null; 78 | } 79 | 80 | 81 | return ( 82 |
83 |
84 | {this.renderItem(itemType, item)} 85 |
86 |
87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/containers/Board/Cards/DraggableCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import { DragSource } from 'react-dnd'; 4 | import { getEmptyImage } from 'react-dnd-html5-backend'; 5 | 6 | import Card from './Card'; 7 | 8 | 9 | function getStyles(isDragging) { 10 | return { 11 | display: isDragging ? 0.5 : 1 12 | }; 13 | } 14 | 15 | const cardSource = { 16 | beginDrag(props, monitor, component) { 17 | // dispatch to redux store that drag is started 18 | const { item, x, y } = props; 19 | const { id, title } = item; 20 | const { clientWidth, clientHeight } = findDOMNode(component); 21 | 22 | return { id, title, item, x, y, clientWidth, clientHeight }; 23 | }, 24 | endDrag(props, monitor) { 25 | document.getElementById(monitor.getItem().id).style.display = 'block'; 26 | props.stopScrolling(); 27 | }, 28 | isDragging(props, monitor) { 29 | const isDragging = props.item && props.item.id === monitor.getItem().id; 30 | return isDragging; 31 | } 32 | }; 33 | 34 | // options: 4rd param to DragSource https://gaearon.github.io/react-dnd/docs-drag-source.html 35 | const OPTIONS = { 36 | arePropsEqual: function arePropsEqual(props, otherProps) { 37 | let isEqual = true; 38 | if (props.item.id === otherProps.item.id && 39 | props.x === otherProps.x && 40 | props.y === otherProps.y 41 | ) { 42 | isEqual = true; 43 | } else { 44 | isEqual = false; 45 | } 46 | return isEqual; 47 | } 48 | }; 49 | 50 | function collectDragSource(connectDragSource, monitor) { 51 | return { 52 | connectDragSource: connectDragSource.dragSource(), 53 | connectDragPreview: connectDragSource.dragPreview(), 54 | isDragging: monitor.isDragging() 55 | }; 56 | } 57 | 58 | @DragSource('card', cardSource, collectDragSource, OPTIONS) 59 | export default class CardComponent extends Component { 60 | static propTypes = { 61 | item: PropTypes.object, 62 | connectDragSource: PropTypes.func.isRequired, 63 | connectDragPreview: PropTypes.func.isRequired, 64 | isDragging: PropTypes.bool.isRequired, 65 | x: PropTypes.number.isRequired, 66 | y: PropTypes.number, 67 | stopScrolling: PropTypes.func 68 | } 69 | 70 | componentDidMount() { 71 | this.props.connectDragPreview(getEmptyImage(), { 72 | captureDraggingState: true 73 | }); 74 | } 75 | 76 | render() { 77 | const { isDragging, connectDragSource, item } = this.props; 78 | 79 | return connectDragSource( 80 |
81 | 82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/containers/Board/Cards/CardsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { DropTarget, DragSource } from 'react-dnd'; 3 | 4 | import Cards from './Cards'; 5 | 6 | const listSource = { 7 | beginDrag(props) { 8 | return { 9 | id: props.id, 10 | x: props.x 11 | }; 12 | }, 13 | endDrag(props) { 14 | props.stopScrolling(); 15 | } 16 | }; 17 | 18 | const listTarget = { 19 | canDrop() { 20 | return false; 21 | }, 22 | hover(props, monitor) { 23 | if (!props.isScrolling) { 24 | if (window.innerWidth - monitor.getClientOffset().x < 200) { 25 | props.startScrolling('toRight'); 26 | } else if (monitor.getClientOffset().x < 200) { 27 | props.startScrolling('toLeft'); 28 | } 29 | } else { 30 | if (window.innerWidth - monitor.getClientOffset().x > 200 && 31 | monitor.getClientOffset().x > 200 32 | ) { 33 | props.stopScrolling(); 34 | } 35 | } 36 | const { id: listId } = monitor.getItem(); 37 | const { id: nextX } = props; 38 | if (listId !== nextX) { 39 | props.moveList(listId, props.x); 40 | } 41 | } 42 | }; 43 | 44 | @DropTarget('list', listTarget, connectDragSource => ({ 45 | connectDropTarget: connectDragSource.dropTarget(), 46 | })) 47 | @DragSource('list', listSource, (connectDragSource, monitor) => ({ 48 | connectDragSource: connectDragSource.dragSource(), 49 | isDragging: monitor.isDragging() 50 | })) 51 | export default class CardsContainer extends Component { 52 | static propTypes = { 53 | connectDropTarget: PropTypes.func.isRequired, 54 | connectDragSource: PropTypes.func.isRequired, 55 | item: PropTypes.object, 56 | x: PropTypes.number, 57 | moveCard: PropTypes.func.isRequired, 58 | moveList: PropTypes.func.isRequired, 59 | isDragging: PropTypes.bool, 60 | startScrolling: PropTypes.func, 61 | stopScrolling: PropTypes.func, 62 | isScrolling: PropTypes.bool 63 | } 64 | 65 | render() { 66 | const { connectDropTarget, connectDragSource, item, x, moveCard, isDragging } = this.props; 67 | const opacity = isDragging ? 0.5 : 1; 68 | 69 | return connectDragSource(connectDropTarget( 70 |
71 |
72 |
{item.name}
73 |
74 | 82 |
83 | )); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/assets/temp.styl: -------------------------------------------------------------------------------- 1 | * 2 | box-sizing border-box 3 | .clearfix 4 | clearfix() 5 | html 6 | position relative 7 | height 100% 8 | body 9 | font 14px "Helvetica Neue", Arial, Helvetica, sans-serif 10 | line-height 18px 11 | color #4d4d4d 12 | font-weight normal 13 | padding 0 10px 14 | background-color rgb(0, 121, 191) 15 | position relative 16 | height 100% 17 | padding 0 18 | margin 0 19 | clearfix() 20 | * 21 | box-sizing border-box 22 | a 23 | text-decoration none 24 | img 25 | max-width 100% 26 | 27 | main 28 | padding 50px 10px 29 | overflow-x auto 30 | overflow-y hidden 31 | height 100% 32 | white-space nowrap 33 | clearfix() 34 | .desk, .desk-placeholder, .desk-placeholder:hover 35 | width 270px 36 | display inline-block 37 | height 100% 38 | overflow hidden 39 | min-height 100px 40 | background-color #e2e4e6 41 | box-shadow 1px 1px 1px rgba(0,0,0,.5) 42 | border-radius 3px 43 | padding-bottom 20px 44 | margin-right 20px 45 | vertical-align top 46 | .desk-head 47 | position relative 48 | padding 5px 8px 49 | margin-bottom 10px 50 | clearfix() 51 | .desk-name 52 | font-weight bold 53 | white-space normal 54 | width 100% 55 | .desk-items 56 | overflow-y auto 57 | height 100% 58 | padding-bottom 20px 59 | .placeholder, .placeholder:hover 60 | height 161px 61 | background-color rgba(208,208,208,1) 62 | .item 63 | width 90% 64 | margin 0 auto 10px 65 | background #fff 66 | min-height 100px 67 | border-radius 5px 68 | box-shadow 1px 1px 1px rgba(0,0,0,.3) 69 | transition 0.15s 70 | cursor pointer 71 | &:hover 72 | background #f7f7f7 73 | .item-name 74 | padding 5px 10px 75 | border-bottom 1px solid #dbdbdb 76 | white-space normal 77 | word-break break-all 78 | word-wrap break-world 79 | .item-container 80 | padding 10px 81 | border-bottom 1px solid #dbdbdb 82 | clearfix() 83 | .item-avatar-wrap 84 | width 70px 85 | float left 86 | margin-bottom 5px 87 | img 88 | width 50px 89 | height 50px 90 | display block 91 | margin 0 auto 92 | border-radius 50% 93 | border 1px solid #dbdbdb 94 | .item-content 95 | white-space normal 96 | .item-author 97 | font-size 16px 98 | font-weight bolder 99 | p 100 | margin 5px 0 101 | .item-perfomers 102 | clearfix() 103 | padding 5px 10px 104 | .add-perfomers 105 | float left 106 | .delete-perfomers 107 | float right 108 | .add-perfomers, 109 | .delete-perfomers 110 | a 111 | display block 112 | float left 113 | margin-right 3px 114 | img 115 | width 15px 116 | height 15px 117 | .perfomer 118 | float left 119 | img 120 | display block 121 | width 15px 122 | height 15px 123 | border-radius 50% 124 | -------------------------------------------------------------------------------- /src/containers/Board/Board.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { DragDropContext } from 'react-dnd'; 5 | import HTML5Backend from 'react-dnd-html5-backend'; 6 | 7 | import * as ListsActions from '../../actions/lists'; 8 | 9 | import CardsContainer from './Cards/CardsContainer'; 10 | import CustomDragLayer from './CustomDragLayer'; 11 | 12 | function mapStateToProps(state) { 13 | return { 14 | lists: state.lists.lists 15 | }; 16 | } 17 | 18 | function mapDispatchToProps(dispatch) { 19 | return bindActionCreators(ListsActions, dispatch); 20 | } 21 | 22 | @connect(mapStateToProps, mapDispatchToProps) 23 | @DragDropContext(HTML5Backend) 24 | export default class Board extends Component { 25 | static propTypes = { 26 | getLists: PropTypes.func.isRequired, 27 | moveCard: PropTypes.func.isRequired, 28 | moveList: PropTypes.func.isRequired, 29 | lists: PropTypes.array.isRequired, 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | this.moveCard = this.moveCard.bind(this); 35 | this.moveList = this.moveList.bind(this); 36 | this.findList = this.findList.bind(this); 37 | this.scrollRight = this.scrollRight.bind(this); 38 | this.scrollLeft = this.scrollLeft.bind(this); 39 | this.stopScrolling = this.stopScrolling.bind(this); 40 | this.startScrolling = this.startScrolling.bind(this); 41 | this.state = { isScrolling: false }; 42 | } 43 | 44 | componentWillMount() { 45 | this.props.getLists(10); 46 | } 47 | 48 | startScrolling(direction) { 49 | // if (!this.state.isScrolling) { 50 | switch (direction) { 51 | case 'toLeft': 52 | this.setState({ isScrolling: true }, this.scrollLeft()); 53 | break; 54 | case 'toRight': 55 | this.setState({ isScrolling: true }, this.scrollRight()); 56 | break; 57 | default: 58 | break; 59 | } 60 | // } 61 | } 62 | 63 | scrollRight() { 64 | function scroll() { 65 | document.getElementsByTagName('main')[0].scrollLeft += 10; 66 | } 67 | this.scrollInterval = setInterval(scroll, 10); 68 | } 69 | 70 | scrollLeft() { 71 | function scroll() { 72 | document.getElementsByTagName('main')[0].scrollLeft -= 10; 73 | } 74 | this.scrollInterval = setInterval(scroll, 10); 75 | } 76 | 77 | stopScrolling() { 78 | this.setState({ isScrolling: false }, clearInterval(this.scrollInterval)); 79 | } 80 | 81 | moveCard(lastX, lastY, nextX, nextY) { 82 | this.props.moveCard(lastX, lastY, nextX, nextY); 83 | } 84 | 85 | moveList(listId, nextX) { 86 | const { lastX } = this.findList(listId); 87 | this.props.moveList(lastX, nextX); 88 | } 89 | 90 | findList(id) { 91 | const { lists } = this.props; 92 | const list = lists.filter(l => l.id === id)[0]; 93 | 94 | return { 95 | list, 96 | lastX: lists.indexOf(list) 97 | }; 98 | } 99 | 100 | render() { 101 | const { lists } = this.props; 102 | 103 | return ( 104 |
105 | 106 | {lists.map((item, i) => 107 | 118 | )} 119 |
120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/containers/Board/Cards/Cards.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { DropTarget } from 'react-dnd'; 3 | import { findDOMNode } from 'react-dom'; 4 | 5 | import Card from './DraggableCard'; 6 | import { CARD_HEIGHT, CARD_MARGIN, OFFSET_HEIGHT } from '../../../constants.js'; 7 | 8 | 9 | function getPlaceholderIndex(y, scrollY) { 10 | // shift placeholder if y position more than card height / 2 11 | const yPos = y - OFFSET_HEIGHT + scrollY; 12 | let placeholderIndex; 13 | if (yPos < CARD_HEIGHT / 2) { 14 | placeholderIndex = -1; // place at the start 15 | } else { 16 | placeholderIndex = Math.floor((yPos - CARD_HEIGHT / 2) / (CARD_HEIGHT + CARD_MARGIN)); 17 | } 18 | return placeholderIndex; 19 | } 20 | 21 | const specs = { 22 | drop(props, monitor, component) { 23 | document.getElementById(monitor.getItem().id).style.display = 'block'; 24 | const { placeholderIndex } = component.state; 25 | const lastX = monitor.getItem().x; 26 | const lastY = monitor.getItem().y; 27 | const nextX = props.x; 28 | let nextY = placeholderIndex; 29 | 30 | if (lastY > nextY) { // move top 31 | nextY += 1; 32 | } else if (lastX !== nextX) { // insert into another list 33 | nextY += 1; 34 | } 35 | 36 | if (lastX === nextX && lastY === nextY) { // if position equel 37 | return; 38 | } 39 | 40 | props.moveCard(lastX, lastY, nextX, nextY); 41 | }, 42 | hover(props, monitor, component) { 43 | // defines where placeholder is rendered 44 | const placeholderIndex = getPlaceholderIndex( 45 | monitor.getClientOffset().y, 46 | findDOMNode(component).scrollTop 47 | ); 48 | 49 | // horizontal scroll 50 | if (!props.isScrolling) { 51 | if (window.innerWidth - monitor.getClientOffset().x < 200) { 52 | props.startScrolling('toRight'); 53 | } else if (monitor.getClientOffset().x < 200) { 54 | props.startScrolling('toLeft'); 55 | } 56 | } else { 57 | if (window.innerWidth - monitor.getClientOffset().x > 200 && 58 | monitor.getClientOffset().x > 200 59 | ) { 60 | props.stopScrolling(); 61 | } 62 | } 63 | 64 | // IMPORTANT! 65 | // HACK! Since there is an open bug in react-dnd, making it impossible 66 | // to get the current client offset through the collect function as the 67 | // user moves the mouse, we do this awful hack and set the state (!!) 68 | // on the component from here outside the component. 69 | // https://github.com/gaearon/react-dnd/issues/179 70 | component.setState({ placeholderIndex }); 71 | 72 | // when drag begins, we hide the card and only display cardDragPreview 73 | const item = monitor.getItem(); 74 | document.getElementById(item.id).style.display = 'none'; 75 | } 76 | }; 77 | 78 | 79 | @DropTarget('card', specs, (connectDragSource, monitor) => ({ 80 | connectDropTarget: connectDragSource.dropTarget(), 81 | isOver: monitor.isOver(), 82 | canDrop: monitor.canDrop(), 83 | item: monitor.getItem() 84 | })) 85 | export default class Cards extends Component { 86 | static propTypes = { 87 | connectDropTarget: PropTypes.func.isRequired, 88 | moveCard: PropTypes.func.isRequired, 89 | cards: PropTypes.array.isRequired, 90 | x: PropTypes.number.isRequired, 91 | isOver: PropTypes.bool, 92 | item: PropTypes.object, 93 | canDrop: PropTypes.bool, 94 | startScrolling: PropTypes.func, 95 | stopScrolling: PropTypes.func, 96 | isScrolling: PropTypes.bool 97 | } 98 | 99 | constructor(props) { 100 | super(props); 101 | this.state = { 102 | placeholderIndex: undefined, 103 | isScrolling: false, 104 | }; 105 | } 106 | 107 | render() { 108 | const { connectDropTarget, x, cards, isOver, canDrop } = this.props; 109 | const { placeholderIndex } = this.state; 110 | 111 | let isPlaceHold = false; 112 | let cardList = []; 113 | cards.forEach((item, i) => { 114 | if (isOver && canDrop) { 115 | isPlaceHold = false; 116 | if (i === 0 && placeholderIndex === -1) { 117 | cardList.push(
); 118 | } else if (placeholderIndex > i) { 119 | isPlaceHold = true; 120 | } 121 | } 122 | if (item !== undefined) { 123 | cardList.push( 124 | 129 | ); 130 | } 131 | if (isOver && canDrop && placeholderIndex === i) { 132 | cardList.push(
); 133 | } 134 | }); 135 | 136 | // if placeholder index is greater than array.length, display placeholder as last 137 | if (isPlaceHold) { 138 | cardList.push(
); 139 | } 140 | 141 | // if there is no items in cards currently, display a placeholder anyway 142 | if (isOver && canDrop && cards.length === 0) { 143 | cardList.push(
); 144 | } 145 | 146 | return connectDropTarget( 147 |
148 | {cardList} 149 |
150 | ); 151 | } 152 | } 153 | --------------------------------------------------------------------------------