Drag & Drop exercise template (#158)

* Started the drag drop functionality

* Fixed a bug

* Deleted old ui boxes

* Hovering over the box doesn't change the title

* The dustbin shows all the placed items instead of the last dropped

* Changed styling to globals.css

* Wordbox doesn't move after it is already placed in a dustbin

* Added wordboxes. Moved itemtypes under exercises-dragdrop

* Made a reset button

* Refactoring code based on comments
This commit is contained in:
Salla Rautiainen
2025-03-13 14:07:58 +02:00
committed by GitHub
parent 000d0f2e28
commit a85a85dd65
10 changed files with 203 additions and 38 deletions
+19 -5
View File
@@ -13,6 +13,7 @@
"@radix-ui/themes": "^3.2.0",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"immutability-helper": "^3.1.1",
"jose": "^5.10.0",
"next": "15.1.5",
"pg": "^8.13.1",
@@ -21,6 +22,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.0.0",
"react-frame-component": "^5.2.7",
"zod": "^3.24.2"
},
"devDependencies": {
@@ -6367,6 +6369,12 @@
"node": ">= 4"
}
},
"node_modules/immutability-helper": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
"integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -6869,7 +6877,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -7060,7 +7067,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -7294,7 +7300,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7792,7 +7797,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -7804,7 +7808,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/proxy-from-env": {
@@ -7991,6 +7994,17 @@
"react": "^19.0.0"
}
},
"node_modules/react-frame-component": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-5.2.7.tgz",
"integrity": "sha512-ROjHtSLoSVYUBfTieazj/nL8jIX9rZFmHC0yXEU+dx6Y82OcBEGgU9o7VyHMrBFUN9FuQ849MtIPNNLsb4krbg==",
"license": "MIT",
"peerDependencies": {
"prop-types": "^15.5.9",
"react": ">= 16.3",
"react-dom": ">= 16.3"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+2
View File
@@ -16,6 +16,7 @@
"@radix-ui/themes": "^3.2.0",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"immutability-helper": "^3.1.1",
"jose": "^5.10.0",
"next": "15.1.5",
"pg": "^8.13.1",
@@ -24,6 +25,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.0.0",
"react-frame-component": "^5.2.7",
"zod": "^3.24.2"
},
"devDependencies": {
+1
View File
@@ -67,6 +67,7 @@
--u-3xl: 42px;
/* border radius sizes */
--radius-2xs: 1px;
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 8px;
@@ -0,0 +1,5 @@
export const ItemTypes = {
DER: "DER",
DIE: "DIE",
DAS: "DAS",
};
@@ -1,9 +1,9 @@
"use client";
import styles from "../../../page.module.css";
import DragdropLayout from "./layout";
import { Grid } from "@radix-ui/themes";
import { Box } from "@/components/ui/box/box";
import { DragBox } from "@/components/ui/dragdrop/dragbox";
import { Area } from "@/components/ui/dragdrop/area";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
export default function Dragdrop({}) {
return (
@@ -11,25 +11,9 @@ export default function Dragdrop({}) {
<div className={styles.page}>
<div className="exercise-container">
<h1>Übung 1 Substantiv</h1>
<Box variant="outline" size="lg" className="top-box">
Kurs <br></br>
Schule <br></br>
Auto
</Box>
{/* <DragBox className="drag-box">
kräääääh
</DragBox> */}
<Grid columns="3" gap="3" rows="2" width="auto" justify="center">
<Box gridrow="2 / 2" variant="outline" size="xl" minwidth="500px">
Kurs
</Box>
<Box gridrow="2 / 2" variant="outline" size="xl">
Schule
</Box>
<Box gridrow="2 / 2" variant="outline" size="xl">
Auto
</Box>
</Grid>
<DndProvider backend={HTML5Backend}>
<Area />
</DndProvider>
</div>
</div>
</DragdropLayout>
@@ -0,0 +1,79 @@
import update from "immutability-helper";
import { memo, useCallback, useState } from "react";
import { ItemTypes } from "../../../app/lessons/exercises/dragdrop/itemtypes.js";
import { WordBox } from "./wordbox.js";
import { Dustbin } from "./dustbin.js";
import { Button } from "@/components/ui/button";
export const Area = memo(function Area() {
const initialDustbins = [
{ accepts: [ItemTypes.DER], droppedItems: [] },
{ accepts: [ItemTypes.DIE], droppedItems: [] },
{ accepts: [ItemTypes.DAS], droppedItems: [] },
];
const initialBoxes = [
{ name: "Kurs", type: ItemTypes.DER },
{ name: "Elefant", type: ItemTypes.DER },
{ name: "Schule", type: ItemTypes.DIE },
{ name: "Eule", type: ItemTypes.DIE },
{ name: "Auto", type: ItemTypes.DAS },
{ name: "Kaninchen", type: ItemTypes.DAS },
];
const [dustbins, setDustbins] = useState(initialDustbins);
const [boxes] = useState(initialBoxes);
const [droppedBoxNames, setDroppedBoxNames] = useState([]);
function isDropped(boxName) {
return droppedBoxNames.indexOf(boxName) > -1;
}
const handleDrop = useCallback(
(index, item) => {
const { name } = item;
setDroppedBoxNames(
update(droppedBoxNames, name ? { $push: [name] } : { $push: [] })
);
setDustbins(
update(dustbins, {
[index]: {
droppedItems: {
$push: item ? [{ name, type: item.type }] : [],
},
},
})
);
},
[droppedBoxNames, dustbins]
);
const reset = () => {
setDustbins(initialDustbins);
setDroppedBoxNames([]);
};
return (
<div>
<div style={{ overflow: "hidden", clear: "both" }}>
{boxes.map(({ name, type }, index) => (
<WordBox
name={name}
type={type}
isDropped={isDropped(name)}
key={index}
/>
))}
</div>
<div style={{ overflow: "hidden", clear: "both" }}>
{dustbins.map(({ accepts, droppedItems }, index) => (
<Dustbin
accept={accepts}
droppedItems={droppedItems}
onDrop={(item) => handleDrop(index, item)}
key={index}
/>
))}
</div>
<Button onClick={reset}>Erneut versuchen</Button>
</div>
);
});
@@ -1,11 +0,0 @@
"use client";
import { useDrag } from "react-dnd";
export function DragBox() {
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: "BOX",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
}
@@ -0,0 +1,22 @@
.wordbox {
border: var(--radius-2xs) dashed;
background-color: var(--fg8);
padding: var(--u-md);
margin-right: var(--u-md);
margin-bottom: var(--u-xl);
cursor: move;
float: left;
}
.dustbin {
height: 12rem;
width: 12rem;
margin-right: 1.5rem;
margin-bottom: 1.5rem;
color: var(--fg3);
font-size: var(--font-lg);
padding: var(--u-lg);
text-align: center;
line-height: normal;
float: left;
}
@@ -0,0 +1,42 @@
import { memo } from "react";
import { useDrop } from "react-dnd";
import { dustbin } from "./dragdrop.css";
export const Dustbin = memo(function Dustbin({ accept, droppedItems, onDrop }) {
const [{ isOver, canDrop }, drop] = useDrop({
accept,
drop: onDrop,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = isOver && canDrop;
let backgroundColor = "var(--bg5)";
if (isActive) {
backgroundColor = "var(--bg5)";
} else if (canDrop) {
backgroundColor = "var(--bg5";
}
return (
<div
ref={drop}
style={{ backgroundColor }}
className="dustbin"
data-testid="dustbin"
>
{isActive ? `${accept.join(", ")}` : `${accept.join(", ")}`}
{droppedItems && (
<div>
{droppedItems.map((item) => (
<span key={item.name}>
{item.name}
<br></br>
</span>
))}
</div>
)}
</div>
);
});
@@ -0,0 +1,27 @@
import { memo } from "react";
import { useDrag } from "react-dnd";
import { wordbox } from "./dragdrop.css";
export const WordBox = memo(function WordBox({ name, type, isDropped }) {
const [{ opacity }, drag] = useDrag(
() => ({
type,
item: { name },
canDrag: !isDropped,
collect: (monitor) => ({
opacity: monitor.isDragging() ? 0.4 : 1,
}),
}),
[name, type, isDropped]
);
return (
<div
ref={drag}
style={{ opacity }}
className="wordbox"
data-testid="wordbox"
>
{isDropped ? <s>{name}</s> : name}
</div>
);
});