From e91ac886c73d3093fc168a549841e5928d5e8c05 Mon Sep 17 00:00:00 2001 From: Hannah <hz018@hdm-stuttgart.de> Date: Tue, 16 Jan 2024 21:38:24 +0100 Subject: [PATCH] #51 show growingTip, plantsReadyToGrow,plantsReadyToWater and plantsReadyToHarvest + fix bug in schemas.ts (imageUrl can now be null) + merged main into my branch --- CHANGELOG.md | 32 ++-- growbros-frontend/package-lock.json | 28 +++ growbros-frontend/package.json | 5 + .../src/components/DropDownFilter.tsx | 55 +++--- .../src/components/FilterPage.tsx | 95 +++++++-- growbros-frontend/src/components/Grandma.tsx | 49 +++-- .../src/components/PlantDetails.tsx | 87 ++++----- .../src/components/StatusMessage.tsx | 30 +++ growbros-frontend/src/index.css | 12 +- growbros-frontend/src/pages/Garten.tsx | 101 +++++++--- growbros-frontend/src/pages/Suche.tsx | 181 +++++++++++++----- growbros-frontend/src/pages/Wunschliste.tsx | 110 ++++++----- growbros-frontend/src/stylesheets/Home.css | 8 +- growbros-frontend/src/stylesheets/Navbar.css | 2 +- .../src/stylesheets/RegisterAndLogin.css | 2 - .../src/stylesheets/StatusMessage.css | 22 +++ .../src/utils/BackendConnectorImpl.ts | 23 ++- .../src/utils/IBackendConnector.ts | 4 +- growbros-frontend/src/utils/commonTypes.d.ts | 12 +- growbros-frontend/src/utils/schemas.ts | 77 ++++++-- .../controllers/GardenController.java | 6 +- .../controllers/WishListController.java | 6 +- .../hdm/mi/growbros/models/WishListEntry.java | 14 +- .../repositories/GardenRepository.java | 7 + .../repositories/WishListRepository.java | 14 ++ .../mi/growbros/service/GardenService.java | 3 +- .../mi/growbros/service/GrandmaService.java | 4 +- .../mi/growbros/service/WishListService.java | 4 +- 28 files changed, 692 insertions(+), 301 deletions(-) create mode 100644 growbros-frontend/src/components/StatusMessage.tsx create mode 100644 growbros-frontend/src/stylesheets/StatusMessage.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ada303..df898cc 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 277b0d4..28e79d6 100644 --- a/growbros-frontend/package-lock.json +++ b/growbros-frontend/package-lock.json @@ -14,11 +14,14 @@ "@mui/material": "^5.15.4", "font-awesome": "^4.7.0", "jwt-decode": "^4.0.0", + "mui": "^0.0.1", "rc-slider": "^10.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", "react-modal": "^3.16.1", "react-router-dom": "^6.18.0", + "reactjs-popup": "^2.0.6", "zod": "^3.22.4" }, "devDependencies": { @@ -2662,6 +2665,11 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/mui": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/mui/-/mui-0.0.1.tgz", + "integrity": "sha512-iB9zfxsJBcMkZ/SY6X+HGSPr4fftCZIQ76ZMH8iSMfVkidVzRtZlLW2gbWXUe+IMcj8JLv1p+dGKvPVlgtiocA==" + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -2970,6 +2978,14 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -3043,6 +3059,18 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reactjs-popup": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.6.tgz", + "integrity": "sha512-A+tt+x9wdgZiZjv0e2WzYLD3IfFwJALaRaqwrCSXGjo0iQdsry/EtBEbQXRSmQs7cHmOi5eytCiSlOm8k4C+dg==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/growbros-frontend/package.json b/growbros-frontend/package.json index 76963cb..d83e50f 100644 --- a/growbros-frontend/package.json +++ b/growbros-frontend/package.json @@ -10,8 +10,13 @@ "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", + "mui": "^0.0.1", "rc-slider": "^10.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/growbros-frontend/src/components/DropDownFilter.tsx b/growbros-frontend/src/components/DropDownFilter.tsx index 655574c..41bb750 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 ebee59d..81657c8 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/Grandma.tsx b/growbros-frontend/src/components/Grandma.tsx index 87f4a4f..1267a8c 100644 --- a/growbros-frontend/src/components/Grandma.tsx +++ b/growbros-frontend/src/components/Grandma.tsx @@ -1,5 +1,5 @@ import 'reactjs-popup/dist/index.css'; -import {useEffect, useState} from "react"; +import React, {useEffect, useState} from "react"; import {checkJwtStatus} from "../jwt/Cookies.ts"; import "../stylesheets/GrandmaPopup.css" import {Link} from "react-router-dom"; @@ -12,12 +12,12 @@ function Grandma() { const [showText, setShowText] = useState<boolean>(false); const [growingTip, setGrowingTip] = useState<string>(''); - const [plantsReadyToGrow, setPlantsReadyToGrow] = useState<string>(''); - const [plantsReadyToWater, setPlantsReadyToWater] = useState<string>(''); - const [plantsReadyToHarvest, setPlantsReadyToHarvest] = useState<string>(''); + const [plantsReadyToGrow, setPlantsReadyToGrow] = useState<React.ReactNode>(); + const [plantsReadyToWater, setPlantsReadyToWater] = useState<React.ReactNode>(); + const [plantsReadyToHarvest, setPlantsReadyToHarvest] = useState<React.ReactNode>(); const [error, setError] = useState<object>(); - const [currentDisplay, setCurrentDisplay] = useState<string>(growingTip); + const [currentDisplay, setCurrentDisplay] = useState<string | React.ReactNode>(growingTip); const [displayIndex, setDisplayIndex] = useState<number>(0); const bc: IBackendConnector = useGrowbrosFetcher(); @@ -92,10 +92,20 @@ function Grandma() { const plants: Plant[] = response.value ?? []; if (plants.length === 0) { console.log("no plants ready to grow"); - setPlantsReadyToGrow("Füge Pflanzen die du gerne in der Zukunft einpflanzen möchtest deiner Wunschliste hinzu.") + setPlantsReadyToGrow( + <> + Im Moment sind keine deiner Pflanzen auf deiner Wunschliste bereit eingepflanzt zu werden. + Falls du Pflanzen deiner Wunschliste hinzufügen möchtest, kannst du das <Link to="/suchen">hier</Link>. + </> + ); } else { const plantNames = plants.map(plant => plant.name).join(', '); - setPlantsReadyToGrow("Folgende Pflanzen aus deiner Wunschliste wären bereit eingepflanzt zu werden:" + plantNames); + setPlantsReadyToGrow( + <> + Folgende Pflanzen aus deiner <Link to="/wunschlist">Wunschliste</Link> wären bereit eingepflanzt zu werden: <br /> <span>{plantNames}</span> + </> + ); + } } } @@ -108,10 +118,19 @@ function Grandma() { const plants: Plant[] = response.value ?? []; if(plants.length === 0) { console.log("no plants ready to water"); - setPlantsReadyToWater("Du hast noch keine Pflanzen in deinem Garten. Füge Pflanzen die du bereits eingepflanzt hast hinzu.") + setPlantsReadyToWater( + <> + Im Moment muss keine deiner Pflanzen im Garten gegossen werden. + Schau doch mal <Link to="/suchen">hier</Link> welche Pflanzen du deinem Garten hinzufügen möchtest. + </> + ) } else { const plantNames = plants.map(plant => plant.name).join(', '); - setPlantsReadyToWater("Folgende Pflanzen in deinem Garten müssen gegossen werden:" + plantNames); + setPlantsReadyToWater( + <> + Folgende Pflanzen in deinem <Link to="/garten">Garten</Link> müssen gegossen werden: <br /> <span>{plantNames}</span> + </> + ) } } } @@ -124,10 +143,18 @@ function Grandma() { const plants: Plant[] = response.value ?? []; if(plants.length === 0) { console.log("no plants ready to harvest"); - setPlantsReadyToHarvest("Du hast noch keine Pflanzen in deinem Garten. Füge Pflanzen die du bereits eingepflanzt hast hinzu.") + setPlantsReadyToHarvest( + <> + Im Moment ist noch keine deiner Pflanzen bereit geerntet zu werden. Habe noch etwas Geduld. + </> + ) } else { const plantNames = plants.map(plant => plant.name).join(', '); - setPlantsReadyToHarvest("Folgende Pflanzen in deinem Garten können geerntet werden:" + plantNames); + setPlantsReadyToHarvest( + <> + Schau doch mal in deinem <Link to="/garten">Garten</Link> nach. Folgende Pflanzen in deinem Garten sind eventuell bereit geerntet zu werden: <br /> <span>{plantNames}</span> + </> + ); } } } diff --git a/growbros-frontend/src/components/PlantDetails.tsx b/growbros-frontend/src/components/PlantDetails.tsx index cb56451..894ae27 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 0000000..13b79e0 --- /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/index.css b/growbros-frontend/src/index.css index 37f5c1d..5d55461 100644 --- a/growbros-frontend/src/index.css +++ b/growbros-frontend/src/index.css @@ -3,7 +3,6 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; @@ -66,3 +65,14 @@ button:focus-visible { background-color: #f9f9f9; } } + +@media (prefers-color-scheme: dark) { + :root { + color: #213547; + background-color: #ffffff; + } + + button, input { + background-color: #f9f9f9; + } +} diff --git a/growbros-frontend/src/pages/Garten.tsx b/growbros-frontend/src/pages/Garten.tsx index 3c083a1..492d0be 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 99c843a..292261c 100644 --- a/growbros-frontend/src/pages/Suche.tsx +++ b/growbros-frontend/src/pages/Suche.tsx @@ -1,68 +1,147 @@ 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); + console.log(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 2a69cac..21fb741 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/Home.css b/growbros-frontend/src/stylesheets/Home.css index c7b6e9d..78cea5d 100644 --- a/growbros-frontend/src/stylesheets/Home.css +++ b/growbros-frontend/src/stylesheets/Home.css @@ -15,7 +15,7 @@ color: #4d5927; } .descriptionAside h1{ - font-family: Garamond; + font-family: Garamond, sans-serif; } .descriptionAside h2 { padding-top: 5px; @@ -23,7 +23,7 @@ .descriptionAside p { padding-top: 20px; - font-family: Century Gothic; + font-family: Century Gothic, sans-serif; } @@ -36,7 +36,7 @@ padding: 10%; font-size: 1.5rem; font-weight: 500; - font-family: Century Gothic; + font-family: Century Gothic, sans-serif; } .smallDiv { @@ -46,7 +46,7 @@ .smallDiv p { padding-top: 10px; - font-family: Century Gothic; + font-family: Century Gothic, sans-serif; font-size: 1.25rem; } diff --git a/growbros-frontend/src/stylesheets/Navbar.css b/growbros-frontend/src/stylesheets/Navbar.css index c38cb58..f2c831f 100644 --- a/growbros-frontend/src/stylesheets/Navbar.css +++ b/growbros-frontend/src/stylesheets/Navbar.css @@ -20,7 +20,7 @@ ul { } nav .title { - font-family: Garamond; + font-family: Garamond, sans-serif; color: rgb(0, 0, 0); font-size: 3rem; font-weight: bold; diff --git a/growbros-frontend/src/stylesheets/RegisterAndLogin.css b/growbros-frontend/src/stylesheets/RegisterAndLogin.css index 95ddc19..7c632cd 100644 --- a/growbros-frontend/src/stylesheets/RegisterAndLogin.css +++ b/growbros-frontend/src/stylesheets/RegisterAndLogin.css @@ -22,7 +22,6 @@ form { width: 60%; background-color: #f9f9f9; border-radius: 5px; - color: #333; } .input:hover{ @@ -35,7 +34,6 @@ form { background-color: #f9f9f9; font-size: 1em; font-weight: bold; - } .submitButton:hover { diff --git a/growbros-frontend/src/stylesheets/StatusMessage.css b/growbros-frontend/src/stylesheets/StatusMessage.css new file mode 100644 index 0000000..5798c65 --- /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 7748367..74a3d04 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}; } @@ -261,12 +265,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}; } @@ -318,6 +322,7 @@ export class BackendConnectorImpl implements IBackendConnector { plants.push(PlantSchema.parse(p)) } catch (e) { if (e instanceof ZodError) { + console.log(result.err) result = {...result, err: "One or more plants could not be parsed correctly."} } } diff --git a/growbros-frontend/src/utils/IBackendConnector.ts b/growbros-frontend/src/utils/IBackendConnector.ts index e8d8176..91cf9bc 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>>; diff --git a/growbros-frontend/src/utils/commonTypes.d.ts b/growbros-frontend/src/utils/commonTypes.d.ts index db20786..02091e4 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 48fec4a..73f7cf2 100644 --- a/growbros-frontend/src/utils/schemas.ts +++ b/growbros-frontend/src/utils/schemas.ts @@ -6,6 +6,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(), @@ -21,47 +31,88 @@ export const PlantSchema = z.object({ waterDemand: waterDemand.transform(s=>translateWaterDemand(s)).nullable(), lightingDemand: lightingDemand.transform(s=>translateLightingDemand(s)).nullable(), nutrientDemand: nutrientDemand.transform(s=>translateNutrientDemand(s)).nullable(), - imageUrl: z.string().optional() + imageUrl: z.string().optional().nullable() }); 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 db636f8..3feeca0 100644 --- a/src/main/java/hdm/mi/growbros/controllers/GardenController.java +++ b/src/main/java/hdm/mi/growbros/controllers/GardenController.java @@ -45,12 +45,12 @@ public class GardenController { return ResponseEntity.status(201).body(plantAdded); } - @DeleteMapping("/remove/{entryId}") + @DeleteMapping("/remove/{plantId}") public ResponseEntity<Void> removePlantFromGarden( - @PathVariable Long entryId, + @PathVariable Long plantId, @AuthenticationPrincipal User user ) { - gardenService.removeFromGarden(entryId, user); + gardenService.removeFromGarden(plantId, user); return ResponseEntity.status(204).build(); } diff --git a/src/main/java/hdm/mi/growbros/controllers/WishListController.java b/src/main/java/hdm/mi/growbros/controllers/WishListController.java index 54f4066..54b9c78 100644 --- a/src/main/java/hdm/mi/growbros/controllers/WishListController.java +++ b/src/main/java/hdm/mi/growbros/controllers/WishListController.java @@ -39,11 +39,11 @@ public class WishListController { return ResponseEntity.status(201).build(); } - @DeleteMapping("/remove/{entryId}") + @DeleteMapping("/remove/{plantId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void removePlantFromWishlist(@PathVariable Long entryId, + public void removePlantFromWishlist(@PathVariable Long plantId, @AuthenticationPrincipal User user) { - wishListService.removeFromWishList(entryId, user); + wishListService.removeFromWishList(plantId, user); } @DeleteMapping("/remove/all") diff --git a/src/main/java/hdm/mi/growbros/models/WishListEntry.java b/src/main/java/hdm/mi/growbros/models/WishListEntry.java index b7f04c2..a8254b6 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 9395c7e..8e6dee7 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 683844f..206b4fa 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 b851da7..e04e5ec 100644 --- a/src/main/java/hdm/mi/growbros/service/GardenService.java +++ b/src/main/java/hdm/mi/growbros/service/GardenService.java @@ -44,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); } diff --git a/src/main/java/hdm/mi/growbros/service/GrandmaService.java b/src/main/java/hdm/mi/growbros/service/GrandmaService.java index 5b7fc89..adc0183 100644 --- a/src/main/java/hdm/mi/growbros/service/GrandmaService.java +++ b/src/main/java/hdm/mi/growbros/service/GrandmaService.java @@ -29,10 +29,10 @@ public class GrandmaService { String tipDesTages; Random random = new Random(); if(usersPlants.isEmpty()) { - return "Du hast noch keine Pflanzen in deinem Garten. Füge Pflanzen die du bereits eingepflanzt hast hinzu."; + return "Hallo ich bin Grandma! Schön, dass du GrowBros nutzt um deinen Garten zu planen."; } else { int randomNumber = random.nextInt(usersPlants.size()); - tipDesTages = "Tip des Tages für deinen Garten: " + usersPlants.get(randomNumber).getGrowingTips(); + tipDesTages = "Tip des Tages für deinen Garten: \n " + usersPlants.get(randomNumber).getGrowingTips(); } return tipDesTages; } diff --git a/src/main/java/hdm/mi/growbros/service/WishListService.java b/src/main/java/hdm/mi/growbros/service/WishListService.java index a8a59b4..eaa82bf 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); -- GitLab