diff --git a/client/react/src/App.css b/client/react/src/App.css index 594ba7a..ea70082 100644 --- a/client/react/src/App.css +++ b/client/react/src/App.css @@ -124,18 +124,16 @@ h1, h2 { } .left { - width: 128px; - display:inline; + width: 50%; float:left; } .left_button { - font-size: 112px !important; + font-size: 124px !important; } .right { - width: 256px; - display:inline; + width: 50%; float:right; } @@ -143,6 +141,10 @@ h1, h2 { outline: 0!important; } +.center { + margin-left: auto; + margin-right: auto;} + #back { position: fixed; left: 0; diff --git a/client/react/src/App.js b/client/react/src/App.js index 79259bd..e7c4899 100644 --- a/client/react/src/App.js +++ b/client/react/src/App.js @@ -8,7 +8,7 @@ import React from 'react'; import { Routes, Route } from 'react-router-dom'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; -import { green, pink, lightBlue, blueGrey, teal, brown } from '@mui/material/colors'; +import { green, pink, lightBlue, blueGrey, teal, brown, yellow, lightGreen} from '@mui/material/colors'; import Plants from './pages/Plants'; import System from './pages/System'; import Home from './pages/Home'; @@ -34,6 +34,15 @@ const darkTheme = createTheme({ }, repot: { main: brown[500], + }, + light: { + main: yellow[500] + }, + type: { + main: lightGreen[500] + }, + genus: { + main: teal[500] } }, }); diff --git a/client/react/src/forms/NewGenusForm.js b/client/react/src/forms/NewGenusForm.js index 42d184d..f335dc8 100644 --- a/client/react/src/forms/NewGenusForm.js +++ b/client/react/src/forms/NewGenusForm.js @@ -80,7 +80,7 @@ const NewGenusForm = ({ isOpen, onRequestClose }) => { - + @@ -98,6 +98,7 @@ const NewGenusForm = ({ isOpen, onRequestClose }) => { label="Genus Name" value={name} variant="standard" + color="genus" onChange={(event) => setName(event.target.value)} /> { label="Watering (days)" value={watering} onChange={(event) => setWatering(event.target.value)} + color="genus" variant="standard" /> diff --git a/client/react/src/forms/NewSystemForm.js b/client/react/src/forms/NewSystemForm.js index c363aa6..5d7ad4f 100644 --- a/client/react/src/forms/NewSystemForm.js +++ b/client/react/src/forms/NewSystemForm.js @@ -7,14 +7,21 @@ import ButtonGroup from '@mui/material/ButtonGroup'; import CheckSharpIcon from '@mui/icons-material/CheckSharp'; import CloseSharpIcon from '@mui/icons-material/CloseSharp'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import AvTimerSharpIcon from '@mui/icons-material/AvTimerSharp'; +import StraightenSharpIcon from '@mui/icons-material/StraightenSharp'; +import InvertColorsSharpIcon from '@mui/icons-material/InvertColorsSharp'; +import DeviceThermostatSharpIcon from '@mui/icons-material/DeviceThermostatSharp'; const NewSystemForm = ({ isOpen, onRequestClose }) => { const [name, setName] = useState(''); - const [humidity, setHumidity] = useState(0); - const [temperature, setTempurature] = useState(0); - const [duration, setDuration] = useState(0); - const [distance, setDistance] = useState(0); + const [description, setDescription] = useState('') + const [humidity, setHumidity] = useState(60); + const [temperature, setTempurature] = useState(68); + const [duration, setDuration] = useState(12); + const [distance, setDistance] = useState(24); const [allSystems, setAllSystems] = useState([]); @@ -48,7 +55,7 @@ const NewSystemForm = ({ isOpen, onRequestClose }) => { clearForm(); onRequestClose(); } - }, [submitted, name, temperature, humidity, distance, duration, onRequestClose]); + }, [submitted, name, description, temperature, humidity, distance, duration, onRequestClose]); const handleSubmit = (event) => { event.preventDefault(); @@ -62,6 +69,7 @@ const NewSystemForm = ({ isOpen, onRequestClose }) => { const clearForm = () => { setName(''); + setDescription(''); setTempurature(0); setHumidity(0); setDistance(0); @@ -69,6 +77,70 @@ const NewSystemForm = ({ isOpen, onRequestClose }) => { setSubmitted(false); }; + // Target temperature marks + const temperatureMarks = [ + { + value: 48, + label: '48', + }, + { + value: 68, + label: '68°F', + }, + { + value: 80, + label: '80°F', + } + ]; + + // Humidity field marks + const humidityMarks = [ + { + value: 0, + label: '0%', + }, + { + value: 60, + label: '60%', + }, + { + value: 100, + label: '100%', + } + ]; + + // Duration field marks + const durationMarks = [ + { + value: 6, + label: '6', + }, + { + value: 12, + label: '12', + }, + { + value: 18, + label: '18', + } + ]; + + // Lighting field marks + const distanceMarks = [ + { + value: 12, + label: '12"', + }, + { + value: 24, + label: '24"', + }, + { + value: 36, + label: '36"', + } + ]; + return ( { border: 'none', }} > - + - - - - - - - - - - - - - setName(event.target.value)} - /> - setHumidity(event.target.value)} - variant="standard" - /> - setTempurature(event.target.value)} - variant="standard" - /> - setDuration(event.target.value)} - variant="standard" - /> - setDistance(event.target.value)} - variant="standard" - /> - + + + + + + + + + + + + + + setName(event.target.value)} + color='info' + /> + setDescription(event.target.value)} + variant="standard" + fullWidth + margin="normal" + color='info' + /> + + + + + + setHumidity(event.target.value)} + variant="standard" + defaultValue={12} + step={1} + marks={humidityMarks} + min={0} + max={100} + valueLabelDisplay="auto" + /> + + + + setTempurature(event.target.value)} + variant="standard" + step={2} + marks={temperatureMarks} + min={48} + max={80} + valueLabelDisplay="auto" + /> + + + + setDuration(event.target.value)} + variant="standard" + defaultValue={12} + step={1} + marks={durationMarks} + min={6} + max={18} + valueLabelDisplay="auto" + /> + + + + setDistance(event.target.value)} + variant="standard" + defaultValue={24} + step={2} + marks={distanceMarks} + min={12} + max={36} + valueLabelDisplay="auto" + /> + + diff --git a/client/react/src/forms/NewTypeForm.js b/client/react/src/forms/NewTypeForm.js new file mode 100644 index 0000000..d1433d4 --- /dev/null +++ b/client/react/src/forms/NewTypeForm.js @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import Modal from '@mui/material/Modal'; +import TextField from '@mui/material/TextField'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import CheckSharpIcon from '@mui/icons-material/CheckSharp'; +import CloseSharpIcon from '@mui/icons-material/CloseSharp'; +import CallSplitSharpIcon from '@mui/icons-material/CallSplitSharp'; +import Autocomplete from '@mui/material/Autocomplete'; + + +const NewTypeForm = ({ isOpen, onRequestClose }) => { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [genus, setGenus] = useState(''); + + const [allGenuses, setAllGenuses] = useState([]); + + const [submitted, setSubmitted] = useState(false); // Initialize submitted state + + useEffect(() => { + if (isOpen) { + fetch('http://127.0.0.1:5000/genus') + .then((response) => response.json()) + .then((data) => setAllGenuses(data)) + .catch((error) => console.error('Error fetching genus data:', error)); + } + }, []); + + useEffect(() => { + if (submitted) { + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, description: description, genus_id: genus.id }) + }; + fetch('http://127.0.0.1:5000/type', requestOptions) + .then(response => response.json()) + .then(data => { + // handle the response data if needed + // maybe update some state based on the response + console.log(data); + }) + .catch(error => console.error('Error posting type data:', error)); + clearForm(); + onRequestClose(); + } + }, [submitted, name, description, genus, onRequestClose]); + + const handleSubmit = (event) => { + event.preventDefault(); + if (allGenuses.find(genus => genus.name === name)) { + return; + } + setSubmitted(true); // Update submitted state + }; + + const handleCancel = () => { + clearForm(); + onRequestClose(); + }; + + const clearForm = () => { + setName(''); + setDescription(''); + setGenus(null); + setSubmitted(false); + }; + + return ( + + + + + + + + + + + + + + + + setName(event.target.value)} + /> + setDescription(event.target.value)} + /> + option.name)} + onChange={(event) => setGenus(allGenuses[event.target.value])} + renderInput={(params) => ( + + )} + /> + + + + + ); +}; + +export default NewTypeForm; + + diff --git a/client/react/src/pages/Home.js b/client/react/src/pages/Home.js index 9c5375e..126f9ce 100644 --- a/client/react/src/pages/Home.js +++ b/client/react/src/pages/Home.js @@ -16,8 +16,10 @@ import CloseSharpIcon from '@mui/icons-material/CloseSharp'; import VisibilitySharpIcon from '@mui/icons-material/VisibilitySharp'; import ReportGmailerrorredSharpIcon from '@mui/icons-material/ReportGmailerrorredSharp'; import NewLightForm from '../forms/NewLightForm'; +import NewTypeForm from '../forms/NewTypeForm'; import TungstenSharpIcon from '@mui/icons-material/TungstenSharp'; - +import Modal from '@mui/material/Modal'; +import CallSplitSharpIcon from '@mui/icons-material/CallSplitSharp'; const Home = () => { // Navigation @@ -34,6 +36,7 @@ const Home = () => { const [isNewSystemFormOpen, setIsNewSystemFormOpen] = useState(false); const [isNewGenusFormOpen, setIsNewGenusFormOpen] = useState(false); const [isNewLightFormOpen, setIsNewLightFormOpen] = useState(false); + const [isNewTypeFormOpen, setIsNewTypeFormOpen] = useState(false); useEffect(() => { // Fetch plant data from the server @@ -64,45 +67,98 @@ const Home = () => { setIsViewButtonsOpen(true)}> - setIsCreateButtonsOpen(true)}> + console.log("WIP")}> {isCreateButtonsOpen && ( - - setIsNewPlantFormOpen(true)}> - - - setIsNewSystemFormOpen(true)}> - - - setIsNewGenusFormOpen(true)}> - - - setIsNewLightFormOpen(true)}> - - - setIsCreateButtonsOpen(false)}> - - - + + + + setIsNewPlantFormOpen(true)}> + + + setIsNewTypeFormOpen(true)}> + + + setIsNewGenusFormOpen(true)}> + + + + + setIsNewSystemFormOpen(true)}> + + + setIsNewLightFormOpen(true)}> + + + + + setIsCreateButtonsOpen(false)}> + + + + + )} {isViewButtonsOpen && ( - - { navigate("/plants")}}> - - - { navigate("/systems")}}> - - - setIsViewButtonsOpen(false)}> - - - + + + + { navigate("/plants")}}> + + + { navigate("/systems")}}> + + + setIsViewButtonsOpen(false)}> + + + + + )} - - setIsNewPlantFormOpen(false)} @@ -119,6 +175,10 @@ const Home = () => { isOpen={isNewLightFormOpen} onRequestClose={() => setIsNewLightFormOpen(false)} /> + setIsNewTypeFormOpen(false)} + /> > ); diff --git a/client/react/src/pages/System.js b/client/react/src/pages/System.js index f8e5a72..39eeded 100644 --- a/client/react/src/pages/System.js +++ b/client/react/src/pages/System.js @@ -7,6 +7,12 @@ import TungstenSharpIcon from '@mui/icons-material/TungstenSharp'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import { CardActionArea, CardHeader } from '@mui/material'; +import Avatar from '@mui/material/Avatar'; +import GrassOutlinedIcon from '@mui/icons-material/GrassOutlined'; +import { green } from '@mui/material/colors'; +import IconButton from '@mui/material/IconButton'; +import ReportGmailerrorredSharpIcon from '@mui/icons-material/ReportGmailerrorredSharp'; +import CardActions from '@mui/material/CardActions'; const System = ({ system, full }) => { if (!system) { @@ -51,37 +57,53 @@ const System = ({ system, full }) => { + + + } title={system.name} + subheader={system.created_on} /> - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ); diff --git a/server/app.py b/server/app.py index 2f015be..a94fb3d 100644 --- a/server/app.py +++ b/server/app.py @@ -15,7 +15,7 @@ from sqlalchemy.engine import URL # Local application imports -from models.plant import Plant, Genus +from models.plant import Plant, Genus,Base, Type from models.system import System, Light # Load database configuration from JSON file @@ -33,8 +33,8 @@ ) engine = create_engine(url) -# Base.metadata.drop_all(engine) -# Base.metadata.create_all(engine) +Base.metadata.drop_all(engine) +Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) @@ -143,6 +143,45 @@ def create_genus(): return jsonify({"message": "Genus added successfully"}), 201 +@app.route("/type", methods=["GET"]) +def get_types(): + """ + Retrieve all types from the database. + """ + logger.info("Received request to retrieve all plant types") + + session = Session() + types = session.query(Type).all() + session.close() + # Transform types to JSON format + types_json = [_type.to_json() for _type in types] + # Return JSON response + return jsonify(types_json) + + +@app.route("/type", methods=["POST"]) +def create_type(): + """ + Create a new type and add it to the database. + """ + logger.info("Attempting to create type") + + new_type_data = request.get_json() + + # Create a new Type object + new_type = Type( + name=new_type_data["name"], + description=new_type_data["description"], + genus_id=new_type_data["genus_id"] + ) + + # Add the new type object to the session + db = Session() + db.add(new_type) + db.commit() + db.close() + + return jsonify({"message": "Type added successfully"}), 201 @app.route("/system", methods=["GET"]) def get_systems(): @@ -175,7 +214,8 @@ def create_system(): temperature=new_system_json["temperature"], humidity=new_system_json["humidity"], duration=new_system_json["duration"], - distance=new_system_json["distance"] + distance=new_system_json["distance"], + description=new_system_json["description"] ) # Add the new system object to the session diff --git a/server/data/genuses/cactus_genuses.csv b/server/data/genuses/cactus_genuses.csv new file mode 100644 index 0000000..3274d21 --- /dev/null +++ b/server/data/genuses/cactus_genuses.csv @@ -0,0 +1,2 @@ +Acanthocereus tetragonus (FairY Castle Castle) +Cephalocereus senilis (Old Man Cactus) \ No newline at end of file diff --git a/server/data/genuses/succulent_genuses.csv b/server/data/genuses/succulent_genuses.csv new file mode 100644 index 0000000..bedb458 --- /dev/null +++ b/server/data/genuses/succulent_genuses.csv @@ -0,0 +1,93 @@ +Agave +Yucca +Argyroderma +Cheiridopsis +Conophytum +Dactylopis +Faucaria +Fenestraria +Frithia +Glottiphyllum +Lapidaria +Lithops +Nananthus +Pleisopilos +Titanopsis +Delosperma +Mestoklema +Trichodiadema +Sphalmanthus +Aloe +Astroloba +Gasteria +Haworthia +Adenium (Desert Rose) +Pachypodium (Madagascar Palm) +Plumeria (Frangipani) +Caralluma +Duvalia +Edithcolea +Hoodia +Huernia +Orbea +Piranthus +Stapelia +Tavaresia +Brachystema +Ceropegia +Hoya (String-of-Hearts, Rosary-Vine, Wax Vine, Hindu Rope) +Dyckia +Hechtia +Orthophytum +Tillandsia +Opuntia (Pricklypear) +Cylindropuntia (cholla) +Tephrocactus +Ariocarpus +Astrophytum +Borzicactus +Copiapoa +Coryphantha +Echinocactus +Echinocereus +Echinopsis +Ferocactus +Frailea +Gymnocalycium +Lobivia +Mammillaria +Neoporteria +Notocactus +Parodia +Rebutia +Sulcorebutia +Discocactus +Melocactus +Carnegia +Cephalocereus +Cereus +Nyctocereus +Pachycereus +Cleistocactus +Espostoa +Oreocereus +Disocactus +Epiphyllum +xEpicactus +Rhipsalis +Schlumbergera +Selenicereus (Christmas Cactus) +Adromischus +Cotyledon +Crassula +Kalanchoe +Tylecodon +Dudleya +Echeveria +Graptopetalum +Pachyphytum +Tacitus +Aeonium +Sempervivum +Sedum +Euphorbia \ No newline at end of file diff --git a/server/models/plant.py b/server/models/plant.py index 4382372..110416b 100644 --- a/server/models/plant.py +++ b/server/models/plant.py @@ -20,10 +20,12 @@ class Plant(Base): __tablename__ = "plant" id = Column(Integer(), primary_key=True) - name = Column(String(100), nullable=False) created_on = Column(DateTime(), default=datetime.now) cost = Column(Integer(), default=0, nullable=False) size = Column(Integer(), default=0, nullable=False) # inches + type_id: Mapped[int] = mapped_column( + ForeignKey("type.id", ondelete="CASCADE") + ) # Type of Genus genus_id: Mapped[int] = mapped_column( ForeignKey("genus.id", ondelete="CASCADE") ) # Genus of Plant @@ -39,7 +41,7 @@ def __repr__(self) -> str: return f"{self.id}" def to_json(self): - """Convert to json for front end.""" + """Convert to json.""" return { "id": self.id, "name": self.name, @@ -52,6 +54,39 @@ def to_json(self): "system_id": self.system_id, } +class Type(Base): + """Type of genus""" + + __tablename__ = "type" + + id = Column(Integer(), primary_key=True) + created_on = Column(DateTime(), default=datetime.now) + name = Column(String(100), nullable=False, unique=True) + description = Column(String(400), nullable=True) + updated_on = Column(DateTime(), nullable=True, onupdate=datetime.now) + + genus_id: Mapped[int] = mapped_column( + ForeignKey("genus.id", ondelete="CASCADE") + ) # Genus of Plant + + plants: Mapped[List["Plant"]] = relationship( + "Plant", backref="genus", passive_deletes=True + ) # Available plants of this type + + def __repr__(self) -> str: + return f"{self.name}" + + def to_json(self): + """Convert to json.""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "created_on": self.created_on, + "updated_on": self.updated_on, + "genus_id": self.genus_id, + "system_id": self.system_id, + } class Genus(Base): """Genus of plant.""" @@ -61,16 +96,28 @@ class Genus(Base): id = Column(Integer(), primary_key=True) created_on = Column(DateTime(), default=datetime.now) name = Column(String(100), nullable=False, unique=True) + description = Column(String(400), nullable=True) watering = Column(Integer(), nullable=False) # days updated_on = Column(DateTime(), nullable=True, onupdate=datetime.now) + types: Mapped[List["Type"]] = relationship( + "Type", backref="genus", passive_deletes=True + ) # Available types of this genus + plants: Mapped[List["Plant"]] = relationship( "Plant", backref="genus", passive_deletes=True - ) # Available plants of this genus + ) # Available plants of this type def __repr__(self) -> str: return f"{self.name}" def to_json(self): - """Convert to json for front end.""" - return {"id": self.id, "name": self.name, "watering": self.watering} + """Convert to json.""" + return { + "id": self.id, + "name": self.name, + "watering": self.watering, + "description": self.description, + "created_on": self.created_on, + "updated_on": self.updated_on + } diff --git a/server/models/system.py b/server/models/system.py index b5ef602..1842521 100644 --- a/server/models/system.py +++ b/server/models/system.py @@ -35,7 +35,7 @@ class System(Base): distance = Column(Integer(), nullable=False) # inches lights: Mapped[List["Light"]] = relationship( "Light", backref="system", passive_deletes=True - ) # Available plants of this system + ) # Available lights of this system def __repr__(self) -> str: return f"{self.name}" @@ -47,6 +47,7 @@ def to_json(self): "name": self.name, "created_on": self.created_on, "updated_on": self.updated_on, + "description": self.description, "humidity": self.humidity, "temperature": self.temperature, "duration": self.duration, @@ -60,7 +61,8 @@ class Light(Base): __tablename__ = "light" id = Column(Integer(), primary_key=True) - name = Column(String(100), nullable=False) + name = Column(String(100), nullable=True) + description = Column(String(400), nullable=False) created_on = Column(DateTime(), default=datetime.now) updated_on = Column(DateTime(), default=datetime.now) cost = Column(Integer())