From f3378796bbdefd010b1c73f3488bcf9c5a3cd846 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Fri, 28 Aug 2026 10:01:13 +0200 Subject: [PATCH] feat(ui): extract drop zone styles into reusable DropActionButton component and rename ActionDropZones to DropZones --- .../dashboard/header/ActionDropZones.tsx | 170 ------------------ app/(app)/dashboard/header/DropZoneButton.tsx | 84 +++++++++ app/(app)/dashboard/header/DropZones.tsx | 115 ++++++++++++ app/(app)/dashboard/header/Header.tsx | 4 +- 4 files changed, 201 insertions(+), 172 deletions(-) delete mode 100644 app/(app)/dashboard/header/ActionDropZones.tsx create mode 100644 app/(app)/dashboard/header/DropZoneButton.tsx create mode 100644 app/(app)/dashboard/header/DropZones.tsx diff --git a/app/(app)/dashboard/header/ActionDropZones.tsx b/app/(app)/dashboard/header/ActionDropZones.tsx deleted file mode 100644 index 5c09f60..0000000 --- a/app/(app)/dashboard/header/ActionDropZones.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @file app/(app)/dashboard/header/ActionDropZones.tsx - * @description Client component rendering interactive drop zones for editing and deleting tasks during drag-and-drop operations. - */ - -"use client"; - -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { Pencil, Trash2, LucideIcon } from "lucide-react"; - -/** - * Properties for the ActionDropZones component. - * - * @interface ActionDropZonesProps - * @property {(taskId: string) => void} onTaskDelete - Callback triggered when a task is dropped onto the delete zone. - * @property {(isDragging: boolean) => void} onDragStateChange - Callback notified when global drag state begins or ends. - */ -interface ActionDropZonesProps { - onTaskDelete: (taskId: string) => void; - onDragStateChange: (isDragging: boolean) => void; -} - -/** - * Configuration structure for individual drop zones (edit and delete). - * - * @interface ActionDropZoneConfig - * @property {"edit" | "delete"} id - Unique identifier for the drop zone. - * @property {string} label - Text description shown inside the drop zone. - * @property {LucideIcon} icon - Icon component displayed next to the label. - * @property {boolean} isOver - Flag indicating if a dragged element is currently hovering over the zone. - * @property {(val: boolean) => void} setIsOver - State updater for the hover flag. - * @property {(taskId: string) => void} onDropAction - Action executed when a task is dropped onto the zone. - * @property {string} activeStyle - Tailwind styling applied when hovered. - * @property {string} inactiveStyle - Tailwind styling applied when idle. - */ -interface ActionDropZoneConfig { - id: "edit" | "delete"; - label: string; - icon: LucideIcon; - isOver: boolean; - setIsOver: (val: boolean) => void; - onDropAction: (taskId: string) => void; - activeStyle: string; - inactiveStyle: string; -} - -/** - * Renders interactive drop targets for editing or deleting tasks when a drag action starts, - * conditionally displaying the edit zone only for authorized creators. - * - * @param {ActionDropZonesProps} props - The component props. - * @returns {JSX.Element | null} The rendered drop zones container or null if no drag is active. - */ -export default function ActionDropZones({ - onTaskDelete, - onDragStateChange, -}: ActionDropZonesProps) { - const router = useRouter(); - const [isDragging, setIsDragging] = useState(false); - const [, setIsCreator] = useState(false); - const [isEditOver, setIsEditOver] = useState(false); - const [isDeleteOver, setIsDeleteOver] = useState(false); - - useEffect(() => { - /** - * Handles the global dragstart window event to activate drop zones if authorized. - * - * @param {DragEvent} e - The native drag event. - */ - const handleDragStart = (e: DragEvent) => { - const allowed = e.dataTransfer?.getData("isCreator") === "true"; - if (allowed) { - setIsDragging(true); - setIsCreator(allowed); - onDragStateChange(true); - } - }; - - /** - * Handles the global dragend window event to reset drop zone states. - */ - const handleDragEnd = () => { - setIsDragging(false); - setIsCreator(false); - setIsEditOver(false); - setIsDeleteOver(false); - onDragStateChange(false); - }; - - window.addEventListener("dragstart", handleDragStart); - window.addEventListener("dragend", handleDragEnd); - - return () => { - window.removeEventListener("dragstart", handleDragStart); - window.removeEventListener("dragend", handleDragEnd); - }; - }, [onDragStateChange]); - - if (!isDragging) return null; - - const zones: ActionDropZoneConfig[] = [ - { - id: "edit" as const, - label: "Drop to Edit", - icon: Pencil, - isOver: isEditOver, - setIsOver: setIsEditOver, - onDropAction: (taskId: string) => - router.push(`/tasks?task=edit&id=${taskId}`), - activeStyle: - "bg-primary text-black border-primary scale-110 shadow-lg animate-pulse", - inactiveStyle: - "bg-primary/10 border-primary/40 text-primary scale-100 opacity-90 hover:opacity-100", - }, - { - id: "delete" as const, - label: "Drop to Delete", - icon: Trash2, - isOver: isDeleteOver, - setIsOver: setIsDeleteOver, - onDropAction: onTaskDelete, - activeStyle: - "bg-destructive border-destructive scale-110 shadow-lg animate-pulse", - inactiveStyle: - "bg-destructive/10 border-destructive/40 text-destructive scale-100 opacity-90 hover:opacity-100", - }, - ]; - - return ( -
- {zones.map( - ({ - id, - label, - icon: Icon, - isOver, - setIsOver, - onDropAction, - activeStyle, - inactiveStyle, - }) => ( -
{ - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - setIsOver(true); - }} - onDragLeave={() => setIsOver(false)} - onDrop={(e) => { - e.preventDefault(); - setIsOver(false); - const taskId = e.dataTransfer.getData("text/plain"); - if (taskId) { - onDropAction(taskId); - } - }} - className={`flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium text-sm border-2 border-dashed cursor-pointer select-none transition-all duration-300 ease-out transform ${ - isOver ? activeStyle : inactiveStyle - }`} - > - - {label} -
- ), - )} -
- ); -} diff --git a/app/(app)/dashboard/header/DropZoneButton.tsx b/app/(app)/dashboard/header/DropZoneButton.tsx new file mode 100644 index 0000000..b897b46 --- /dev/null +++ b/app/(app)/dashboard/header/DropZoneButton.tsx @@ -0,0 +1,84 @@ +/** + * @file app/(app)/dashboard/header/DropZoneButton.tsx + * @description Client component rendering an interactive drop zone target button with dynamic visual feedback for drag-and-drop operations. + */ + +"use client"; + +import { LucideIcon } from "lucide-react"; + +/** + * Properties for the DropZoneButton component. + * + * @interface DropZoneButtonProps + * @property {string} label - The text label displayed inside the drop zone. + * @property {LucideIcon} icon - The Lucide icon component to render alongside the label. + * @property {"primary" | "secondary" | "danger"} [variant="primary"] - Visual style variant of the button. + * @property {boolean} isOver - Indicates whether a dragged item is currently hovering over the drop zone. + * @property {(e: React.DragEvent) => void} onDragOver - Callback handler triggered when a drag element moves over the zone. + * @property {() => void} onDragLeave - Callback handler triggered when a drag element leaves the zone. + * @property {(e: React.DragEvent) => void} onDrop - Callback handler triggered when an item is dropped onto the zone. + */ +interface DropZoneButtonProps { + label: string; + icon: LucideIcon; + variant?: "primary" | "secondary" | "danger"; + isOver: boolean; + onDragOver: (e: React.DragEvent) => void; + onDragLeave: () => void; + onDrop: (e: React.DragEvent) => void; +} + +/** + * Renders a drop target button with dynamic hover state animations and variant-based color schemes for drag operations. + * + * @param {DropZoneButtonProps} props - The component props. + * @returns {JSX.Element} The rendered drop zone button component. + */ +export function DropZoneButton({ + label, + icon: Icon, + variant = "primary", + isOver, + onDragOver, + onDragLeave, + onDrop, +}: DropZoneButtonProps) { + const baseStyles = + "flex items-center gap-2 px-4 py-2.5 rounded-xl font-medium text-sm border-2 border-dashed cursor-pointer select-none transition-all duration-300 ease-out transform"; + + const activeStyles = "scale-110 shadow-lg animate-pulse"; + const inactiveStyles = "scale-100 opacity-90 hover:opacity-100"; + + const variants = { + primary: { + active: "bg-primary text-black border-primary", + inactive: "bg-primary/10 border-primary/40 text-primary", + }, + secondary: { + active: "bg-card text-foreground border-foreground", + inactive: + "bg-card/60 border-foreground-muted/50 text-foreground hover:border-foreground", + }, + danger: { + active: "bg-destructive text-destructive-foreground border-destructive", + inactive: "bg-destructive/10 border-destructive/40 text-destructive", + }, + }; + + const stateStyles = isOver + ? `${activeStyles} ${variants[variant].active}` + : `${inactiveStyles} ${variants[variant].inactive}`; + + return ( +
+ + {label} +
+ ); +} diff --git a/app/(app)/dashboard/header/DropZones.tsx b/app/(app)/dashboard/header/DropZones.tsx new file mode 100644 index 0000000..f2a35e7 --- /dev/null +++ b/app/(app)/dashboard/header/DropZones.tsx @@ -0,0 +1,115 @@ +/** + * @file app/(app)/dashboard/header/DropZones.tsx + * @description Client component rendering interactive drop zones for editing and deleting tasks during drag-and-drop operations. + */ + +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { Pencil, Trash2 } from "lucide-react"; +import { DropZoneButton } from "./DropZoneButton"; + +/** + * Properties for the DropZones component. + * + * @interface DropZonesProps + * @property {(taskId: string) => void} onTaskDelete - Callback function triggered when a task is dropped onto the delete zone. + * @property {(isDragging: boolean) => void} onDragStateChange - Callback function notifying parent components of global drag state changes. + */ +interface DropZonesProps { + onTaskDelete: (taskId: string) => void; + onDragStateChange: (isDragging: boolean) => void; +} + +/** + * Renders drag-and-drop zones (Edit and Delete) that appear when a user initiates a task drag operation. + * + * @param {DropZonesProps} props - The component props. + * @returns {JSX.Element | null} The rendered drop zones container or null when no active drag operation is ongoing. + */ +export default function DropZones({ + onTaskDelete, + onDragStateChange, +}: DropZonesProps) { + const router = useRouter(); + const [isDragging, setIsDragging] = useState(false); + const [activeZone, setActiveZone] = useState<"edit" | "delete" | null>(null); + + useEffect(() => { + /** + * Handles global dragstart events to detect creator task dragging and reveal drop zones. + * + * @param {DragEvent} e - The native DOM drag event object. + */ + const handleDragStart = (e: DragEvent) => { + const allowed = e.dataTransfer?.getData("isCreator") === "true"; + if (allowed) { + setIsDragging(true); + onDragStateChange(true); + } + }; + + /** + * Handles global dragend events to reset active drop zones and state. + */ + const handleDragEnd = () => { + setIsDragging(false); + setActiveZone(null); + onDragStateChange(false); + }; + + window.addEventListener("dragstart", handleDragStart); + window.addEventListener("dragend", handleDragEnd); + return () => { + window.removeEventListener("dragstart", handleDragStart); + window.removeEventListener("dragend", handleDragEnd); + }; + }, [onDragStateChange]); + + if (!isDragging) return null; + + return ( +
+ {/* Edit Zone */} + { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setActiveZone("edit"); + }} + onDragLeave={() => setActiveZone(null)} + onDrop={(e) => { + e.preventDefault(); + setActiveZone(null); + const taskId = e.dataTransfer.getData("text/plain"); + if (taskId) router.push(`/tasks?task=edit&id=${taskId}`); + }} + /> + + {/* Delete Zone */} + { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setActiveZone("delete"); + }} + onDragLeave={() => setActiveZone(null)} + onDrop={(e) => { + e.preventDefault(); + setActiveZone(null); + const taskId = e.dataTransfer.getData("text/plain"); + if (taskId) onTaskDelete(taskId); + }} + /> +
+ ); +} diff --git a/app/(app)/dashboard/header/Header.tsx b/app/(app)/dashboard/header/Header.tsx index 26545b6..3b5b812 100644 --- a/app/(app)/dashboard/header/Header.tsx +++ b/app/(app)/dashboard/header/Header.tsx @@ -8,7 +8,7 @@ import { Plus, Sparkles } from "lucide-react"; import TrashLink from "./TrashLink"; import { useState } from "react"; -import ActionDropZones from "./ActionDropZones"; +import DropZones from "./DropZones"; import { ActionButton } from "@/app/components/ui/buttons/ActionButton"; /** @@ -40,7 +40,7 @@ export default function Header({
-