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