diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..b6be142eca1df3e42e1381c80349b2c040bf8a16 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,22 @@ +cache: + paths: + - .m2/ + +stages: + - build + - test + +maven-build: + image: maven:3.8.5-openjdk-17 + stage: build + script: "mvn clean package -DskipTests" + artifacts: + paths: + - target/ + +maven-test: + image: maven:3.8.5-openjdk-17 + stage: test + script: "mvn test" + dependencies: + - maven-build diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ada30396ed112681190b7ca34f87260243185b5..df898ccc9ef2bcf407c74dc84a984e8ff978e2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - This Changelog - Tags and Semantic Versioning -### Fixed - -### Changed - -### Removed - ## [0.1.0] - 31.12.2023 ### Added @@ -29,11 +23,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - JWT token is persisted as cookie - Wishlist - Plant details -- Plants overview -- Random plants +- Plants overview +- Random plants -### Fixed +## [0.2.0] - 13.01.2024 + +### Added + +- Buttons to remove & add plants to wishlist and garden. Includes status message popup -### Changed +## [Released] + +## [1.0.0] + +### Added + +- Search functionality +- Fully functional release +- Garden, wishlist all working + +## [1.0.1] + +### Fixed -### Removed +- Re-add empty option to dropdown on search / filter page diff --git a/growbros-frontend/package-lock.json b/growbros-frontend/package-lock.json index b85ec7a34722b10b8daa36b9831e2f9d4f5cff75..277b0d49497e9ff09b4258a8c5e29b85bb1ff093 100644 --- a/growbros-frontend/package-lock.json +++ b/growbros-frontend/package-lock.json @@ -8,6 +8,10 @@ "name": "growbros-frontend", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.4", + "@mui/material": "^5.15.4", "font-awesome": "^4.7.0", "jwt-decode": "^4.0.0", "rc-slider": "^10.5.0", @@ -40,10 +44,190 @@ "node": ">=0.10.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -51,6 +235,152 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -459,6 +789,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz", + "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz", + "integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==", + "dependencies": { + "@floating-ui/core": "^1.5.3", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.5.tgz", + "integrity": "sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q==", + "dependencies": { + "@floating-ui/dom": "^1.5.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -492,6 +856,251 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.31.tgz", + "integrity": "sha512-+uNbP3OHJuZVI00WyMg7xfLZotaEY7LgvYXDfONVJbrS+K9wyjCIPNfjy8r9XJn4fbHo/5ibiZqjWnU9LMNv+A==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@floating-ui/react-dom": "^2.0.5", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.4", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.4.tgz", + "integrity": "sha512-0OZN9O6hAtBpx70mMNFOPaAIol/ytwZYPY+z7Rf9dK3+1Xlzwvj5/IeShJKvtp76S1qJyhPuvZg0+BGqQaUnUw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.4.tgz", + "integrity": "sha512-q/Yk7aokN8qGMpR7bwoDpBSeaNe6Bv7vaY9yHYodP37c64TM6ime05ueb/wgksOVszrKkNXC67E/XYbRWOoUFA==", + "dependencies": { + "@babel/runtime": "^7.23.7" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.4.tgz", + "integrity": "sha512-T/LGRAC+M0c+D3+y67eHwIN5bSje0TxbcJCWR0esNvU11T0QwrX3jedXItPNBwMupF2F5VWCDHBVLlFnN3+ABA==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@mui/base": "5.0.0-beta.31", + "@mui/core-downloads-tracker": "^5.15.4", + "@mui/system": "^5.15.4", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.4", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.4.tgz", + "integrity": "sha512-9N5myIMEEQTM5WYWPGvvYADzjFo12LgJ7S+2iTZkBNOcJpUxQYM1tvYjkHCDV+t1ocMOEgjR2EfJ9Dus30dBlg==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@mui/utils": "^5.15.4", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.4.tgz", + "integrity": "sha512-vtrZUXG5XI8CNiNLcxjIirW4dEbOloR+ikfm6ePBo7jXpJdpXjVzBWetrfE+5eI0cHkKWlTptnJ2voKV8pBRfw==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.4.tgz", + "integrity": "sha512-KCwkHajGBXPs2TK1HJjIyab4NDk0cZoBDYN/TTlXVo1qBAmCjY0vjqrlsjeoG+wrwwcezXMLs/e6OGP66fPCog==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@mui/private-theming": "^5.15.4", + "@mui/styled-engine": "^5.15.4", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.4", + "clsx": "^2.1.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.4.tgz", + "integrity": "sha512-E2wLQGBcs3VR52CpMRjk46cGscC4cbf3Q2uyHNaAeL36yTTm+aVNbtsTCazXtjOP4BDd8lu6VtlTpVC8Rtl4mg==", + "dependencies": { + "@babel/runtime": "^7.23.7", + "@types/prop-types": "^15.7.11", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -527,6 +1136,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", @@ -751,17 +1369,20 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, "node_modules/@types/prop-types": { - "version": "15.7.9", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", - "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==", - "dev": true + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.2.33", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -786,11 +1407,18 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.5", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", - "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==", - "dev": true + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" }, "node_modules/@types/semver": { "version": "7.5.4", @@ -1081,6 +1709,20 @@ "node": ">=8" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1113,7 +1755,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1139,6 +1780,14 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1163,6 +1812,26 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1180,8 +1849,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/debug": { "version": "4.3.4", @@ -1230,6 +1898,23 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -1271,7 +1956,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -1526,6 +2210,11 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1590,6 +2279,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1672,6 +2369,30 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1685,7 +2406,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1722,6 +2442,22 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1790,6 +2526,11 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1832,6 +2573,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2008,7 +2754,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -2016,6 +2761,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2043,11 +2805,15 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -2262,16 +3028,46 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -2393,6 +3189,14 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -2426,6 +3230,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2438,12 +3247,31 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2604,6 +3432,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/growbros-frontend/package.json b/growbros-frontend/package.json index a45121749b4db8e5c22a113fc4947254ca9de468..c5039c3c610885fe09055057713ad0918398e6a0 100644 --- a/growbros-frontend/package.json +++ b/growbros-frontend/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.4", + "@mui/material": "^5.15.4", "font-awesome": "^4.7.0", "jwt-decode": "^4.0.0", "rc-slider": "^10.5.0", diff --git a/growbros-frontend/src/components/DropDownFilter.tsx b/growbros-frontend/src/components/DropDownFilter.tsx index 655574ccd78d210d0e99f03b34e143089751180b..41bb750497f458213545ade9bad315b1c461c12f 100644 --- a/growbros-frontend/src/components/DropDownFilter.tsx +++ b/growbros-frontend/src/components/DropDownFilter.tsx @@ -1,37 +1,32 @@ -import { useState } from "react"; -import { propsDropDownFilter } from "../utils/commonTypes"; +import {useState} from "react"; +import {DropdownOption, PropsDropDownFilter} from "../utils/commonTypes"; import "../stylesheets/DropDownFilter.css"; +import "../pages/Wunschliste.tsx"; -function DropDownFilter(props: PropsDropDownFilter) { - const options = props.options; - const topic = props.topic; - const [selectedOption, setSelectedOption] = useState(""); +function DropDownFilter<T extends DropdownOption>(props: PropsDropDownFilter<T>) { + const options = props.options; + const topic = props.topic; + const hasEmptyOption = props.hasEmptyOption; + const [selectedOption, setSelectedOption] = useState(""); - const handleOptionChange = (event: any) => { - setSelectedOption(event.target.value); - props.filterOnChange!(event.target.value); - }; + const handleOptionChange = (event: any) => { + setSelectedOption(event.target.value); + props.filterOnChange(event.target.value); + }; - return ( - <div className="dropdown-filter"> - <label>{topic}</label> - <select value={selectedOption} onChange={handleOptionChange}> - <option value=""></option> - {options.map((option, index) => ( - <option key={index} value={option}> - {option} - </option> - ))} - </select> - </div> - ); + return ( + <div className="dropdown-filter"> + <label>{topic}</label> + <select value={selectedOption} onChange={handleOptionChange}> + {hasEmptyOption && <option value=""/>} + {options.map((option, index) => ( + <option key={index} value={option}> + {option} + </option> + ))} + </select> + </div> + ); } -//TODO filteronchange ? weg wenn lara Garten fertig -type PropsDropDownFilter = { - options: Array<string>; - topic: string; - filterOnChange?: (selectedOption: string) => void; -}; - export default DropDownFilter; diff --git a/growbros-frontend/src/components/FilterPage.tsx b/growbros-frontend/src/components/FilterPage.tsx index ebee59dab028ce2e01f08388bebc5bf5d6c21fc5..81657c88c7926bc0cc3019ef157eafea461248a3 100644 --- a/growbros-frontend/src/components/FilterPage.tsx +++ b/growbros-frontend/src/components/FilterPage.tsx @@ -1,11 +1,53 @@ -import { useState } from "react"; +import {useState} from "react"; import Slider from "rc-slider"; import "rc-slider/assets/index.css"; import "../stylesheets/FilterPage.css"; import DropDownFilter from "./DropDownFilter"; -import { propsSliderFilter } from "../utils/commonTypes"; +import {PropsSliderFilter} from "../utils/commonTypes"; +import { + GroundType, + GroundTypeTranslated, + LightingDemand, + LightingDemandTranslated, + NutrientDemand, + NutrientDemandTranslated, + translateGroundTypeReverse, + translateLightingDemandReverse, + translateNutrientDemandReverse, + translateWaterDemandReverse, + WaterDemand, + WaterDemandTranslated, +} from "../utils/schemas"; -function FilterPage() { +type FilterPageProps = { + setWaterDemand: (value: WaterDemand) => void; + setNutrientDemand: (value: NutrientDemand) => void; + setLightingDemand: (value: LightingDemand) => void; + setGroundType: (value: GroundType) => void; + setPlantDuration: (value: [number, number]) => void; + setGrowthDuration: (value: [number, number]) => void; +}; + +function FilterPage({ + setGroundType, + setGrowthDuration, + setLightingDemand, + setNutrientDemand, + setPlantDuration, + setWaterDemand, +}: FilterPageProps) { + const handleGroundType = (groundType: GroundTypeTranslated) => { + setGroundType(translateGroundTypeReverse(groundType)); + }; + const handleWaterDemand = (waterDemand: WaterDemandTranslated) => { + setWaterDemand(translateWaterDemandReverse(waterDemand)); + }; + const handleNutrientDemand = (nutrientDemand: NutrientDemandTranslated) => { + setNutrientDemand(translateNutrientDemandReverse(nutrientDemand)); + }; + const handleLightingDemand = (lightingDemand: LightingDemandTranslated) => { + setLightingDemand(translateLightingDemandReverse(lightingDemand)); + }; return ( <> <div className="filterPage"> @@ -13,37 +55,52 @@ function FilterPage() { </div> <div> <DropDownFilter - topic={"Wasserbedarf "} - options={["Trocken", "Feucht", "Sehr Feucht"]} + topic={"Wasserbedarf "} + options={["Trocken", "Feucht", "Sehr feucht"]} + filterOnChange={handleWaterDemand} + hasEmptyOption /> <DropDownFilter - topic={"Nährstoffbedarf "} - options={["Niedrig", "Mittel", "Hoch"]} + topic={"Nährstoffbedarf "} + options={["Niedrig", "Mittel", "Hoch"]} + filterOnChange={handleNutrientDemand} + hasEmptyOption /> <DropDownFilter - topic={"Lichtbedarf "} - options={["Niedrig", "Mittel", "Hoch"]} + topic={"Lichtbedarf "} + options={["Niedrig", "Mittel", "Hoch"]} + filterOnChange={handleLightingDemand} + hasEmptyOption /> <DropDownFilter - topic={"Bodentyp"} - options={["Leicht", "Mittel", "Schwer"]} + topic={"Bodentyp"} + options={["Leicht", "Mittel", "Schwer"]} + filterOnChange={handleGroundType} + hasEmptyOption /> </div> - <SliderFilter topic={"Anbauphase"} min={1} max={52} /> - <SliderFilter topic={"Erntezeitraum"} min={1} max={52} /> - <SliderFilter topic={"Wachstumsdauer"} min={1} max={52} />{" "} + <SliderFilter + topic={"Anbauphase"} + min={1} + max={52} + filterOnChange={setPlantDuration} + /> + <SliderFilter + topic={"Wachstumsdauer"} + min={1} + max={52} + filterOnChange={setGrowthDuration} + />{" "} </> ); } - - - -function SliderFilter({ topic, min, max }: propsSliderFilter) { +function SliderFilter({ topic, min, max, filterOnChange }: PropsSliderFilter) { const [range, setRange] = useState([min, max]); const handleRangeChange = (newRange: any) => { setRange(newRange); + filterOnChange!(newRange); }; return ( @@ -62,6 +119,4 @@ function SliderFilter({ topic, min, max }: propsSliderFilter) { ); } - export default FilterPage; - diff --git a/growbros-frontend/src/components/PlantDetails.tsx b/growbros-frontend/src/components/PlantDetails.tsx index cb56451ee96fb0a5d6a001adc0ab4ee70a49f21b..894ae27f9af83be5cd65d87f08ba4efc0bcb583a 100644 --- a/growbros-frontend/src/components/PlantDetails.tsx +++ b/growbros-frontend/src/components/PlantDetails.tsx @@ -1,10 +1,14 @@ import "../stylesheets/PlantDetails.css"; import "font-awesome/css/font-awesome.min.css"; -import {useEffect, useState} from "react"; +import AgricultureIcon from '@mui/icons-material/Agriculture'; +import HeartBrokenIcon from '@mui/icons-material/HeartBroken'; +import {ReactNode, useEffect, useState} from "react"; import {Plant} from "../utils/schemas"; import {useParams} from "react-router"; import {IBackendConnector} from "../utils/IBackendConnector"; import useGrowbrosFetcher from "../utils/useGrowbrosFetcher"; +import {FetchResult} from "../utils/FetchResult.ts"; +import StatusMessage from "./StatusMessage.tsx"; function PlantDetails() { const {plantId} = useParams(); @@ -73,8 +77,18 @@ function PlantDetails() { margin: "0 0 40px 0", }} > - <ButtonAddToGarden plantId={plant.id}></ButtonAddToGarden> - <ButtonAddToWishlist plantId={plant.id}></ButtonAddToWishlist> + <ActionButton buttonTitle={"Add to garden"} + buttonContent={<i className="fa fa-leaf"></i>} + buttonAction={() => bc.addToGarden(plant.id)}/> + <ActionButton buttonTitle={"Remove from garden"} + buttonContent={<AgricultureIcon/>} + buttonAction={() => bc.removeFromGarden(plant.id)}/> + <ActionButton buttonTitle={"Add to wishlist"} + buttonContent={<i className="fa fa-heart"></i>} + buttonAction={() => bc.addToWishlist(plant.id)}/> + <ActionButton buttonTitle={"Remove from wishlist"} + buttonContent={<HeartBrokenIcon sx={{fontSize: 19}}/>} + buttonAction={() => bc.removeFromWishlist(plant.id)}/> </div> </div> } @@ -83,59 +97,40 @@ function PlantDetails() { } } -function ButtonAddToGarden({plantId}: any) { - const [status, setStatus] = useState<null | String>(null); - - const bc: IBackendConnector = useGrowbrosFetcher(); - - const handleButtonClick = async () => { - await bc.addToGarden(plantId); - }; - - return ( - <> - {status && ( - <div - className={`status-message ${ - status.includes("erfolgreich") ? "success-message" : "error-message" - }`} - > - {status} - </div> - )} - <button title="Zum Garten hinzufügen" onClick={handleButtonClick}> - <i className="fa fa-leaf"></i> - </button> - </> - ); +type ActionButtonProps = { + buttonTitle: string, + buttonContent: ReactNode, + buttonAction: () => Promise<FetchResult<unknown>>; } -//TODO status+error handling -//TODO css für status message -function ButtonAddToWishlist({plantId}: any) { - const [status, setStatus] = useState<null | String>(null); +function ActionButton({buttonContent, buttonTitle, buttonAction}: ActionButtonProps) { + const [status, setStatus] = useState<"error" | "success">(); + const message = () => { + if (status === "error") { + return "Something went wrong..." + } else { + return "Success!" + } + }; - const bc: IBackendConnector = useGrowbrosFetcher(); + const statusVisibleFor = 3000; const handleButtonClick = async () => { - await bc.addToWishlist(plantId); + const result = await buttonAction(); + if (result.err) { + setStatus("error"); + } else { + setStatus("success") + } + setTimeout(() => setStatus(undefined), statusVisibleFor); }; return ( <> - {status && ( - <div - className={`status-message ${ - status.includes("erfolgreich") ? "success-message" : "error-message" - }`} - > - {" "} - {status} - </div> - )} - <button title="Zur Wunschliste hinzufügen" onClick={handleButtonClick}> - <i className="fa fa-heart"></i> + <button title={buttonTitle} onClick={handleButtonClick}> + {buttonContent} </button> + {status && <StatusMessage message={message()} status={status} visibleFor={3000}/>} </> ); } diff --git a/growbros-frontend/src/components/StatusMessage.tsx b/growbros-frontend/src/components/StatusMessage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..13b79e0176d6fbf7cd75116b41b6f2a691272a50 --- /dev/null +++ b/growbros-frontend/src/components/StatusMessage.tsx @@ -0,0 +1,30 @@ +import {useEffect, useState} from "react"; +import "../stylesheets/StatusMessage.css" + +type StatusMessageProps = { + status: "success" | "error", + visibleFor: number, + message: string +} + +function StatusMessage({message, status, visibleFor}: StatusMessageProps) { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + }, visibleFor); + + return () => { + clearTimeout(timer); + }; + }, [visibleFor]); + + return visible ? ( + <div className={`status-message ${status === "success" ? "success-message" : "error-message"}`}> + {message} + </div> + ) : null; +} + +export default StatusMessage; diff --git a/growbros-frontend/src/pages/Garten.tsx b/growbros-frontend/src/pages/Garten.tsx index 3c083a18eaeb2c90d57c9adc7a647407f74f2f52..492d0beffcde11368e4414d5a20ced1531463e45 100644 --- a/growbros-frontend/src/pages/Garten.tsx +++ b/growbros-frontend/src/pages/Garten.tsx @@ -6,37 +6,76 @@ import {IBackendConnector} from "../utils/IBackendConnector"; import PlantsOverview from "../components/PlantsOverview"; function Garten() { - const [plants, setPlantsInGarden] = useState<Plant[]>([]); - const [error, setError] = useState({}); - const bc: IBackendConnector = useGrowbrosFetcher(); - - useEffect(() => { - (async () => { - const result = await bc.getGardenEntries(); - if (result.err) { - setError(result.err); - } else { - setPlantsInGarden(result.value!); - } - })(); - }, []); - - return ( - <> - <div style={{padding: "20px"}}> - <h2>Dein Garten</h2> - <DropDownFilter - topic={"Sortierung der Pflanzen im Garten"} - options={[ - "neueste zuerst", - "als nächstes anpflanzbar", - "als nächstes erntbar", - ]} - /> - </div> - <PlantsOverview plants={plants} /> - </> - ); + const [plants, setPlantsInGarden] = useState<Plant[]>([]); + const [error, setError] = useState<object>(); + const [currentSort, setCurrentSort] = useState<string>("") + + const bc: IBackendConnector = useGrowbrosFetcher(); + + useEffect(() => { + (async () => { + const result = await bc.getGardenEntries(currentSort); + if (result.err) { + setError(result.err); + } else { + setPlantsInGarden(result.value!); + } + })(); + }, [currentSort]); + + + const handleClearGarden = async () => { + if (confirm("Möchtest du wirklich alle Pflanzen aus deinem Garten entfernen?" + "\n" + + "Diese Aktion kann nicht rückgängig gemacht werden.")) { + const result = await bc.clearGarden(); + if (result.err) { + setError(result.err); + } else { + setError(undefined); + setPlantsInGarden([]); + } + } + } + + const handleSortOptions = async (selectedOption: string) => { + switch (selectedOption) { + case "neueste zuerst": + setCurrentSort("createdAt"); + break; + case "als nächstes anpflanzbar": + setCurrentSort("plantDate"); + break; + case "als nächstes erntbar": + setCurrentSort("harvestDate"); + break; + case "alphabetisch sortiert": + default: + setCurrentSort(""); + } + } + + return ( + <> + <div style={{padding: "20px"}}> + <h2>Dein Garten</h2> + <DropDownFilter + filterOnChange={handleSortOptions} + topic={"Sortierung der Pflanzen im Garten"} + options={[ + "alphabetisch sortiert", + "neueste zuerst", + "als nächstes anpflanzbar", + "als nächstes erntbar", + ]} + /> + <button onClick={handleClearGarden}>Garten leeren</button> + </div> + {error && ( + <div style={{padding: "20px"}}>Es ist ein Fehler aufgetreten...</div> + )} + <PlantsOverview plants={plants}/> + </> + ); } export default Garten; diff --git a/growbros-frontend/src/pages/Suche.tsx b/growbros-frontend/src/pages/Suche.tsx index 99c843a1a13c99d46ac3027dcdb30a02965a41a7..7a200239abaf30ccc30212744e02c5283189774c 100644 --- a/growbros-frontend/src/pages/Suche.tsx +++ b/growbros-frontend/src/pages/Suche.tsx @@ -1,68 +1,146 @@ import "font-awesome/css/font-awesome.min.css"; import "../stylesheets/Suche.css"; import PlantsOverview from "../components/PlantsOverview"; -import { useState, useEffect } from "react"; +import {useEffect, useState} from "react"; import FilterPage from "../components/FilterPage"; -import { Plant } from "../utils/schemas"; -import { IBackendConnector } from "../utils/IBackendConnector"; +import {GroundType, LightingDemand, NutrientDemand, Plant, WaterDemand,} from "../utils/schemas"; +import {IBackendConnector} from "../utils/IBackendConnector"; import useGrowbrosFetcher from "../utils/useGrowbrosFetcher"; +type SearchBarProps = { + setError: (value: any) => void; + setPlants: (value: Plant[]) => void; +}; + function Suche() { - const [randomPlants, setRandomPlants] = useState<Plant[]>([]); - const [error, setError] = useState({}); - const randomPlantsCount: number = 100; + const [plants, setPlants] = useState<Plant[]>([]); + const [error, setError] = useState({}); + const randomPlantsCount: number = 100; - const bc: IBackendConnector = useGrowbrosFetcher(); + const bc: IBackendConnector = useGrowbrosFetcher(); - useEffect(() => { - (async () => { - const result = await bc.getRandomPlants(randomPlantsCount); - if (result.err) { - setError(result.err); - } else { - setRandomPlants(result.value!); - } - })(); - }, []); + useEffect(() => { + (async () => { + const result = await bc.getRandomPlants(randomPlantsCount); + if (result.err) { + setError(result.err); + } else { + setPlants(result.value!); + } + })(); + }, []); - //TODO error handling - return ( - <> - <SearchBar /> - <div> - <PlantsOverview plants={randomPlants} /> - </div> - </> - ); + return ( + <> + <SearchBar setError={setError} setPlants={setPlants}/> + <div> + {plants.length === 0 && ( + <div> + Es wurden keine Pflanzen passend zu deiner Suchanfrage gefunden + </div> + )} + { + error && ( + <div> + Es ist ein Fehler aufgetreten. + </div> + ) + } + <PlantsOverview plants={plants}/> + </div> + </> + ); } -function SearchBar() { - const [isComponentVisible, setComponentVisible] = useState(false); +function SearchBar({setPlants, setError}: SearchBarProps) { + const [isComponentVisible, setComponentVisible] = useState(false); + const [searchTerm, setSearchTerm] = useState<string>(""); + const [waterDemand, setWaterDemand] = useState<WaterDemand>(); + const [nutrientDemand, setNutrientDemand] = useState<NutrientDemand>(); + const [lightingDemand, setLightingDemand] = useState<LightingDemand>(); + const [groundType, setGroundType] = useState<GroundType>(); + const [plantDuration, setPlantDuration] = useState<[number, number]>(); + const [growthDuration, setGrowthDuration] = useState<[number, number]>(); + const bc: IBackendConnector = useGrowbrosFetcher(); + + const handleSearchChange = (event: any) => { + setSearchTerm(event.target.value); + }; + const loadComponent = () => { + setComponentVisible(!isComponentVisible); + }; + + const handleSearchRequestSubmit = async () => { + let result; + + if (isSearchSetToDefault()) { + result = await bc.getRandomPlants(100); + } else { + result = await bc.searchPlants({ + searchTerm, + waterDemand, + nutrientDemand, + lightingDemand, + groundType, + growthDurationMin: growthDuration && growthDuration[0], + growthDurationMax: growthDuration && growthDuration[1], + plantWeekStart: plantDuration && plantDuration[0], + plantWeekEnd: plantDuration && plantDuration[1], + }); + } + + if (result.err) { + setError(result.err); + } else { + console.log(result.value); + setPlants(result.value!); + } + }; - const loadComponent = () => { - setComponentVisible(!isComponentVisible); - }; + const isSearchSetToDefault = () => { + return (!searchTerm || searchTerm.trim() === "") && !waterDemand && !lightingDemand && !nutrientDemand && !groundType + && (!plantDuration || plantDuration[0] === 1 && plantDuration[1] === 52) + && (!growthDuration || growthDuration[0] === 1 && growthDuration[1] === 52) + } - return ( - <> - <div className="searchBar"> - <h2>Suche nach einer Pflanze</h2> - <p> - und füge diese dann zu deinem Garten oder zu deiner Wunschliste hinzu - </p> - <div className="searchFilter"> - <input type="text" placeholder="Pflanze suchen..." /> - <button> - <i className="fa fa-search"></i> - </button> - <button onClick={loadComponent} className="filter"> - <i className="fa fa-filter" /> - </button> - </div> - {isComponentVisible && <FilterPage />} - </div> - </> - ); + return ( + <> + <div className="searchBar"> + <h2>Suche nach einer Pflanze</h2> + <p> + und füge diese dann zu deinem Garten oder zu deiner Wunschliste hinzu + </p> + <div className="searchFilter"> + <input + onChange={handleSearchChange} + value={searchTerm} + type="text" + placeholder="Pflanze suchen..." + /> + <button onClick={loadComponent} className="filter"> + <i className="fa fa-filter"/> + </button> + <button onClick={handleSearchRequestSubmit}> + <i className="fa fa-search"></i> + </button> + </div> + <div + style={ + isComponentVisible ? {display: "block"} : {display: "none"} + } + > + <FilterPage + setGroundType={setGroundType} + setGrowthDuration={setGrowthDuration} + setLightingDemand={setLightingDemand} + setNutrientDemand={setNutrientDemand} + setPlantDuration={setPlantDuration} + setWaterDemand={setWaterDemand} + /> + </div> + </div> + </> + ); } export default Suche; diff --git a/growbros-frontend/src/pages/Wunschliste.tsx b/growbros-frontend/src/pages/Wunschliste.tsx index 2a69cacf40ca3aff5c94142bf1de27e48b167fdc..21fb7416dfd40c5e629f42299bc08564a9eab5a3 100644 --- a/growbros-frontend/src/pages/Wunschliste.tsx +++ b/growbros-frontend/src/pages/Wunschliste.tsx @@ -6,57 +6,71 @@ import PlantsOverview from "../components/PlantsOverview"; import DropDownFilter from "../components/DropDownFilter"; function Wunschliste() { - const [plants, setPlantsInWishlist] = useState<Plant[]>([]); - console.log(plants); - const [error, setError] = useState({}); - const [currentSort, setCurrentSort] = useState<string>(""); + const [plants, setPlantsInWishlist] = useState<Plant[]>([]); + console.log(plants); + const [error, setError] = useState({}); + const [currentSort, setCurrentSort] = useState<string>("") - const bc: IBackendConnector = useGrowbrosFetcher(); + const bc: IBackendConnector = useGrowbrosFetcher(); - useEffect(() => { - (async () => { - const result = await bc.getWishlistEntries(currentSort); - if (result.err) { - setError(result.err); - } else { - setPlantsInWishlist(result.value!); - } - })(); - }, [currentSort]); + useEffect(() => { + (async () => { + const result = await bc.getWishlistEntries(currentSort); + if (result.err) { + setError(result.err); + } else { + setPlantsInWishlist(result.value!); + } + })(); + }, [currentSort]); - const handleSortOptions = async (selectedOption: string) => { - switch (selectedOption) { - case "neueste zuerst": - setCurrentSort("createdAt"); - break; - case "als nächstes anpflanzbar": - setCurrentSort("plantDate"); - break; - case "als nächstes erntbar": - setCurrentSort("currentPlantable"); - break; - default: - setCurrentSort(""); + const handleClearWishList = async () => { + if (confirm("Möchtest du wirklich alle Pflanzen aus deiner Wunschliste entfernen?" + "\n" + + "Diese Aktion kann nicht rückgängig gemacht werden.")) { + const result = await bc.clearWishlist(); + if (result.err) { + setError(result.err); + } else { + setPlantsInWishlist([]); + } + } } - }; - return ( - <> - <div style={{padding: "20px"}}> - <h2>Deine Wunschliste</h2> - <DropDownFilter - filterOnChange={handleSortOptions} - options={[ - "neueste zuerst", - "als nächstes anpflanzbar", - "gerade anpflanzbar", - ]} - topic={"Sortierung der Pflanzen in der Wunschliste"} - /> - </div> - <PlantsOverview plants={plants} /> - </> - ); -} + const handleSortOptions = async (selectedOption: string) => { + switch (selectedOption) { + case "neueste zuerst": + setCurrentSort("createdAt"); + break; + case "als nächstes anpflanzbar": + setCurrentSort("plantDate"); + break; + case "gerade anpflanzbar": + setCurrentSort("currentPlantable"); + break; + case "alphabetisch sortiert": + default: + setCurrentSort(""); + } + }; -export default Wunschliste; + return ( + <> + <div style={{padding: "20px"}}> + <h2>Deine Wunschliste</h2> + <DropDownFilter + filterOnChange={handleSortOptions} + topic={"Sortierung der Pflanzen in der Wunschliste"} + options={[ + "alphabetisch sortiert", + "neueste zuerst", + "als nächstes anpflanzbar", + "gerade anpflanzbar", + ]} + /> + <button onClick={handleClearWishList}>Wunschliste leeren</button> + </div> + <PlantsOverview plants={plants}/> + </> + ); +} +export default Wunschliste; \ No newline at end of file diff --git a/growbros-frontend/src/stylesheets/StatusMessage.css b/growbros-frontend/src/stylesheets/StatusMessage.css new file mode 100644 index 0000000000000000000000000000000000000000..5798c65c548d82af84510af57ffb3c24f187fbe7 --- /dev/null +++ b/growbros-frontend/src/stylesheets/StatusMessage.css @@ -0,0 +1,22 @@ +.status-message { + text-align: center; + position: fixed; + bottom: 100px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + padding: 10px 20px; + border-radius: 0.5rem; +} + +.success-message { + background: #6a945c; + color: #e5ffca; + border: 2px solid #406240; +} + +.error-message { + background: #622424; + color: #ffabab; + border: 2px solid #460808; +} diff --git a/growbros-frontend/src/utils/BackendConnectorImpl.ts b/growbros-frontend/src/utils/BackendConnectorImpl.ts index 43965fff52db699f173e748a22dc8f71b29ac0f3..1ea670a4e72ad7bdb505519e9f6afbc1752cc86a 100644 --- a/growbros-frontend/src/utils/BackendConnectorImpl.ts +++ b/growbros-frontend/src/utils/BackendConnectorImpl.ts @@ -36,9 +36,13 @@ export class BackendConnectorImpl implements IBackendConnector { async searchPlants(search: SearchRequest): Promise<FetchResult<Plant[]>> { let result: FetchResult<Plant[]>; try { - const searchParams = new URLSearchParams( - Object.entries(search) - .map(([key, value]) => [key, value as string])); + const searchParams = new URLSearchParams() + Object.entries(search) + .forEach(([key, value]) => { + if (value !== undefined && value !== "") { + searchParams.append(key, String(value)); + } + }); const response = await this.makeFetchWithAuth("/plants/search?" + searchParams); const json = await response.json(); result = this.parsePlants(json); @@ -135,12 +139,12 @@ export class BackendConnectorImpl implements IBackendConnector { return result; } - async removeEntryFromGarden(entryId: number): Promise<FetchResult<void>> { + async removeFromGarden(plantId: number): Promise<FetchResult<void>> { let result: FetchResult<void> = {value: undefined}; try { - await this.makeFetchWithAuthAndOptions("/garden/remove/" + entryId, {method: "DELETE"}); + await this.makeFetchWithAuthAndOptions("/garden/remove/" + plantId, {method: "DELETE"}); } catch (e) { - console.error("An error occurred while removing garden entry with ID", entryId); + console.error("An error occurred while removing garden entry with plantID", plantId); console.error(e); result = {err: e}; } @@ -204,12 +208,12 @@ export class BackendConnectorImpl implements IBackendConnector { return result; } - async removeFromWishlist(entryId: number): Promise<FetchResult<void>> { + async removeFromWishlist(plantId: number): Promise<FetchResult<void>> { let result: FetchResult<void> = {value: undefined}; try { - await this.makeFetchWithAuthAndOptions("/wishlist/remove/" + entryId, {method: "DELETE"}); + await this.makeFetchWithAuthAndOptions("/wishlist/remove/" + plantId, {method: "DELETE"}); } catch (e) { - console.error("An error occurred while removing entry from wishlist with id", entryId) + console.error("An error occurred while removing entry from wishlist with plantID", plantId) console.error(e); result = {err: e}; } diff --git a/growbros-frontend/src/utils/IBackendConnector.ts b/growbros-frontend/src/utils/IBackendConnector.ts index 0ed10025e4bdc6b4ce7a4598d7d19436e290c574..4889a177af3dc9275103098eb3a69a759156bf73 100644 --- a/growbros-frontend/src/utils/IBackendConnector.ts +++ b/growbros-frontend/src/utils/IBackendConnector.ts @@ -16,7 +16,7 @@ export interface IBackendConnector { getGardenSize(): Promise<FetchResult<number>>; - removeEntryFromGarden(entryId: number): Promise<FetchResult<void>>; + removeFromGarden(plantId: number): Promise<FetchResult<void>>; clearGarden(): Promise<FetchResult<number>>; @@ -26,7 +26,7 @@ export interface IBackendConnector { getWishlistSize(): Promise<FetchResult<number>>; - removeFromWishlist(entryId: number): Promise<FetchResult<void>> + removeFromWishlist(plantId: number): Promise<FetchResult<void>> clearWishlist(): Promise<FetchResult<number>>; } \ No newline at end of file diff --git a/growbros-frontend/src/utils/commonTypes.d.ts b/growbros-frontend/src/utils/commonTypes.d.ts index db207868945c8abb3552bb62e980e993769e7d42..02091e41cd4232a2304c53d06e9dad0efbd0f408 100644 --- a/growbros-frontend/src/utils/commonTypes.d.ts +++ b/growbros-frontend/src/utils/commonTypes.d.ts @@ -1,13 +1,17 @@ -export type propsDropDownFilter = { - options: Array<string>; +export type PropsDropDownFilter<T> = { + options: T[]; topic: string; + filterOnChange: (selectedOption: T) => void; + hasEmptyOption?: boolean }; - -export type propsSliderFilter = { +export type PropsSliderFilter = { topic: string; min: number; max: number; + filterOnChange: (selectedRange: [number,number])=> void }; +export type DropdownOption = string | number | readonly string[] + export type HttpMethod = "GET" | "POST" | "DELETE" | "PUT" | "PATCH"; diff --git a/growbros-frontend/src/utils/schemas.ts b/growbros-frontend/src/utils/schemas.ts index 48fec4a41f377a92dbb46dcf504d7dbeb41320fe..1a6a19d46e908555701201b1b69cb407bcf3961a 100644 --- a/growbros-frontend/src/utils/schemas.ts +++ b/growbros-frontend/src/utils/schemas.ts @@ -1,3 +1,4 @@ +import { satisfies } from "semver" import z from "zod" const groundType=z.enum(["HEAVY", "MEDIUM", "LIGHT"]) @@ -6,6 +7,16 @@ const lightingDemand=z.enum(["LOW","MEDIUM","HIGH"]) const nutrientDemand=z.enum(["LOW","MEDIUM","HIGH"]) const weekNumber= z.number().int().min(0).max(51) +export type WaterDemand = z.infer<typeof waterDemand>; +export type GroundType= z.infer<typeof groundType> +export type LightingDemand= z.infer<typeof lightingDemand> +export type NutrientDemand= z.infer<typeof nutrientDemand> + +export type NutrientDemandTranslated = "Niedrig" | "Mittel" | "Hoch"; +export type WaterDemandTranslated = "Trocken" | "Feucht" | "Sehr feucht"; +export type LightingDemandTranslated = "Niedrig" | "Mittel" | "Hoch"; +export type GroundTypeTranslated = "Leicht" | "Mittel" | "Schwer"; + export const PlantSchema = z.object({ id: z.number(), name: z.string(), @@ -26,42 +37,83 @@ export const PlantSchema = z.object({ export type Plant = z.infer<typeof PlantSchema>; - function translateLightingDemand(key: "LOW"|"MEDIUM"|"HIGH"){ + + +function translateGroundType(key: GroundType) { const map = { - LOW: "Niedrig", + HEAVY: "Schwer", MEDIUM: "Mittel", - HIGH: "Hoch" - } + LIGHT: "Leicht" + } satisfies Record<GroundType, GroundTypeTranslated> return map[key]; } - function translateGroundType(key: "HEAVY" | "MEDIUM" | "LIGHT") { +export function translateGroundTypeReverse(key: GroundTypeTranslated){ const map = { - HEAVY: "Schwer", + Schwer: "HEAVY", + Mittel: "MEDIUM", + Leicht: "LIGHT" + } satisfies Record<GroundTypeTranslated, GroundType> + return map[key] +} + +function translateLightingDemand(key: LightingDemand){ + const map = { + LOW: "Niedrig", MEDIUM: "Mittel", - LIGHT: "Leicht" - } + HIGH: "Hoch" + } satisfies Record<LightingDemand, LightingDemandTranslated> return map[key]; } - function translateWaterDemand(key: "DRY"|"WET"|"VERY_WET"){ + export function translateLightingDemandReverse(key: LightingDemandTranslated){ + const map = { + Niedrig :"LOW", + Mittel : "MEDIUM", + Hoch: "HIGH" + }satisfies Record<LightingDemandTranslated,LightingDemand> + return map[key] + } + + + function translateWaterDemand(key: WaterDemand){ const map = { DRY: "Trocken", WET: "Feucht", VERY_WET: "Sehr feucht" - } + } satisfies Record<WaterDemand, WaterDemandTranslated> + return map[key]; + } + + export function translateWaterDemandReverse(key: WaterDemandTranslated){ + const map = { + Trocken: "DRY", + Feucht: "WET", + "Sehr feucht" :"VERY_WET" + } satisfies Record<WaterDemandTranslated, WaterDemand> return map[key]; } - function translateNutrientDemand(key: "LOW"|"MEDIUM"|"HIGH"){ + + +function translateNutrientDemand(key: NutrientDemand){ const map = { LOW: "Niedrig", MEDIUM: "Mittel", HIGH: "Hoch" - } + } satisfies Record<NutrientDemand, NutrientDemandTranslated> return map[key]; } + export function translateNutrientDemandReverse(key: "Niedrig" | "Mittel" |"Hoch"){ + const map = { + Niedrig :"LOW", + Mittel : "MEDIUM", + Hoch: "HIGH" + }satisfies Record<NutrientDemandTranslated, NutrientDemand> + return map[key] + } + export const SearchRequestSchema = z.object({ searchTerm: z.string().optional(), waterDemand: waterDemand.optional(), diff --git a/src/main/java/hdm/mi/growbros/controllers/GardenController.java b/src/main/java/hdm/mi/growbros/controllers/GardenController.java index a2903103d0999d548f9f22baef2463ae268366cb..2e1332768a164b18d4cc53e0f332bad54d344035 100644 --- a/src/main/java/hdm/mi/growbros/controllers/GardenController.java +++ b/src/main/java/hdm/mi/growbros/controllers/GardenController.java @@ -35,8 +35,7 @@ public class GardenController { @RequestParam(value = "sort", required = false) String sort, @AuthenticationPrincipal User user ) { - List<Plant> response = gardenService.getUserPlants(user, sort); - return new ResponseEntity<>(response, HttpStatus.OK); + return ResponseEntity.ok(gardenService.getUserPlants(user, sort)); } @GetMapping("/count") @@ -61,20 +60,20 @@ public class GardenController { @PathVariable Long plantId, @AuthenticationPrincipal User user ) { - Plant response = gardenService.addPlantToGarden(plantId, user); - return new ResponseEntity<>(response, HttpStatus.OK); + final Plant plantAdded = gardenService.addPlantToGarden(plantId, user); + return ResponseEntity.status(201).body(plantAdded); } - @DeleteMapping("/remove/{entryId}") + @DeleteMapping("/remove/{plantId}") @Operation(description = "Remove a plant from the garden") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Successfully removed plant from the garden"), }) public ResponseEntity<Void> removePlantFromGarden( - @PathVariable Long entryId, + @PathVariable Long plantId, @AuthenticationPrincipal User user ) { - gardenService.removeFromGarden(entryId, user); + gardenService.removeFromGarden(plantId, user); return ResponseEntity.ok(null); } @@ -90,6 +89,7 @@ public class GardenController { return new ResponseEntity<>(response, HttpStatus.OK); } + @GetMapping("/getPlantsNeedingWatering") @Operation(description = "Get plants needing watering") @ApiResponses(value = { diff --git a/src/main/java/hdm/mi/growbros/controllers/GrandmaController.java b/src/main/java/hdm/mi/growbros/controllers/GrandmaController.java index b3a38812885b3ec51ff0f7f46cccf3932aa65d82..15892ffa7ac652afad2b920a0d157e17ce428aaf 100644 --- a/src/main/java/hdm/mi/growbros/controllers/GrandmaController.java +++ b/src/main/java/hdm/mi/growbros/controllers/GrandmaController.java @@ -1,5 +1,8 @@ package hdm.mi.growbros.controllers; + +import hdm.mi.growbros.models.GardenEntry; +import hdm.mi.growbros.models.plant.Plant; import hdm.mi.growbros.models.user.User; import hdm.mi.growbros.service.GrandmaService; import io.swagger.v3.oas.annotations.Operation; @@ -8,10 +11,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import java.util.List; @RestController @RequestMapping("/api/v1/Grandma") @@ -35,15 +37,24 @@ public class GrandmaController { return new ResponseEntity<>(response, HttpStatus.OK); } - @GetMapping("/getGrowablePlants") + @GetMapping ("/getGrowablePlants") @Operation(description = "Get plants ready to grow") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved plants ready to grow"), }) - public ResponseEntity<String> getPlantsReadyToGrow( - @AuthenticationPrincipal User user - ) { - String response = grandmaService.notifyAboutPlantsReadyToGrow(user); - return new ResponseEntity<>(response, HttpStatus.OK); + public ResponseEntity<List<Plant>> getPlantsReadyToGrow(@AuthenticationPrincipal User user){ + return ResponseEntity.ok(grandmaService.getPlantsReadyToGrow(user)); + + } + + @GetMapping ("/getPlantsNeedingWatering") + public ResponseEntity<List<GardenEntry>> getPlantsNeedingWatring (@AuthenticationPrincipal User user){ + return ResponseEntity.ok(grandmaService.getGardenEntriesNeedingWatering(user)); + } + + @GetMapping("/getPlantsReadyToHarvest") + public ResponseEntity<List<GardenEntry>> getPlantsReadyToHarvest(@AuthenticationPrincipal User user){ + List<GardenEntry> plantsReadyToHarvest = grandmaService.getPlantsReadyToHarvest(user); + return ResponseEntity.ok(plantsReadyToHarvest); } } diff --git a/src/main/java/hdm/mi/growbros/controllers/WishListController.java b/src/main/java/hdm/mi/growbros/controllers/WishListController.java index 82e8dffdb338bacfe97105ea4cc8058c60c758a3..b9a5f05a1c1a926eda60f28eaed1a5a8ab730d44 100644 --- a/src/main/java/hdm/mi/growbros/controllers/WishListController.java +++ b/src/main/java/hdm/mi/growbros/controllers/WishListController.java @@ -59,24 +59,23 @@ public class WishListController { @AuthenticationPrincipal User user ) { wishListService.addPlantToWishList(plantId, user); - return ResponseEntity.ok(null); + return ResponseEntity.status(201).build(); } - @DeleteMapping("/remove/{entryId}") + @DeleteMapping("/remove/{plantId}") @Operation(description = "Remove plant from wishlist") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Successfully removed from wishlist"), @ApiResponse(responseCode = "404", description = "Wishlist entry not found", content = @Content(mediaType = "text/plain")), }) public ResponseEntity<Void> removePlantFromWishlist( - @PathVariable Long entryId, + @PathVariable Long plantId, @AuthenticationPrincipal User user ) { - wishListService.removeFromWishList(entryId, user); + wishListService.removeFromWishList(plantId, user); return ResponseEntity.ok(null); } - @DeleteMapping("/remove/all") @Operation(description = "Remove all plants from wishlist") @ApiResponses(value = { diff --git a/src/main/java/hdm/mi/growbros/models/GardenEntry.java b/src/main/java/hdm/mi/growbros/models/GardenEntry.java index dde6744331a505fc97f7e39be576a67f2ae7e558..5678e0d3923e66dd3640aff4ad016a1f9f74228f 100644 --- a/src/main/java/hdm/mi/growbros/models/GardenEntry.java +++ b/src/main/java/hdm/mi/growbros/models/GardenEntry.java @@ -4,9 +4,9 @@ import hdm.mi.growbros.models.plant.Plant; import hdm.mi.growbros.models.user.User; import jakarta.persistence.*; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Builder; +import lombok.Data; import lombok.NoArgsConstructor; -import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -16,8 +16,8 @@ import java.util.Objects; @Entity @EntityListeners(AuditingEntityListener.class) -@Getter -@Setter +@Data +@Builder @AllArgsConstructor @NoArgsConstructor public class GardenEntry { diff --git a/src/main/java/hdm/mi/growbros/models/WishListEntry.java b/src/main/java/hdm/mi/growbros/models/WishListEntry.java index b7f04c20df2ed54fd7c33d7212ac7ff17a3c40b3..a8254b63146156a422a07b1787cc0ba7e5af7d8c 100644 --- a/src/main/java/hdm/mi/growbros/models/WishListEntry.java +++ b/src/main/java/hdm/mi/growbros/models/WishListEntry.java @@ -18,17 +18,17 @@ import java.util.Date; @NoArgsConstructor public class WishListEntry { - @ManyToOne(optional = false) - @CreatedBy - private User user; + @Id + @GeneratedValue + private Long id; @ManyToOne(optional = false) private Plant plant; + @ManyToOne(optional = false) + @CreatedBy + private User user; + @CreatedDate private Date createdAt; - - @Id - @GeneratedValue - private Long id; } \ No newline at end of file diff --git a/src/main/java/hdm/mi/growbros/repositories/GardenRepository.java b/src/main/java/hdm/mi/growbros/repositories/GardenRepository.java index 9395c7e11627899a225ae21c155a39317a5eee77..8e6dee73621a4ff07de67af4b60be87ad8a42337 100644 --- a/src/main/java/hdm/mi/growbros/repositories/GardenRepository.java +++ b/src/main/java/hdm/mi/growbros/repositories/GardenRepository.java @@ -51,4 +51,11 @@ public interface GardenRepository extends JpaRepository<GardenEntry, Long> { ASC """) List<GardenEntry> findAllByNearestPlantingWeek(@Param("user") User user, @Param("week") int currentWeek); + + @Query(""" + SELECT ge.id FROM GardenEntry ge + WHERE ge.user = :user + AND ge.plant.id = :plantId + """) + Long findEntryIdByUserAndPlantId(@Param("user") User user, @Param("plantId") Long plantId); } diff --git a/src/main/java/hdm/mi/growbros/repositories/WishListRepository.java b/src/main/java/hdm/mi/growbros/repositories/WishListRepository.java index 683844f80524993f169e5715379ee30c05286693..206b4fa4cdedd8372b3976dd4b05117ed32e50b7 100644 --- a/src/main/java/hdm/mi/growbros/repositories/WishListRepository.java +++ b/src/main/java/hdm/mi/growbros/repositories/WishListRepository.java @@ -47,6 +47,20 @@ public interface WishListRepository extends JpaRepository<WishListEntry, Long> { """) List<WishListEntry> findByCurrentPlantingWeek(@Param("user") User user, @Param("week") int currentWeek); + @Query(""" + SELECT ge.plant FROM WishListEntry ge + WHERE ge.user = :user + AND ge.plant.id = :plantId + """) + Plant findPlantByUserAndPlantId(@Param("user") User user, @Param("plantId") Long plantId); + + @Query(""" + SELECT ge.id FROM WishListEntry ge + WHERE ge.user = :user + AND ge.plant.id = :plantId + """) + Long findEntryIdByUserAndPlantId(@Param("user") User user, @Param("plantId") Long plantId); + boolean existsByUserAndPlant(User user, Plant plant); int deleteAllByPlant_Id(long id); diff --git a/src/main/java/hdm/mi/growbros/service/GardenService.java b/src/main/java/hdm/mi/growbros/service/GardenService.java index c9e3a1e5c4bfa76ecbfc9d9b8cf911b8db59ce39..e04e5ec8316a719c2492c9029f0b011e07ddd026 100644 --- a/src/main/java/hdm/mi/growbros/service/GardenService.java +++ b/src/main/java/hdm/mi/growbros/service/GardenService.java @@ -13,8 +13,6 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -46,7 +44,8 @@ public class GardenService { } @Transactional - public void removeFromGarden(Long entryId, User authenticatedUser) { + public void removeFromGarden(Long plantId, User authenticatedUser) { + Long entryId = gardenRepository.findEntryIdByUserAndPlantId(authenticatedUser, plantId); gardenRepository.deleteByIdAndUser(entryId, authenticatedUser); } @@ -92,78 +91,7 @@ public class GardenService { - public List<GardenEntry> getGardenEntriesNeedingWatering(User user) { - List<GardenEntry> plantsNeedingWatering = new ArrayList<>(); - List<GardenEntry> plantsInUsersGarden = gardenRepository.findByUser(user); - if(plantsInUsersGarden.isEmpty()){ - return plantsNeedingWatering; - } - - for( int index = 1 ; index < plantsInUsersGarden.size();index++){ - if(plantsInUsersGarden.get(index).getWateringFrequency() == 0 ){ - switch (plantsInUsersGarden.get(index).getPlant().getWaterDemand()){ - case DRY -> { - if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 14) { - plantsNeedingWatering.add(plantsInUsersGarden.get(index)); - } - } - case WET -> { - if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 7) { - plantsNeedingWatering.add(plantsInUsersGarden.get(index)); - } - } - case VERY_WET -> { - if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 3) { - plantsNeedingWatering.add(plantsInUsersGarden.get(index)); - } - } - } - }else if (getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > plantsNeedingWatering.get(index).getWateringFrequency()){ - plantsNeedingWatering.add(plantsInUsersGarden.get(index)); - } - } - - return plantsNeedingWatering; - - } - - - - - - - - public String notifyAboutPlantsNeedingWatering(User user) { - List<GardenEntry> plantsNeedingWatering = getGardenEntriesNeedingWatering(user); - StringBuilder notification = new StringBuilder("Diese Pflanzen in deinem Garten brauchen Wasser"); - if(!plantsNeedingWatering.isEmpty()){ - int size = plantsNeedingWatering.size(); - for (int i = 0; i < size; i++) { - notification.append(plantsNeedingWatering.get(i).getPlant().getName()); - if (i < size - 2) { - notification.append(", "); - } else if (i == size - 2) { - notification.append(" und "); - } - } - - } - return String.valueOf(notification); - } - - - - - - - private long getDaysSinceLastWatering(LocalDate lastWateringDate) { - if (lastWateringDate != null) { - LocalDate currentDate = LocalDate.now(); - return ChronoUnit.DAYS.between(lastWateringDate, currentDate); - } - return 0; - } } diff --git a/src/main/java/hdm/mi/growbros/service/GrandmaService.java b/src/main/java/hdm/mi/growbros/service/GrandmaService.java index 7c0b8b4d500dced98537b0d4bcabec09ace2aedf..669833cf1ffbe33d5742a6ec580be878eeb1d587 100644 --- a/src/main/java/hdm/mi/growbros/service/GrandmaService.java +++ b/src/main/java/hdm/mi/growbros/service/GrandmaService.java @@ -1,20 +1,22 @@ package hdm.mi.growbros.service; +import hdm.mi.growbros.models.GardenEntry; import hdm.mi.growbros.models.WishListEntry; import hdm.mi.growbros.models.plant.Plant; import hdm.mi.growbros.models.user.User; +import hdm.mi.growbros.repositories.GardenRepository; import hdm.mi.growbros.repositories.WishListRepository; import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.time.temporal.WeekFields; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Random; +import java.util.*; @Service - +@RequiredArgsConstructor public class GrandmaService { @@ -25,11 +27,9 @@ public class GrandmaService { private final GardenService gardenService; - public GrandmaService(WishListRepository wishListRepository , GardenService gardenService) { + private final GardenRepository gardenRepository; + - this.wishListRepository = wishListRepository; - this.gardenService = gardenService; - } public String growingTippDesTages(User user) { @@ -42,26 +42,7 @@ public class GrandmaService { } - public String notifyAboutPlantsReadyToGrow(User user){ - List<Plant> plantsReadyToGrow = getPlantsReadyToGrow(user); - if (plantsReadyToGrow.isEmpty()) { - return ""; - } - StringBuilder notification = new StringBuilder("Diese Pflanzen in deiner Wunschliste wären bereit zum Einpflanzen: "); - int size = plantsReadyToGrow.size(); - for (int i = 0; i < size; i++) { - notification.append(plantsReadyToGrow.get(i).getName()); - if (i < size - 2) { - notification.append(", "); - } else if (i == size - 2) { - notification.append(" und "); - } - } - return notification.toString(); - } - - - private List<Plant> getPlantsReadyToGrow( User user) { + public List<Plant> getPlantsReadyToGrow( User user) { LocalDate today = LocalDate.now(); int weekNumber = today.get(WeekFields.of(Locale.getDefault()).weekOfWeekBasedYear()); @@ -78,4 +59,71 @@ public class GrandmaService { return plantsReadyToGrow; } + + public List<GardenEntry> getGardenEntriesNeedingWatering(User user) { + List<GardenEntry> plantsNeedingWatering = new ArrayList<>(); + List<GardenEntry> plantsInUsersGarden = gardenRepository.findByUser(user); + if(plantsInUsersGarden.isEmpty()){ + return plantsNeedingWatering; + } + + for( int index = 1 ; index < plantsInUsersGarden.size();index++){ + if(plantsInUsersGarden.get(index).getWateringFrequency() == 0 ){ + switch (plantsInUsersGarden.get(index).getPlant().getWaterDemand()){ + case DRY -> { + if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 14) { + plantsNeedingWatering.add(plantsInUsersGarden.get(index)); + } + } + case WET -> { + if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 7) { + plantsNeedingWatering.add(plantsInUsersGarden.get(index)); + } + } + case VERY_WET -> { + if(getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > 3) { + plantsNeedingWatering.add(plantsInUsersGarden.get(index)); + } + } + } + }else if (getDaysSinceLastWatering(plantsInUsersGarden.get(index).getTimeSinceLastWatering()) > plantsNeedingWatering.get(index).getWateringFrequency()){ + plantsNeedingWatering.add(plantsInUsersGarden.get(index)); + } + } + + return plantsNeedingWatering; + + + } + + + private long getDaysSinceLastWatering(LocalDate lastWateringDate) { + if (lastWateringDate != null) { + LocalDate currentDate = LocalDate.now(); + return ChronoUnit.DAYS.between(lastWateringDate, currentDate); + } + return 0; + } + + public List<GardenEntry> getPlantsReadyToHarvest (User user){ + + + List<GardenEntry> usersPlants = gardenRepository.findByUser(user); + + List<GardenEntry> plantsReadyToHarvest = new ArrayList<>(); + + for(GardenEntry entry : usersPlants){ + int growthDuration = entry.getPlant().getGrowthDuration(); + LocalDate createdAt = entry.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate currentDate = LocalDate.now(); + if(growthDuration != 0 ) { + if (growthDuration >= ChronoUnit.WEEKS.between(createdAt, currentDate)) { + plantsReadyToHarvest.add(entry); + } + } + + } + return plantsReadyToHarvest; + + } } diff --git a/src/main/java/hdm/mi/growbros/service/WishListService.java b/src/main/java/hdm/mi/growbros/service/WishListService.java index a8a59b4edc54a9738db967e1fe84e00dadfa0af9..eaa82bff8c08f5a782d8d9612d0f2c3f9d9c7432 100644 --- a/src/main/java/hdm/mi/growbros/service/WishListService.java +++ b/src/main/java/hdm/mi/growbros/service/WishListService.java @@ -47,10 +47,12 @@ public class WishListService { } @Transactional - public void removeFromWishList(Long entryId, User user) { + public void removeFromWishList(Long plantId, User user) { + Long entryId = wishListRepository.findEntryIdByUserAndPlantId(user, plantId); wishListRepository.deleteByIdAndUser(entryId, user); } + @Transactional public int clearWishList(User user) { return wishListRepository.deleteAllByUser(user); diff --git a/src/test/java/hdm/mi/growbros/repositories/GardenRepositoryTest.java b/src/test/java/hdm/mi/growbros/repositories/GardenRepositoryTest.java index 6aa97c9ff6e3a85017ae3880d45f037e5bd7a8ae..7873ad501cf4e1af2bcf684a6d563eff7d698a6a 100644 --- a/src/test/java/hdm/mi/growbros/repositories/GardenRepositoryTest.java +++ b/src/test/java/hdm/mi/growbros/repositories/GardenRepositoryTest.java @@ -70,10 +70,10 @@ class GardenRepositoryTest { .harvestWeekEnd(30) .build(); entityManager.persist(p3); - ge1 = new GardenEntry(0L, p1, user1, null); - ge2 = new GardenEntry(0L, p2, user1, null); - ge3 = new GardenEntry(0L, p3, user1, null); - ge4 = new GardenEntry(0L, p3, user2, null); + ge1 = GardenEntry.builder().id(0L).plant(p1).user(user1).build(); + ge2 = GardenEntry.builder().id(0L).plant(p2).user(user1).build(); + ge3 = GardenEntry.builder().id(0L).plant(p3).user(user1).build(); + ge4 = GardenEntry.builder().id(0L).plant(p3).user(user2).build(); underTest.saveAll(List.of(ge1, ge2, ge3, ge4)); } diff --git a/src/test/java/hdm/mi/growbros/service/GardenServiceTest.java b/src/test/java/hdm/mi/growbros/service/GardenServiceTest.java index b672464d13dfd5083c5f9ad318c46f44f17a476d..b61a4b092a847327a26f1db4831ae2cefc61c5f6 100644 --- a/src/test/java/hdm/mi/growbros/service/GardenServiceTest.java +++ b/src/test/java/hdm/mi/growbros/service/GardenServiceTest.java @@ -48,7 +48,7 @@ class GardenServiceTest { when(plantRepository.findById(1L)).thenReturn(Optional.of(plant)); when(gardenRepository.save( any(GardenEntry.class)) - ).thenReturn(new GardenEntry(0L, plant, null, null)); + ).thenReturn(GardenEntry.builder().plant(plant).build()); //assert var result = gardenService.addPlantToGarden(1L, UserTestData.getUser1()); @@ -68,23 +68,23 @@ class GardenServiceTest { User user = UserTestData.getUser1(); - when(gardenRepository.findByUser(user, Sort.by("createdDate"))).thenReturn(mapPlantsToGardenEntries(orderedByCreatedMock, user)); + when(gardenRepository.findByUser(user, Sort.by("createdAt").descending())).thenReturn(mapPlantsToGardenEntries(orderedByCreatedMock, user)); when(gardenRepository.findByUser(user, Sort.by("plant.name"))).thenReturn(mapPlantsToGardenEntries(orderedByNameMock, user)); when(gardenRepository.findAllByNearestHarvestWeek(eq(user), any(int.class))).thenReturn(mapPlantsToGardenEntries(orderedByHarvestMock, user)); when(gardenRepository.findAllByNearestPlantingWeek(eq(user), any(int.class))).thenReturn(mapPlantsToGardenEntries(orderedByPlantDateMock, user)); assertAll( - () -> assertEquals(gardenService.getUserPlants(user, null), orderedByNameMock), - () -> assertEquals(gardenService.getUserPlants(user, "random stuff"), orderedByNameMock), - () -> assertEquals(gardenService.getUserPlants(user, "createdAt"), orderedByCreatedMock), - () -> assertEquals(gardenService.getUserPlants(user, "plantDate"), orderedByPlantDateMock), - () -> assertEquals(gardenService.getUserPlants(user, "harvestDate"), orderedByHarvestMock) + () -> assertEquals(orderedByNameMock, gardenService.getUserPlants(user, null)), + () -> assertEquals(orderedByNameMock, gardenService.getUserPlants(user, "random stuff")), + () -> assertEquals(orderedByCreatedMock, gardenService.getUserPlants(user, "createdAt")), + () -> assertEquals(orderedByPlantDateMock, gardenService.getUserPlants(user, "plantDate")), + () -> assertEquals(orderedByHarvestMock, gardenService.getUserPlants(user, "harvestDate")) ); } private List<GardenEntry> mapPlantsToGardenEntries(List<Plant> plants, User user) { return plants.stream() - .map(plant -> new GardenEntry(0L, plant, user, null)) + .map(plant -> GardenEntry.builder().plant(plant).id(0L).user(user).build()) .toList(); } } diff --git a/src/test/java/hdm/mi/growbros/service/PlantsServiceIntegrationTest.java b/src/test/java/hdm/mi/growbros/service/PlantsServiceIntegrationTest.java index b67724a08b4a083c7c2eb8a9ee40ea874741a88d..8538b5828a894e0104f25afa35b79f1907a2a2d6 100644 --- a/src/test/java/hdm/mi/growbros/service/PlantsServiceIntegrationTest.java +++ b/src/test/java/hdm/mi/growbros/service/PlantsServiceIntegrationTest.java @@ -7,8 +7,14 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @@ -29,6 +35,9 @@ class PlantsServiceIntegrationTest { @Test void canUpdatePlant() { + Authentication authentication = new UsernamePasswordAuthenticationToken("admin", "password", List.of(new SimpleGrantedAuthority("ADMIN"))); + SecurityContextHolder.getContext().setAuthentication(authentication); + System.out.println("set mock security context"); String newName = "new name!"; plantsService.updatePlant(plant.getId(), PlantUpdateRequest.builder().name(newName).build()); assertEquals(newName, plantRepository.findById(plant.getId()).get().getName());