feat(ui): extract drop zone styles into reusable DropActionButton component and rename ActionDropZones to DropZones
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 51s

This commit is contained in:
Chneemann 2026-08-28 10:01:13 +02:00
parent 1a8bed2d4f
commit f3378796bb
No known key found for this signature in database
4 changed files with 201 additions and 172 deletions

View file

@ -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 (
<div className="flex items-center gap-3 animate-in fade-in duration-200">
{zones.map(
({
id,
label,
icon: Icon,
isOver,
setIsOver,
onDropAction,
activeStyle,
inactiveStyle,
}) => (
<div
key={id}
onDragOver={(e) => {
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
}`}
>
<Icon size={16} className={isOver ? "animate-bounce" : ""} />
<span>{label}</span>
</div>
),
)}
</div>
);
}

View file

@ -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 (
<div
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
className={`${baseStyles} ${stateStyles}`}
>
<Icon size={16} className={isOver ? "animate-bounce" : ""} />
<span>{label}</span>
</div>
);
}

View file

@ -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 (
<div className="flex items-center gap-3 animate-in fade-in duration-200">
{/* Edit Zone */}
<DropZoneButton
label="Drop to Edit"
icon={Pencil}
variant="primary"
isOver={activeZone === "edit"}
onDragOver={(e) => {
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 */}
<DropZoneButton
label="Drop to Delete"
icon={Trash2}
variant="danger"
isOver={activeZone === "delete"}
onDragOver={(e) => {
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);
}}
/>
</div>
);
}

View file

@ -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({
</div>
<div className="flex items-center gap-3">
<ActionDropZones
<DropZones
onTaskDelete={onTaskDelete}
onDragStateChange={setIsDraggingActive}
/>