diff --git a/web/src/app/dashboards/[id]/page.tsx b/web/src/app/dashboards/[id]/page.tsx index d70eb2c..6227430 100644 --- a/web/src/app/dashboards/[id]/page.tsx +++ b/web/src/app/dashboards/[id]/page.tsx @@ -160,6 +160,7 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string const gridRef = useRef(null); const [resizingId, setResizingId] = useState(null); const [draggingId, setDraggingId] = useState(null); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); // Resize: track mousedown → mousemove → mouseup const handleResizeStart = (e: React.MouseEvent, widgetId: string) => { @@ -235,82 +236,88 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string document.addEventListener("mouseup", onUp); }; - // Drag: free-form grid positioning - const handleDragStart = (e: React.DragEvent, widgetId: string) => { + // Mouse-based drag: mousedown on grip → mousemove → mouseup + const handleDragMouseDown = (e: React.MouseEvent, widgetId: string) => { + e.preventDefault(); + e.stopPropagation(); + if (!gridRef.current) return; setDraggingId(widgetId); - e.dataTransfer.setData("text/plain", widgetId); - e.dataTransfer.effectAllowed = "move"; - }; - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; + const widget = widgets.find((w) => w.id === widgetId); + if (!widget) return; + const unitSize = gridRef.current.offsetWidth / 12; + const startX = e.clientX; + const startY = e.clientY; + const startGridX = widget.position.x; + const startGridY = widget.position.y; - const handleGridDrop = (e: React.DragEvent) => { - e.preventDefault(); - const sourceId = e.dataTransfer.getData("text/plain"); - if (!sourceId) { setDraggingId(null); return; } - if (!gridRef.current) { setDraggingId(null); return; } + // Store offset from widget origin to mouse for visual tracking + const widgetEl = (e.target as HTMLElement).closest('[data-widget-id]') as HTMLElement; + if (widgetEl) { + const rect = widgetEl.getBoundingClientRect(); + setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top }); + } - const gridRect = gridRef.current.getBoundingClientRect(); - const unitSize = gridRect.width / 12; - // Calculate grid position from mouse coordinates - const relX = e.clientX - gridRect.left; - const relY = e.clientY - gridRect.top; - const sourceWidget = widgets.find((w) => w.id === sourceId); - if (!sourceWidget) { setDraggingId(null); return; } + const onMove = (ev: MouseEvent) => { + const dx = Math.round((ev.clientX - startX) / unitSize); + const dy = Math.round((ev.clientY - startY) / unitSize); + const newX = Math.max(0, Math.min(12 - widget.position.w, startGridX + dx)); + const newY = Math.max(0, startGridY + dy); + setWidgets((prev) => + prev.map((w) => + w.id === widgetId + ? { ...w, position: { ...w.position, x: newX, y: newY } } + : w + ) + ); + }; - // Snap to nearest grid unit, clamp to bounds - const newX = Math.max(0, Math.min(12 - sourceWidget.position.w, Math.round(relX / unitSize))); - const newY = Math.max(0, Math.round(relY / (unitSize * 0.6))); // rows are shorter + const onUp = () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + setDraggingId(null); - setWidgets((current) => { - let resolved = current.map((w) => { - if (w.id === sourceId) { - return { ...w, position: { ...w.position, x: newX, y: newY } }; - } - return { ...w, position: { ...w.position } }; - }); + setWidgets((current) => { + const updated = current.find((w) => w.id === widgetId); + if (!updated) return current; - // Push overlapping widgets down (skip the moved widget) - for (let pass = 0; pass < 10; pass++) { - let hasOverlap = false; - for (let i = 0; i < resolved.length; i++) { - for (let j = i + 1; j < resolved.length; j++) { - const a = resolved[i].position; - const b = resolved[j].position; - if (a.x + a.w > b.x && a.x < b.x + b.w && a.y + a.h > b.y && a.y < b.y + b.h) { - hasOverlap = true; - // Push the widget that wasn't just moved (or the lower one) - const moveIdx = resolved[i].id === sourceId ? j : i; - const fixedIdx = moveIdx === i ? j : i; - resolved[moveIdx] = { - ...resolved[moveIdx], - position: { ...resolved[moveIdx].position, y: resolved[fixedIdx].position.y + resolved[fixedIdx].position.h }, - }; + let resolved = current.map((w) => ({ ...w, position: { ...w.position } })); + + // Push overlapping widgets down + for (let pass = 0; pass < 10; pass++) { + let hasOverlap = false; + for (let i = 0; i < resolved.length; i++) { + for (let j = i + 1; j < resolved.length; j++) { + const a = resolved[i].position; + const b = resolved[j].position; + if (a.x + a.w > b.x && a.x < b.x + b.w && a.y + a.h > b.y && a.y < b.y + b.h) { + hasOverlap = true; + const moveIdx = resolved[i].id === widgetId ? j : i; + const fixedIdx = moveIdx === i ? j : i; + resolved[moveIdx] = { + ...resolved[moveIdx], + position: { ...resolved[moveIdx].position, y: resolved[fixedIdx].position.y + resolved[fixedIdx].position.h }, + }; + } } } + if (!hasOverlap) break; } - if (!hasOverlap) break; - } - // Persist - for (const w of resolved) { - const orig = current.find((o) => o.id === w.id); - if (orig && (orig.position.x !== w.position.x || orig.position.y !== w.position.y)) { - updateWidget(id, w.id, { position: w.position }); + // Persist changed positions + for (const w of resolved) { + const orig = current.find((o) => o.id === w.id); + if (orig && (orig.position.x !== w.position.x || orig.position.y !== w.position.y)) { + updateWidget(id, w.id, { position: w.position }); + } } - } - return resolved; - }); + return resolved; + }); + }; - setDraggingId(null); - }; - - const handleDragEnd = () => { - setDraggingId(null); + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); }; const renderWidget = (widget: DashboardWidget & { data?: WidgetData }) => { @@ -448,26 +455,24 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string
{widgets.map((widget) => (
editMode && handleDragStart(e, widget.id)} - onDragEnd={handleDragEnd} > {/* Drag handle */} {editMode && ( -
+
handleDragMouseDown(e, widget.id)} + >
)}