138 | +
139 | {content.title}
140 |
141 | );
142 | };
143 | ```
144 |
145 | Now you can see that it highlights.
146 |
147 | 
148 |
149 | Shall we go for the drop?
150 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ejemplo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@atlaskit/pragmatic-drag-and-drop": "^1.1.11",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "tiny-invariant": "^1.3.3"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^18.2.66",
20 | "@types/react-dom": "^18.2.22",
21 | "@typescript-eslint/eslint-plugin": "^7.2.0",
22 | "@typescript-eslint/parser": "^7.2.0",
23 | "@vitejs/plugin-react": "^4.2.1",
24 | "eslint": "^8.57.0",
25 | "eslint-plugin-react-hooks": "^4.6.0",
26 | "eslint-plugin-react-refresh": "^0.4.6",
27 | "typescript": "^5.2.2",
28 | "vite": "^5.2.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/public/02-drag-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/02-drag/public/02-drag-01.gif
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/public/02-drag-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/02-drag/public/02-drag-02.gif
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/public/02-drag-03.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/02-drag/public/02-drag-03.gif
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | flex: 1;
4 | margin: 0 auto;
5 | padding: 2rem;
6 | text-align: center;
7 | }
8 |
9 | .logo {
10 | height: 6em;
11 | padding: 1.5em;
12 | will-change: filter;
13 | transition: filter 300ms;
14 | }
15 | .logo:hover {
16 | filter: drop-shadow(0 0 2em #646cffaa);
17 | }
18 | .logo.react:hover {
19 | filter: drop-shadow(0 0 2em #61dafbaa);
20 | }
21 |
22 | @keyframes logo-spin {
23 | from {
24 | transform: rotate(0deg);
25 | }
26 | to {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @media (prefers-reduced-motion: no-preference) {
32 | a:nth-of-type(2) .logo {
33 | animation: logo-spin infinite 20s linear;
34 | }
35 | }
36 |
37 | .card {
38 | padding: 2em;
39 | }
40 |
41 | .read-the-docs {
42 | color: #888;
43 | }
44 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { KanbanContainer } from "./kanban";
3 |
4 | function App() {
5 | return
;
6 | }
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.api";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/api/kanban.api.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "../model";
2 | import { mockData } from "../mock-data";
3 |
4 | // TODO: Move this outside kanban component folder
5 | export const loadKanbanContent = async (): Promise
=> {
6 | return mockData;
7 | };
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/card/card.component.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
4 | padding: 5px 15px;
5 | background-color: white;
6 | color: black;
7 | width: 210px;
8 | }
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/card/card.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CardContent } from "../../model";
3 | import classes from "./card.component.module.css";
4 | import { useEffect, useRef, useState } from "react";
5 | import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
6 | import invariant from "tiny-invariant";
7 |
8 | interface Props {
9 | content: CardContent;
10 | }
11 |
12 | export const Card: React.FC = (props) => {
13 | const { content } = props;
14 | const [dragging, setDragging] = useState(false);
15 | const ref = useRef(null);
16 |
17 | useEffect(() => {
18 | const el = ref.current;
19 | // Add this to avoid typescript in strict mode complaining about null
20 | // on draggable({ element: el }); call
21 | invariant(el);
22 |
23 | return draggable({
24 | element: el,
25 | onDragStart: () => setDragging(true),
26 | onDrop: () => setDragging(false),
27 | });
28 | }, []);
29 |
30 | return (
31 |
36 | {content.title}
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/column/column.component.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | row-gap: 5px;
5 | align-items: center;
6 | width: 250px; /* TODO: relative sizes or media queries?*/
7 | height: 100vh; /* TODO: review height, shouldn't be 100vh*/
8 | overflow: hidden; /*TODO: scroll? */
9 | border: 1px solid rgb(4, 1, 19); /* TODO: Theme colors, variables, CSS API? */
10 | background-color: aliceblue;
11 | color: black;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/column/column.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classes from "./column.component.module.css";
3 | import { CardContent } from "../../model";
4 | import { Card } from "../card/";
5 |
6 | interface Props {
7 | name: string;
8 | content: CardContent[];
9 | }
10 |
11 | export const Column: React.FC = (props) => {
12 | const { name, content } = props;
13 |
14 | return (
15 |
16 |
{name}
17 | {content.map((card) => (
18 |
19 | ))}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./column.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card";
2 | export * from "./column";
3 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.container";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/kanban.container.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: row;
4 | flex: 1;
5 | column-gap: 5px;
6 | min-width: 0;
7 | width: 100%;
8 | height: 100%;
9 | overflow: hidden;
10 | border: 1px solid rgb(89, 118, 10);
11 | background-color: burlywood;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/kanban.container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { KanbanContent, createDefaultKanbanContent } from "./model";
3 | import { loadKanbanContent } from "./api";
4 | import { Column } from "./components/column/";
5 | import classes from "./kanban.container.module.css";
6 |
7 | export const KanbanContainer: React.FC = () => {
8 | const [kanbanContent, setKanbanContent] = React.useState(
9 | createDefaultKanbanContent()
10 | );
11 |
12 | React.useEffect(() => {
13 | loadKanbanContent().then((content) => setKanbanContent(content));
14 | }, []);
15 |
16 | return (
17 |
18 | {kanbanContent.columns.map((column) => (
19 |
20 | ))}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/mock-data.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "./model";
2 |
3 | // TODO: Move this in the future outside the kanban component folder
4 | export const mockData: KanbanContent = {
5 | columns: [
6 | {
7 | id: 1,
8 | name: "Backglog",
9 | content: [
10 | {
11 | id: 1,
12 | title: "Create the cards",
13 | },
14 | {
15 | id: 2,
16 | title: "Place the cards in the columns",
17 | },
18 | {
19 | id: 3,
20 | title: "Implement card dragging",
21 | },
22 | {
23 | id: 4,
24 | title: "Implement drop card",
25 | },
26 | {
27 | id: 5,
28 | title: "Implement drag & drop column",
29 | },
30 | ],
31 | },
32 | {
33 | id: 2,
34 | name: "Doing",
35 | content: [
36 | {
37 | id: 6,
38 | title: "Delete a card",
39 | },
40 | ],
41 | },
42 | {
43 | id: 3,
44 | name: "Done",
45 | content: [
46 | {
47 | id: 7,
48 | title: "Create boilerplate",
49 | },
50 | {
51 | id: 8,
52 | title: "Define data model",
53 | },
54 | {
55 | id: 9,
56 | title: "Create columns",
57 | },
58 | ],
59 | },
60 | ],
61 | };
62 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/kanban/model.ts:
--------------------------------------------------------------------------------
1 | export interface CardContent {
2 | id: number;
3 | title: string;
4 | }
5 |
6 | export interface Column {
7 | id: number;
8 | name: string;
9 | content: CardContent[];
10 | }
11 |
12 | export interface KanbanContent {
13 | columns: Column[];
14 | }
15 |
16 | export const createDefaultKanbanContent = (): KanbanContent => ({
17 | columns: [],
18 | });
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/01-simple-kanban/02-drag/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ejemplo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@atlaskit/pragmatic-drag-and-drop": "^1.1.11",
14 | "immer": "^10.1.1",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "tiny-invariant": "^1.3.3"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^18.2.66",
21 | "@types/react-dom": "^18.2.22",
22 | "@typescript-eslint/eslint-plugin": "^7.2.0",
23 | "@typescript-eslint/parser": "^7.2.0",
24 | "@vitejs/plugin-react": "^4.2.1",
25 | "eslint": "^8.57.0",
26 | "eslint-plugin-react-hooks": "^4.6.0",
27 | "eslint-plugin-react-refresh": "^0.4.6",
28 | "typescript": "^5.2.2",
29 | "vite": "^5.2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/public/03-drop-column.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/03-drop-column/public/03-drop-column.gif
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | flex: 1;
4 | margin: 0 auto;
5 | padding: 2rem;
6 | text-align: center;
7 | }
8 |
9 | .logo {
10 | height: 6em;
11 | padding: 1.5em;
12 | will-change: filter;
13 | transition: filter 300ms;
14 | }
15 | .logo:hover {
16 | filter: drop-shadow(0 0 2em #646cffaa);
17 | }
18 | .logo.react:hover {
19 | filter: drop-shadow(0 0 2em #61dafbaa);
20 | }
21 |
22 | @keyframes logo-spin {
23 | from {
24 | transform: rotate(0deg);
25 | }
26 | to {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @media (prefers-reduced-motion: no-preference) {
32 | a:nth-of-type(2) .logo {
33 | animation: logo-spin infinite 20s linear;
34 | }
35 | }
36 |
37 | .card {
38 | padding: 2em;
39 | }
40 |
41 | .read-the-docs {
42 | color: #888;
43 | }
44 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { KanbanContainer } from "./kanban";
3 |
4 | function App() {
5 | return ;
6 | }
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.api";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/api/kanban.api.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "../model";
2 | import { mockData } from "../mock-data";
3 |
4 | // TODO: Move this outside kanban component folder
5 | export const loadKanbanContent = async (): Promise => {
6 | return mockData;
7 | };
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/card/card.component.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
4 | padding: 5px 15px;
5 | background-color: white;
6 | color: black;
7 | width: 210px;
8 | }
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/card/card.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CardContent } from "../../model";
3 | import classes from "./card.component.module.css";
4 | import { useEffect, useRef, useState } from "react";
5 | import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
6 | import invariant from "tiny-invariant";
7 |
8 | interface Props {
9 | content: CardContent;
10 | }
11 |
12 | export const Card: React.FC = (props) => {
13 | const { content } = props;
14 | const [dragging, setDragging] = useState(false);
15 | const ref = useRef(null);
16 |
17 | useEffect(() => {
18 | const el = ref.current;
19 | // Add this to avoid typescript in strict mode complaining about null
20 | // on draggable({ element: el }); call
21 | invariant(el);
22 |
23 | return draggable({
24 | element: el,
25 | getInitialData: () => ({ card: content }),
26 | onDragStart: () => setDragging(true),
27 | onDrop: () => setDragging(false),
28 | });
29 | }, []);
30 |
31 | return (
32 |
37 | {content.title}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/column/column.component.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | row-gap: 5px;
5 | align-items: center;
6 | width: 250px; /* TODO: relative sizes or media queries?*/
7 | height: 100vh; /* TODO: review height, shouldn't be 100vh*/
8 | overflow: hidden; /*TODO: scroll? */
9 | border: 1px solid rgb(4, 1, 19); /* TODO: Theme colors, variables, CSS API? */
10 | background-color: aliceblue;
11 | color: black;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/column/column.component.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3 | import invariant from "tiny-invariant";
4 | import classes from "./column.component.module.css";
5 | import { CardContent } from "../../model";
6 | import { Card } from "../card/";
7 |
8 | interface Props {
9 | columnId: number;
10 | name: string;
11 | content: CardContent[];
12 | }
13 |
14 | export const Column: React.FC = (props) => {
15 | const { columnId, name, content } = props;
16 | const ref = useRef(null);
17 | const [isDraggedOver, setIsDraggedOver] = useState(false);
18 |
19 | useEffect(() => {
20 | const el = ref.current;
21 | invariant(el);
22 |
23 | return dropTargetForElements({
24 | element: el,
25 | getData: () => ({ columnId }),
26 | onDragEnter: () => setIsDraggedOver(true),
27 | onDragLeave: () => setIsDraggedOver(false),
28 | onDrop: () => setIsDraggedOver(false),
29 | });
30 | }, []);
31 |
32 | return (
33 |
38 |
{name}
39 | {content.map((card) => (
40 |
41 | ))}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./column.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card";
2 | export * from "./column";
3 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.container";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/kanban.business.ts:
--------------------------------------------------------------------------------
1 | import { CardContent, Column, KanbanContent } from "./model";
2 | import { produce } from "immer";
3 |
4 | // Esto se podría hacer más optimo
5 |
6 | const removeCardFromColumn = (
7 | card: CardContent,
8 | kanbanContent: KanbanContent
9 | ): KanbanContent => {
10 | const newColumns = kanbanContent.columns.map((column) => {
11 | const newContent = column.content.filter((c) => c.id !== card.id);
12 |
13 | return {
14 | ...column,
15 | content: newContent,
16 | };
17 | });
18 |
19 | return {
20 | ...kanbanContent,
21 | columns: newColumns,
22 | };
23 | };
24 |
25 | const dropCardAfter = (
26 | origincard: CardContent,
27 | destinationCardId: number,
28 | destinationColumn: Column
29 | ): Column => {
30 | if (destinationCardId === -1) {
31 | return produce(destinationColumn, (draft) => {
32 | draft.content.push(origincard);
33 | });
34 | }
35 |
36 | return produce(destinationColumn, (draft: { content: CardContent[] }) => {
37 | const index = draft.content.findIndex(
38 | (card: { id: number }) => card.id === destinationCardId
39 | );
40 | draft.content.splice(index, 0, origincard);
41 | });
42 | };
43 |
44 | const addCardToColumn = (
45 | card: CardContent,
46 | columnId: number,
47 | kanbanContent: KanbanContent
48 | ): KanbanContent => {
49 | const newColumns = kanbanContent.columns.map((column) => {
50 | if (column.id === columnId) {
51 | return dropCardAfter(card, -1, column);
52 | }
53 | return column;
54 | });
55 |
56 | return {
57 | ...kanbanContent,
58 | columns: newColumns,
59 | };
60 | };
61 |
62 | export const moveCard = (
63 | card: CardContent,
64 | destinationColumnId: number,
65 | kanbanContent: KanbanContent
66 | ): KanbanContent => {
67 | const newKanbanContent = removeCardFromColumn(card, kanbanContent);
68 | return addCardToColumn(card, destinationColumnId, newKanbanContent);
69 | };
70 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/kanban.container.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: row;
4 | flex: 1;
5 | column-gap: 5px;
6 | min-width: 0;
7 | width: 100%;
8 | height: 100%;
9 | overflow: hidden;
10 | border: 1px solid rgb(89, 118, 10);
11 | background-color: burlywood;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/kanban.container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3 | import {
4 | CardContent,
5 | KanbanContent,
6 | createDefaultKanbanContent,
7 | } from "./model";
8 | import { loadKanbanContent } from "./api";
9 | import { Column } from "./components/column/";
10 | import classes from "./kanban.container.module.css";
11 | import { moveCard } from "./kanban.business";
12 |
13 | export const KanbanContainer: React.FC = () => {
14 | const [kanbanContent, setKanbanContent] = React.useState(
15 | createDefaultKanbanContent()
16 | );
17 |
18 | React.useEffect(() => {
19 | loadKanbanContent().then((content) => setKanbanContent(content));
20 | }, []);
21 |
22 | React.useEffect(() => {
23 | return monitorForElements({
24 | onDrop({ source, location }) {
25 | const destination = location.current.dropTargets[0];
26 | if (!destination) {
27 | // si se suelta fuera de cualquier target
28 | return;
29 | }
30 |
31 | const card = source.data.card as CardContent;
32 | const columnId = destination.data.columnId as number;
33 |
34 | // También aquí nos aseguramos de que estamos trabajando con el último estado
35 | setKanbanContent((kanbanContent) =>
36 | moveCard(card, columnId, kanbanContent)
37 | );
38 | },
39 | });
40 | }, [kanbanContent]);
41 |
42 | return (
43 |
44 | {kanbanContent.columns.map((column) => (
45 |
51 | ))}
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/mock-data.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "./model";
2 |
3 | // TODO: Move this in the future outside the kanban component folder
4 | export const mockData: KanbanContent = {
5 | columns: [
6 | {
7 | id: 1,
8 | name: "Backglog",
9 | content: [
10 | {
11 | id: 1,
12 | title: "Create the cards",
13 | },
14 | {
15 | id: 2,
16 | title: "Place the cards in the columns",
17 | },
18 | {
19 | id: 3,
20 | title: "Implement card dragging",
21 | },
22 | {
23 | id: 4,
24 | title: "Implement drop card",
25 | },
26 | {
27 | id: 5,
28 | title: "Implement drag & drop column",
29 | },
30 | ],
31 | },
32 | {
33 | id: 2,
34 | name: "Doing",
35 | content: [
36 | {
37 | id: 6,
38 | title: "Delete a card",
39 | },
40 | ],
41 | },
42 | {
43 | id: 3,
44 | name: "Done",
45 | content: [
46 | {
47 | id: 7,
48 | title: "Create boilerplate",
49 | },
50 | {
51 | id: 8,
52 | title: "Define data model",
53 | },
54 | {
55 | id: 9,
56 | title: "Create columns",
57 | },
58 | ],
59 | },
60 | ],
61 | };
62 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/kanban/model.ts:
--------------------------------------------------------------------------------
1 | export interface CardContent {
2 | id: number;
3 | title: string;
4 | }
5 |
6 | export interface Column {
7 | id: number;
8 | name: string;
9 | content: CardContent[];
10 | }
11 |
12 | export interface KanbanContent {
13 | columns: Column[];
14 | }
15 |
16 | export const createDefaultKanbanContent = (): KanbanContent => ({
17 | columns: [],
18 | });
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/01-simple-kanban/03-drop-column/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ejemplo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@atlaskit/pragmatic-drag-and-drop": "^1.1.11",
14 | "immer": "^10.1.1",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "tiny-invariant": "^1.3.3"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^18.2.66",
21 | "@types/react-dom": "^18.2.22",
22 | "@typescript-eslint/eslint-plugin": "^7.2.0",
23 | "@typescript-eslint/parser": "^7.2.0",
24 | "@vitejs/plugin-react": "^4.2.1",
25 | "eslint": "^8.57.0",
26 | "eslint-plugin-react-hooks": "^4.6.0",
27 | "eslint-plugin-react-refresh": "^0.4.6",
28 | "typescript": "^5.2.2",
29 | "vite": "^5.2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/public/04-drop-card-01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/04-drop-card/public/04-drop-card-01.gif
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/public/04-drop-card-02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/04-drop-card/public/04-drop-card-02.gif
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | flex: 1;
4 | margin: 0 auto;
5 | padding: 2rem;
6 | text-align: center;
7 | }
8 |
9 | .logo {
10 | height: 6em;
11 | padding: 1.5em;
12 | will-change: filter;
13 | transition: filter 300ms;
14 | }
15 | .logo:hover {
16 | filter: drop-shadow(0 0 2em #646cffaa);
17 | }
18 | .logo.react:hover {
19 | filter: drop-shadow(0 0 2em #61dafbaa);
20 | }
21 |
22 | @keyframes logo-spin {
23 | from {
24 | transform: rotate(0deg);
25 | }
26 | to {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @media (prefers-reduced-motion: no-preference) {
32 | a:nth-of-type(2) .logo {
33 | animation: logo-spin infinite 20s linear;
34 | }
35 | }
36 |
37 | .card {
38 | padding: 2em;
39 | }
40 |
41 | .read-the-docs {
42 | color: #888;
43 | }
44 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { KanbanContainer } from "./kanban";
3 |
4 | function App() {
5 | return ;
6 | }
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.api";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/api/kanban.api.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "../model";
2 | import { mockData } from "../mock-data";
3 |
4 | // TODO: Move this outside kanban component folder
5 | export const loadKanbanContent = async (): Promise => {
6 | return mockData;
7 | };
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/card/card.component.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
4 | padding: 5px 15px;
5 | background-color: white;
6 | color: black;
7 | width: 210px;
8 | }
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/card/card.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { CardContent } from "../../model";
3 | import classes from "./card.component.module.css";
4 | import { useEffect, useRef, useState } from "react";
5 | import {
6 | draggable,
7 | dropTargetForElements,
8 | } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
9 | import invariant from "tiny-invariant";
10 |
11 | interface Props {
12 | columnId: number;
13 | content: CardContent;
14 | }
15 |
16 | export const Card: React.FC = (props) => {
17 | const { content, columnId } = props;
18 | const [dragging, setDragging] = useState(false);
19 | const [isDraggedOver, setIsDraggedOver] = useState(false);
20 | const ref = useRef(null);
21 |
22 | useEffect(() => {
23 | const el = ref.current;
24 | // Add this to avoid typescript in strict mode complaining about null
25 | // on draggable({ element: el }); call
26 | invariant(el);
27 |
28 | return draggable({
29 | element: el,
30 | getInitialData: () => ({ card: content }),
31 | onDragStart: () => setDragging(true),
32 | onDrop: () => setDragging(false),
33 | });
34 | }, []);
35 |
36 | useEffect(() => {
37 | const el = ref.current;
38 | invariant(el);
39 |
40 | return dropTargetForElements({
41 | element: el,
42 | getData: () => ({ columnId, cardId: content.id }),
43 | onDragEnter: () => setIsDraggedOver(true),
44 | onDragLeave: () => setIsDraggedOver(false),
45 | onDrop: () => setIsDraggedOver(false),
46 | });
47 | }, []);
48 |
49 | return (
50 |
58 | {content.title}
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/column/column.component.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | row-gap: 5px;
5 | align-items: center;
6 | width: 250px; /* TODO: relative sizes or media queries?*/
7 | height: 100vh; /* TODO: review height, shouldn't be 100vh*/
8 | overflow: hidden; /*TODO: scroll? */
9 | border: 1px solid rgb(4, 1, 19); /* TODO: Theme colors, variables, CSS API? */
10 | background-color: aliceblue;
11 | color: black;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/column/column.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classes from "./column.component.module.css";
3 | import { CardContent } from "../../model";
4 | import { Card } from "../card/";
5 | import { EmptySpaceDropZone } from "../empty-space-drop-zone.component";
6 |
7 | interface Props {
8 | columnId: number;
9 | name: string;
10 | content: CardContent[];
11 | }
12 |
13 | export const Column: React.FC = (props) => {
14 | const { columnId, name, content } = props;
15 |
16 | return (
17 |
18 |
{name}
19 | {content.map((card) => (
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./column.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/empty-space-drop-zone.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useEffect, useRef } from "react";
3 | import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4 | import invariant from "tiny-invariant";
5 |
6 | interface Props {
7 | columnId: number;
8 | }
9 |
10 | export const EmptySpaceDropZone: React.FC = (props) => {
11 | const { columnId } = props;
12 | const ref = useRef(null);
13 |
14 | useEffect(() => {
15 | const el = ref.current;
16 | invariant(el);
17 |
18 | return dropTargetForElements({
19 | element: el,
20 | getData: () => ({ columnId, cardId: -1 }),
21 | });
22 | }, []);
23 |
24 | return (
25 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card";
2 | export * from "./column";
3 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.container";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/kanban.business.ts:
--------------------------------------------------------------------------------
1 | import { CardContent, Column, KanbanContent } from "./model";
2 | import { produce } from "immer";
3 |
4 | // Esto se podría hacer más optimo
5 |
6 | const removeCardFromColumn = (
7 | card: CardContent,
8 | kanbanContent: KanbanContent
9 | ): KanbanContent => {
10 | const newColumns = kanbanContent.columns.map((column) => {
11 | const newContent = column.content.filter((c) => c.id !== card.id);
12 |
13 | return {
14 | ...column,
15 | content: newContent,
16 | };
17 | });
18 |
19 | return {
20 | ...kanbanContent,
21 | columns: newColumns,
22 | };
23 | };
24 |
25 | const dropCardAfter = (
26 | origincard: CardContent,
27 | destinationCardId: number,
28 | destinationColumn: Column
29 | ): Column => {
30 | return produce(destinationColumn, (draft) => {
31 | const index = draft.content.findIndex(
32 | (card) => card.id === destinationCardId
33 | );
34 | draft.content.splice(index, 0, origincard);
35 | });
36 | };
37 |
38 | const addCardToColumn = (
39 | card: CardContent,
40 | dropArgs: DropArgs,
41 | kanbanContent: KanbanContent
42 | ): KanbanContent => {
43 | const newColumns = kanbanContent.columns.map((column) => {
44 | if (column.id === dropArgs.columnId) {
45 | return dropCardAfter(card, dropArgs.cardId, column);
46 | }
47 | return column;
48 | });
49 |
50 | return {
51 | ...kanbanContent,
52 | columns: newColumns,
53 | };
54 | };
55 |
56 | type DropArgs = { columnId: number; cardId: number };
57 |
58 | export const moveCard = (
59 | card: CardContent,
60 | dropArgs: DropArgs,
61 | kanbanContent: KanbanContent
62 | ): KanbanContent => {
63 | const newKanbanContent = removeCardFromColumn(card, kanbanContent);
64 | return addCardToColumn(card, dropArgs, newKanbanContent);
65 | };
66 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/kanban.container.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: row;
4 | flex: 1;
5 | column-gap: 5px;
6 | min-width: 0;
7 | width: 100%;
8 | height: 100%;
9 | overflow: hidden;
10 | border: 1px solid rgb(89, 118, 10);
11 | background-color: burlywood;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/kanban.container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3 | import {
4 | CardContent,
5 | KanbanContent,
6 | createDefaultKanbanContent,
7 | } from "./model";
8 | import { loadKanbanContent } from "./api";
9 | import { Column } from "./components/column/";
10 | import classes from "./kanban.container.module.css";
11 | import { moveCard } from "./kanban.business";
12 |
13 | export const KanbanContainer: React.FC = () => {
14 | const [kanbanContent, setKanbanContent] = React.useState(
15 | createDefaultKanbanContent()
16 | );
17 |
18 | React.useEffect(() => {
19 | loadKanbanContent().then((content) => setKanbanContent(content));
20 | }, []);
21 |
22 | React.useEffect(() => {
23 | return monitorForElements({
24 | onDrop({ source, location }) {
25 | const destination = location.current.dropTargets[0];
26 | if (!destination) {
27 | // si se suelta fuera de cualquier target
28 | return;
29 | }
30 |
31 | const card = source.data.card as CardContent;
32 | const columnId = destination.data.columnId as number;
33 | const destinationCardId = destination.data.cardId as number;
34 |
35 | // También aquí nos aseguramos de que estamos trabajando con el último estado
36 | setKanbanContent((kanbanContent) =>
37 | moveCard(card, { columnId, cardId: destinationCardId }, kanbanContent)
38 | );
39 | },
40 | });
41 | }, [kanbanContent]);
42 |
43 | return (
44 |
45 | {kanbanContent.columns.map((column) => (
46 |
52 | ))}
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/mock-data.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "./model";
2 |
3 | // TODO: Move this in the future outside the kanban component folder
4 | export const mockData: KanbanContent = {
5 | columns: [
6 | {
7 | id: 1,
8 | name: "Backglog",
9 | content: [
10 | {
11 | id: 1,
12 | title: "Create the cards",
13 | },
14 | {
15 | id: 2,
16 | title: "Place the cards in the columns",
17 | },
18 | {
19 | id: 3,
20 | title: "Implement card dragging",
21 | },
22 | {
23 | id: 4,
24 | title: "Implement drop card",
25 | },
26 | {
27 | id: 5,
28 | title: "Implement drag & drop column",
29 | },
30 | ],
31 | },
32 | {
33 | id: 2,
34 | name: "Doing",
35 | content: [
36 | {
37 | id: 6,
38 | title: "Delete a card",
39 | },
40 | ],
41 | },
42 | {
43 | id: 3,
44 | name: "Done",
45 | content: [
46 | {
47 | id: 7,
48 | title: "Create boilerplate",
49 | },
50 | {
51 | id: 8,
52 | title: "Define data model",
53 | },
54 | {
55 | id: 9,
56 | title: "Create columns",
57 | },
58 | ],
59 | },
60 | ],
61 | };
62 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/kanban/model.ts:
--------------------------------------------------------------------------------
1 | export interface CardContent {
2 | id: number;
3 | title: string;
4 | }
5 |
6 | export interface Column {
7 | id: number;
8 | name: string;
9 | content: CardContent[];
10 | }
11 |
12 | export interface KanbanContent {
13 | columns: Column[];
14 | }
15 |
16 | export const createDefaultKanbanContent = (): KanbanContent => ({
17 | columns: [],
18 | });
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/01-simple-kanban/04-drop-card/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/README.md:
--------------------------------------------------------------------------------
1 | # 05 Fine tune drop
2 |
3 | We already have implemented a very basic kanban board, let's start refining it (this can take up to 80% of the development time of a project :)).
4 |
5 | Right now, when we drop the card, we're not sure where it will be dropped - on top of the card? Below it?
6 |
7 | As a first step, let's show a _ghost card_ indicating the position where it will be placed. Later on, we could consider having a top area (for dropping the card above) and a bottom area (for dropping the card below).
8 |
9 | 
10 |
11 |
12 | > If you are looking for more refined solution you can check the line separator solution in the [Pragmatic Drag And Drop Examples](https://atlassian.design/components/pragmatic-drag-and-drop/examples/), if you want us to implement this separator step by step, please open an issue and we will delighted of adding it.
13 |
14 | ## Step by step
15 |
16 | Starting from the previous example, let's copy it, install dependencies, and run the project.
17 |
18 | ```bash
19 | npm install
20 | ```
21 |
22 | ```bash
23 | npm run dev
24 | ```
25 |
26 | To display the _ghost card_, let's go to the card component and if we're in edit mode, we'll show an empty space. First, let's display a simple text:
27 |
28 | _./src/kanban/components/card/card.component.tsx_
29 |
30 | ```diff
31 | return (
32 | + <>
33 | + {(isDraggedOver) ?
34 | +
35 | + Card will be dropped here !
36 | +
37 | + : null
38 | + }
39 |
47 | {content.title}
48 |
49 | + >
50 | );
51 | ```
52 |
53 | Now we can create a more realistic ghost card. In order to do that, let's create a new component _ghost-card_ that will be responsible for displaying the _ghost card_.
54 |
55 | _./src/kanban/components/ghost-card/ghost-card.component.module.css_
56 |
57 | ```css
58 | .card {
59 | display: flex;
60 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
61 | padding: 5px 15px;
62 | background-color: gray;
63 | width: 210px;
64 | }
65 | ```
66 |
67 | _./src/kanban/components/ghost-card/ghost-card.component.tsx_
68 |
69 | ```tsx
70 | import React from "react";
71 | import classes from "./ghost-card.component.module.css";
72 |
73 | interface Props {
74 | show: boolean;
75 | }
76 |
77 | export const GhostCard: React.FC = ({ show }) => {
78 | return show ?
: null;
79 | };
80 | ```
81 |
82 | And let's replace it in our card component.
83 |
84 | _./src/kanban/components/card/card.component.tsx_
85 |
86 | ```diff
87 | + import { GhostCard } from "../ghost-card/ghost-card.component";
88 | // (...)
89 |
90 | return (
91 | <>
92 | - {isDraggedOver ? Card will be dropped here !
: null}
93 | +
94 |
102 | {content.title}
103 |
104 | >
105 | );
106 | ```
107 |
108 | Now we have to do the same for the the _empty-space-drop-zone_ (do you remember the special edge case when you drop on the bottom the column?).
109 |
110 | _./src/kanban/components/empty-space-drop-zone.component.tsx_
111 |
112 | ```diff
113 | - import { useEffect, useRef } from "react";
114 | + import { useEffect, useRef, useState } from "react";
115 | + import { GhostCard } from "./ghost-card/ghost-card.component";
116 |
117 | export const EmptySpaceDropZone: React.FC = (props) => {
118 | const { columnId } = props;
119 | const ref = useRef(null);
120 | + const [isDraggedOver, setIsDraggedOver] = useState(false);
121 |
122 | useEffect(() => {
123 | const el = ref.current;
124 | invariant(el);
125 |
126 | return dropTargetForElements({
127 | element: el,
128 | getData: () => ({ columnId, cardId: -1 }),
129 | + onDragEnter: () => setIsDraggedOver(true),
130 | + onDragLeave: () => setIsDraggedOver(false),
131 | + onDrop: () => setIsDraggedOver(false),
132 | });
133 | }, []);
134 |
135 | return (
136 | +
146 | );
147 | };
148 | ```
149 |
150 | Le'ts give a try and you will see it in action :)
151 |
152 | ```bash
153 | npm run dev
154 | ```
155 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/README_es.md:
--------------------------------------------------------------------------------
1 | # 05 Fine tune drop
2 |
3 | Ya tenemos un kanban muy básico, vamos a empezar a afinarlo (esto se puede llevar el 80% del tiempo de desarrollo de un proyecto :)).
4 |
5 | Ahora mismo cuando soltamos la carta, no estamos muy seguros de donde se va a soltar ¿Encima del card? ¿Debajo?
6 |
7 | Como primer paso vamos a mostrar una _carta fantasma_ indicando la posición en la que se va a colocar, más adelante podríamos plantear tener una zona de la carta (superior) para que la carta se coloque encima y otra zona (inferior) para que se coloque debajo.
8 |
9 | 
10 |
11 | > Si estás buscando una solución más refinada, puedes consultar la solución de separador de línea en los [Ejemplos de pragmatic drag an drop](https://atlassian.design/components/pragmatic-drag-and-drop/examples/). Si quieres que implementemos este separador paso a paso, por favor abre un issue y estaremos encantados de agregarlo.
12 |
13 | ## Paso a paso
14 |
15 | Partimos del ejemplo anterior, lo copiamos, instalamos dependencias y ejecutamos el proyecto.
16 |
17 | ```bash
18 | npm install
19 | ```
20 |
21 | ```bash
22 | npm run dev
23 | ```
24 |
25 | Para mostrar la _carta fantasma_, nos vamos al componente card y si estamos en modo edición vamos a mostrar un hueco vacío, primero mostramos un texto simple:
26 |
27 | _./src/kanban/components/card/card.component.tsx_
28 |
29 | ```diff
30 | return (
31 | + <>
32 | + {(isDraggedOver) ?
33 | +
34 | + Card will be dropped here !
35 | +
36 | + : null
37 | + }
38 |
46 | {content.title}
47 |
48 | + >
49 | );
50 | ```
51 |
52 | Ahora podemos crear un card fantasma más realista, para ello vamos a crear un nuevo componente _ghost-card_ que se encargará de mostrar la _carta fantasma_.
53 |
54 | _./src/kanban/components/ghost-card/ghost-card.component.module.css_
55 |
56 | ```css
57 | .card {
58 | display: flex;
59 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
60 | padding: 5px 15px;
61 | background-color: gray;
62 | width: 210px;
63 | }
64 | ```
65 |
66 | _./src/kanban/components/ghost-card/ghost-card.component.tsx_
67 |
68 | ```tsx
69 | import React from "react";
70 | import classes from "./ghost-card.component.module.css";
71 |
72 | interface Props {
73 | show: boolean;
74 | }
75 |
76 | export const GhostCard: React.FC = ({ show }) => {
77 | return show ?
: null;
78 | };
79 | ```
80 |
81 | Y vamos a reemplazarlo en nuestro componente card.
82 |
83 | _./src/kanban/components/card/card.component.tsx_
84 |
85 | ```diff
86 | + import { GhostCard } from "../ghost-card/ghost-card.component";
87 | // (...)
88 |
89 | return (
90 | <>
91 | - {isDraggedOver ? Card will be dropped here !
: null}
92 | +
93 |
101 | {content.title}
102 |
103 | >
104 | );
105 | ```
106 |
107 | Nos queda la parte de abajo, el _empty-space-drop-zone_
108 |
109 | _./src/kanban/components/empty-space-drop-zone/empty-space-drop-zone.component.tsx_
110 |
111 | ```diff
112 | - import { useEffect, useRef } from "react";
113 | + import { useEffect, useRef, useState } from "react";
114 | + import { GhostCard } from "./ghost-card/ghost-card.component";
115 |
116 | export const EmptySpaceDropZone: React.FC = (props) => {
117 | const { columnId } = props;
118 | const ref = useRef(null);
119 | + const [isDraggedOver, setIsDraggedOver] = useState(false);
120 |
121 | useEffect(() => {
122 | const el = ref.current;
123 | invariant(el);
124 |
125 | return dropTargetForElements({
126 | element: el,
127 | getData: () => ({ columnId, cardId: -1 }),
128 | + onDragEnter: () => setIsDraggedOver(true),
129 | + onDragLeave: () => setIsDraggedOver(false),
130 | + onDrop: () => setIsDraggedOver(false),
131 | });
132 | }, []);
133 |
134 | return (
135 | +
145 | );
146 | };
147 | ```
148 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ejemplo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@atlaskit/pragmatic-drag-and-drop": "^1.1.11",
14 | "immer": "^10.1.1",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "tiny-invariant": "^1.3.3"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^18.2.66",
21 | "@types/react-dom": "^18.2.22",
22 | "@typescript-eslint/eslint-plugin": "^7.2.0",
23 | "@typescript-eslint/parser": "^7.2.0",
24 | "@vitejs/plugin-react": "^4.2.1",
25 | "eslint": "^8.57.0",
26 | "eslint-plugin-react-hooks": "^4.6.0",
27 | "eslint-plugin-react-refresh": "^0.4.6",
28 | "typescript": "^5.2.2",
29 | "vite": "^5.2.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/public/05-fine-tune-drop.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/05-fine-tune-drop/public/05-fine-tune-drop.gif
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | flex: 1;
4 | margin: 0 auto;
5 | padding: 2rem;
6 | text-align: center;
7 | }
8 |
9 | .logo {
10 | height: 6em;
11 | padding: 1.5em;
12 | will-change: filter;
13 | transition: filter 300ms;
14 | }
15 | .logo:hover {
16 | filter: drop-shadow(0 0 2em #646cffaa);
17 | }
18 | .logo.react:hover {
19 | filter: drop-shadow(0 0 2em #61dafbaa);
20 | }
21 |
22 | @keyframes logo-spin {
23 | from {
24 | transform: rotate(0deg);
25 | }
26 | to {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | @media (prefers-reduced-motion: no-preference) {
32 | a:nth-of-type(2) .logo {
33 | animation: logo-spin infinite 20s linear;
34 | }
35 | }
36 |
37 | .card {
38 | padding: 2em;
39 | }
40 |
41 | .read-the-docs {
42 | color: #888;
43 | }
44 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { KanbanContainer } from "./kanban";
3 |
4 | function App() {
5 | return ;
6 | }
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.api";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/api/kanban.api.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "../model";
2 | import { mockData } from "../mock-data";
3 |
4 | // TODO: Move this outside kanban component folder
5 | export const loadKanbanContent = async (): Promise => {
6 | return mockData;
7 | };
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/card/card.component.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
4 | padding: 5px 15px;
5 | background-color: white;
6 | color: black;
7 | width: 210px;
8 | }
9 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/card/card.component.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { CardContent } from "../../model";
3 | import classes from "./card.component.module.css";
4 | import {
5 | draggable,
6 | dropTargetForElements,
7 | } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
8 | import invariant from "tiny-invariant";
9 | import { GhostCard } from "../ghost-card/ghost-card.component";
10 |
11 | interface Props {
12 | columnId: number;
13 | content: CardContent;
14 | }
15 |
16 | export const Card: React.FC = (props) => {
17 | const { content, columnId } = props;
18 | const [dragging, setDragging] = useState(false);
19 | const [isDraggedOver, setIsDraggedOver] = useState(false);
20 | const ref = useRef(null);
21 |
22 | useEffect(() => {
23 | const el = ref.current;
24 | // Add this to avoid typescript in strict mode complaining about null
25 | // on draggable({ element: el }); call
26 | invariant(el);
27 |
28 | return draggable({
29 | element: el,
30 | getInitialData: () => ({ card: content }),
31 | onDragStart: () => setDragging(true),
32 | onDrop: () => setDragging(false),
33 | });
34 | }, []);
35 |
36 | useEffect(() => {
37 | const el = ref.current;
38 | invariant(el);
39 |
40 | return dropTargetForElements({
41 | element: el,
42 | getData: () => ({ columnId, cardId: content.id }),
43 | onDragEnter: () => setIsDraggedOver(true),
44 | onDragLeave: () => setIsDraggedOver(false),
45 | onDrop: () => setIsDraggedOver(false),
46 | });
47 | }, []);
48 |
49 | return (
50 | <>
51 |
52 |
60 | {content.title}
61 |
62 | >
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/column/column.component.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | row-gap: 5px;
5 | align-items: center;
6 | width: 250px; /* TODO: relative sizes or media queries?*/
7 | height: 100vh; /* TODO: review height, shouldn't be 100vh*/
8 | overflow: hidden; /*TODO: scroll? */
9 | border: 1px solid rgb(4, 1, 19); /* TODO: Theme colors, variables, CSS API? */
10 | background-color: aliceblue;
11 | color: black;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/column/column.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classes from "./column.component.module.css";
3 | import { CardContent } from "../../model";
4 | import { Card } from "../card/";
5 | import { EmptySpaceDropZone } from "../empty-space-drop-zone.component";
6 |
7 | interface Props {
8 | columnId: number;
9 | name: string;
10 | content: CardContent[];
11 | }
12 |
13 | export const Column: React.FC = (props) => {
14 | const { columnId, name, content } = props;
15 |
16 | return (
17 |
18 |
{name}
19 | {content.map((card) => (
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/column/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./column.component";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/empty-space-drop-zone.component.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3 | import invariant from "tiny-invariant";
4 | import { GhostCard } from "./ghost-card/ghost-card.component";
5 |
6 | interface Props {
7 | columnId: number;
8 | }
9 |
10 | export const EmptySpaceDropZone: React.FC = (props) => {
11 | const { columnId } = props;
12 | const ref = useRef(null);
13 | const [isDraggedOver, setIsDraggedOver] = useState(false);
14 |
15 | useEffect(() => {
16 | const el = ref.current;
17 | invariant(el);
18 |
19 | return dropTargetForElements({
20 | element: el,
21 | getData: () => ({ columnId, cardId: -1 }),
22 | onDragEnter: () => setIsDraggedOver(true),
23 | onDragLeave: () => setIsDraggedOver(false),
24 | onDrop: () => setIsDraggedOver(false),
25 | });
26 | }, []);
27 |
28 | return (
29 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/ghost-card/ghost-card.component.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | display: flex;
3 | border: 1px dashed gray; /* TODO: review sizes, colors...*/
4 | padding: 5px 15px;
5 | background-color: gray;
6 | width: 210px;
7 | }
8 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/ghost-card/ghost-card.component.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classes from "./ghost-card.component.module.css";
3 |
4 | interface Props {
5 | show: boolean;
6 | }
7 |
8 | export const GhostCard: React.FC = ({ show }) => {
9 | return show ?
: null;
10 | };
11 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./card";
2 | export * from "./column";
3 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./kanban.container";
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/kanban.business.ts:
--------------------------------------------------------------------------------
1 | import { CardContent, Column, KanbanContent } from "./model";
2 | import { produce } from "immer";
3 |
4 | // Esto se podría hacer más optimo
5 |
6 | const removeCardFromColumn = (
7 | card: CardContent,
8 | kanbanContent: KanbanContent
9 | ): KanbanContent => {
10 | const newColumns = kanbanContent.columns.map((column) => {
11 | const newContent = column.content.filter((c) => c.id !== card.id);
12 |
13 | return {
14 | ...column,
15 | content: newContent,
16 | };
17 | });
18 |
19 | return {
20 | ...kanbanContent,
21 | columns: newColumns,
22 | };
23 | };
24 |
25 | const dropCardAfter = (
26 | origincard: CardContent,
27 | destinationCardId: number,
28 | destinationColumn: Column
29 | ): Column => {
30 | return produce(destinationColumn, (draft) => {
31 | const index = draft.content.findIndex(
32 | (card) => card.id === destinationCardId
33 | );
34 | draft.content.splice(index, 0, origincard);
35 | });
36 | };
37 |
38 | const addCardToColumn = (
39 | card: CardContent,
40 | dropArgs: DropArgs,
41 | kanbanContent: KanbanContent
42 | ): KanbanContent => {
43 | const newColumns = kanbanContent.columns.map((column) => {
44 | if (column.id === dropArgs.columnId) {
45 | return dropCardAfter(card, dropArgs.cardId, column);
46 | }
47 | return column;
48 | });
49 |
50 | return {
51 | ...kanbanContent,
52 | columns: newColumns,
53 | };
54 | };
55 |
56 | type DropArgs = { columnId: number; cardId: number };
57 |
58 | export const moveCard = (
59 | card: CardContent,
60 | dropArgs: DropArgs,
61 | kanbanContent: KanbanContent
62 | ): KanbanContent => {
63 | const newKanbanContent = removeCardFromColumn(card, kanbanContent);
64 | return addCardToColumn(card, dropArgs, newKanbanContent);
65 | };
66 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/kanban.container.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: row;
4 | flex: 1;
5 | column-gap: 5px;
6 | min-width: 0;
7 | width: 100%;
8 | height: 100%;
9 | overflow: hidden;
10 | border: 1px solid rgb(89, 118, 10);
11 | background-color: burlywood;
12 | }
13 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/kanban.container.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
3 | import {
4 | CardContent,
5 | KanbanContent,
6 | createDefaultKanbanContent,
7 | } from "./model";
8 | import { loadKanbanContent } from "./api";
9 | import { Column } from "./components/column/";
10 | import classes from "./kanban.container.module.css";
11 | import { moveCard } from "./kanban.business";
12 |
13 | export const KanbanContainer: React.FC = () => {
14 | const [kanbanContent, setKanbanContent] = React.useState(
15 | createDefaultKanbanContent()
16 | );
17 |
18 | React.useEffect(() => {
19 | loadKanbanContent().then((content) => setKanbanContent(content));
20 | }, []);
21 |
22 | React.useEffect(() => {
23 | return monitorForElements({
24 | onDrop({ source, location }) {
25 | const destination = location.current.dropTargets[0];
26 | if (!destination) {
27 | // si se suelta fuera de cualquier target
28 | return;
29 | }
30 |
31 | const card = source.data.card as CardContent;
32 | const columnId = destination.data.columnId as number;
33 | const destinationCardId = destination.data.cardId as number;
34 |
35 | // También aquí nos aseguramos de que estamos trabajando con el último estado
36 | setKanbanContent((kanbanContent) =>
37 | moveCard(card, { columnId, cardId: destinationCardId }, kanbanContent)
38 | );
39 | },
40 | });
41 | }, [kanbanContent]);
42 |
43 | return (
44 |
45 | {kanbanContent.columns.map((column) => (
46 |
52 | ))}
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/mock-data.ts:
--------------------------------------------------------------------------------
1 | import { KanbanContent } from "./model";
2 |
3 | // TODO: Move this in the future outside the kanban component folder
4 | export const mockData: KanbanContent = {
5 | columns: [
6 | {
7 | id: 1,
8 | name: "Backglog",
9 | content: [
10 | {
11 | id: 1,
12 | title: "Create the cards",
13 | },
14 | {
15 | id: 2,
16 | title: "Place the cards in the columns",
17 | },
18 | {
19 | id: 3,
20 | title: "Implement card dragging",
21 | },
22 | {
23 | id: 4,
24 | title: "Implement drop card",
25 | },
26 | {
27 | id: 5,
28 | title: "Implement drag & drop column",
29 | },
30 | ],
31 | },
32 | {
33 | id: 2,
34 | name: "Doing",
35 | content: [
36 | {
37 | id: 6,
38 | title: "Delete a card",
39 | },
40 | ],
41 | },
42 | {
43 | id: 3,
44 | name: "Done",
45 | content: [
46 | {
47 | id: 7,
48 | title: "Create boilerplate",
49 | },
50 | {
51 | id: 8,
52 | title: "Define data model",
53 | },
54 | {
55 | id: 9,
56 | title: "Create columns",
57 | },
58 | ],
59 | },
60 | ],
61 | };
62 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/kanban/model.ts:
--------------------------------------------------------------------------------
1 | export interface CardContent {
2 | id: number;
3 | title: string;
4 | }
5 |
6 | export interface Column {
7 | id: number;
8 | name: string;
9 | content: CardContent[];
10 | }
11 |
12 | export interface KanbanContent {
13 | columns: Column[];
14 | }
15 |
16 | export const createDefaultKanbanContent = (): KanbanContent => ({
17 | columns: [],
18 | });
19 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/01-simple-kanban/05-fine-tune-drop/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------