├── .gitignore ├── README.md ├── babel.config.js ├── client ├── src │ ├── App.jsx │ ├── components │ │ ├── Col.jsx │ │ ├── DropWrapper.jsx │ │ ├── Header.jsx │ │ ├── Item.jsx │ │ └── Window.jsx │ ├── data │ │ ├── index.js │ │ └── types.js │ ├── index.js │ └── pages │ │ └── Homepage.jsx ├── style │ └── index.css └── templates │ └── index.ejs ├── commit.log ├── package-lock.json ├── package.json ├── server └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | .DS_Store 4 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Drag n Drop Tutorial 2 | 3 | Create a Trello Dashboard clone in React.js. The code needed for this is super easy thanks to react-dnd! 4 | 5 | ## Installation 6 | 7 | Use the cmd line to install dependencies. 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` 16 | npm run build 17 | npm run dev 18 | ``` 19 | 20 | ## Contributing 21 | 22 | Add your commit notes to commit.log 23 | When committing use 24 | 25 | ``` 26 | git commit -F commmit.log 27 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/env", 4 | { 5 | targets: { 6 | edge: "17", 7 | firefox: "60", 8 | chrome: "67", 9 | safari: "11.1" 10 | }, 11 | "corejs": "3.2.1", 12 | useBuiltIns: "entry" 13 | } 14 | ], 15 | "@babel/preset-react" 16 | ]; 17 | 18 | const plugins = [ 19 | "@babel/plugin-proposal-class-properties", 20 | "@babel/plugin-transform-runtime" 21 | ]; 22 | 23 | module.exports = { presets, plugins }; -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Homepage from "./pages/Homepage"; 3 | import Header from "./components/Header"; 4 | import { DndProvider } from "react-dnd"; 5 | import Backend from "react-dnd-html5-backend"; 6 | 7 | const App = () => { 8 | return ( 9 | 10 |
11 | 12 | 13 | ); 14 | }; 15 | 16 | export default App; -------------------------------------------------------------------------------- /client/src/components/Col.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Col = ({ isOver, children }) => { 4 | const className = isOver ? " highlight-region" : ""; 5 | 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | }; 12 | 13 | export default Col; -------------------------------------------------------------------------------- /client/src/components/DropWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDrop } from "react-dnd"; 3 | import ITEM_TYPE from "../data/types"; 4 | import { statuses } from "../data"; 5 | 6 | const DropWrapper = ({ onDrop, children, status }) => { 7 | const [{ isOver }, drop] = useDrop({ 8 | accept: ITEM_TYPE, 9 | canDrop: (item, monitor) => { 10 | const itemIndex = statuses.findIndex(si => si.status === item.status); 11 | const statusIndex = statuses.findIndex(si => si.status === status); 12 | return [itemIndex + 1, itemIndex - 1, itemIndex].includes(statusIndex); 13 | }, 14 | drop: (item, monitor) => { 15 | onDrop(item, monitor, status); 16 | }, 17 | collect: monitor => ({ 18 | isOver: monitor.isOver() 19 | }) 20 | }); 21 | 22 | return ( 23 |
24 | {React.cloneElement(children, { isOver })} 25 |
26 | ) 27 | }; 28 | 29 | export default DropWrapper; -------------------------------------------------------------------------------- /client/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |

Trello Dashboard 🗂

7 |
8 | ); 9 | }; 10 | 11 | export default Header; -------------------------------------------------------------------------------- /client/src/components/Item.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useRef } from "react"; 2 | import { useDrag, useDrop } from "react-dnd"; 3 | import Window from "./Window"; 4 | import ITEM_TYPE from "../data/types"; 5 | 6 | const Item = ({ item, index, moveItem, status }) => { 7 | const ref = useRef(null); 8 | 9 | const [, drop] = useDrop({ 10 | accept: ITEM_TYPE, 11 | hover(item, monitor) { 12 | if (!ref.current) { 13 | return 14 | } 15 | const dragIndex = item.index; 16 | const hoverIndex = index; 17 | 18 | if (dragIndex === hoverIndex) { 19 | return 20 | } 21 | 22 | const hoveredRect = ref.current.getBoundingClientRect(); 23 | const hoverMiddleY = (hoveredRect.bottom - hoveredRect.top) / 2; 24 | const mousePosition = monitor.getClientOffset(); 25 | const hoverClientY = mousePosition.y - hoveredRect.top; 26 | 27 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 28 | return; 29 | } 30 | 31 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 32 | return; 33 | } 34 | moveItem(dragIndex, hoverIndex); 35 | item.index = hoverIndex; 36 | }, 37 | }); 38 | 39 | const [{ isDragging }, drag] = useDrag({ 40 | item: { type: ITEM_TYPE, ...item, index }, 41 | collect: monitor => ({ 42 | isDragging: monitor.isDragging() 43 | }) 44 | }); 45 | 46 | const [show, setShow] = useState(false); 47 | 48 | const onOpen = () => setShow(true); 49 | 50 | const onClose = () => setShow(false); 51 | 52 | drag(drop(ref)); 53 | 54 | return ( 55 | 56 |
62 |
63 |

{item.content}

64 |

{item.icon}

65 |
66 | 71 | 72 | ); 73 | }; 74 | 75 | export default Item; -------------------------------------------------------------------------------- /client/src/components/Window.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Modal from "react-modal"; 3 | 4 | Modal.setAppElement("#app"); 5 | 6 | const Window = ({ show, onClose, item }) => { 7 | return ( 8 | 14 |
15 |

{item.title}

16 | 17 |
18 |
19 |

Description

20 |

{item.content}

21 |

Status

22 |

{item.icon} {`${item.status.charAt(0).toUpperCase()}${item.status.slice(1)}`}

23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Window; -------------------------------------------------------------------------------- /client/src/data/index.js: -------------------------------------------------------------------------------- 1 | const data = [{ 2 | id: 1, 3 | icon: "⭕️", 4 | status: "open", 5 | title: "Human Interest Form", 6 | content: "Fill out human interest distribution form" 7 | }, { 8 | id: 2, 9 | icon: "⭕️", 10 | status: "open", 11 | title: "Purchase present", 12 | content: "Get an anniversary gift" 13 | }, { 14 | id: 3, 15 | icon: "⭕️", 16 | status: "open", 17 | title: "Invest in investments", 18 | content: "Call the bank to talk about investments" 19 | }, { 20 | id: 4, 21 | icon: "⭕️", 22 | status: "open", 23 | title: "Daily reading", 24 | content: "Finish reading Intro to UI/UX" 25 | }]; 26 | 27 | const statuses = [{ 28 | status: "open", 29 | icon: "⭕️", 30 | color: "#EB5A46" 31 | }, { 32 | status: "in progress", 33 | icon: "🔆️", 34 | color: "#00C2E0" 35 | }, { 36 | status: "in review", 37 | icon: "📝", 38 | color: "#C377E0" 39 | }, { 40 | status: "done", 41 | icon: "✅", 42 | color: "#3981DE" 43 | }]; 44 | 45 | 46 | export { data, statuses }; -------------------------------------------------------------------------------- /client/src/data/types.js: -------------------------------------------------------------------------------- 1 | const ITEM_TYPE = "ITEM"; 2 | 3 | export default ITEM_TYPE; -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "../style/index.css"; 5 | 6 | ReactDOM.render(, document.getElementById("app")); -------------------------------------------------------------------------------- /client/src/pages/Homepage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Item from "../components/Item"; 3 | import DropWrapper from "../components/DropWrapper"; 4 | import Col from "../components/Col"; 5 | import { data, statuses } from "../data"; 6 | 7 | const Homepage = () => { 8 | const [items, setItems] = useState(data); 9 | 10 | const onDrop = (item, monitor, status) => { 11 | const mapping = statuses.find(si => si.status === status); 12 | 13 | setItems(prevState => { 14 | const newItems = prevState 15 | .filter(i => i.id !== item.id) 16 | .concat({ ...item, status, icon: mapping.icon }); 17 | return [ ...newItems ]; 18 | }); 19 | }; 20 | 21 | const moveItem = (dragIndex, hoverIndex) => { 22 | const item = items[dragIndex]; 23 | setItems(prevState => { 24 | const newItems = prevState.filter((i, idx) => idx !== dragIndex); 25 | newItems.splice(hoverIndex, 0, item); 26 | return [ ...newItems ]; 27 | }); 28 | }; 29 | 30 | return ( 31 |
32 | {statuses.map(s => { 33 | return ( 34 |
35 |

{s.status.toUpperCase()}

36 | 37 | 38 | {items 39 | .filter(i => i.status === s.status) 40 | .map((i, idx) => ) 41 | } 42 | 43 | 44 |
45 | ); 46 | })} 47 |
48 | ); 49 | }; 50 | 51 | export default Homepage; -------------------------------------------------------------------------------- /client/style/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: rgb(62, 100, 255); 3 | --complete-color: #27aa80; 4 | --text-color: #172b4d; 5 | --disabled-color: #fad6d6; 6 | --background-color: #f5eaea; 7 | } 8 | 9 | html { 10 | background: rgb(0,73,191); 11 | background: linear-gradient(90deg, rgba(0,73,191,1) 0%, rgba(190,190,255,1) 46%, rgba(0,212,255,1) 100%); 12 | } 13 | 14 | body { 15 | color: var(--text-color); 16 | font-family: sans-serif; 17 | margin: 0; 18 | } 19 | 20 | a { 21 | color: unset; 22 | text-decoration: unset; 23 | cursor: pointer; 24 | } 25 | 26 | p { 27 | margin: 10px 0; 28 | overflow-wrap: break-word; 29 | text-align: left; 30 | } 31 | 32 | label { 33 | font-size: 16px; 34 | display: block; 35 | } 36 | 37 | button, input { 38 | padding: 4px; 39 | border: 1px solid var(--disabled-color); 40 | } 41 | 42 | button { 43 | outline: none; 44 | background: transparent; 45 | border-radius: 5px; 46 | color: var(--primary-color); 47 | transition: all ease 0.8s; 48 | cursor: pointer; 49 | } 50 | 51 | button.active { 52 | color: var(--primary-color); 53 | } 54 | 55 | button.active:after { 56 | content: ""; 57 | display: block; 58 | margin: 0 auto; 59 | width: 50%; 60 | padding-top: 4px; 61 | border-bottom: 1px solid var(--primary-color); 62 | } 63 | 64 | input:focus { 65 | outline: none; 66 | } 67 | 68 | select { 69 | outline: none; 70 | height: 40px; 71 | } 72 | 73 | .row { 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: center; 77 | } 78 | 79 | .item { 80 | font-size: 15px; 81 | margin-bottom: 10px; 82 | padding: 10px; 83 | border-radius: 5px; 84 | z-index: 1; 85 | background-color: white; 86 | } 87 | 88 | .item:hover { 89 | cursor: pointer; 90 | } 91 | 92 | .item-title { 93 | font-weight: 600; 94 | font-size: 16px; 95 | } 96 | 97 | .item-status { 98 | text-align: right; 99 | } 100 | 101 | .color-bar { 102 | width: 40px; 103 | height: 10px; 104 | border-radius: 5px; 105 | } 106 | 107 | .drop-wrapper { 108 | flex: 1 25%; 109 | width: 100%; 110 | height: 100%; 111 | } 112 | 113 | .col-wrapper { 114 | display: flex; 115 | flex-direction: column; 116 | margin: 20px; 117 | padding: 20px; 118 | background-color: var(--background-color); 119 | border-radius: 5px; 120 | } 121 | 122 | .col-header { 123 | font-size: 20px; 124 | font-weight: 600; 125 | margin-bottom: 20px; 126 | margin-top: 0; 127 | } 128 | 129 | .col { 130 | min-height: 300px; 131 | max-width: 300px; 132 | width: 300px; 133 | } 134 | 135 | .highlight-region { 136 | background-color: yellow; 137 | } 138 | 139 | .page-header { 140 | background-color: #054F7C; 141 | padding: 20px; 142 | color: white; 143 | font-size: 30px; 144 | flex: 1 100%; 145 | margin-top: 0; 146 | text-align: center; 147 | } 148 | 149 | .modal { 150 | background-color: #F4F5F7; 151 | border-radius: 2px; 152 | margin: 48px 0 80px; 153 | min-height: 450px; 154 | width: 800px; 155 | outline: none; 156 | padding: 20px; 157 | } 158 | 159 | .overlay { 160 | display: flex; 161 | justify-content: center; 162 | position: fixed; 163 | top: 0; 164 | left: 0; 165 | right: 0; 166 | bottom: 0; 167 | background-color: rgba(0,0,0,0.5);; 168 | } 169 | 170 | .close-btn-ctn { 171 | display: flex; 172 | } 173 | 174 | .close-btn { 175 | height: 40px; 176 | width: 35px; 177 | font-size: 20px; 178 | color: #031D2C; 179 | border: none; 180 | border-radius: 25px; 181 | } 182 | 183 | .close-btn:hover { 184 | background-color: #DCDCDC; 185 | } -------------------------------------------------------------------------------- /client/templates/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /commit.log: -------------------------------------------------------------------------------- 1 | Update README.md, update base stylesheet for HTML tags and css classes. Add file for commit -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "client/src/index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "dev": "nodemon server/index.js" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.2.1", 12 | "ejs": "^2.7.1", 13 | "express": "^4.17.1", 14 | "react": "^16.10.2", 15 | "react-dnd": "^10.0.2", 16 | "react-dnd-html5-backend": "^10.0.2", 17 | "react-dom": "^16.10.2", 18 | "react-modal": "^3.11.2", 19 | "react-router-dom": "^5.1.2" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.6.2", 23 | "@babel/core": "^7.6.2", 24 | "@babel/node": "^7.6.2", 25 | "@babel/plugin-proposal-class-properties": "^7.5.5", 26 | "@babel/plugin-transform-runtime": "^7.6.2", 27 | "@babel/polyfill": "^7.6.0", 28 | "@babel/preset-env": "^7.6.2", 29 | "@babel/preset-react": "^7.6.3", 30 | "@babel/runtime": "^7.6.3", 31 | "babel-loader": "^8.0.6", 32 | "css-loader": "^3.2.0", 33 | "html-loader": "^0.5.5", 34 | "html-webpack-plugin": "^3.2.0", 35 | "nodemon": "^1.19.4", 36 | "style-loader": "^1.0.0", 37 | "webpack": "^4.41.0", 38 | "webpack-cli": "^3.3.9", 39 | "webpack-dev-middleware": "^3.7.2", 40 | "webpack-hot-middleware": "^2.25.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const webpack = require("webpack"); 4 | const webpackDevMiddleware = require("webpack-dev-middleware"); 5 | const webpackHotMiddleware = require("webpack-hot-middleware"); 6 | const config = require(path.join(__dirname, "../webpack.config.js")); 7 | const compiler = webpack(config); 8 | const app = express(); 9 | 10 | app.use(webpackDevMiddleware(compiler, config.devServer)); 11 | app.use(webpackHotMiddleware(compiler)); 12 | app.use(express.static(path.join(__dirname, '../build'))); 13 | 14 | app.get('/*', (req, res) => { 15 | res.sendFile(path.join(__dirname, '../build', 'index.html')); 16 | }); 17 | 18 | app.listen(4000); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | main: path.join(__dirname, 'client/src/index.js') 7 | }, 8 | output: { 9 | path: path.join(__dirname, 'build'), 10 | filename: 'bundle.js' 11 | }, 12 | plugins: [new HtmlWebpackPlugin({ 13 | title: 'React DnD Dashboard', 14 | template: path.join(__dirname, 'client/templates/index.ejs'), 15 | filename: 'index.html' 16 | })], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | exclude: /(node_modules|express)/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | presets: ['@babel/preset-env'] 26 | } 27 | } 28 | }, 29 | { 30 | test: /\.(html)$/, 31 | use: { 32 | loader: 'html-loader', 33 | options: { 34 | attrs: [':data-src'] 35 | } 36 | } 37 | }, 38 | { 39 | test: /\.css$/i, 40 | use: ['style-loader', 'css-loader'], 41 | } 42 | ] 43 | }, 44 | devServer: { 45 | contentBase: path.join(__dirname, 'build'), 46 | compress: true, 47 | proxy: { 48 | '/api': 'http://localhost:4000' 49 | } 50 | }, 51 | resolve: { 52 | extensions: ["*", ".js", ".jsx"] 53 | }, 54 | resolveLoader: { 55 | moduleExtensions: ["babel-loader"] 56 | }, 57 | devtool: 'source-map', 58 | mode: 'development', 59 | node: { global: true, fs: 'empty', net: 'empty', tls: 'empty' } 60 | }; --------------------------------------------------------------------------------