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
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 51s
This commit is contained in:
parent
1a8bed2d4f
commit
f3378796bb
4 changed files with 201 additions and 172 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
84
app/(app)/dashboard/header/DropZoneButton.tsx
Normal file
84
app/(app)/dashboard/header/DropZoneButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
115
app/(app)/dashboard/header/DropZones.tsx
Normal file
115
app/(app)/dashboard/header/DropZones.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue