├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── layout.tsx │ └── globals.css ├── lib │ └── utils.ts └── components │ ├── ui │ └── button.tsx │ ├── SortableRow.tsx │ └── List.tsx ├── next.config.mjs ├── postcss.config.mjs ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── db ├── bad-example.json └── data.json ├── package.json ├── README.md └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/react-drag-n-drop/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "@/components/List" 2 | 3 | const getData = async () => { 4 | const res = await fetch('http://localhost:3500/data') 5 | const data = await res.json() 6 | return data 7 | } 8 | 9 | export default async function Home() { 10 | const data = await getData() 11 | 12 | return ( 13 |
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /db/bad-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "artist": "The Beatles", 6 | "title": "Hey Jude" 7 | }, 8 | { 9 | "id": 2, 10 | "artist": "Neil Young", 11 | "title": "My My, Hey Hey" 12 | }, 13 | { 14 | "id": 3, 15 | "artist": "The Rolling Stones", 16 | "title": "Wild Horses" 17 | }, 18 | { 19 | "id": 4, 20 | "artist": "Led Zeppelin", 21 | "title": "Ten Years Gone" 22 | }, 23 | { 24 | "id": 5, 25 | "artist": "Triumph", 26 | "title": "Magic Power" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /db/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": 1, 5 | "artist": "The Beatles", 6 | "title": "Hey Jude", 7 | "sequence": 1 8 | }, 9 | { 10 | "id": 2, 11 | "artist": "Neil Young", 12 | "title": "My My, Hey Hey", 13 | "sequence": 2 14 | }, 15 | { 16 | "id": 3, 17 | "artist": "The Rolling Stones", 18 | "title": "Wild Horses", 19 | "sequence": 3 20 | }, 21 | { 22 | "id": 4, 23 | "artist": "Led Zeppelin", 24 | "title": "Ten Years Gone", 25 | "sequence": 4 26 | }, 27 | { 28 | "id": 5, 29 | "artist": "Triumph", 30 | "title": "Magic Power", 31 | "sequence": 5 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnd-kit-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@dnd-kit/core": "^6.1.0", 13 | "@dnd-kit/sortable": "^8.0.0", 14 | "@dnd-kit/utilities": "^3.2.2", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.1", 18 | "lucide-react": "^0.424.0", 19 | "next": "14.2.5", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "tailwind-merge": "^2.4.0", 23 | "tailwindcss-animate": "^1.0.7" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20", 27 | "@types/react": "^18", 28 | "@types/react-dom": "^18", 29 | "eslint": "^8", 30 | "eslint-config-next": "14.2.5", 31 | "postcss": "^8", 32 | "tailwindcss": "^3.4.1", 33 | "typescript": "^5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Drag and Drop: 2 | ## the missing example 3 | --- 4 | ### Author Links 5 | 6 | 👋 Hello, I'm Dave Gray. 7 | 8 | 📚 [My Courses](https://courses.davegray.codes/) 9 | 10 | ✅ [Check out my YouTube Channel with hundreds of tutorials](https://www.youtube.com/DaveGrayTeachesCode). 11 | 12 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn) 13 | 14 | 💖 [Support My Content](https://patreon.com/davegray) 15 | 16 | 🚀 Follow Me: 17 | 18 | - [Twitter](https://twitter.com/yesdavidgray) 19 | - [LinkedIn](https://www.linkedin.com/in/davidagray/) 20 | - [Blog](https://davegray.codes) 21 | 22 | --- 23 | 24 | ### Description 25 | 26 | 📺 [YouTube Video](https://youtu.be/a6lYZWN4lVA) for this repository. 27 | 28 | 📖 [Blog Article](https://www.davegray.codes/posts/missing-example-for-react-drag-n-drop) on this topic. 29 | 30 | --- 31 | 32 | ### ⚙ Usage 33 | 34 | - `npm install` 35 | - `npm run dev` 36 | - Then run the development server in a separate terminal window: 37 | ```bash 38 | npx json-server db/data.json -w -p 3500 39 | ``` 40 | --- 41 | 42 | ### 🎓 Academic Honesty 43 | 44 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiarism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/). 45 | 46 | --- 47 | 48 | ### 📚 Tutorial References 49 | 50 | - 🔗 [dndkit](https://docs.dndkit.com/) 51 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 20 14.3% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 20 14.3% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 20 14.3% 4.1%; 13 | --primary: 24 9.8% 10%; 14 | --primary-foreground: 60 9.1% 97.8%; 15 | --secondary: 60 4.8% 95.9%; 16 | --secondary-foreground: 24 9.8% 10%; 17 | --muted: 60 4.8% 95.9%; 18 | --muted-foreground: 25 5.3% 44.7%; 19 | --accent: 60 4.8% 95.9%; 20 | --accent-foreground: 24 9.8% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 60 9.1% 97.8%; 23 | --border: 20 5.9% 90%; 24 | --input: 20 5.9% 90%; 25 | --ring: 20 14.3% 4.1%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | :root { 35 | --background: 20 14.3% 4.1%; 36 | --foreground: 60 9.1% 97.8%; 37 | --card: 20 14.3% 4.1%; 38 | --card-foreground: 60 9.1% 97.8%; 39 | --popover: 20 14.3% 4.1%; 40 | --popover-foreground: 60 9.1% 97.8%; 41 | --primary: 60 9.1% 97.8%; 42 | --primary-foreground: 24 9.8% 10%; 43 | --secondary: 12 6.5% 15.1%; 44 | --secondary-foreground: 60 9.1% 97.8%; 45 | --muted: 12 6.5% 15.1%; 46 | --muted-foreground: 24 5.4% 63.9%; 47 | --accent: 12 6.5% 15.1%; 48 | --accent-foreground: 60 9.1% 97.8%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 12 6.5% 15.1%; 52 | --input: 12 6.5% 15.1%; 53 | --ring: 24 5.7% 82.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | 67 | body { 68 | @apply bg-background text-foreground; 69 | } 70 | } -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /src/components/SortableRow.tsx: -------------------------------------------------------------------------------- 1 | import { XIcon } from 'lucide-react' 2 | 3 | import { useSortable } from '@dnd-kit/sortable' 4 | import { CSS } from '@dnd-kit/utilities' 5 | 6 | import { Button } from './ui/button' 7 | 8 | import type { Item } from "./List" 9 | 10 | type Props = { 11 | item: Item, 12 | removeItem: (id: number) => void, 13 | forceDragging?: boolean, 14 | } 15 | 16 | export function SortableRow({ item, removeItem, forceDragging = false }: Props) { 17 | 18 | const { 19 | attributes, 20 | isDragging, 21 | listeners, 22 | setNodeRef, 23 | setActivatorNodeRef, 24 | transform, 25 | transition 26 | } = useSortable({ 27 | id: item.sequence, 28 | }) 29 | 30 | const parentStyles = { 31 | transform: CSS.Transform.toString(transform), 32 | transition: transition || undefined, 33 | opacity: isDragging ? "0.4" : "1", 34 | lineHeight: "4", 35 | } 36 | 37 | const draggableStyles = { 38 | cursor: isDragging || forceDragging ? "grabbing" : "grab", 39 | } 40 | 41 | return ( 42 |
46 |
47 | 48 |
49 |

{item.sequence}

50 |
51 | 52 |
58 |

59 | {item.title} 60 |

61 |

{item.artist}

62 |
63 | 64 |
65 | 73 |
74 | 75 |
76 | 77 |
78 | ) 79 | } -------------------------------------------------------------------------------- /src/components/List.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import dynamic from 'next/dynamic' 4 | import { useState } from 'react' 5 | 6 | import { 7 | closestCenter, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, TouchSensor, useSensor, 8 | useSensors 9 | } from '@dnd-kit/core' 10 | import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' 11 | 12 | import { SortableRow } from './SortableRow' 13 | 14 | const DndContextWithNoSSR = dynamic( 15 | () => import('@dnd-kit/core').then((mod) => mod.DndContext), 16 | { ssr: false } 17 | ) 18 | 19 | export type Item = { 20 | id: number, 21 | artist: string, 22 | title: string, 23 | sequence: number 24 | } 25 | 26 | type Props = { 27 | data: Item[] 28 | } 29 | 30 | export function List({ data }: Props) { 31 | const [items, setItems] = useState(data) 32 | const [activeItem, setActiveItem] = useState(undefined) 33 | 34 | // for input methods detection 35 | const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor)) 36 | 37 | const removeItem = (id: number) => { 38 | const updated = items.filter(item => item.id !== id).map((item, i) => ({ ...item, sequence: i + 1 })) 39 | 40 | setItems(updated) 41 | } 42 | 43 | // triggered when dragging starts 44 | const handleDragStart = (event: DragStartEvent) => { 45 | const { active } = event 46 | setActiveItem(items?.find(item => item.sequence === active.id)) 47 | } 48 | 49 | const handleDragEnd = (event: DragEndEvent) => { 50 | const { active, over } = event 51 | 52 | if (!over) return 53 | 54 | const activeItem = items.find(ex => ex.sequence === active.id) 55 | const overItem = items.find(ex => ex.sequence === over.id) 56 | 57 | if (!activeItem || !overItem) { 58 | return 59 | } 60 | 61 | const activeIndex = items.findIndex(ex => ex.sequence === active.id) 62 | const overIndex = items.findIndex(ex => ex.sequence === over.id) 63 | 64 | if (activeIndex !== overIndex) { 65 | setItems(prev => { 66 | const updated = arrayMove(prev, activeIndex, overIndex).map((ex, i) => ({ ...ex, sequence: i + 1 })) 67 | 68 | return updated 69 | }) 70 | } 71 | setActiveItem(undefined) 72 | } 73 | 74 | const handleDragCancel = () => { 75 | setActiveItem(undefined) 76 | } 77 | 78 | return ( 79 |
80 | {items?.length ? ( 81 | 88 | item.sequence)} 90 | strategy={verticalListSortingStrategy} 91 | > 92 | {items.map(item => ( 93 | 98 | ))} 99 | 100 | 101 | 102 | {activeItem ? ( 103 | 108 | ) : null} 109 | 110 | 111 | ) : null} 112 |
113 | ) 114 | } --------------------------------------------------------------------------------