├── .gitignore ├── 00-chessboard ├── 00-starter-code │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 01-step-1-example.gif │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── king.png │ │ │ └── pawn.png │ │ ├── board │ │ │ ├── board.component.tsx │ │ │ ├── board.model.ts │ │ │ ├── board.module.css │ │ │ ├── board.utils.tsx │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── pieces.component.tsx │ │ │ │ ├── pieces.module.css │ │ │ │ ├── squares.component.tsx │ │ │ │ └── squares.module.css │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 01-step-1-pieces-draggable │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 01-step-1-example.gif │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── king.png │ │ │ └── pawn.png │ │ ├── board │ │ │ ├── board.component.tsx │ │ │ ├── board.model.ts │ │ │ ├── board.module.css │ │ │ ├── board.utils.tsx │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── pieces.component.tsx │ │ │ │ ├── pieces.module.css │ │ │ │ ├── squares.component.tsx │ │ │ │ └── squares.module.css │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 02-step-2-square-drop-target │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 02-step-2-example.gif │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── king.png │ │ │ └── pawn.png │ │ ├── board │ │ │ ├── board.component.tsx │ │ │ ├── board.model.ts │ │ │ ├── board.module.css │ │ │ ├── board.utils.tsx │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── pieces.component.tsx │ │ │ │ ├── pieces.module.css │ │ │ │ ├── square.component.tsx │ │ │ │ ├── square.module.css │ │ │ │ └── squares.component.tsx │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── 03-step-3-moving-the-pieces │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── 03-step-3-example.gif │ └── vite.svg │ ├── src │ ├── App.tsx │ ├── assets │ │ ├── king.png │ │ └── pawn.png │ ├── board │ │ ├── board.component.tsx │ │ ├── board.model.ts │ │ ├── board.module.css │ │ ├── board.utils.tsx │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── pieces.component.tsx │ │ │ ├── pieces.module.css │ │ │ ├── square.component.tsx │ │ │ ├── square.module.css │ │ │ └── squares.component.tsx │ │ └── index.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 01-simple-kanban ├── 00-boilerplate │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 01-board │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 01-board-01.jpg │ │ ├── 01-board-02.jpg │ │ ├── 01-board-03.jpg │ │ ├── 01-board-04.jpg │ │ ├── 01-board-05.jpg │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── kanban │ │ │ ├── api │ │ │ │ ├── index.ts │ │ │ │ └── kanban.api.ts │ │ │ ├── components │ │ │ │ ├── card │ │ │ │ │ ├── card.component.module.css │ │ │ │ │ ├── card.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── column │ │ │ │ │ ├── column.component.module.css │ │ │ │ │ ├── column.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── kanban.container.module.css │ │ │ ├── kanban.container.tsx │ │ │ ├── mock-data.ts │ │ │ └── model.ts │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 02-drag │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 02-drag-01.gif │ │ ├── 02-drag-02.gif │ │ ├── 02-drag-03.gif │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── kanban │ │ │ ├── api │ │ │ │ ├── index.ts │ │ │ │ └── kanban.api.ts │ │ │ ├── components │ │ │ │ ├── card │ │ │ │ │ ├── card.component.module.css │ │ │ │ │ ├── card.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── column │ │ │ │ │ ├── column.component.module.css │ │ │ │ │ ├── column.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── kanban.container.module.css │ │ │ ├── kanban.container.tsx │ │ │ ├── mock-data.ts │ │ │ └── model.ts │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 03-drop-column │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 03-drop-column.gif │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── kanban │ │ │ ├── api │ │ │ │ ├── index.ts │ │ │ │ └── kanban.api.ts │ │ │ ├── components │ │ │ │ ├── card │ │ │ │ │ ├── card.component.module.css │ │ │ │ │ ├── card.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── column │ │ │ │ │ ├── column.component.module.css │ │ │ │ │ ├── column.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── kanban.business.ts │ │ │ ├── kanban.container.module.css │ │ │ ├── kanban.container.tsx │ │ │ ├── mock-data.ts │ │ │ └── model.ts │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── 04-drop-card │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── 04-drop-card-01.gif │ │ ├── 04-drop-card-02.gif │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── kanban │ │ │ ├── api │ │ │ │ ├── index.ts │ │ │ │ └── kanban.api.ts │ │ │ ├── components │ │ │ │ ├── card │ │ │ │ │ ├── card.component.module.css │ │ │ │ │ ├── card.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── column │ │ │ │ │ ├── column.component.module.css │ │ │ │ │ ├── column.component.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── empty-space-drop-zone.component.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── kanban.business.ts │ │ │ ├── kanban.container.module.css │ │ │ ├── kanban.container.tsx │ │ │ ├── mock-data.ts │ │ │ └── model.ts │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── 05-fine-tune-drop │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── README_es.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── 05-fine-tune-drop.gif │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ ├── kanban │ │ ├── api │ │ │ ├── index.ts │ │ │ └── kanban.api.ts │ │ ├── components │ │ │ ├── card │ │ │ │ ├── card.component.module.css │ │ │ │ ├── card.component.tsx │ │ │ │ └── index.ts │ │ │ ├── column │ │ │ │ ├── column.component.module.css │ │ │ │ ├── column.component.tsx │ │ │ │ └── index.ts │ │ │ ├── empty-space-drop-zone.component.tsx │ │ │ ├── ghost-card │ │ │ │ ├── ghost-card.component.module.css │ │ │ │ └── ghost-card.component.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── kanban.business.ts │ │ ├── kanban.container.module.css │ │ ├── kanban.container.tsx │ │ ├── mock-data.ts │ │ └── model.ts │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── README.md └── README_ES.md /.gitignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | ======= 22 | # Dependencies 23 | node_modules/ 24 | 25 | # Output directories 26 | dist/ 27 | build/ 28 | 29 | # Vite 30 | .vite/ 31 | 32 | # Logs 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Local environment files 38 | .env 39 | .env.local 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | 44 | # Editor directories and files 45 | .vscode/ 46 | .idea/ 47 | >>>>>>> main 48 | *.suo 49 | *.ntvs* 50 | *.njsproj 51 | *.sln 52 | *.sw? 53 | <<<<<<< HEAD 54 | ======= 55 | 56 | # macOS files 57 | .DS_Store 58 | 59 | # Windows files 60 | Thumbs.db 61 | >>>>>>> main 62 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/.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 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Output directories 5 | dist/ 6 | build/ 7 | 8 | # Vite 9 | .vite/ 10 | 11 | # Logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Local environment files 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Editor directories and files 24 | .vscode/ 25 | .idea/ 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # macOS files 33 | .DS_Store 34 | 35 | # Windows files 36 | Thumbs.db 37 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/README.md: -------------------------------------------------------------------------------- 1 | # Pragmatic Drag and Drop Starter Code 2 | 3 | This project is based on the tutorial for Pragmatic Drag and Drop from Atlassian Design, which you can find [here](https://atlassian.design/components/pragmatic-drag-and-drop/tutorial). 4 | 5 | In this project: 6 | 7 | - We have removed dependencies (instead of using emotion, just use plain css). 8 | - We have structured the project in a way that is easy to understand and extend. 9 | - We have added step by step guides to reproduce the tutorial. 10 | 11 | ## Getting started 12 | 13 | Install dependencies: 14 | 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | Run the project 20 | 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | Now you can open your browser and go to `http://localhost:5173` to see the project. 26 | 27 | And starting building the next step by following the guide located in: 28 | 29 | https://github.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/blob/main/01-step-1-pieces-draggable/README.md 30 | 31 | Follow the step-by-step guides in the guides directory for each example. 32 | 33 | ## Project Structure 34 | 35 | The project is structured as follows: 36 | 37 | ``` 38 | ├── src 39 | │ ├── assets 40 | │ ├── board 41 | │ ├── App.tsx 42 | │ ├── index.css 43 | │ ├── main.tsx 44 | │ └── vite-env.d.ts 45 | ├── package.json 46 | ├── tsconfig.node.json 47 | ├── vite.config.ts 48 | └── README.md 49 | ``` 50 | 51 | Main folders: 52 | 53 | - `assets`: Contains the images used in the project. 54 | - `board`: Contains the components for the board (pieces, square, squares, chessboard). 55 | - `App.tsx`: Main component of the project. 56 | - `index.css`: Main css file (contains some generic CSS styling). 57 | - `main.tsx`: Entry point of the project (`ReactDOM.createRoot`). 58 | - `vite-env.d.ts`: Typescript file for Vite. 59 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/README_es.md: -------------------------------------------------------------------------------- 1 | # Código Inicial para Pragmatic Drag and Drop 2 | 3 | Este proyecto se basa en el tutorial para Pragmatic Drag and Drop de Atlassian Design, que puedes encontrar [aquí](https://atlassian.design/components/pragmatic-drag-and-drop/tutorial). 4 | 5 | En este proyecto: 6 | 7 | - Hemos eliminado dependencias (en lugar de usar Emotion, solo usamos CSS plano). 8 | - Hemos estructurado el proyecto de una manera que es fácil de entender y extender. 9 | - Hemos añadido guías paso a paso para reproducir el tutorial. 10 | 11 | ## Empezando 12 | 13 | Instala las dependencias: 14 | 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | Ejecuta el proyecto 20 | 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | Ahora puedes abrir tu navegador y dirigirte a `http://localhost:5173` para ver el proyecto. 26 | 27 | Y comienza a construir el siguiente paso siguiendo la guía ubicada en: 28 | 29 | https://github.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/blob/main/01-step-1-pieces-draggable/README.md 30 | 31 | Sigue las guías paso a paso en el directorio de guías para cada ejemplo. 32 | 33 | ## Estructura del Proyecto 34 | 35 | El proyecto está estructurado de la siguiente manera: 36 | 37 | ``` 38 | ├── src 39 | │ ├── assets 40 | │ ├── board 41 | │ ├── App.tsx 42 | │ ├── index.css 43 | │ ├── main.tsx 44 | │ └── vite-env.d.ts 45 | ├── package.json 46 | ├── tsconfig.node.json 47 | ├── vite.config.ts 48 | └── README.md 49 | ``` 50 | 51 | Carpetas principales: 52 | 53 | - `assets`: Contiene las imágenes usadas en el proyecto. 54 | - `board`: Contiene los componentes para el tablero (piezas, cuadrado, cuadrados, tablero de ajedrez). 55 | - `App.tsx`: Componente principal del proyecto. 56 | - `index.css`: Archivo CSS principal (contiene algunos estilos CSS genéricos). 57 | - `main.tsx`: Punto de entrada del proyecto (`ReactDOM.createRoot`). 58 | - `vite-env.d.ts`: Archivo TypeScript para Vite. 59 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-example", 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-docs": "1.0.10", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.3.3", 19 | "@types/react-dom": "^18.2.22", 20 | "@typescript-eslint/eslint-plugin": "^7.11.0", 21 | "@vitejs/plugin-react": "^4.3.0", 22 | "typescript": "^5.4.5", 23 | "vite": "^5.2.12" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/public/01-step-1-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/00-starter-code/public/01-step-1-example.gif -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Chessboard } from "./board"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/assets/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/00-starter-code/src/assets/king.png -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/assets/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/00-starter-code/src/assets/pawn.png -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/board.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord } from "./board.model"; 2 | import { renderSquares } from "./components"; 3 | import styles from "./board.module.css"; 4 | 5 | export function Chessboard() { 6 | const pieces: PieceRecord[] = [ 7 | { type: "king", location: [3, 2] }, 8 | { type: "pawn", location: [1, 6] }, 9 | ]; 10 | 11 | return
{renderSquares(pieces)}
; 12 | } 13 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/board.model.ts: -------------------------------------------------------------------------------- 1 | export type Coord = [number, number]; 2 | 3 | export type PieceRecord = { 4 | type: PieceType; 5 | location: Coord; 6 | }; 7 | 8 | export type PieceType = "king" | "pawn"; 9 | 10 | 11 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/board.module.css: -------------------------------------------------------------------------------- 1 | /* Board.module.css */ 2 | .board { 3 | display: grid; 4 | grid-template-columns: repeat(8, 1fr); 5 | grid-template-rows: repeat(8, 1fr); 6 | width: 500px; 7 | height: 500px; 8 | border: 3px solid lightgrey; 9 | } 10 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/board.utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Coord, PieceType } from "./board.model"; 3 | import { King, Pawn } from "./components"; 4 | 5 | export function isEqualCoord(c1: Coord, c2: Coord): boolean { 6 | return c1[0] === c2[0] && c1[1] === c2[1]; 7 | } 8 | 9 | export const pieceLookup: { 10 | [Key in PieceType]: () => ReactElement; 11 | } = { 12 | king: () => , 13 | pawn: () => , 14 | }; 15 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pieces.component"; 2 | export * from "./squares.component"; 3 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/components/pieces.component.tsx: -------------------------------------------------------------------------------- 1 | import king from "../../assets/king.png"; 2 | import pawn from "../../assets/pawn.png"; 3 | import styles from "./pieces.module.css"; 4 | 5 | export type PieceProps = { 6 | image: string; 7 | alt: string; 8 | }; 9 | 10 | function Piece({ image, alt }: PieceProps) { 11 | return ( 12 | {alt} 13 | ); // draggable set to false to prevent dragging of the images 14 | } 15 | 16 | export function King() { 17 | return ; 18 | } 19 | 20 | export function Pawn() { 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/components/pieces.module.css: -------------------------------------------------------------------------------- 1 | /* Piece.module.css */ 2 | .piece { 3 | width: 45px; 4 | height: 45px; 5 | padding: 4px; 6 | border-radius: 6px; 7 | box-shadow: 1px 3px 3px rgba(9, 30, 66, 0.25), 8 | 0px 0px 1px rgba(9, 30, 66, 0.31); 9 | } 10 | 11 | .piece:hover { 12 | background-color: rgba(168, 168, 168, 0.25); 13 | } 14 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/components/squares.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord, Coord } from "../board.model"; 2 | import { isEqualCoord, pieceLookup } from "../board.utils"; 3 | import styles from "./squares.module.css"; 4 | 5 | export function renderSquares(pieces: PieceRecord[]) { 6 | const squares = []; 7 | for (let row = 0; row < 8; row++) { 8 | for (let col = 0; col < 8; col++) { 9 | const squareCoord: Coord = [row, col]; 10 | 11 | const piece = pieces.find((piece) => 12 | isEqualCoord(piece.location, squareCoord) 13 | ); 14 | 15 | const isDark = (row + col) % 2 === 1; 16 | const squareClass = isDark ? styles.dark : styles.light; 17 | 18 | squares.push( 19 |
23 | {piece && pieceLookup[piece.type]()} 24 |
25 | ); 26 | } 27 | } 28 | return squares; 29 | } 30 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/components/squares.module.css: -------------------------------------------------------------------------------- 1 | /* Square.module.css */ 2 | .square { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .dark { 11 | background-color: lightgrey; 12 | } 13 | 14 | .light { 15 | background-color: white; 16 | } -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/board/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./board.component"; 2 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/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 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/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 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/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 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/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 | -------------------------------------------------------------------------------- /00-chessboard/00-starter-code/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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/.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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Output directories 5 | dist/ 6 | build/ 7 | 8 | # Vite 9 | .vite/ 10 | 11 | # Logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Local environment files 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Editor directories and files 24 | .vscode/ 25 | .idea/ 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # macOS files 33 | .DS_Store 34 | 35 | # Windows files 36 | Thumbs.db 37 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/README.md: -------------------------------------------------------------------------------- 1 | # Step 1: Making the pieces draggable 2 | 3 | The first step to make our chess board functional is to allow the pieces to be dragged around. 4 | 5 | Let's install the `pragmatic-drag-and-drop` package: 6 | 7 | ```bash 8 | npm install @atlaskit/pragmatic-drag-and-drop 9 | ``` 10 | 11 | And let's install `tiny-invariant`: 12 | 13 | ```bash 14 | npm install tiny-invariant 15 | ``` 16 | 17 | Let's run the project 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | Pragmatic drag and drop provides a draggable function that you attach to an element to enable the draggable behavior. When using React this is done in an effect: 24 | 25 | ![Demonstration of the drag attached to the element](./public/01-step-1-example.gif) 26 | 27 | _./src/board/components/pieces.component.tsx_ 28 | 29 | ```diff 30 | + import { useEffect, useRef } from "react"; 31 | + import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; 32 | + import invariant from "tiny-invariant"; 33 | import { PieceProps } from "../board.model"; 34 | import king from "../../assets/king.png"; 35 | import pawn from "../../assets/pawn.png"; 36 | import styles from "./pieces.module.css"; 37 | 38 | export type PieceProps = { 39 | image: string; 40 | alt: string; 41 | }; 42 | 43 | function Piece({ image, alt }: PieceProps) { 44 | + const ref = useRef(null); 45 | 46 | + useEffect(() => { 47 | + const el = ref.current; 48 | + // Add this to avoid typescript in strict mode complaining about null 49 | + // on draggable({ element: el }); call 50 | + invariant(el); 51 | + 52 | + return draggable({ 53 | + element: el, 54 | + }); 55 | + }, []); 56 | 57 | return ( 58 | {alt} 61 | ); // draggable set to false to prevent dragging of the images 62 | } 63 | ``` 64 | 65 | ```tsx 66 | function Piece({ image, alt }: PieceProps) { 67 | const ref = useRef(null); 68 | 69 | useEffect(() => { 70 | const el = ref.current; 71 | invariant(el); 72 | 73 | return draggable({ 74 | element: el, 75 | }); 76 | }, []); 77 | 78 | return {alt}; 79 | } 80 | ``` 81 | 82 | Although the piece can now be dragged around, it doesn't feel as though the piece is being 'picked up', as the piece stays in place while being dragged. 83 | 84 | To make the piece fade while being dragged we can use the onDragStart and onDrop arguments within draggable to set state. We can then use this state to toggle css within the style prop to reduce the opacity. 85 | 86 | _./src/board/components/pieces.component.tsx_ 87 | 88 | ```diff 89 | - import { useEffect, useRef } from "react"; 90 | + import { useEffect, useRef, useState } from "react"; 91 | 92 | function Piece({ image, alt }: PieceProps) { 93 | + const [dragging, setDragging] = useState(false); 94 | const ref = useRef(null); 95 | 96 | useEffect(() => { 97 | const el = ref.current; 98 | invariant(el); 99 | 100 | return draggable({ 101 | element: el, 102 | + onDragStart: () => setDragging(true), 103 | + onDrop: () => setDragging(false), 104 | }); 105 | }, []); 106 | 107 | - return {alt}; 108 | + return {alt}; 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/README_es.md: -------------------------------------------------------------------------------- 1 | # Paso 1: Haciendo las piezas arrastrables 2 | 3 | El primer paso para hacer que nuestro tablero de ajedrez sea funcional es permitir que las piezas se puedan arrastrar. 4 | 5 | Vamos a instalar el paquete `pragmatic-drag-and-drop`: 6 | 7 | ```bash 8 | npm install @atlaskit/pragmatic-drag-and-drop 9 | ``` 10 | 11 | También hay que instalar `tiny-invariant`: 12 | 13 | ```bash 14 | npm install tiny-invariant 15 | ``` 16 | 17 | Ejecutemos el proyecto 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | Pragmatic drag and drop proporciona una función de arrastre que se adjunta a un elemento para habilitar el comportamiento de arrastre. Al usar React, esto se hace en un useEffect: 24 | 25 | ![Demostración del arrastre adjuntado al elemento](./public/01-step-1-example.gif) 26 | 27 | _./src/board/components/pieces.component.tsx_ 28 | 29 | ```diff 30 | + import { useEffect, useRef } from "react"; 31 | + import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; 32 | + import invariant from "tiny-invariant"; 33 | import { PieceProps } from "../board.model"; 34 | import king from "../../assets/king.png"; 35 | import pawn from "../../assets/pawn.png"; 36 | import styles from "./pieces.module.css"; 37 | 38 | export type PieceProps = { 39 | image: string; 40 | alt: string; 41 | }; 42 | 43 | function Piece({ image, alt }: PieceProps) { 44 | + const ref = useRef(null); 45 | 46 | + useEffect(() => { 47 | + const el = ref.current; 48 | + // Agrega esto para evitar que TypeScript en modo estricto se queje sobre null 49 | + // en la llamada a draggable({ element: el }); 50 | + invariant(el); 51 | + 52 | + return draggable({ 53 | + element: el, 54 | + }); 55 | + }, []); 56 | 57 | return ( 58 | {alt} 61 | ); // se deja draggable establecido en false para evitar el arrastre de las imágenes 62 | } 63 | ``` 64 | 65 | ```tsx 66 | function Piece({ image, alt }: PieceProps) { 67 | const ref = useRef(null); 68 | 69 | useEffect(() => { 70 | const el = ref.current; 71 | invariant(el); 72 | 73 | return draggable({ 74 | element: el, 75 | }); 76 | }, []); 77 | 78 | return {alt}; 79 | } 80 | ``` 81 | 82 | Aunque ahora se puede arrastrar la pieza, no parece que la pieza esté siendo 'levantada', ya que la pieza se queda en su lugar mientras se arrastra. 83 | 84 | Para hacer que la pieza se desvanezca mientras se arrastra, podemos usar los argumentos onDragStart y onDrop dentro de draggable para establecer el estado. Luego podemos usar este estado para alternar el CSS dentro de la propiedad style para reducir la opacidad. 85 | 86 | _./src/board/components/pieces.component.tsx_ 87 | 88 | ```diff 89 | - import { useEffect, useRef } from "react"; 90 | + import { useEffect, useRef, useState } from "react"; 91 | 92 | function Piece({ image, alt }: PieceProps) { 93 | + const [dragging, setDragging] = useState(false); 94 | const ref = useRef(null); 95 | 96 | useEffect(() => { 97 | const el = ref.current; 98 | invariant(el); 99 | 100 | return draggable({ 101 | element: el, 102 | + onDragStart: () => setDragging(true), 103 | + onDrop: () => setDragging(false), 104 | }); 105 | }, []); 106 | 107 | - return {alt}; 108 | + return {alt}; 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-example", 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.9", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.2.0", 16 | "tiny-invariant": "^1.3.3" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.2.22", 21 | "@typescript-eslint/eslint-plugin": "^7.11.0", 22 | "@vitejs/plugin-react": "^4.3.0", 23 | "typescript": "^5.4.5", 24 | "vite": "^5.2.12" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/public/01-step-1-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/01-step-1-pieces-draggable/public/01-step-1-example.gif -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Chessboard } from "./board"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/assets/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/01-step-1-pieces-draggable/src/assets/king.png -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/assets/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/01-step-1-pieces-draggable/src/assets/pawn.png -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/board.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord } from "./board.model"; 2 | import { renderSquares } from "./components"; 3 | import styles from "./board.module.css"; 4 | 5 | export function Chessboard() { 6 | const pieces: PieceRecord[] = [ 7 | { type: "king", location: [3, 2] }, 8 | { type: "pawn", location: [1, 6] }, 9 | ]; 10 | 11 | return
{renderSquares(pieces)}
; 12 | } 13 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/board.model.ts: -------------------------------------------------------------------------------- 1 | export type Coord = [number, number]; 2 | 3 | export type PieceRecord = { 4 | type: PieceType; 5 | location: Coord; 6 | }; 7 | 8 | export type PieceType = "king" | "pawn"; 9 | 10 | export type PieceProps = { 11 | image: string; 12 | alt: string; 13 | }; 14 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/board.module.css: -------------------------------------------------------------------------------- 1 | /* Board.module.css */ 2 | .board { 3 | display: grid; 4 | grid-template-columns: repeat(8, 1fr); 5 | grid-template-rows: repeat(8, 1fr); 6 | width: 500px; 7 | height: 500px; 8 | border: 3px solid lightgrey; 9 | } 10 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/board.utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Coord, PieceType } from "./board.model"; 3 | import { King, Pawn } from "./components"; 4 | 5 | export function isEqualCoord(c1: Coord, c2: Coord): boolean { 6 | return c1[0] === c2[0] && c1[1] === c2[1]; 7 | } 8 | 9 | export const pieceLookup: { 10 | [Key in PieceType]: () => ReactElement; 11 | } = { 12 | king: () => , 13 | pawn: () => , 14 | }; 15 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pieces.component"; 2 | export * from "./squares.component"; 3 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/components/pieces.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 3 | import { PieceProps } from "../board.model"; 4 | import invariant from "tiny-invariant"; 5 | import king from "../../assets/king.png"; 6 | import pawn from "../../assets/pawn.png"; 7 | import styles from "./pieces.module.css"; 8 | 9 | function Piece({ image, alt }: PieceProps) { 10 | const [dragging, setDragging] = useState(false); 11 | const ref = useRef(null); 12 | 13 | useEffect(() => { 14 | const el = ref.current; 15 | invariant(el); 16 | 17 | return draggable({ 18 | element: el, 19 | onDragStart: () => setDragging(true), 20 | onDrop: () => setDragging(false), 21 | }); 22 | }, []); 23 | 24 | return ( 25 | {alt} 32 | ); // draggable set to false to prevent dragging of the images 33 | } 34 | 35 | export function King() { 36 | return ; 37 | } 38 | 39 | export function Pawn() { 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/components/pieces.module.css: -------------------------------------------------------------------------------- 1 | /* Piece.module.css */ 2 | .piece { 3 | width: 45px; 4 | height: 45px; 5 | padding: 4px; 6 | border-radius: 6px; 7 | box-shadow: 1px 3px 3px rgba(9, 30, 66, 0.25), 8 | 0px 0px 1px rgba(9, 30, 66, 0.31); 9 | } 10 | 11 | .piece:hover { 12 | background-color: rgba(168, 168, 168, 0.25); 13 | } 14 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/components/squares.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord, Coord } from "../board.model"; 2 | import { isEqualCoord, pieceLookup } from "../board.utils"; 3 | import styles from "./squares.module.css"; 4 | 5 | export function renderSquares(pieces: PieceRecord[]) { 6 | const squares = []; 7 | for (let row = 0; row < 8; row++) { 8 | for (let col = 0; col < 8; col++) { 9 | const squareCoord: Coord = [row, col]; 10 | 11 | const piece = pieces.find((piece) => 12 | isEqualCoord(piece.location, squareCoord) 13 | ); 14 | 15 | const isDark = (row + col) % 2 === 1; 16 | const squareClass = isDark ? styles.dark : styles.light; 17 | 18 | squares.push( 19 |
23 | {piece && pieceLookup[piece.type]()} 24 |
25 | ); 26 | } 27 | } 28 | return squares; 29 | } 30 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/components/squares.module.css: -------------------------------------------------------------------------------- 1 | /* Square.module.css */ 2 | .square { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .dark { 11 | background-color: lightgrey; 12 | } 13 | 14 | .light { 15 | background-color: white; 16 | } -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/board/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./board.component"; 2 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/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 | -------------------------------------------------------------------------------- /00-chessboard/01-step-1-pieces-draggable/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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/.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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Output directories 5 | dist/ 6 | build/ 7 | 8 | # Vite 9 | .vite/ 10 | 11 | # Logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Local environment files 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Editor directories and files 24 | .vscode/ 25 | .idea/ 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # macOS files 33 | .DS_Store 34 | 35 | # Windows files 36 | Thumbs.db 37 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-example", 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.9", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.2.0", 16 | "tiny-invariant": "^1.3.3" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.2.22", 21 | "@typescript-eslint/eslint-plugin": "^7.11.0", 22 | "@vitejs/plugin-react": "^4.3.0", 23 | "typescript": "^5.4.5", 24 | "vite": "^5.2.12" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/public/02-step-2-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/02-step-2-square-drop-target/public/02-step-2-example.gif -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Chessboard } from "./board"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/assets/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/02-step-2-square-drop-target/src/assets/king.png -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/assets/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/02-step-2-square-drop-target/src/assets/pawn.png -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/board.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord } from "./board.model"; 2 | import { renderSquares } from "./components"; 3 | import styles from "./board.module.css"; 4 | 5 | export function Chessboard() { 6 | const pieces: PieceRecord[] = [ 7 | { type: "king", location: [3, 2] }, 8 | { type: "pawn", location: [1, 6] }, 9 | ]; 10 | 11 | return
{renderSquares(pieces)}
; 12 | } 13 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/board.model.ts: -------------------------------------------------------------------------------- 1 | export type Coord = [number, number]; 2 | 3 | export type PieceRecord = { 4 | type: PieceType; 5 | location: Coord; 6 | }; 7 | 8 | export type PieceType = "king" | "pawn"; 9 | 10 | export type PieceProps = { 11 | image: string; 12 | alt: string; 13 | }; 14 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/board.module.css: -------------------------------------------------------------------------------- 1 | /* Board.module.css */ 2 | .board { 3 | display: grid; 4 | grid-template-columns: repeat(8, 1fr); 5 | grid-template-rows: repeat(8, 1fr); 6 | width: 500px; 7 | height: 500px; 8 | border: 3px solid lightgrey; 9 | } 10 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/board.utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Coord, PieceRecord, PieceType } from "./board.model"; 3 | import { King, Pawn } from "./components"; 4 | 5 | export function isEqualCoord(c1: Coord, c2: Coord): boolean { 6 | return c1[0] === c2[0] && c1[1] === c2[1]; 7 | } 8 | 9 | export const pieceLookup: { 10 | [Key in PieceType]: (props: { location: Coord }) => ReactElement; 11 | } = { 12 | king: ({ location }) => , 13 | pawn: ({ location }) => , 14 | }; 15 | 16 | export function isCoord(token: unknown): token is Coord { 17 | return ( 18 | Array.isArray(token) && 19 | token.length === 2 && 20 | token.every(val => typeof val === 'number') 21 | ); 22 | } 23 | 24 | const pieceTypes: PieceType[] = ['king', 'pawn']; 25 | 26 | export function isPieceType(value: unknown): value is PieceType { 27 | return typeof value === 'string' && pieceTypes.includes(value as PieceType); 28 | } 29 | 30 | export function canMove( 31 | start: Coord, 32 | destination: Coord, 33 | pieceType: PieceType, 34 | pieces: PieceRecord[], 35 | ) { 36 | const rowDist = Math.abs(start[0] - destination[0]); 37 | const colDist = Math.abs(start[1] - destination[1]); 38 | 39 | if (pieces.find(piece => isEqualCoord(piece.location, destination))) { 40 | return false; 41 | } 42 | 43 | switch (pieceType) { 44 | case 'king': 45 | return [0, 1].includes(rowDist) && [0, 1].includes(colDist); 46 | case 'pawn': 47 | return colDist === 0 && start[0] - destination[0] === -1; 48 | default: 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pieces.component"; 2 | export * from "./squares.component"; 3 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/pieces.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 3 | import invariant from "tiny-invariant"; 4 | import king from "../../assets/king.png"; 5 | import pawn from "../../assets/pawn.png"; 6 | import styles from "./pieces.module.css"; 7 | import { Coord } from "../board.model"; 8 | 9 | export type PieceProps = { 10 | image: string; 11 | alt: string; 12 | pieceType: string; 13 | location: Coord; 14 | }; 15 | 16 | function Piece({ image, alt, pieceType, location }: PieceProps) { 17 | const [dragging, setDragging] = useState(false); 18 | const ref = useRef(null); 19 | 20 | useEffect(() => { 21 | const el = ref.current; 22 | invariant(el); 23 | 24 | 25 | 26 | const cleanup = draggable({ 27 | element: el, 28 | getInitialData: () => ({ location, pieceType }), 29 | onDragStart: () => setDragging(true), 30 | onDrop: () => setDragging(false), 31 | }); 32 | 33 | return cleanup; 34 | }, []); 35 | 36 | return ( 37 | {alt} 44 | ); // draggable set to false to prevent dragging of the images 45 | } 46 | 47 | interface KingProps { 48 | location: Coord; 49 | } 50 | 51 | export function King({ location }: KingProps) { 52 | return ; 53 | } 54 | 55 | interface PawnProps { 56 | location: Coord; 57 | } 58 | 59 | export function Pawn({ location }: PawnProps) { 60 | return ; 61 | } 62 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/pieces.module.css: -------------------------------------------------------------------------------- 1 | /* Piece.module.css */ 2 | .piece { 3 | width: 45px; 4 | height: 45px; 5 | padding: 4px; 6 | border-radius: 6px; 7 | box-shadow: 1px 3px 3px rgba(9, 30, 66, 0.25), 8 | 0px 0px 1px rgba(9, 30, 66, 0.31); 9 | } 10 | 11 | .piece:hover { 12 | background-color: rgba(168, 168, 168, 0.25); 13 | } 14 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/square.component.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef, useState } from "react"; 2 | import invariant from "tiny-invariant"; 3 | import { Coord, PieceRecord } from "../board.model"; 4 | import styles from "./square.module.css"; 5 | import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 6 | import { canMove, isCoord, isEqualCoord, isPieceType } from "../board.utils"; 7 | 8 | type HoveredState = "idle" | "validMove" | "invalidMove"; 9 | 10 | function getColor(state: HoveredState, isDark: boolean): string { 11 | if (state === "validMove") { 12 | return "lightgreen"; 13 | } else if (state === "invalidMove") { 14 | return "pink"; 15 | } 16 | return isDark ? "lightgrey" : "white"; 17 | } 18 | 19 | interface SquareProps { 20 | location: Coord; 21 | children: ReactNode; 22 | pieces: PieceRecord[]; 23 | } 24 | 25 | export function Square({ location, children, pieces }: SquareProps) { 26 | const ref = useRef(null); 27 | const [state, setState] = useState("idle"); 28 | 29 | useEffect(() => { 30 | const el = ref.current; 31 | invariant(el); 32 | 33 | return dropTargetForElements({ 34 | element: el, 35 | canDrop: ({ source }) => { 36 | if (!isCoord(source.data.location)) { 37 | return false; 38 | } 39 | 40 | return !isEqualCoord(source.data.location, location); 41 | }, 42 | onDragEnter: ({ source }) => { 43 | // source is the piece being dragged over the drop target 44 | if ( 45 | // type guards 46 | !isCoord(source.data.location) || 47 | !isPieceType(source.data.pieceType) 48 | ) { 49 | return; 50 | } 51 | 52 | if ( 53 | canMove(source.data.location, location, source.data.pieceType, pieces) 54 | ) { 55 | setState("validMove"); 56 | } else { 57 | setState("invalidMove"); 58 | } 59 | }, 60 | onDragLeave: () => setState("idle"), 61 | onDrop: () => setState("idle"), 62 | }); 63 | }, []); 64 | 65 | const isDark = (location[0] + location[1]) % 2 === 1; 66 | 67 | return ( 68 |
73 | {children} 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/square.module.css: -------------------------------------------------------------------------------- 1 | .square { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/components/squares.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord, Coord } from "../board.model"; 2 | import { isEqualCoord, pieceLookup } from "../board.utils"; 3 | import { Square } from "./square.component"; 4 | 5 | export function renderSquares(pieces: PieceRecord[]) { 6 | const squares = []; 7 | for (let row = 0; row < 8; row++) { 8 | for (let col = 0; col < 8; col++) { 9 | const squareCoord: Coord = [row, col]; 10 | 11 | const piece = pieces.find((piece) => 12 | isEqualCoord(piece.location, squareCoord) 13 | ); 14 | 15 | squares.push( 16 | 17 | {piece && pieceLookup[piece.type]({ location: [row, col] })} 18 | 19 | ); 20 | } 21 | } 22 | return squares; 23 | } 24 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/board/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./board.component"; 2 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/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 | -------------------------------------------------------------------------------- /00-chessboard/02-step-2-square-drop-target/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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/.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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Output directories 5 | dist/ 6 | build/ 7 | 8 | # Vite 9 | .vite/ 10 | 11 | # Logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Local environment files 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Editor directories and files 24 | .vscode/ 25 | .idea/ 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # macOS files 33 | .DS_Store 34 | 35 | # Windows files 36 | Thumbs.db 37 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/README.md: -------------------------------------------------------------------------------- 1 | # Step 3: Moving the pieces 2 | 3 | Finally let's allow the pieces to move squares when dropped. To achieve this we will use a monitorForElements from Pragmatic drag and drop. 4 | 5 | Monitors allow you to observe drag and drop interactions from anywhere in your codebase. This allows them to recieve draggable and drop target data and perform operations without needing state to be passed from components. 6 | 7 | Therefore we can place a monitor within a useEffect at the top level of our chessboard and listen for when pieces are dropped into squares. 8 | 9 | To achieve this we first need to surface the location of the squares within the drop target, as we did for the draggable pieces in the previous step: 10 | 11 | ![Demonstration of the movement of the pieces](./public/03-step-3-example.gif) 12 | 13 | _./src/board/components/square.component.tsx_ 14 | 15 | ```diff 16 | const ref = useRef(null); 17 | const [state, setState] = useState("idle"); 18 | 19 | useEffect(() => { 20 | const el = ref.current; 21 | invariant(el); 22 | 23 | return dropTargetForElements({ 24 | element: el, 25 | + getData: () => ({ location }), 26 | canDrop: ({ source }) => { 27 | ``` 28 | 29 | We then add a monitor to the chessboard. Much of this logic mirrors the logic explained above for coloring squares. 30 | 31 | _./src/board.component.tsx_ 32 | 33 | ```diff 34 | + import { useEffect, useState } from "react"; 35 | + import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 36 | import { PieceRecord } from "./board.model"; 37 | import { renderSquares } from "./components"; 38 | import styles from "./board.module.css"; 39 | + import { canMove, isCoord, isEqualCoord, isPieceType } from "./board.utils"; 40 | 41 | export function Chessboard() { 42 | - const pieces: PieceRecord[] = [ 43 | - { type: "king", location: [3, 2] }, 44 | - { type: "pawn", location: [1, 6] }, 45 | - ]; 46 | + const [pieces, setPieces] = useState([ 47 | + { type: 'king', location: [3, 2] }, 48 | + { type: 'pawn', location: [1, 6] }, 49 | + ]); 50 | 51 | + useEffect(() => { 52 | + return monitorForElements({ 53 | + onDrop({ source, location }) { 54 | + const destination = location.current.dropTargets[0]; 55 | + if (!destination) { 56 | + // if dropped outside of any drop targets 57 | + return; 58 | + } 59 | + const destinationLocation = destination.data.location; 60 | + const sourceLocation = source.data.location; 61 | + const pieceType = source.data.pieceType; 62 | + 63 | + if ( 64 | + // type guarding 65 | + !isCoord(destinationLocation) || 66 | + !isCoord(sourceLocation) || 67 | + !isPieceType(pieceType) 68 | + ) { 69 | + return; 70 | + } 71 | + 72 | + const piece = pieces.find(p => 73 | + isEqualCoord(p.location, sourceLocation), 74 | + ); 75 | + const restOfPieces = pieces.filter(p => p !== piece); 76 | + 77 | + if ( 78 | + canMove(sourceLocation, destinationLocation, pieceType, pieces) && 79 | + piece !== undefined 80 | + ) { 81 | + // moving the piece! 82 | + setPieces([ 83 | + { type: piece.type, location: destinationLocation }, 84 | + ...restOfPieces, 85 | + ]); 86 | + } 87 | + }, 88 | + }); 89 | + // Adding 'pieces' as dependencies ensures the effect is re-run 90 | + // whenever the location or pieces change, keeping the drop target logic updated. 91 | + }, [pieces]); 92 | 93 | return
{renderSquares(pieces)}
; 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/README_es.md: -------------------------------------------------------------------------------- 1 | # Paso 3: Mover las piezas 2 | 3 | Finalmente, vamos a permitir que las piezas se muevan a otras casillas cuando se suelten. Para lograr esto, usaremos `monitorForElements` de Pragmatic drag and drop. 4 | 5 | Los monitores te permiten observar interacciones de arrastrar y soltar desde cualquier lugar en tu código. Esto permite recibir datos de elementos arrastrables y de los objetivos de soltado (targets) y realizar operaciones sin necesidad de pasar estado entre componentes. 6 | 7 | Por lo tanto, podemos colocar un monitor dentro de un `useEffect` en el nivel superior de nuestro tablero de ajedrez y escuchar cuando las piezas se sueltan en las casillas. 8 | 9 | Para lograr esto, primero necesitamos exponer la ubicación de las casillas dentro del objetivo de soltado, como hicimos para las piezas arrastrables en el paso anterior: 10 | 11 | ![Demostración del moviento de las piezas](./public/03-step-3-example.gif) 12 | 13 | _./src/board/components/square.component.tsx_ 14 | 15 | ```diff 16 | const ref = useRef(null); 17 | const [state, setState] = useState("idle"); 18 | 19 | useEffect(() => { 20 | const el = ref.current; 21 | invariant(el); 22 | 23 | return dropTargetForElements({ 24 | element: el, 25 | + getData: () => ({ location }), 26 | canDrop: ({ source }) => { 27 | ``` 28 | 29 | Luego añadimos un monitor al tablero de ajedrez. Gran parte de esta lógica refleja lo explicado anteriormente para colorear las casillas. 30 | 31 | _./src/board.component.tsx_ 32 | 33 | ```diff 34 | + import { useEffect, useState } from "react"; 35 | + import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 36 | import { PieceRecord } from "./board.model"; 37 | import { renderSquares } from "./components"; 38 | import styles from "./board.module.css"; 39 | + import { canMove, isCoord, isEqualCoord, isPieceType } from "./board.utils"; 40 | 41 | export function Chessboard() { 42 | - const pieces: PieceRecord[] = [ 43 | - { type: "king", location: [3, 2] }, 44 | - { type: "pawn", location: [1, 6] }, 45 | - ]; 46 | + const [pieces, setPieces] = useState([ 47 | + { type: 'king', location: [3, 2] }, 48 | + { type: 'pawn', location: [1, 6] }, 49 | + ]); 50 | 51 | + useEffect(() => { 52 | + return monitorForElements({ 53 | + onDrop({ source, location }) { 54 | + const destination = location.current.dropTargets[0]; 55 | + if (!destination) { 56 | + // si se suelta fuera de cualquier target 57 | + return; 58 | + } 59 | + const destinationLocation = destination.data.location; 60 | + const sourceLocation = source.data.location; 61 | + const pieceType = source.data.pieceType; 62 | + 63 | + if ( 64 | + // comprobaciones de tipo 65 | + !isCoord(destinationLocation) || 66 | + !isCoord(sourceLocation) || 67 | + !isPieceType(pieceType) 68 | + ) { 69 | + return; 70 | + } 71 | + 72 | + const piece = pieces.find(p => 73 | + isEqualCoord(p.location, sourceLocation), 74 | + ); 75 | + const restOfPieces = pieces.filter(p => p !== piece); 76 | + 77 | + if ( 78 | + canMove(sourceLocation, destinationLocation, pieceType, pieces) && 79 | + piece !== undefined 80 | + ) { 81 | + // moviendo la pieza 82 | + setPieces([ 83 | + { type: piece.type, location: destinationLocation }, 84 | + ...restOfPieces, 85 | + ]); 86 | + } 87 | + }, 88 | + }); 89 | + // Añadir 'pieces' como dependencias garantiza que el efecto se vuelva a ejecutar 90 | + // cada vez que la ubicación o las piezas cambien, manteniendo actualizada la lógica del target. 91 | + }, [pieces]); 92 | 93 | return
{renderSquares(pieces)}
; 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-example", 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.9", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.2.0", 16 | "tiny-invariant": "^1.3.3" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.3.3", 20 | "@types/react-dom": "^18.2.22", 21 | "@typescript-eslint/eslint-plugin": "^7.11.0", 22 | "@vitejs/plugin-react": "^4.3.0", 23 | "typescript": "^5.4.5", 24 | "vite": "^5.2.12" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/public/03-step-3-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/03-step-3-moving-the-pieces/public/03-step-3-example.gif -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Chessboard } from "./board"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/assets/king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/03-step-3-moving-the-pieces/src/assets/king.png -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/assets/pawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/00-chessboard/03-step-3-moving-the-pieces/src/assets/pawn.png -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/board.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { PieceRecord } from "./board.model"; 3 | import { renderSquares } from "./components"; 4 | import styles from "./board.module.css"; 5 | import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 6 | import { canMove, isCoord, isEqualCoord, isPieceType } from "./board.utils"; 7 | 8 | export function Chessboard() { 9 | const [pieces, setPieces] = useState([ 10 | { type: "king", location: [3, 2] }, 11 | { type: "pawn", location: [1, 6] }, 12 | ]); 13 | 14 | useEffect(() => { 15 | return monitorForElements({ 16 | onDrop({ source, location }) { 17 | const destination = location.current.dropTargets[0]; 18 | if (!destination) { 19 | // if dropped outside of any drop targets 20 | return; 21 | } 22 | const destinationLocation = destination.data.location; 23 | const sourceLocation = source.data.location; 24 | const pieceType = source.data.pieceType; 25 | 26 | if ( 27 | // type guarding 28 | !isCoord(destinationLocation) || 29 | !isCoord(sourceLocation) || 30 | !isPieceType(pieceType) 31 | ) { 32 | return; 33 | } 34 | 35 | const piece = pieces.find((p) => 36 | isEqualCoord(p.location, sourceLocation) 37 | ); 38 | const restOfPieces = pieces.filter((p) => p !== piece); 39 | 40 | if ( 41 | canMove(sourceLocation, destinationLocation, pieceType, pieces) && 42 | piece !== undefined 43 | ) { 44 | // moving the piece! 45 | setPieces([ 46 | { type: piece.type, location: destinationLocation }, 47 | ...restOfPieces, 48 | ]); 49 | } 50 | }, 51 | }); 52 | }, [pieces]); 53 | 54 | return
{renderSquares(pieces)}
; 55 | } 56 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/board.model.ts: -------------------------------------------------------------------------------- 1 | export type Coord = [number, number]; 2 | 3 | export type PieceRecord = { 4 | type: PieceType; 5 | location: Coord; 6 | }; 7 | 8 | export type PieceType = "king" | "pawn"; 9 | 10 | export type PieceProps = { 11 | image: string; 12 | alt: string; 13 | }; 14 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/board.module.css: -------------------------------------------------------------------------------- 1 | /* Board.module.css */ 2 | .board { 3 | display: grid; 4 | grid-template-columns: repeat(8, 1fr); 5 | grid-template-rows: repeat(8, 1fr); 6 | width: 500px; 7 | height: 500px; 8 | border: 3px solid lightgrey; 9 | } -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/board.utils.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { Coord, PieceRecord, PieceType } from "./board.model"; 3 | import { King, Pawn } from "./components"; 4 | 5 | export function isEqualCoord(c1: Coord, c2: Coord): boolean { 6 | return c1[0] === c2[0] && c1[1] === c2[1]; 7 | } 8 | 9 | export const pieceLookup: { 10 | [Key in PieceType]: (props: { location: Coord }) => ReactElement; 11 | } = { 12 | king: ({ location }) => , 13 | pawn: ({ location }) => , 14 | }; 15 | 16 | export function isCoord(token: unknown): token is Coord { 17 | return ( 18 | Array.isArray(token) && 19 | token.length === 2 && 20 | token.every(val => typeof val === 'number') 21 | ); 22 | } 23 | 24 | const pieceTypes: PieceType[] = ['king', 'pawn']; 25 | 26 | export function isPieceType(value: unknown): value is PieceType { 27 | return typeof value === 'string' && pieceTypes.includes(value as PieceType); 28 | } 29 | 30 | export function canMove( 31 | start: Coord, 32 | destination: Coord, 33 | pieceType: PieceType, 34 | pieces: PieceRecord[], 35 | ) { 36 | const rowDist = Math.abs(start[0] - destination[0]); 37 | const colDist = Math.abs(start[1] - destination[1]); 38 | 39 | if (pieces.find(piece => isEqualCoord(piece.location, destination))) { 40 | return false; 41 | } 42 | 43 | switch (pieceType) { 44 | case 'king': 45 | return [0, 1].includes(rowDist) && [0, 1].includes(colDist); 46 | case 'pawn': 47 | return colDist === 0 && start[0] - destination[0] === -1; 48 | default: 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pieces.component"; 2 | export * from "./squares.component"; 3 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/pieces.component.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 3 | import invariant from "tiny-invariant"; 4 | import king from "../../assets/king.png"; 5 | import pawn from "../../assets/pawn.png"; 6 | import styles from "./pieces.module.css"; 7 | import { Coord } from "../board.model"; 8 | 9 | export type PieceProps = { 10 | image: string; 11 | alt: string; 12 | pieceType: string; 13 | location: Coord; 14 | }; 15 | 16 | function Piece({ image, alt, pieceType, location }: PieceProps) { 17 | const [dragging, setDragging] = useState(false); 18 | const ref = useRef(null); 19 | 20 | useEffect(() => { 21 | const el = ref.current; 22 | invariant(el); 23 | 24 | return draggable({ 25 | element: el, 26 | getInitialData: () => ({ location, pieceType }), 27 | onDragStart: () => setDragging(true), 28 | onDrop: () => setDragging(false), 29 | }); 30 | }, []); 31 | 32 | return ( 33 | {alt} 40 | ); // draggable set to false to prevent dragging of the images 41 | } 42 | 43 | interface KingProps { 44 | location: Coord; 45 | } 46 | 47 | export function King({ location }: KingProps) { 48 | return ; 49 | } 50 | 51 | interface PawnProps { 52 | location: Coord; 53 | } 54 | 55 | export function Pawn({ location }: PawnProps) { 56 | return ; 57 | } 58 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/pieces.module.css: -------------------------------------------------------------------------------- 1 | /* Piece.module.css */ 2 | .piece { 3 | width: 45px; 4 | height: 45px; 5 | padding: 4px; 6 | border-radius: 6px; 7 | box-shadow: 1px 3px 3px rgba(9, 30, 66, 0.25), 8 | 0px 0px 1px rgba(9, 30, 66, 0.31); 9 | } 10 | 11 | .piece:hover { 12 | background-color: rgba(168, 168, 168, 0.25); 13 | } -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/square.component.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef, useState } from "react"; 2 | import invariant from "tiny-invariant"; 3 | import { Coord, PieceRecord } from "../board.model"; 4 | import styles from "./square.module.css"; 5 | import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; 6 | import { canMove, isCoord, isEqualCoord, isPieceType } from "../board.utils"; 7 | 8 | type HoveredState = "idle" | "validMove" | "invalidMove"; 9 | 10 | function getColor(state: HoveredState, isDark: boolean): string { 11 | if (state === "validMove") { 12 | return "lightgreen"; 13 | } else if (state === "invalidMove") { 14 | return "pink"; 15 | } 16 | return isDark ? "lightgrey" : "white"; 17 | } 18 | 19 | interface SquareProps { 20 | location: Coord; 21 | children: ReactNode; 22 | pieces: PieceRecord[]; 23 | } 24 | 25 | export function Square({ location, children, pieces }: SquareProps) { 26 | const ref = useRef(null); 27 | const [state, setState] = useState("idle"); 28 | 29 | useEffect(() => { 30 | const el = ref.current; 31 | invariant(el); 32 | 33 | return dropTargetForElements({ 34 | element: el, 35 | getData: () => ({ location }), 36 | canDrop: ({ source }) => { 37 | if (!isCoord(source.data.location)) { 38 | return false; 39 | } 40 | 41 | return !isEqualCoord(source.data.location, location); 42 | }, 43 | onDragEnter: ({ source }) => { 44 | // source is the piece being dragged over the drop target 45 | if ( 46 | // type guards 47 | !isCoord(source.data.location) || 48 | !isPieceType(source.data.pieceType) 49 | ) { 50 | return; 51 | } 52 | 53 | if ( 54 | canMove(source.data.location, location, source.data.pieceType, pieces) 55 | ) { 56 | setState("validMove"); 57 | } else { 58 | setState("invalidMove"); 59 | } 60 | }, 61 | onDragLeave: () => setState("idle"), 62 | onDrop: () => setState("idle"), 63 | }); 64 | // Adding 'pieces' as dependencies ensures the effect is re-run 65 | // whenever the location or pieces change, keeping the drop target logic updated. 66 | }, [pieces]); 67 | 68 | const isDark = (location[0] + location[1]) % 2 === 1; 69 | 70 | return ( 71 |
76 | {children} 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/square.module.css: -------------------------------------------------------------------------------- 1 | .square { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/components/squares.component.tsx: -------------------------------------------------------------------------------- 1 | import { PieceRecord, Coord } from "../board.model"; 2 | import { isEqualCoord, pieceLookup } from "../board.utils"; 3 | import { Square } from "./square.component"; 4 | 5 | export function renderSquares(pieces: PieceRecord[]) { 6 | const squares = []; 7 | for (let row = 0; row < 8; row++) { 8 | for (let col = 0; col < 8; col++) { 9 | const squareCoord: Coord = [row, col]; 10 | 11 | const piece = pieces.find((piece) => 12 | isEqualCoord(piece.location, squareCoord) 13 | ); 14 | 15 | squares.push( 16 | 17 | {piece && pieceLookup[piece.type]({ location: [row, col] })} 18 | 19 | ); 20 | } 21 | } 22 | return squares; 23 | } 24 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/board/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./board.component"; 2 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/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 | -------------------------------------------------------------------------------- /00-chessboard/03-step-3-moving-the-pieces/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/00-boilerplate/.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/00-boilerplate/.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/00-boilerplate/README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate 2 | 3 | This is the starting point for the simple Kanban example. 4 | 5 | To install the dependencies, run: 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | To run the example: 12 | 13 | ```bash 14 | npm run dev 15 | ``` 16 | 17 | And open the browser at [http://localhost:5173](http://localhost:5173). 18 | 19 | If the port is busy, it will use the next available port (e.g., localhost:5174). 20 | 21 | Now, open the readme in ./01-board and follow the step-by-step guide. I hope you find it useful :), if you find any errors or something that can be improved, feel free to make a PR or open an issue. 22 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/README_es.md: -------------------------------------------------------------------------------- 1 | # Boilerplate 2 | 3 | Este es el punto de partida para el ejemplo de Kanban simple. 4 | 5 | Para instalar las dependencias, ejecuta: 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | Para ejecutar el ejemplo: 12 | 13 | ```bash 14 | npm run dev 15 | ``` 16 | 17 | Y abre el navegador en [http://localhost:5173](http://localhost:5173). 18 | 19 | Si ese puerto está ocupado, se mostrará en el siguiente puerto disponible (e.g. localhost:5174). 20 | 21 | Ahora abre el readme de ./01-board y sigue la guía paso a paso, espero que te sea de utilidad :), si ves cualquier error o algo que se pueda mejorar, no dudes en hacer un PR o abrir un issue. 22 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/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 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.66", 18 | "@types/react-dom": "^18.2.22", 19 | "@typescript-eslint/eslint-plugin": "^7.2.0", 20 | "@typescript-eslint/parser": "^7.2.0", 21 | "@vitejs/plugin-react": "^4.2.1", 22 | "eslint": "^8.57.0", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.6", 25 | "typescript": "^5.2.2", 26 | "vite": "^5.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | 3 | function App() { 4 | return ( 5 |
6 |

React Boiler plate

7 |
8 | ) 9 | } 10 | 11 | export default App 12 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/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/00-boilerplate/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/00-boilerplate/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /01-simple-kanban/00-boilerplate/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/00-boilerplate/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/00-boilerplate/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/01-board/.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/01-board/.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/01-board/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.66", 18 | "@types/react-dom": "^18.2.22", 19 | "@typescript-eslint/eslint-plugin": "^7.2.0", 20 | "@typescript-eslint/parser": "^7.2.0", 21 | "@vitejs/plugin-react": "^4.2.1", 22 | "eslint": "^8.57.0", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.6", 25 | "typescript": "^5.2.2", 26 | "vite": "^5.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/01-board-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/01-board/public/01-board-01.jpg -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/01-board-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/01-board/public/01-board-02.jpg -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/01-board-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/01-board/public/01-board-03.jpg -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/01-board-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/01-board/public/01-board-04.jpg -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/01-board-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/pragmatic-drag-and-drop-tutorial-typescript/ec1cff98e582c123cf8b5920168042ad0b9691f5/01-simple-kanban/01-board/public/01-board-05.jpg -------------------------------------------------------------------------------- /01-simple-kanban/01-board/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/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/01-board/src/kanban/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kanban.api"; 2 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/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 | 5 | interface Props { 6 | content: CardContent; 7 | } 8 | 9 | export const Card: React.FC = (props) => { 10 | const { content } = props; 11 | 12 | return
{content.title}
; 13 | }; 14 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/src/kanban/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card.component"; 2 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/src/kanban/components/column/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./column.component"; 2 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/src/kanban/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card"; 2 | export * from "./column"; 3 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/src/kanban/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kanban.container"; 2 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/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 | }; -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /01-simple-kanban/01-board/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/01-board/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/01-board/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/02-drag/.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/02-drag/.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/02-drag/README.md: -------------------------------------------------------------------------------- 1 | # 02 Drag 2 | 3 | We already have a basic board, let's go for the first step, let the user drag a card. 4 | 5 | ![Dragging a card using the mouse](./public/02-drag-01.gif) 6 | 7 | ## Step by step 8 | 9 | We will use Atlassian's `Pragmatic drag and drop`, so the first action to take is install the library: 10 | 11 | ```bash 12 | npm install @atlaskit/pragmatic-drag-and-drop 13 | ``` 14 | 15 | Pragmatic drag and drop provides a drag function that attaches to an element to enable drag behavior. When using React, this is done in a `useEffect`, which we will implement: 16 | 17 | - The drag starts in the card component. 18 | - We import `draggable` from the pragmatic drag and drop library. 19 | - We use `useRef` to reference the parent div of the `card`. 20 | - We run a `useEffect` that will make a call to _Pragamatic Drag And Drop_ `draggable` function, including the reference to the parent div of the `card`. 21 | 22 | > Once we have written the code, we will fix an issue and see how `useEffect` and the cleanup function work. 23 | 24 | _./src/kanban/components/card/card.component.tsx_ 25 | 26 | ```diff 27 | import React from "react"; 28 | + import { useEffect, useRef } from "react"; 29 | + import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; 30 | import { CardContent } from "../../model"; 31 | import classes from "./card.component.module.css"; 32 | 33 | interface Props { 34 | content: CardContent; 35 | } 36 | 37 | export const Card: React.FC = (props) => { 38 | const { content } = props; 39 | 40 | + const ref = useRef(null); 41 | 42 | + useEffect(() => { 43 | + const el = ref.current; 44 | + return draggable({ 45 | + element: el, 46 | + }); 47 | + }, []); 48 | 49 | - return
{content.title}
; 50 | + return
{content.title}
; 51 | }; 52 | ``` 53 | 54 | Notice that we get an error, this is because `draggable` expects an element of type `HTMLElement` and `useRef` might have a null type (this example is using `TypeScript` _strict mode_). We know that it won't be `null`, but in strict mode, it always plays safe. To make this work, we will install the `tiny-invariant` library which checks if the object exists, and if not, throws an exception (so, for `TypeScript` we always have a non-null value). 55 | 56 | ```bash 57 | npm install tiny-invariant 58 | ``` 59 | 60 | ```diff 61 | + import invariant from "tiny-invariant"; 62 | 63 | useEffect(() => { 64 | const el = ref.current; 65 | 66 | + // Add this to prevent TypeScript in strict mode from complaining about null 67 | + // in the call to draggable({ element: el }); 68 | + invariant(el); 69 | 70 | return draggable({ 71 | element: el, 72 | }); 73 | }, []); 74 | ``` 75 | 76 | > It's worth pausing for a second and to look at the code for this small typescript library: https://github.com/alexreardon/tiny-invariant/blob/master/src/tiny-invariant.ts 77 | 78 | Let's take a moment to study the `useEffect` code, specifically the cleanup function. 79 | 80 | You might think, “Hey, does the cleanup function only run when the component unmounts?” Here’s a trick: we execute the `draggable` function (note the parentheses at the end) and it returns a cleanup function, so: 81 | 82 | - On the first render, `draggable` is executed and returns the cleanup function. 83 | - When the component unmounts, the cleanup function returned by `draggable` will be executed. 84 | 85 | To make this clearer, we could write the code like this: 86 | 87 | **Only do this for understanding, then return to the previous function** 88 | 89 | ```diff 90 | useEffect(() => { 91 | const el = ref.current; 92 | 93 | invariant(el); 94 | 95 | + const cleanup = draggable({ 96 | + element: el, 97 | + }); 98 | 99 | - return draggable({ 100 | - element: el, 101 | - }); 102 | + return cleanup; 103 | }, []); 104 | ``` 105 | 106 | Now, if we run the application, we will see that we can drag the cards. 107 | 108 | ![Dragging a card using the mouse](./public/02-drag-02.gif) 109 | 110 | But, when we drag the card, it looks a bit odd; there’s nothing indicating which card is being dragged. Let’s do something about that by playing with the opacity to show the original card that is being dragged a bit faded. 111 | 112 | _./src/kanban/components/card/card.component.tsx_ 113 | 114 | ```diff 115 | - import { useEffect, useRef } from "react"; 116 | + import { useEffect, useRef, useState } from "react"; 117 | // (...) 118 | 119 | export const Card: React.FC = (props) => { 120 | const { content } = props; 121 | + const [dragging, setDragging] = useState(false); 122 | const ref = useRef(null); 123 | 124 | useEffect(() => { 125 | const el = ref.current; 126 | 127 | invariant(el); 128 | 129 | return draggable({ 130 | element: el, 131 | + onDragStart: () => setDragging(true), 132 | + onDrop: () => setDragging(false), 133 | }); 134 | }, []); 135 | 136 | return ( 137 | -
138 | +
139 | {content.title} 140 |
141 | ); 142 | }; 143 | ``` 144 | 145 | Now you can see that it highlights. 146 | 147 | ![Dragging a card using the mouse with opacity](./public/02-drag-03.gif) 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 | ![Insert the card and indicator](./public/05-fine-tune-drop.gif) 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 | +
140 | + 141 | -
145 | +
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 | ![Insertar la carta entre otras cartas](./public/05-fine-tune-drop.gif) 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 | +
139 | + 140 | -
144 | +
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 | --------------------------------------------------------------------------------