Skip to content

Commit

Permalink
✨ Drag and Drop with @dnd-kit
Browse files Browse the repository at this point in the history
  • Loading branch information
Bakhaw committed Dec 22, 2023
1 parent 2b56939 commit ffba83e
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 96 deletions.
62 changes: 5 additions & 57 deletions components/CustomDndContext/index.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,30 @@
import { useCallback } from "react";
import Link from "next/link";
import {
DndContext,
DragEndEvent,
DragStartEvent,
useSensor,
useSensors,
PointerSensor,
pointerWithin,
} from "@dnd-kit/core";

import useSpotify from "@/hooks/useSpotify";

import { ToastAction } from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
import DragOverlayWrapper from "@/components/DragOverlayWrapper";

interface CustomDndContextProps {
children: React.ReactNode;
}

const CustomDndContext: React.FC<CustomDndContextProps> = ({ children }) => {
const spotifyApi = useSpotify();
const { toast } = useToast();
const addTracksToPlaylist = useCallback(
(overId: string, activeId: string) =>
spotifyApi.addTracksToPlaylist(overId, [
`spotify:track:${getActiveIdFromDraggable(activeId)}`,
]),
[spotifyApi]
);

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
distance: 10,
},
})
);

function getActiveIdFromDraggable(activeId: string) {
// activeId from <Draggable /> looks like this --> closed_player:<track_id>
const id = activeId.split(":")[1];
return id;
}

function handleDragStart(event: DragStartEvent) {
const activeId = event.active.data.current?.id; // id from <Draggable />
console.log("handleDragStart", event);
}

async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;

if (!over) return;

const activeId = active.data.current?.id; // id from <Draggable />
const overId = over.data.current?.id; // id from <Droppable />

await addTracksToPlaylist(overId, activeId);
toast({
action: (
<ToastAction altText="See changes">
<Link href={`/playlist/${overId}`} className="text-xs">
See changes
</Link>
</ToastAction>
),
title: "Added to your playlist !",
duration: 2200,
});
}

return (
<DndContext
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
sensors={sensors}
>
<DndContext sensors={sensors} collisionDetection={pointerWithin}>
{children}
<DragOverlayWrapper />
</DndContext>
);
};
Expand Down
110 changes: 110 additions & 0 deletions components/DragOverlayWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import { useCallback, useState } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import {
Active,
defaultDropAnimation,
DragEndEvent,
DragOverlay,
DragStartEvent,
DropAnimation,
useDndMonitor,
} from "@dnd-kit/core";
import { snapCenterToCursor } from "@dnd-kit/modifiers";

import useSpotify from "@/hooks/useSpotify";
import useTrack from "@/hooks/useTrack";

import Cover from "@/components/Cover";

import { ToastAction } from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";

function DragOverlayWrapper() {
const spotifyApi = useSpotify();
const { toast } = useToast();

const [draggedItem, setDraggedItem] = useState<Active | null>(null);

function getIdFromDraggable(id: string) {
if (!id) return;

// id from <Draggable /> looks like this --> closed_player:<track_id>
return id.split(":")[1];
}

const track = useTrack(getIdFromDraggable(draggedItem?.data?.current?.id));

const addTracksToPlaylist = useCallback(
(overId: string, id: string) =>
spotifyApi.addTracksToPlaylist(overId, [
`spotify:track:${getIdFromDraggable(id)}`,
]),
[spotifyApi]
);

function handleDragStart(event: DragStartEvent) {
setDraggedItem(event?.active);
}

async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;

if (!over) return;

const activeId = active.data.current?.id; // id from <Draggable />
const overId = over.data.current?.id; // id from <Droppable />

await addTracksToPlaylist(overId, activeId);
toast({
action: (
<ToastAction altText="See changes">
<Link href={`/playlist/${overId}`} className="text-xs">
See changes
</Link>
</ToastAction>
),
title: "Added to your playlist !",
duration: 2200,
});
}

useDndMonitor({
onDragStart: (event) => {
handleDragStart(event);
},
onDragCancel: (event) => {},
onDragEnd: (event) => {
handleDragEnd(event);
},
});

const dropAnimation: DropAnimation = {
...defaultDropAnimation,
};

if (!draggedItem || !track) return null;

return createPortal(
<DragOverlay dropAnimation={dropAnimation} modifiers={[snapCenterToCursor]}>
<div className="flex gap-2 items-center text-sm rounded-md ring-2 ring-green-primary bg-black/80 p-2 w-max cursor-grabbing">
<Cover
alt="cover"
additionalCss="h-[36px] w-[36px]"
src={track.album.images[0].url}
/>

<div className="space-x-1">
<span>{track.artists[0].name}</span>
<span>-</span>
<span>{track.name}</span>
</div>
</div>
</DragOverlay>,
document.body
);
}

export default DragOverlayWrapper;
44 changes: 24 additions & 20 deletions components/Draggable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import { useDraggable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";

import { cn } from "@/lib/utils";

interface DraggableProps {
children: React.ReactNode;
id: string;
}

const Draggable: React.FC<DraggableProps> = ({ children, id }) => {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: `draggable-${id}`,
data: {
id,
},
});
const { attributes, listeners, setNodeRef, transform, isDragging } =
useDraggable({
id: `draggable-${id}`,
data: {
id,
},
});

const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
// opacity: 0.2,
// transform: CSS.Transform.toString(transform),
}
: undefined;

return (
<>
<div
className="hidden sm:block active:border"
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}
>
{children}
</div>

<div className="block sm:hidden">{children}</div>
</>
<div
className={cn(
"hidden sm:block border-none outline-none rounded-md",
isDragging && "bg-[#66677070] text-white"
)}
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}
>
{children}
</div>
);
};

Expand Down
9 changes: 6 additions & 3 deletions components/Droppable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classNames from "classnames";

import { useDroppable } from "@dnd-kit/core";

import { cn } from "@/lib/utils";

interface DroppableProps {
children: React.ReactNode;
id: string;
Expand All @@ -18,7 +18,10 @@ const Droppable: React.FC<DroppableProps> = ({ children, id }) => {
return (
<div
ref={setNodeRef}
className={classNames("rounded-md", isOver && "border")}
className={cn(
"rounded-md",
isOver && "ring ring-green-primary/80 scale-105 transition-all"
)}
>
{children}
</div>
Expand Down
6 changes: 3 additions & 3 deletions components/Player/CurrentTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const CurrentTrack: React.FC<CurrentTrackProps> = ({ track }) => {
return (
<div className="flex">
<Draggable id={`closed_player:${track.id}`}>
<div className="flex flex-1 justify-start items-center gap-3 h-full">
<div className="flex flex-1 justify-start items-center gap-3 h-full pr-2 rounded-md">
<div className="w-[48px]">
<Cover
alt={`${track.name} cover`}
Expand All @@ -23,10 +23,10 @@ const CurrentTrack: React.FC<CurrentTrackProps> = ({ track }) => {
</div>

<div className="display-arrowicon max-w-[50vw] md:max-w-[30vw]">
<div className="font-bold flex gap-3 transition-all hover:scale-110 sm:font-normal">
<div className="font-bold flex gap-3 transition-all hover:scale-105 sm:font-normal">
<TrackLink track={track} />
</div>
<div className="transition-all hover:scale-110 text-span text-xs">
<div className="transition-all hover:scale-105 text-span text-xs">
<ArtistLink artists={track.artists} />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions components/SideBar/Playlists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ function Playlists() {
if (!playlists) return <PlaylistsSkeleton />;

return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
{playlists.items.map((playlist) => (
<Droppable key={playlist.id} id={playlist.id}>
<Button
className="flex w-full h-full justify-start gap-0 p-0 sm:p-0 bg-transparen"
className="flex w-full h-full justify-start gap-0 p-0 sm:p-0 bg-transparent"
size="sm"
>
<Link
Expand Down
22 changes: 11 additions & 11 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";

const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
Expand Down Expand Up @@ -32,26 +32,26 @@ const buttonVariants = cva(
size: "default",
},
}
)
);

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
);
}
)
Button.displayName = "Button"
);
Button.displayName = "Button";

export { Button, buttonVariants }
export { Button, buttonVariants };
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@heroicons/react": "^2.0.18",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
Expand Down
Loading

0 comments on commit ffba83e

Please sign in to comment.