From 61b40bf0092b5d404a1b021b64d403ba5299c5e2 Mon Sep 17 00:00:00 2001 From: Daniel <you@example.com> Date: Fri, 13 Sep 2024 21:00:06 +0200 Subject: [PATCH] testing2 --- next.config.mjs | 8 ++ src/app/layout.jsx | 19 +++ src/app/page.jsx | 5 +- src/components/SortingVisualizer.jsx | 156 +++++++++++++++++++++++++ src/libs/BubbleSort.jsx | 23 ++++ src/styles/SortingVisualizer.css | 165 +++++++++++++++++++++++++++ src/styles/favicon.ico | Bin 0 -> 15406 bytes 7 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 next.config.mjs create mode 100644 src/app/layout.jsx create mode 100644 src/components/SortingVisualizer.jsx create mode 100644 src/libs/BubbleSort.jsx create mode 100644 src/styles/SortingVisualizer.css create mode 100644 src/styles/favicon.ico diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..ee4fab1 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'export', + distDir: 'public', + +} + +export default nextConfig diff --git a/src/app/layout.jsx b/src/app/layout.jsx new file mode 100644 index 0000000..0471ac6 --- /dev/null +++ b/src/app/layout.jsx @@ -0,0 +1,19 @@ +import Head from "next/head"; + +export const Metadata = { + title: 'Sorting Visualizer', + description: 'Visualize sorting algorithms', +} + +export default function RootLayout({children}) { + return ( + <html lang="en"> + <Head> + <link rel="icon" href="/src/styles/favicon.ico" /> + </Head> + <body> + <div id="root">{children}</div> + </body> + </html> + ) +} \ No newline at end of file diff --git a/src/app/page.jsx b/src/app/page.jsx index 5aae9c6..6a8d251 100644 --- a/src/app/page.jsx +++ b/src/app/page.jsx @@ -1,12 +1,9 @@ import SortingVisualizer from '../components/SortingVisualizer.jsx' -import Head from "next/head"; export default function Home() { return ( <main> - <Head> - <link rel="icon" href="/src/styles/favicon.ico" /> - </Head> + <SortingVisualizer /> </main> ) diff --git a/src/components/SortingVisualizer.jsx b/src/components/SortingVisualizer.jsx new file mode 100644 index 0000000..6220c82 --- /dev/null +++ b/src/components/SortingVisualizer.jsx @@ -0,0 +1,156 @@ +"use client"; + +import {useState, useEffect} from 'react'; +import '../styles/SortingVisualizer.css' +import BubbleSort from "../libs/BubbleSort.jsx"; +import RangeSlider from 'react-bootstrap-range-slider'; +import 'react-bootstrap-range-slider/dist/react-bootstrap-range-slider.css'; +import {Dropdown} from 'react-bootstrap'; +import 'bootstrap/dist/css/bootstrap.min.css'; + +function SortingVisualizer() { + + const moduloFive = ((window.innerWidth / 45)).toFixed(0) % 5; + const monitorSize = (window.innerWidth / 45).toFixed(0) - moduloFive; + const [BarNumber, setBarNumber] = useState(() => monitorSize > 100 ? 100 : monitorSize); // max of 100 bars + const [bars, setbars] = useState(() => generateArray(BarNumber)); + const [sorting, setSorting] = useState(false); + const [alreadySorted, setAlreadySorted] = useState(); + const [isSorted, setIsSorted] = useState(false); + const [sortSpeed, setSortSpeed] = useState('40'); + + const SliderWithInputFormControl = () => { + const [sliderValue_intern, setSliderValue_intern] = useState(BarNumber); + return ( + <RangeSlider + value={sliderValue_intern} + step={5} + max={100} + min={5} + disabled={sorting} + onChange={changeEvent => setSliderValue_intern(Number(changeEvent.target.value))} + onAfterChange={() => setBarNumber(sliderValue_intern)} + /> + ); + }; + + useEffect(() => { + resetBars(); + }, [BarNumber]); + + function getRandomArbitrary(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + function generateArray(numberBars) { + return Array.from({length: numberBars}, () => getRandomArbitrary(50, 300)); + } + + async function handleSorting() { + if (sorting) return; + setSorting(true); + await BubbleSort(bars, setbars, setAlreadySorted, sortSpeed); + setSorting(false); + } + + function resetBars() { + setAlreadySorted(1000); + setbars(generateArray(BarNumber)); + let ColoredBars = document.getElementsByClassName('sorted'); + document.getElementById('startButton').innerText = 'Sort!'; + while (ColoredBars.length > 0) { + ColoredBars[0].className = 'bar'; + } + setIsSorted(false); + + + } + + function finishAnimation() { + let ColoredBars = document.getElementsByClassName('sorted'); + while (ColoredBars.length > 0) { + ColoredBars[0].className = 'finished'; + } + } + + function handleButton() { + if (!sorting && !isSorted) { + setAlreadySorted(1000); + handleSorting(); + } else if (isSorted) { + resetBars(); + + } + } + + useEffect(() => { + if (alreadySorted == 0) { + setIsSorted(true); + finishAnimation(); + document.getElementById('startButton').innerText = 'Reset!'; + } + }, [alreadySorted]); + + + return ( + <> + <div className='Container'> + <div id='ToolBar'> + <div className="Label">How many bars?</div> + <SliderWithInputFormControl style={{className: 'range-slider'}}/> + <input type='number' max={100} min={0} disabled={sorting} + onChange={changeEvent => { + let value = Number(changeEvent.target.value); + if (value > 100) value = 100; + if (value < 5) value = 5; + setBarNumber(value); + }} defaultValue={BarNumber} + value={BarNumber} style={{ + width: '42px', + margin: '10px', + borderRadius: '5px', + border: 'none', + textAlign: 'center' + }}/> + <div style={{background: 'gray', height: '70%', marginLeft: '10px', width: '2px'}}></div> + <Dropdown id="dropdown"> + <Dropdown.Toggle + id="dropdown" + disabled={sorting} + > + Sorting-Speed: { + { + 100: 'Slow', + 40: 'Medium', + 0: 'Fast' + }[sortSpeed] || '' + } + </Dropdown.Toggle> + <Dropdown.Menu id="dropdown-menu"> + <Dropdown.Item id="dropdown-item" onClick={() => setSortSpeed(100)}> + Slow + </Dropdown.Item> + <Dropdown.Item id="dropdown-item" onClick={() => setSortSpeed(40)}> + Medium + </Dropdown.Item> + <Dropdown.Item id="dropdown-item" onClick={() => setSortSpeed(0)}> + Fast + </Dropdown.Item> + </Dropdown.Menu> + </Dropdown> + <button id='startButton' className="Button" disabled={sorting} + style={{background: `${sorting ? 'red' : ''}`}} onClick={() => { + handleButton() + }}>{sorting ? 'Sorting...' : 'Sort!'}</button> + </div> + <div className='DivArray'> + {bars.map((height, index) => (<div key={index} data-key={index} + className={`bar + ${alreadySorted <= index ? 'sorted' : ''}`} + style={{height: `${height}px`}}></div>))} + </div> + + </div> + </> + ) +}; +export default SortingVisualizer; \ No newline at end of file diff --git a/src/libs/BubbleSort.jsx b/src/libs/BubbleSort.jsx new file mode 100644 index 0000000..03b4c9d --- /dev/null +++ b/src/libs/BubbleSort.jsx @@ -0,0 +1,23 @@ +async function BubbleSort(EingangsArray, updateBars, setColor, sortSpeed){ + const len = EingangsArray.length; + for(let i = 0; i< len -1; ++i){ + for(let k=0; k< len -1-i; k++){ + let tmp; + + if (k == len -2-i){ + setColor([(len-1-i)]); + } + if(EingangsArray[k] > EingangsArray[k+1]){ + tmp = EingangsArray[k]; + EingangsArray[k] = EingangsArray[k+1]; + EingangsArray[k+1] = tmp; + } + updateBars([...EingangsArray]); + await new Promise(resolve => setTimeout(resolve, sortSpeed)); // Pause für Animation + } + } + setColor([(len-len)]); + console.log(EingangsArray); + return EingangsArray; +} +export default BubbleSort; \ No newline at end of file diff --git a/src/styles/SortingVisualizer.css b/src/styles/SortingVisualizer.css new file mode 100644 index 0000000..4d120a8 --- /dev/null +++ b/src/styles/SortingVisualizer.css @@ -0,0 +1,165 @@ +body { + margin: 0; +} + +.Container { + display: flex; + flex-direction: column; + align-items: center; +} + + +.bar { + background-color: blue; + display: inline-block; + height: 100px; + flex: 1 1 20px; + min-width: 2px; +} + +.sorted { + background-color: deeppink; + animation: ScaleAnimation 0.5s ease-in-out; + +} + +.finished { + background-color: lawngreen; + flex: 1 1 20px; + min-width: 2px; + margin: 2px; + display: inline-block; + animation: GreenScaleAnimation 1s ease-in; + +} + +.range-slider { + padding: 0px; +} + +#ToolBar { + height: 50px; + width: 100%; + background: #34495e; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + +} + +#startButton { + height: 78%; + width: 85px; + font-size: 12px; + margin: 10px; + background: #1abc9c; + text-align: center; + display: grid; + place-items: center; + border-radius: 5px; + border: none; + color: white; + font-size: 14px; + font-family: Arial, sans-serif; + font-weight: bold; + +} + +button:active { + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.46); + transform: translateY(2px); +} + +.Label { + margin: 10px; + font-size: 15px; + font-family: Arial, sans-serif; + font-weight: bold; + color: white; +} + +.DivArray { + transform: scaleY(-1); + display: flex; + flex-wrap: nowrap; + justify-content: center; + gap: 2px; + width: 90%; + margin-top: 5px; +} + +#dropdown-menu { + background-color: #34495e; + border: none; + border-radius: 5px; + margin-top: 3px; +} + + +#dropdown-item { + color: white; + font-size: 16px; + font-weight: normal; + border-radius: 5px; +} + +#dropdown-item:hover { + background-color: #1abc9c; + color: white; +} + +#dropdown { + background: none; + border: none; + border-radius: 0px; + color: white; + fontSize: 14px; + fontWeight: normal; + padding: 0; + height: 100%; + display: flex; + alignItems: center; + place-items: center; + margin-inline: 10px; + width: 190px; +} + +#dropdown:hover { + color: #1abc9c; +} + +#dropdown:active { + box-shadow: none; + transform: none; +} + +#dropdown.show { + background-color: #1abc9c; + color: white; +} + + +@keyframes ScaleAnimation { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +@keyframes GreenScaleAnimation { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} diff --git a/src/styles/favicon.ico b/src/styles/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3acbd6c1b785768b3563e233314d14e7e75ac21 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 -- GitLab