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 { Plus, Sparkles } from "lucide-react";
|
||||||
import TrashLink from "./TrashLink";
|
import TrashLink from "./TrashLink";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ActionDropZones from "./ActionDropZones";
|
import DropZones from "./DropZones";
|
||||||
import { ActionButton } from "@/app/components/ui/buttons/ActionButton";
|
import { ActionButton } from "@/app/components/ui/buttons/ActionButton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,7 +40,7 @@ export default function Header({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<ActionDropZones
|
<DropZones
|
||||||
onTaskDelete={onTaskDelete}
|
onTaskDelete={onTaskDelete}
|
||||||
onDragStateChange={setIsDraggingActive}
|
onDragStateChange={setIsDraggingActive}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue