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