├── .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 | 
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 |

30 |
31 |

35 |
36 |
37 |

41 |
42 |
43 |

47 |
48 |
49 |
50 |

51 |
52 |

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 |
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 |
--------------------------------------------------------------------------------