mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
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:
Generated
+19
-5
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user