From aa11a86f9a17ad1f62470f7375963690024e487c Mon Sep 17 00:00:00 2001 From: Andrija-Sevaljevic Date: Wed, 12 Aug 2026 09:47:48 -0600 Subject: [PATCH 1/3] initial draggable component change --- pages/index.js | 107 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/pages/index.js b/pages/index.js index f3354e1..bc3c173 100644 --- a/pages/index.js +++ b/pages/index.js @@ -26,7 +26,7 @@ import { } from "@mui/material"; import { Container } from "react-bootstrap"; import { useProblemProvider } from "../components/hooks/ProblemProvider"; -import { useEffect, memo } from "react"; +import { useEffect, memo, useState } from "react"; // CHANGED: added useState for row order import { useUnload } from "../components/eventHandlers/handleUnload"; import ShareButton from "../components/widgets/ShareButton"; import { useHandleParameters } from "../components/eventHandlers/handleParameters"; @@ -80,6 +80,62 @@ function MainPageContent() { //useUnload(problem, solver, verifier, reducer); + // ==================== CHANGED: added drag-and-drop state and handlers ==================== + const [rowOrder, setRowOrder] = useState([ + "problem", + "reduce", + "visualize", + "solve", + "verify", + ]); + + const rowMap = { + problem: , + reduce: , + visualize: ( + + ), + solve: ( + + ), + verify: , + }; + + function handleDragStart(e, key) { + e.dataTransfer.setData("text/plain", key); + e.dataTransfer.effectAllowed = "move"; + } + + function handleDragOver(e) { + e.preventDefault(); + } + + function handleDrop(e, targetKey) { + e.preventDefault(); + const draggedKey = e.dataTransfer.getData("text/plain"); + if (draggedKey === targetKey) return; + + setRowOrder((prev) => { + const next = prev.filter((k) => k !== draggedKey); + const targetIndex = next.indexOf(targetKey); + next.splice(targetIndex, 0, draggedKey); + return next; + }); + } + // ==================== END CHANGED ==================== + return ( <> @@ -97,36 +153,23 @@ function MainPageContent() { /> -
- -
-
- -
- -
- -
- -
- -
- -
- -
+ {/* ==================== CHANGED: whole row is draggable, no handle indicator ==================== */} + {rowOrder.map((key) => ( +
handleDragStart(e, key)} + onDragOver={handleDragOver} + onDrop={(e) => handleDrop(e, key)} + className="p-2 col-example" + style={{ cursor: "grab" }} + > + {rowMap[key]} +
+ ))} + {/* ==================== END CHANGED ==================== + Previously five separate hardcoded divs (Problem/ReduceTo/Visualize/Solve/Verify) + in a fixed order. Now generated from rowOrder + rowMap, each row itself draggable. */} @@ -160,4 +203,4 @@ export default function MainPage() { ); -} +} \ No newline at end of file From 8c5fafe073a37784f82257c0504ff7a26ec52d25 Mon Sep 17 00:00:00 2001 From: Andrija-Sevaljevic Date: Thu, 13 Aug 2026 08:37:55 -0600 Subject: [PATCH 2/3] Added dragging functionality to components --- components/pageblocks/ProblemRowReact.js | 67 +++++++----- components/pageblocks/ReduceToRowReact.js | 20 ++++ components/pageblocks/SolveRowReact.js | 20 ++++ components/pageblocks/VerifyRowReact.js | 20 ++++ components/pageblocks/VisualizeRowReact.js | 19 ++++ package-lock.json | 56 ++++++++++ package.json | 3 + pages/index.js | 119 +++++++++++++-------- 8 files changed, 258 insertions(+), 66 deletions(-) diff --git a/components/pageblocks/ProblemRowReact.js b/components/pageblocks/ProblemRowReact.js index 92f053f..bc62fb0 100644 --- a/components/pageblocks/ProblemRowReact.js +++ b/components/pageblocks/ProblemRowReact.js @@ -15,6 +15,8 @@ import { TextField } from '@mui/material'; import { Button, Stack, Box } from "@mui/material"; import { Folder as FolderIcon } from '@mui/icons-material'; import { Download as DownloadIcon } from '@mui/icons-material'; +import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import PopoverTooltipClick from '../widgets/PopoverTooltipClick'; import ProblemFilterMenu from '../widgets/ProblemFilterMenu'; @@ -38,7 +40,7 @@ const COMPLEXITY_CLASS_ORDER = ["P", "NPComplete", "NPHard", "NPIntermediate", " /** * Creates an accordion that has a nested autocomplete search bar, as well as an editable problem instance textbox */ -export default function ProblemRowReact({ url, problemName, setProblemName, problemNameMap, setProblemInstance }) { +export default function ProblemRowReact({ url, problemName, setProblemName, problemNameMap, setProblemInstance, dragHandleProps }) { const problemInfo = useProblemInfo(url, problemName); const { problemIndex, reductionGraph } = useProblemIndex(url); const { @@ -166,28 +168,28 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob const tip = problemName ? { - header: problemInfo.problemName ?? "", - formalDef: problemInfo.formalDefinition ?? "", - // It makes description clean - info: problemInfo.problemDefinition ?? "", - classification: [ - { label: "Complexity class", value: problemInfo.complexityClass || "Unclassified" }, - ], - // Source shown on its own line here - source: - problemInfo.source || - (Array.isArray(problemInfo.citations) ? problemInfo.citations.join("; ") : "") || - "", - // Contributors - credit: - Array.isArray(problemInfo.contributors) && problemInfo.contributors.length - ? problemInfo.contributors.join(", ") - : "", - // Popover builds Wikipedia URL - componentLink: problemInfo.problemLink || "", - sourceLink: problemInfo.sourceLink || "", - isMathDef: true, // only this file adds the flag - } + header: problemInfo.problemName ?? "", + formalDef: problemInfo.formalDefinition ?? "", + // It makes description clean + info: problemInfo.problemDefinition ?? "", + classification: [ + { label: "Complexity class", value: problemInfo.complexityClass || "Unclassified" }, + ], + // Source shown on its own line here + source: + problemInfo.source || + (Array.isArray(problemInfo.citations) ? problemInfo.citations.join("; ") : "") || + "", + // Contributors + credit: + Array.isArray(problemInfo.contributors) && problemInfo.contributors.length + ? problemInfo.contributors.join(", ") + : "", + // Popover builds Wikipedia URL + componentLink: problemInfo.problemLink || "", + sourceLink: problemInfo.sourceLink || "", + isMathDef: true, // only this file adds the flag + } : TOOLTIP; return ( @@ -216,7 +218,24 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob setSelectedSolverComplexityBuckets={setSelectedSolverComplexityBuckets} clearFilters={clearFilters} />{" "} - + + {dragHandleProps && ( + + + + )} diff --git a/components/pageblocks/ReduceToRowReact.js b/components/pageblocks/ReduceToRowReact.js index 7a8d770..e00b209 100644 --- a/components/pageblocks/ReduceToRowReact.js +++ b/components/pageblocks/ReduceToRowReact.js @@ -15,6 +15,8 @@ import 'bootstrap/dist/css/bootstrap.min.css' import { Card } from 'react-bootstrap' import { Button } from '@mui/material' import { Download as DownloadIcon } from '@mui/icons-material'; +import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import { requestReducedInstanceFromPath } from '../redux' import { useProblemInfo, useReducerInfo } from '../hooks/ProblemProvider' @@ -56,6 +58,7 @@ export default function ReduceToRowReact({ setChosenReductionType, reducedInstance, setReducedInstance, + dragHandleProps, }) { const reduceToInfo = useProblemInfo(url, chosenReduceTo); const reducerInfo = useReducerInfo(url, chosenReductionType); @@ -176,6 +179,23 @@ export default function ReduceToRowReact({ : TOOLTIP2 } > + {dragHandleProps && ( + + + + )} diff --git a/components/pageblocks/SolveRowReact.js b/components/pageblocks/SolveRowReact.js index 2fb5f6c..c115a3e 100644 --- a/components/pageblocks/SolveRowReact.js +++ b/components/pageblocks/SolveRowReact.js @@ -13,6 +13,8 @@ import { useContext } from "react"; import "bootstrap/dist/css/bootstrap.min.css"; import { Button } from "@mui/material"; import { Download as DownloadIcon } from '@mui/icons-material'; +import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import { requestSolvedInstance } from "../redux"; import PopoverTooltipClick from "../widgets/PopoverTooltipClick"; @@ -45,6 +47,7 @@ export default function SolveRowReact({ solverNameMap, problemNameMap, chosenReduceTo, + dragHandleProps, }) { const solverInfo = useSolverInfo(url, chosenSolver); @@ -114,6 +117,23 @@ export default function SolveRowReact({ }} />{" "} + {dragHandleProps && ( + + + + )} diff --git a/components/pageblocks/VerifyRowReact.js b/components/pageblocks/VerifyRowReact.js index a15fc5a..8352c6c 100644 --- a/components/pageblocks/VerifyRowReact.js +++ b/components/pageblocks/VerifyRowReact.js @@ -13,6 +13,8 @@ import { useContext, useEffect, useState } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css' import { FormControl } from 'react-bootstrap' import { Button } from '@mui/material' +import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import { requestVerifiedInstance, requestIsCertificateValid } from '../redux'; import PopoverTooltipClick from '../widgets/PopoverTooltipClick'; @@ -34,6 +36,7 @@ export default function VerifyRowReact({ setChosenVerifier, verifierOptions, verifierNameMap, + dragHandleProps, }) { const [certificate, setCertificate] = useState(""); const [verifyResult, setVerifyResult] = useState(""); @@ -98,6 +101,23 @@ export default function VerifyRowReact({ : TOOLTIP } > + {dragHandleProps && ( + + + + )} diff --git a/components/pageblocks/VisualizeRowReact.js b/components/pageblocks/VisualizeRowReact.js index 0f48268..9eae8a7 100644 --- a/components/pageblocks/VisualizeRowReact.js +++ b/components/pageblocks/VisualizeRowReact.js @@ -22,6 +22,7 @@ import { FastForward, } from "@mui/icons-material"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { DragIndicator as DragIndicatorIcon } from '@mui/icons-material'; import Link from "next/link"; // <-- IMPORTANT for Quantum button import PopoverTooltipClick from "../widgets/PopoverTooltipClick"; @@ -71,6 +72,7 @@ export default function VisualizeRowReact({ VisualizationOptions, defaultVisualizationMap, visualizationTypeMap, + dragHandleProps, }) { const visualizationInfo = useVisualizationInfo(url, chosenVisualization); @@ -380,6 +382,23 @@ export default function VisualizeRowReact({ /> + {dragHandleProps && ( + + + + )} diff --git a/package-lock.json b/package-lock.json index 2ffad47..ae8633e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.3.0", @@ -600,6 +603,59 @@ "node": ">=18" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", diff --git a/package.json b/package.json index 91f1fec..6ebb04e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "postinstall": "next telemetry disable" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.3.0", diff --git a/pages/index.js b/pages/index.js index bc3c173..ce5c18f 100644 --- a/pages/index.js +++ b/pages/index.js @@ -31,6 +31,24 @@ import { useUnload } from "../components/eventHandlers/handleUnload"; import ShareButton from "../components/widgets/ShareButton"; import { useHandleParameters } from "../components/eventHandlers/handleParameters"; +// ==================== CHANGED: dnd-kit imports ==================== +import { + DndContext, + closestCenter, + PointerSensor, + TouchSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy, + useSortable, + arrayMove, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +// ==================== END CHANGED ==================== + const SHOW_QUANTUM_VIS = false; //Flag to show a quantum circuit visualizer (sandbox feature) const ProblemRowMemo = memo(ProblemRowReact); const ReduceToRowMemo = memo(ReduceToRowReact); @@ -39,6 +57,36 @@ const SolveRowMemo = memo(SolveRowReact); const VerifyRowMemo = memo(VerifyRowReact); const reduxBaseUrl = '/api/redux/'; + +function SortableRow({ id, children }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + position: "relative", + opacity: isDragging ? 0.6 : 1, + }; + + // Pass dragHandleProps directly into child component + const childrenWithProps = React.cloneElement(children, { + dragHandleProps: { attributes, listeners }, + }); + + return ( +
+ {childrenWithProps} +
+ ); +} + /** * Generates the actual page contents * @@ -78,9 +126,6 @@ function MainPageContent() { const { problem, solver, verifier, reducer, visualization } = useProblemProvider(reduxBaseUrl); - //useUnload(problem, solver, verifier, reducer); - - // ==================== CHANGED: added drag-and-drop state and handlers ==================== const [rowOrder, setRowOrder] = useState([ "problem", "reduce", @@ -89,6 +134,12 @@ function MainPageContent() { "verify", ]); + // PointerSensor covers mouse; TouchSensor adds mobile/tablet support + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(TouchSensor) + ); + const rowMap = { problem: , reduce: , @@ -113,29 +164,18 @@ function MainPageContent() { verify: , }; - function handleDragStart(e, key) { - e.dataTransfer.setData("text/plain", key); - e.dataTransfer.effectAllowed = "move"; + // Replaces the old handleDragStart/handleDragOver/handleDrop trio. + function handleDragEnd(event) { + const { active, over } = event; + if (over && active.id !== over.id) { + setRowOrder((items) => { + const oldIndex = items.indexOf(active.id); + const newIndex = items.indexOf(over.id); + return arrayMove(items, oldIndex, newIndex); + }); + } } - function handleDragOver(e) { - e.preventDefault(); - } - - function handleDrop(e, targetKey) { - e.preventDefault(); - const draggedKey = e.dataTransfer.getData("text/plain"); - if (draggedKey === targetKey) return; - - setRowOrder((prev) => { - const next = prev.filter((k) => k !== draggedKey); - const targetIndex = next.indexOf(targetKey); - next.splice(targetIndex, 0, draggedKey); - return next; - }); - } - // ==================== END CHANGED ==================== - return ( <> @@ -152,24 +192,19 @@ function MainPageContent() { reducer={reducer} /> - - {/* ==================== CHANGED: whole row is draggable, no handle indicator ==================== */} - {rowOrder.map((key) => ( -
handleDragStart(e, key)} - onDragOver={handleDragOver} - onDrop={(e) => handleDrop(e, key)} - className="p-2 col-example" - style={{ cursor: "grab" }} - > - {rowMap[key]} -
- ))} - {/* ==================== END CHANGED ==================== - Previously five separate hardcoded divs (Problem/ReduceTo/Visualize/Solve/Verify) - in a fixed order. Now generated from rowOrder + rowMap, each row itself draggable. */} + + + {rowOrder.map((key) => ( + + {rowMap[key]} + + ))} + + From 2d300e24fe95e45936214c04c5309d1d7f2e67fd Mon Sep 17 00:00:00 2001 From: Andrija-Sevaljevic Date: Thu, 13 Aug 2026 09:51:13 -0600 Subject: [PATCH 3/3] deleted old comments --- pages/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/pages/index.js b/pages/index.js index ce5c18f..37caaf4 100644 --- a/pages/index.js +++ b/pages/index.js @@ -31,7 +31,6 @@ import { useUnload } from "../components/eventHandlers/handleUnload"; import ShareButton from "../components/widgets/ShareButton"; import { useHandleParameters } from "../components/eventHandlers/handleParameters"; -// ==================== CHANGED: dnd-kit imports ==================== import { DndContext, closestCenter, @@ -47,7 +46,6 @@ import { arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -// ==================== END CHANGED ==================== const SHOW_QUANTUM_VIS = false; //Flag to show a quantum circuit visualizer (sandbox feature) const ProblemRowMemo = memo(ProblemRowReact);