From 47288849319b7cec46aaf83f81845fb1ba5986d4 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 4 Jul 2025 01:30:24 +0200 Subject: [PATCH] refactor: new reusable draggable-item component and use it --- .../editor/media-panel/views/media.tsx | 80 +++------- .../editor/media-panel/views/text.tsx | 22 ++- apps/web/src/components/ui/draggable-item.tsx | 150 ++++++++++++++++++ 3 files changed, 194 insertions(+), 58 deletions(-) create mode 100644 apps/web/src/components/ui/draggable-item.tsx diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index e2c7210..50fb6fa 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -7,7 +7,6 @@ import { useTimelineStore } from "@/stores/timeline-store"; import { Image, Music, Plus, Upload, Video } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Button } from "@/components/ui/button"; import { DragOverlay } from "@/components/ui/drag-overlay"; import { @@ -24,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { DraggableMediaItem } from "@/components/ui/draggable-item"; export function MediaView() { const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); @@ -98,19 +98,6 @@ export function MediaView() { return `${min}:${sec.toString().padStart(2, "0")}`; }; - const startDrag = (e: React.DragEvent, item: MediaItem) => { - // When dragging a media item, set drag data for timeline to read - e.dataTransfer.setData( - "application/x-media-item", - JSON.stringify({ - id: item.id, - type: item.type, - name: item.name, - }) - ); - e.dataTransfer.effectAllowed = "copy"; - }; - const [filteredMediaItems, setFilteredMediaItems] = useState(mediaItems); useEffect(() => { @@ -140,7 +127,7 @@ export function MediaView() { {item.name} @@ -293,45 +280,30 @@ export function MediaView() { > {/* Render each media item as a draggable button */} {filteredMediaItems.map((item) => ( -
- - - - - - Export clips - handleRemove(e, item.id)} - > - Delete - - - -
+ + + + + + Export clips + handleRemove(e, item.id)} + > + Delete + + + ))} )} diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index 9e4f619..ab69852 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,11 +1,25 @@ -import { Card } from "@/components/ui/card"; +import { DraggableMediaItem } from "@/components/ui/draggable-item"; export function TextView() { return (
- - Default text - + + Default text +
+ } + dragData={{ + id: "default-text", + type: "text", + name: "Default text", + content: "Default text", + }} + aspectRatio={1} + className="w-24" + showLabel={false} + /> ); } diff --git a/apps/web/src/components/ui/draggable-item.tsx b/apps/web/src/components/ui/draggable-item.tsx new file mode 100644 index 0000000..c24e31d --- /dev/null +++ b/apps/web/src/components/ui/draggable-item.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { AspectRatio } from "@/components/ui/aspect-ratio"; +import { Button } from "@/components/ui/button"; +import { ReactNode, useState, useRef, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { Plus } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface DraggableMediaItemProps { + name: string; + preview: ReactNode; + dragData: Record; + onDragStart?: (e: React.DragEvent) => void; + aspectRatio?: number; + className?: string; + showPlusOnDrag?: boolean; + showLabel?: boolean; + rounded?: boolean; +} + +export function DraggableMediaItem({ + name, + preview, + dragData, + onDragStart, + aspectRatio = 16 / 9, + className = "", + showPlusOnDrag = true, + showLabel = true, + rounded = true, +}: DraggableMediaItemProps) { + const [isDragging, setIsDragging] = useState(false); + const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); + const dragRef = useRef(null); + + useEffect(() => { + if (!isDragging) return; + + const handleDragOver = (e: DragEvent) => { + setDragPosition({ x: e.clientX, y: e.clientY }); + }; + + document.addEventListener("dragover", handleDragOver); + + return () => { + document.removeEventListener("dragover", handleDragOver); + }; + }, [isDragging]); + + const handleDragStart = (e: React.DragEvent) => { + // Hide the default ghost image + const emptyImg = new Image(); + emptyImg.src = + "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + e.dataTransfer.setDragImage(emptyImg, 0, 0); + + // Set drag data + e.dataTransfer.setData( + "application/x-media-item", + JSON.stringify(dragData) + ); + e.dataTransfer.effectAllowed = "copy"; + + // Set initial position and show custom drag preview + setDragPosition({ x: e.clientX, y: e.clientY }); + setIsDragging(true); + + onDragStart?.(e); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + return ( + <> +
+ +
+ + {/* Custom drag preview */} + {isDragging && + typeof document !== "undefined" && + createPortal( +
+
+ +
+ {preview} +
+ {showPlusOnDrag && } +
+
+
, + document.body + )} + + ); +} + +function PlusButton({ className }: { className?: string }) { + return ( + + ); +}