diff --git a/client/components/App.jsx b/client/components/App.jsx index 73b357e..1a66ebf 100644 --- a/client/components/App.jsx +++ b/client/components/App.jsx @@ -1,9 +1,20 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'; import FeatureContainer from '../containers/FeaturePageContainer'; +import Login from './Login'; + function App() { + const [globalUser, setGlobalUser] = useState(''); return ( - + + + + } /> + : null} /> + + + ); } diff --git a/client/components/DropDown.jsx b/client/components/DropDown.jsx index d4f4b8c..1e49ac9 100644 --- a/client/components/DropDown.jsx +++ b/client/components/DropDown.jsx @@ -5,7 +5,8 @@ function DropDown(props) { const { ailment, handleChange, handleClick } = props; return ( -
+
+
+ + + + + + + +
+ ); +} +export default Login; diff --git a/client/components/Table.jsx b/client/components/Table.jsx index b417468..84e3606 100644 --- a/client/components/Table.jsx +++ b/client/components/Table.jsx @@ -12,18 +12,31 @@ function Table(props) { ); - + // row is the particular food object + // add name + // iterate over nutrients + // add a cell for each nutrient + // wrap quantity+unit in a td tag const tableBody = rows.map((row) => ( - {dataProperties.map((property) => - // if (column === 'linkedinValue') { - // return {row[column]}; - // } - // if (column === 'lastConnectionValue' || column === 'nextConnectionValue') { - // const date = new Date(row[column]); - // return {date.toDateString()}; - // } - {row[property]})} + + {[{row.name}, ...row.nutrients.map((property) => { + console.log('property', property); + if (!property) { + return N/A; + } + return {`${property.quantity ? property.quantity.toFixed(2) : 0} ${property.unit}`}; + }, + + // if (column === 'linkedinValue') { + // return {row[column]}; + // } + // if (column === 'lastConnectionValue' || column === 'nextConnectionValue') { + // const date = new Date(row[column]); + // return {date.toDateString()}; + // } + ), + ]} )); diff --git a/client/containers/FeaturePageContainer.jsx b/client/containers/FeaturePageContainer.jsx index bfc71b8..18b71c6 100644 --- a/client/containers/FeaturePageContainer.jsx +++ b/client/containers/FeaturePageContainer.jsx @@ -1,11 +1,38 @@ import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import Table from '../components/Table'; import DropDown from '../components/DropDown'; -function FeatureContainer() { - const tableHeaders = ['Insert', 'food', 'headers']; - const tableProperties = ['Insert', 'food', 'properties']; +function FeatureContainer(props) { + // const tableProperties = ['Insert', 'food', 'properties']; const [ailment, setAilment] = React.useState('headache'); + const navigate = useNavigate(); + + const tableProperties = [ + 'CA', + 'K', + 'FE', + 'ZN', + 'VITA_RAE', + 'VITC', + 'VITB12', + 'VITD', + 'TOCPHA', + 'NIA', + ]; + const tableHeaders = [ + 'Name', + 'Calcium', + 'Potassium', + 'Iron', + 'Zinc', + 'Vitamin A', + 'Vitamin C', + 'Vitamin B-12', + 'Vitamin D', + 'Vitamin E', + 'Niacin', + ]; const [foodEntries, setFoodEntries] = useState([]); @@ -13,22 +40,60 @@ function FeatureContainer() { setAilment(event.target.value); }; + const handleLogout = () => { + props.setGlobalUser(''); + navigate('/'); + } + const handleClick = () => { + console.log('ailment', ailment); fetch('http://localhost:3000/search', { method: 'POST', + body: JSON.stringify({ ailment }), + headers: { 'Content-Type': 'application/json' }, }) .then((res) => res.json()) .then((data) => { - setFoodEntries(data); + // parse the data that has been sent back + const parsedData = []; + data.forEach((food) => { + const foodRow = { + name: food.ingredients[0].text, + nutrients: [], + }; + tableProperties.forEach((prop) => { + foodRow.nutrients.push(food.totalNutrients[prop]); + }); + parsedData.push(foodRow); + }); + console.log('parsedData', parsedData); + setFoodEntries(parsedData); }) .catch((err) => console.log(err)); }; return ( <> -

Placeholder

- - + + + +
); } diff --git a/client/index.js b/client/index.js index 461de4b..c95dd5b 100644 --- a/client/index.js +++ b/client/index.js @@ -1,14 +1,9 @@ import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import App from './components/App.jsx'; -// import styles from './scss/application.scss'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import App from './components/App'; -const domNode = document.getElementById('app'); -const root = createRoot(domNode); -root.render( - - - -); +import styles from './scss/application.scss'; + +ReactDOM.createRoot(document.getElementById('app')).render(); diff --git a/client/scss/_variables.scss b/client/scss/_variables.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/scss/application.scss b/client/scss/application.scss new file mode 100644 index 0000000..a47136a --- /dev/null +++ b/client/scss/application.scss @@ -0,0 +1,166 @@ +@import '_variables'; +@import url('https://fonts.googleapis.com/css2?family=Lora&family=Sen&display=swap'); + +// Colors + +$primaryBlack: #050505; +$secondaryBlack: #5d5d5d; + +$primaryLime: #00ff00; +$secondaryLime: #acffac; +$lightestLime: #e2ffe2; + +$primarySalmon: salmon; +$secondarySalmon: #fcc0b9; +$lightestSalmon: #fdebe9; + +// Global +body { + height: 100vh; + margin: 0px; + font-family: 'Sen', sans-serif; +} +.loginWrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: $lightestLime; + height: 100vh; +} + +button { + background-color: $primarySalmon; + border: 1px solid white; + height: 30px; +} + +#button :hover { + cursor: help; +} + +// 1b. Login page + + +.loginContents { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + h1{ + margin-top: 100px; + } +} + + +.loginContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border: 1px solid $secondarySalmon; + border-radius: 5%; + width: 300px; + box-shadow: 8px 10px 14px 0px rgb(128, 128, 128, 0.1); + background-color: white; + margin-top: 50px; +} + +.loginContainer input { + margin-left: 10px; + margin-bottom: 20px; + border-radius: 5%; +} + +.loginContainer button { + width: 200px; + margin-bottom: 20px; + border-radius: 5%; +} + +// 2. Feature Page +.feature-container { + margin-top: 100px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border-radius: 2%; + margin-left: 5px; + margin-right: 5px; +} + +//navbar +nav { + display: flex; + justify-content: space-between; + border-bottom: 1px solid $secondarySalmon; +} + +.logoutContainer { + display: flex; + align-items: center; + justify-content: end; + gap: 50px; +} + +//dropdown + +.dropdown-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100vw; + background-color: $lightestLime; +} + +.dropdown { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid $secondarySalmon; + border-radius: 5%; + width: 300px; + box-shadow: 8px 10px 14px 0px rgb(128, 128, 128, 0.1); + background-color: white; + margin-top: 100px; + margin-bottom: 100px; +} + +.dropdown label { + padding: 20px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.dropdown select { + margin-top: 10px; + margin-bottom: 10px; +} + +table { + margin: 5px; + margin-top: 50px; +} + +thead { + background-color: $lightestSalmon; + font-size: 14px; +} + +tbody { + background-color: white; + font-size: 12px; +} + +td { + margin-left: 20px; +} + +tr { + padding: 10px; +} diff --git a/package.json b/package.json index 8729a5c..7645b64 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "webpack.config.js", "scripts": { "start": "nodemon server/server.js", - "dev": "webpack serve --config ./webpack.config.js --mode development", + "build": "webpack", + "dev": "webpack serve --config ./webpack.config.js --mode development & nodemon server/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "eslintConfig": { diff --git a/server/controllers/foodController.js b/server/controllers/foodController.js index 632608a..148776b 100644 --- a/server/controllers/foodController.js +++ b/server/controllers/foodController.js @@ -16,8 +16,9 @@ const preciseURL = // gets foodController.getFoods = (req, res, next) => { // queries mongoDB for illness, saves related foods in res locals + console.log('hitting get foods'); try { - Illness.findOne({ illness: req.body }).then((data) => { + Illness.findOne({ ailment: req.body.ailment }).then((data) => { res.locals.foods = data.foods; return next(); }); @@ -31,6 +32,7 @@ foodController.getFoods = (req, res, next) => { }; foodController.getFacts = async (req, res, next) => { + console.log('hitting get facts'); res.locals.facts = []; try { for (let food of res.locals.foods) { diff --git a/server/controllers/userController.js b/server/controllers/userController.js new file mode 100644 index 0000000..f00254e --- /dev/null +++ b/server/controllers/userController.js @@ -0,0 +1,127 @@ +const User = require('../models/userModel'); +const bcrypt = require('bcrypt'); +const userController = {}; + +userController.createUser = async (req, res, next) => { + const { username, password } = req.body; + + try { + //check first to see if user is already created + const found = await User.findOne({ username }); + if (found) { + console.log('yes user is created'); + throw new Error('username is already in use'); + } + const newUser = await new User({ username, password }); + await newUser.save(); + res.locals.user = newUser; + return next(); + } catch (error) { + return next({ + log: 'Error in userController.createUser middleware function', + status: 500, + message: { err: error.message }, + }); + } +}; + +userController.verifyUser = async (req, res, next) => { + const { username, password } = req.body; + try { + //grab encrypted password from db + const user = await User.find({ username }); + console.log(user); + if (!user[0]) { + console.log('no user found'); + throw new Error('no user found'); + } + const matched = await bcrypt.compare(password, user[0].password); + if (!matched) { + throw new Error('password incorrect'); + } + res.locals.username = username; + return next(); + } catch (error) { + return next({ + log: 'Error in userController.verifyUser middleware function', + status: 500, + message: { err: error.message }, + }); + } +}; + +userController.addFavorite = async (req, res, next) => { + const username = req.params.username; + try { + const user = await User.findOne({ username }); + const favorite = user?.favorite; + if (!user) { + throw Error('user not found'); + } + //const favorite = user.favorite; + //findoneandupdate {username}, {favorite:[...favorite,food]} + const addFavorite = await User.findOneAndUpdate( + { username }, + { favorite: [...favorite, req.body] } + ); + if (!addFavorite) { + throw Error('user cannot be updated'); + } + res.locals.favorite = addFavorite; + return next(); + } catch (error) { + return next({ + log: 'Error in userController.addFavorite middleware function', + status: 500, + message: { err: error.message }, + }); + } + //findOne => return the user +}; + +userController.getFavorite = async (req, res, next) => { + const username = req.params.username; + try { + const user = await User.findOne({ username }); + res.locals.favorite = user.favorite; + next(); + } catch (error) { + return next({ + log: 'Error in userController.getFavorites middleware function', + status: 500, + message: { err: error.message }, + }); + } +}; + +userController.deleteFavorite = async (req, res, next) => { + const username = req.params.username; + const { food } = req.body; + try { + const user = await User.findOne({ username }); + const favorite = user?.favorite; + if (!user) { + throw Error('user not found'); + } + //const favorite = user.favorite; + //findoneandupdate {username}, {favorite:[...favorite,food]} + const deleteFavorite = await User.findOneAndUpdate( + { username }, + { favorite: favorite.filter((obj) => obj.food !== food) } + ); + if (!deleteFavorite) { + throw Error('user cannot be updated'); + } + res.locals.favorite = deleteFavorite; + return next(); + } catch (error) { + return next({ + log: 'Error in userController.addFavorite middleware function', + status: 500, + message: { err: error.message }, + }); + } + //findOne => return the user +}; + +module.exports = userController; diff --git a/server/models/userModel.js b/server/models/userModel.js new file mode 100644 index 0000000..20fb316 --- /dev/null +++ b/server/models/userModel.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); +const SALT_WORK_FACTOR = 10; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + favorite: { + type: Array, + default: [], + }, +}); + +userSchema.pre('save', async function (next) { + try { + // "modification" includes creating a new pw, per mongoose + if (!this.isModified('password')) { + return next(); + } + const hashedPassword = await bcrypt.hash(this.password, SALT_WORK_FACTOR); + this.password = hashedPassword; + return next(); + } catch (err) { + return next(err); + } +}); + +const User = mongoose.model('User', userSchema); + +module.exports = User; diff --git a/server/server.js b/server/server.js index 51bd91e..4003d65 100644 --- a/server/server.js +++ b/server/server.js @@ -12,6 +12,8 @@ const PORT = 3000; const foodController = require('./controllers/foodController'); +const userController = require('./controllers/userController'); + //connect to database const mongoURI = 'mongodb+srv://goblinshark:codesmith@foodremedy.nl2qzoj.mongodb.net/?retryWrites=true&w=majority'; @@ -26,14 +28,10 @@ mongoose // needed to fix fetching problem in react app.use(cors()); +app.use(express.json()); app.use(express.static(path.resolve(__dirname, '../client'))); -// listens, confirms connection -app.listen(PORT, () => { - console.log(`Success! Your application is running on port ${PORT}.`); -}); - // handles POST requests from illness dropdown app.post( '/search', @@ -42,6 +40,33 @@ app.post( (req, res) => res.status(200).send(res.locals.facts) ); +//route for signing up +app.get('/signup', (req, res) => { + res.sendFile(path.resolve(__dirname, '../client/signup.html')); +}); + +app.post('/signup', userController.createUser, (req, res) => { + res.status(200).json(res.locals.user); +}); + +app.post('/login', userController.verifyUser, (req, res) => { + res.status(200).json(res.locals.username); +}); + +//save favorite food to user's favorite folder +app.patch('/user/addfav/:username',userController.addFavorite,(req, res)=> { + res.status(200).json(res.locals.favorite); +}) +//get a collection of favorite food for a user +app.get('/user/:username',userController.getFavorite,(req,res)=>{ + res.status(200).json(res.locals.favorite); +}) + +//delete a favorite food from a user's favorite collection +app.patch('/user/deletefav/:username',userController.deleteFavorite,(req, res)=> { + res.status(200).json(res.locals.favorite); +}) + // global error handler app.use((err, req, res, next) => { const defaultErr = { @@ -53,3 +78,8 @@ app.use((err, req, res, next) => { console.log(errorObj.log); return res.status(errorObj.status).json(errorObj.message); }); + +// listens, confirms connection +app.listen(PORT, () => { + console.log(`Success! Your application is running on port ${PORT}.`); +});