diff --git a/.gitignore b/.gitignore index 7b7b4a8..4324f10 100644 --- a/.gitignore +++ b/.gitignore @@ -113,4 +113,9 @@ ehthumbs.db Thumbs.db # bundle.js file -bundle.js \ No newline at end of file +bundle.js + +#vscode setting +.vscode +# keys.js file +keys.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ac694bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "workbench.colorCustomizations": { + "statusBar.background": "#2980b9", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#409ad5", + "titleBar.activeBackground": "#2980b9", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#2980b999", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#2980B9" +} \ No newline at end of file diff --git a/config/keys.js b/config/keys.js new file mode 100644 index 0000000..446ef2a --- /dev/null +++ b/config/keys.js @@ -0,0 +1,24 @@ +//add this file to gitignore + +module.exports = { + google: { + // this clientID and clientSecret are given to us from google + clientID: + '979104596982-qkkdoare2fms3gaqne9v3pkbfs6bnppt.apps.googleusercontent.com', + clientSecret: '2h4K44PG0SUQzPq6eU1abQs7', + }, + postgres: { + // here is our postgres URL + PG_URI: + 'postgres://fojeewoc:yKLXZvm6DqRW840uEqmWDnT3OdGmYW_n@suleiman.db.elephantsql.com:5432/fojeewoc', + }, + session: { + // this cookieKey is going to encrypt our cookie so that folks can't just see our cookie + cookieKey: 'dfkjghsjdhasdakjsdan', + }, + + facebook: { + clientID: '383299809591566', + AppSecret: 'cd417d5c4a4272542fe535826b91e7c1', + }, +}; diff --git a/config/passport-setup.js b/config/passport-setup.js new file mode 100644 index 0000000..b360e35 --- /dev/null +++ b/config/passport-setup.js @@ -0,0 +1,110 @@ +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20'); +const keys = require('./keys'); +const db = require('../models/closetModels'); +const FacebookStrategy = require('passport-facebook').Strategy; + +//serialize user's oauth_id for our cookie +passport.serializeUser((user, done) => { + done(null, user.rows[0]._id); +}); + +passport.deserializeUser(async (id, done) => { + // we need to pass in the user we're making the cookie for in done, so we will grab that user (again) via their oath_id in our query string + const userQuery = 'SELECT * FROM users WHERE oauth_id = $1'; + const queryParam = [id]; + //query db to find user based on their oath_id + const user = await db.query(userQuery, queryParam); + //pass user into done + done(null, user); +}); + +passport.use( + new GoogleStrategy( + { + //options for the strategy + //need to add a redirectURL, found in our google developer console + callbackURL: '/auth/google/redirect', + //need a client ID and a client secret + clientID: keys.google.clientID, + clientSecret: keys.google.clientSecret, + }, + async (req, accessToken, refreshToken, profile, done) => { + //passport callback function + // check if user exists in our db first + try { + const findUserStr = `SELECT EXISTS (SELECT * FROM USERS WHERE oauth_id=$1)`; + //finduser params + const authID = profile.id; + const query2 = [authID]; + + //insert string params + const { givenName, familyName } = profile.name; + const emails = profile.emails[0].value; + const profileImage = profile.photos[0].value; + const insertUser = `INSERT INTO USERS (first_name1, last_name1, username, email, profile_Image, display_name_1, oauth_id) VALUES + ($1, $2, $3, $4, $5, $6, $7)`; + const findUserQuery = [ + givenName, + familyName, + emails, + emails, + profileImage, + givenName, + authID, + ]; + //check if user exists in db + const response = await db.query(findUserStr, query2); + //if user does not exists, quuery the db again to add the user + if (!response.rows[0].exists) await db.query(insertUser, findUserQuery); + //otherwise query the database to find the ID of the newly added user on line 37 + const userQuery = 'SELECT * FROM USERS WHERE oauth_id = $1'; + //assign a var the value of findind the newly added or already added user in table and assign it as second arg in done() + const user = await db.query(userQuery, query2); + done(null, user); + } catch (error) { + console.log('we got here somehow'); + } + } + ) +); +passport.use( + new FacebookStrategy( + { + clientID: keys.facebook.clientID, + clientSecret: keys.facebook.AppSecret, + callbackURL: 'http://localhost:3000/auth/facebook/callback', + }, + async (accessToken, refreshToken, profile, done) => { + console.log(profile); + try { + const findUserStr = `SELECT EXISTS (SELECT * FROM USERS WHERE oauth_id=$1)`; + //finduser params + const authID = profile.id; + const query2 = [authID]; + + //insert string params + const displayName = profile.displayName; + let first_Name = displayName.split(' ')[0]; + let last_Name = displayName.split(' ')[1]; + + // const profileImage = profile.photos[0].value; + const insertUser = `INSERT INTO USERS (first_name1, last_name1, username, email, profile_Image, display_name_1, oauth_id) VALUES + ($1, $2, $3, 'N/A', 'N/A', $3, $4)`; + + const findUserQuery = [first_Name, last_Name, displayName, authID]; + //check if user exists in db + const response = await db.query(findUserStr, query2); + //if user does not exists, quuery the db again to add the user + if (!response.rows[0].exists) await db.query(insertUser, findUserQuery); + //otherwise query the database to find the ID of the newly added user on line 37 + const userQuery = 'SELECT * FROM USERS WHERE oauth_id = $1'; + //assign a var the value of findind the newly added or already added user in table and assign it as second arg in done() + const user = await db.query(userQuery, query2); + done(null, user); + } catch (error) { + console.log('we got here somehow'); + } + } + ) +); diff --git a/controllers/closetController.js b/controllers/closetController.js new file mode 100644 index 0000000..557ac1d --- /dev/null +++ b/controllers/closetController.js @@ -0,0 +1,167 @@ +const db = require('../models/closetModels'); + +const closetController = {}; + +// adds a new item to a user's closet +closetController.newClothingItem = async (req, res, next) => { + const { + itemName, + itemClothingType, + itemColor, + itemImage, + user_id, + season, + size, + availability, + } = req.body; + + const queryStr = + 'INSERT INTO Closet (itemName, itemClothingType, itemColor, itemImage, user_id, season, size, availability) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)'; + const queryParams = [ + itemName, + itemClothingType, + itemColor, + itemImage, + user_id, + season, + size, + availability, + ]; + + try { + await db.query(queryStr, queryParams); + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +// retrieves all clothes from Clothes table +closetController.getClothes = async (req, res, next) => { + // would this be req.params?? + const userId = req.params.id; + const queryStr = + 'SELECT u._id, c.* FROM Users u LEFT OUTER JOIN Closet c ON c.user_id = $1'; + // const queryStr = 'SELECT * FROM Closet ORDER BY _id ASC'; + try { + const data = await db.query(queryStr, [userId]); + res.locals.clothes = data.rows; + return next(); + } catch (error) { + return next(error); + } +}; + +// updates how many times an item has been worn and date of last wear +// id of item is passed through as key/value on request body +closetController.updateClosetItem = async (req, res, next) => { + const worn = req.body.worn; + const { id, itemname, itemclothingtype, itemcolor } = req.body; + console.log(id); + console.log(worn); + const firstQuery = 'SELECT times_worn FROM Closet WHERE _id = $1'; + + if (worn) { + try { + const data = await db.query(firstQuery, [id]); + let { times_worn } = data.rows[0]; + console.log(times_worn); + times_worn += 1; + const todaysDate = new Date(); + + const queryStr = + 'UPDATE Closet SET itemname = $1, itemclothingtype = $2, itemcolor = $3, last_worn = $4, times_worn = $5 WHERE _id= $6'; + const queryParams = [ + itemname, + itemclothingtype, + itemcolor, + todaysDate, + times_worn, + id, + ]; + await db.query(queryStr, queryParams); + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } + } +}; + +// removes a piece of clothing from the Closet table +// request body includes key/value of clothing item id +closetController.deleteClothingItem = async (req, res, next) => { + const id = req.params.id; + + // query string is to delete item from closet table where id matches passed in id + const queryStr = 'DELETE FROM Closet WHERE _id = $1'; + const queryParams = [id]; + + try { + await db.query(queryStr, queryParams); + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +// update donation status in closet table +// select donation_status matching clothing item _id +closetController.donationStatusUpdate = async (req, res, next) => { + const id = req.body.id; + console.log(id); + const firstQuery = 'SELECT donation_status FROM Closet WHERE _id = $1'; + + try { + const data = await db.query(firstQuery, [id]); + let { donation_status } = data.rows[0]; + console.log(donation_status); + // swap donation_status based on current value + donation_status = donation_status === 'Inactive' ? 'Active' : 'Inactive'; + + const queryStr = 'UPDATE Closet SET donation_status = $1 WHERE _id= $2'; + const queryParams = [donation_status, id]; + + await db.query(queryStr, queryParams); + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +// query to find donation items for marketplace +// pulls everything matching donation status of 'Active' +closetController.getMarketplaceItems = async (req, res, next) => { + console.log('inside marketplace middleware!'); + const queryStr = 'SELECT * FROM Closet WHERE donation_status = $1'; + queryParams = ['Active']; + + try { + const data = await db.query(queryStr, queryParams); + res.locals.clothes = data.rows; + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +module.exports = closetController; diff --git a/controllers/userController.js b/controllers/userController.js new file mode 100644 index 0000000..0ec3687 --- /dev/null +++ b/controllers/userController.js @@ -0,0 +1,96 @@ +const db = require('../models/closetModels'); + +const userController = {}; + +// adds a user to the db +userController.addUser = async (req, res, next) => { + const { + first_name, + last_name, + username, + password, + email, + zip_code, + profile_image, + city, + state, + display_name_1, + oauth_id, + } = req.body; + + const queryStr = + 'INSERT INTO Users (_id, first_name, last_name, username, password, email, zip_code, profile_image, city, state, display_name_1, oauth_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)'; + const queryParams = [ + DEFAULT, + first_name, + last_name, + username, + password, + email, + zip_code, + profile_image, + city, + state, + display_name_1, + oauth_id, + ]; + // need to check first if a user with username/password already exists...? + + try { + await db.query(queryStr, queryParams); + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +userController.userLogin = async (req, res, next) => { + const { username, password } = req.body; + + const firstQuery = + 'SELECT EXISTS (SELECT 1 FROM Users WHERE username = $1 AND password = $2)'; + const queryParams = [username, password]; + + try { + const validUser = await db.query(firstQuery, queryParams); + // if validUser is true: + // query db for id belonging to username and password and return to client? + if (validUser) { + const queryString = + 'SELECT _id FROM Users WHERE username = $1 AND password = $2'; + const data = await db.query(queryString, queryParams); + res.locals._id = data.rows[0]; + return next(); + } + res.send('Invalid username or password'); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +userController.getUser = async (req, res, next) => { + const userId = req.params.id; + const queryStr = 'SELECT * FROM Users WHERE _id = $1'; + + try { + const data = await db.query(queryStr, [userId]); + res.locals.user = data.rows; + return next(); + } catch (error) { + return next({ + log: `Database error`, + status: 502, + message: { err: `${error.stack}` }, + }); + } +}; + +module.exports = userController; diff --git a/fileController.js b/fileController.js deleted file mode 100644 index 37f7af6..0000000 --- a/fileController.js +++ /dev/null @@ -1,73 +0,0 @@ -const db = require('./models/closetModels') - -const fileController = {}; - -fileController.getClothes = async (req, res, next) => { - const queryStr = 'SELECT * FROM Closet ORDER BY _id ASC'; - try { - const data = await db.query(queryStr); - res.locals.clothes = data.rows; - return next(); - } catch (error) { - return next(error); - } -}; - -fileController.newClothingItem = async (req, res, next) => { - const { itemName, itemClothingType, itemColor, itemImage } = req.body - - const queryStr = 'INSERT INTO Closet (itemName, itemClothingType, itemColor, itemImage) VALUES ($1, $2, $3, $4)'; - const queryParams = [itemName, itemClothingType, itemColor, itemImage]; - - try { - await db.query(queryStr, queryParams); - return next(); - } catch (error) { - return next({ - log: `Database error`, - status: 502, - message: { err: `${error.stack}` }, - }); - } -}; - -fileController.updateClosetItem = async (req, res, next) => { - const id = req.body._id; - const firstQuery = 'SELECT times_worn FROM Closet WHERE _id = $1'; - - try { - const data = await db.query(firstQuery, [id]); - let { times_worn } = data.rows[0]; - times_worn += 1; - const todaysDate = new Date(); - - const queryStr = 'UPDATE Closet SET last_worn = $1, times_worn = $2 WHERE _id= $3'; - const queryParams = [todaysDate, times_worn, id]; - await db.query(queryStr, queryParams); - return next(); - } catch (error) { - return next({ - log: `Database error`, - status: 502, - message: { err: `${error.stack}` }, - }); - } -} - -fileController.deleteClothingItem = async (req, res, next) => { - const id = req.body._id - const queryStr = 'DELETE FROM Closet WHERE _id = $1'; - const queryParams = [id]; - - try { - await db.query(queryStr, queryParams); - return next(); - } catch (error) { - return next({ - log: `Database error`, - status: 502, - message: { err: `${error.stack}` }, - }); - } -} -module.exports = fileController; diff --git a/index.html b/index.html index 7a8b372..b279d69 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,35 @@ - - - - + + + + Hanger - + +
- + + - \ No newline at end of file + diff --git a/package-lock.json b/package-lock.json index 0e6f5fc..0710638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1169,6 +1169,11 @@ "to-fast-properties": "^2.0.0" } }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, "@eslint/eslintrc": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz", @@ -1288,6 +1293,108 @@ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" }, + "@material-ui/core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.1.tgz", + "integrity": "sha512-C6hYsjkWCTfBx9FaqxhCZCITBagh7fyCKFtHyvO3tTOcBw6NJaktdhNZ2n82jQdQdgfFvg6OOxi7OOzsAdAcBQ==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.4", + "@material-ui/system": "^4.12.1", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + } + }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, + "@material-ui/lab": { + "version": "4.0.0-alpha.60", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz", + "integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, + "@material-ui/styles": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", + "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.17", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", + "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" + } + } + }, + "@material-ui/system": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", + "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "dependencies": { + "csstype": { + "version": "2.6.17", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", + "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" + } + } + }, + "@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + }, + "@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1303,6 +1410,11 @@ "defer-to-connect": "^1.0.1" } }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -1313,6 +1425,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/http-proxy": { "version": "1.17.4", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz", @@ -1346,6 +1467,28 @@ "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==", "dev": true }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", + "requires": { + "@types/react": "*" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2010,6 +2153,11 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -2605,6 +2753,11 @@ "mimic-response": "^1.0.0" } }, + "clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2800,9 +2953,9 @@ } }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-parser": { "version": "1.4.5", @@ -2811,6 +2964,23 @@ "requires": { "cookie": "0.4.0", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } + } + }, + "cookie-session": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-1.4.0.tgz", + "integrity": "sha512-0hhwD+BUIwMXQraiZP/J7VP2YFzqo6g4WqZlWHtEHQ22t0MeZZrNBSCxC1zcaLAs8ApT3BzAKizx9gW/AP9vNA==", + "requires": { + "cookies": "0.8.0", + "debug": "2.6.9", + "on-headers": "~1.0.2" } }, "cookie-signature": { @@ -2818,6 +2988,22 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -2999,6 +3185,15 @@ } } }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "csscolorparser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", @@ -3010,6 +3205,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -3318,6 +3518,15 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -4089,6 +4298,13 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } } }, "extend-shallow": { @@ -4775,6 +4991,11 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5112,6 +5333,11 @@ "is-extglob": "^2.1.1" } }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, "is-installed-globally": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", @@ -5329,6 +5555,84 @@ "minimist": "^1.2.5" } }, + "jss": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.7.1.tgz", + "integrity": "sha512-5QN8JSVZR6cxpZNeGfzIjqPEP+ZJwJJfZbXmeABNdxiExyO+eJJDy6WDtqTf8SDKnbL5kZllEpAP71E/Lt7PXg==", + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-camel-case": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.7.1.tgz", + "integrity": "sha512-+ioIyWvmAfgDCWXsQcW1NMnLBvRinOVFkSYJUgewQ6TynOcSj5F1bSU23B7z0p1iqK0PPHIU62xY1iNJD33WGA==", + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.7.1" + } + }, + "jss-plugin-default-unit": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.7.1.tgz", + "integrity": "sha512-tW+dfYVNARBQb/ONzBwd8uyImigyzMiAEDai+AbH5rcHg5h3TtqhAkxx06iuZiT/dZUiFdSKlbe3q9jZGAPIwA==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.7.1" + } + }, + "jss-plugin-global": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.7.1.tgz", + "integrity": "sha512-FbxCnu44IkK/bw8X3CwZKmcAnJqjAb9LujlAc/aP0bMSdVa3/MugKQRyeQSu00uGL44feJJDoeXXiHOakBr/Zw==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.7.1" + } + }, + "jss-plugin-nested": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.7.1.tgz", + "integrity": "sha512-RNbICk7FlYKaJyv9tkMl7s6FFfeLA3ubNIFKvPqaWtADK0KUaPsPXVYBkAu4x1ItgsWx67xvReMrkcKA0jSXfA==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.7.1", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.7.1.tgz", + "integrity": "sha512-eyd5FhA+J0QrpqXxO7YNF/HMSXXl4pB0EmUdY4vSJI4QG22F59vQ6AHtP6fSwhmBdQ98Qd9gjfO+RMxcE39P1A==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.7.1" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.7.1.tgz", + "integrity": "sha512-fGAAImlbaHD3fXAHI3ooX6aRESOl5iBt3LjpVjxs9II5u9tzam7pqFUmgTcrip9VpRqYHn8J3gA7kCtm8xKwHg==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.7.1", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.7.1.tgz", + "integrity": "sha512-1UHFmBn7hZNsHXTkLLOL8abRl8vi+D1EVzWD4WmLFj55vawHZfnH1oEz6TUf5Y61XHv0smdHabdXds6BgOXe3A==", + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.7.1" + } + }, "jsx-ast-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz", @@ -5344,6 +5648,14 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -5979,6 +6291,11 @@ "path-key": "^3.0.0" } }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6157,8 +6474,7 @@ "on-headers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, "once": { "version": "1.4.0", @@ -6351,6 +6667,48 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-facebook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-3.0.0.tgz", + "integrity": "sha512-K/qNzuFsFISYAyC1Nma4qgY/12V3RSLFdFVsPKXiKZt434wOvthFW1p7zKa1iQihQMRhaWorVE1o3Vi1o+ZgeQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "requires": { + "passport-oauth2": "1.x.x" + } + }, + "passport-oauth2": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -6415,6 +6773,11 @@ } } }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "pbf": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", @@ -6526,6 +6889,11 @@ "find-up": "^3.0.0" } }, + "popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -6910,6 +7278,16 @@ "object-assign": "^4.1.1" } }, + "react-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz", + "integrity": "sha512-cmi6IpdVgTSvjqssqIEvo779Gfqc4uPGHRrKMEdHcqkmGtPmxolGfsyKj95bhdLEKqMdbX8MLBCwezlnhkHK0g==", + "requires": { + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" + } + }, "react-dom": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", @@ -6920,6 +7298,19 @@ "scheduler": "^0.20.1" } }, + "react-google-button": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/react-google-button/-/react-google-button-0.7.2.tgz", + "integrity": "sha512-LPIqU2hIlc212kqks8MtKjRstquVkP3SIjxlK5B1nIfg2R7YqSusJAxZUkJA5dv/z6QeSuGyI9ujwV/VWMTyAA==", + "requires": { + "prop-types": "^15.7.2" + } + }, + "react-hook-form": { + "version": "6.11.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.11.5.tgz", + "integrity": "sha512-y5RRz3ty/9I0Ps1VONcRw+L9QcFoq01lMDJt15Se1ax9lg7ThLSLpeNALXSRJhmbOhLPJnTTxC8W21EE+8SJNw==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6971,6 +7362,17 @@ "tiny-warning": "^1.0.0" } }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -8536,6 +8938,11 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -8586,6 +8993,11 @@ "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", @@ -8668,6 +9080,15 @@ "crypto-random-string": "^2.0.0" } }, + "universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "requires": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 8510678..16670cc 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.5", "@babel/runtime": "^7.12.5", + "@webpack-cli/serve": "^1.1.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.1", "css-loader": "^5.0.1", @@ -46,15 +47,26 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { + "@material-ui/core": "^4.12.1", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.60", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", + "cookie": "^0.4.1", "cookie-parser": "^1.4.5", + "cookie-session": "^1.4.0", "express": "^4.17.1", "mapbox-gl": "^1.13.0", "node-fetch": "^2.6.1", + "passport": "^0.4.1", + "passport-facebook": "^3.0.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.5.1", "react": "^17.0.1", + "react-cookie": "^4.0.3", "react-dom": "^17.0.1", + "react-google-button": "^0.7.2", + "react-hook-form": "^6.11.5", "react-router-dom": "^5.2.0", "sass": "^1.29.0" } diff --git a/routes/closet.js b/routes/closet.js index c199130..d0f1e3e 100644 --- a/routes/closet.js +++ b/routes/closet.js @@ -1,33 +1,81 @@ const express = require('express'); -const path = require('path') -const fileController = require('../fileController.js') -const router = express.Router() +const path = require('path'); +const closetController = require('../controllers/closetController.js'); +const userController = require('../controllers/userController.js'); +const router = express.Router(); - -// Jason: created a new GET endpoint to /closet that queries the database -// for all entries and returns the result to the response +// // should handle getting all items from the database -router.get('/', fileController.getClothes, (req, res) => { - res.json(res.locals.clothes); -}); +// CLOSET ROUTES: -//should handle entering items into database // -router.post('/', fileController.newClothingItem, fileController.getClothes, (req, res) => { - console.log(res.locals.clothes) +// tested and works in postman +router.get('/closet/:id', closetController.getClothes, (req, res) => { res.json(res.locals.clothes); }); - + +//should handle entering clothing items into database +router.post( + '/', + closetController.newClothingItem, + closetController.getClothes, + (req, res) => { + // console.log(res.locals.clothes); + res.json(res.locals.clothes); + } +); + //deletes an item from the closet using the id -router.delete('/', fileController.deleteClothingItem, (req, res) => { +router.delete( + '/delete/:id', + closetController.deleteClothingItem, + (req, res) => { + // res.json(res.locals.clothes); + } +); + +//updates an item +router.patch('/updateItem', closetController.updateClosetItem, (req, res) => { + res.send('happy stuff'); +}); + +// get marketplace items +// tested and works in postman +router.get('/marketplace', closetController.getMarketplaceItems, (req, res) => { res.json(res.locals.clothes); }); - -//updates an item from the closet using the id -router.patch('/', fileController.updateClosetItem, (req, res) => { - res.send('happy stuff') -// res.json(res.locals.clothes); -}) +// donation status +// tested and works in postman +router.post( + '/closet/donation', + closetController.donationStatusUpdate, + (req, res) => { + // res.json(res.locals.clothes); + res.send('donation status updated!'); + } +); -module.exports = router; \ No newline at end of file +// USER ROUTES: + +// adds a new user to the db +router.post('/signup', userController.addUser, (req, res) => { + res.send('Account signup success!'); +}); + +// logs user in +router.post('/login', userController.userLogin, (req, res) => { + res.send('Successfully logged in'); +}); + +// tested and works in postman +router.get('/user/:id', userController.getUser, (req, res) => { + res.json(res.locals.user); +}); +router.get('/logout', (req, res) => { + //this will be handled later with passport + req.logout(); + req.session.destroy(); + res.redirect('/'); +}); +module.exports = router; diff --git a/routes/donation.js b/routes/donation.js deleted file mode 100644 index fc2fbfb..0000000 --- a/routes/donation.js +++ /dev/null @@ -1,6 +0,0 @@ -const express = require('express'); -const router = express.Router() - - - -module.exports = router; \ No newline at end of file diff --git a/routes/oauth-routes.js b/routes/oauth-routes.js new file mode 100644 index 0000000..c2f27ab --- /dev/null +++ b/routes/oauth-routes.js @@ -0,0 +1,83 @@ +const express = require('express'); +const { route } = require('./closet'); +const router = express.Router(); +const passport = require('passport'); +// const fileController = require('..') + +//auth login +router.get('/login', (req, res) => { + //here is where we serve up our login page + res.redirect('/'); +}); + +//auth with google +router.get( + '/facebook', + passport.authenticate('facebook', { + scope: ['email', 'public_profile'], + }) +); + +router.get( + '/facebook/callback', + passport.authenticate('facebook'), + (req, res) => { + // successRedirect: '/', + // failureRedirect: '/login', + // res.locals.isVerified = true; + const user = req.user.rows[0]._id; + // res.redirect(`/api/${user}`); + // res.json(user); + res.cookie('success', user); + res.redirect('/'); + } +); + +//auth with google +router.get( + '/google', + passport.authenticate('google', { + scope: ['profile', 'email'], + }) +); + +//auth logout +router.get('/logout', (req, res) => { + //this will be handled later with passport + req.logout(); + res.clearCookie('success'); + res.redirect('/'); +}); + +//callback route for google to redirect to + +// this will be used to check if a user is logged in so they can see their profile, or if they should be sent to the login page +// const authCheck = (req,res,next) => { +// if(!req.user){ +// //if user is not logged in +// res.redirect('/auth/login') +// } else { +// next() +// } +// } +router.get('/google/redirect', passport.authenticate('google'), (req, res) => { + // res.send(req.user) + console.log(req.user.rows[0]._id); + //sending our OAuth user data + res.locals.isVerified = true; + const user = req.user.rows[0]._id; + // res.redirect(`/api/${user}`); + // res.json(user); + res.cookie('success', user); + return res.redirect('/'); + + //router needs to route to where we want to render + //if the route is to a username as the endpoint, write a function that renders an html page with everything for their closet + //make another get request to get the user information that is stored in the database + //as far as react router -- the only private route we need is instead of sending it to /closet, send it to /closet/joeschmoe and /closet would need some js that would populate /:name, which can be done as a route in middleware + //render the page with the data you get back from /:name + //with that :name absolute path, you can send that in react router, but in the frontend you would have to create that path + //you could create it if you have a function that runs when you have a path to go to +}); + +module.exports = router; diff --git a/server/server.js b/server/server.js index 30be6b3..5fad6f3 100644 --- a/server/server.js +++ b/server/server.js @@ -1,58 +1,57 @@ -const express = require("express"); -const path = require("path"); -const bodyParser = require('body-parser'); -const fileController = require('../fileController.js'); +const express = require('express'); +const path = require('path'); const closetRouter = require('../routes/closet.js'); +const authRoutes = require('../routes/oauth-routes.js'); +const passportSetup = require('../config/passport-setup.js'); let app = express(); - +const keys = require('../config/keys'); +const cookieSession = require('cookie-session'); +const passport = require('passport'); //parse request body// -//app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); - -//handle requests for static files -//uncomment and correct below when we know which folder we are using -//app.use('?', express.static(path.resolve(__dirname, ?))) - -//handle get requests to home page// - -// Jason: the following code does not need to invoke the getClothes middleware -// the .get to '/' should just return the index.html page when the server -// is in production mode. commenting this code out for now -// for development mode, this server is just an api to our database - -// app.get('/', fileController.getClothes, (req,res, next) => { -// res.sendFile(path.resolve(__dirname, '../index.html'), function (err) { -// if (err) { -// next(err) -// } else { -// console.log('data sent!'); -// } -// }) -// }) - +app.use(express.json()); + +app.use( + cookieSession({ + maxAge: 24 * 60 * 60 * 1000, + keys: [keys.session.cookieKey], + }) +); + +//initialize passport +app.use(passport.initialize()); +app.use(passport.session()); + +if (process.env.NODE_ENV === 'production') { + // statically serve everything in the build folder on the route '/build' + app.use('/build', express.static(path.join(__dirname, '../build'))); + // serve index.html on the route '/' + app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../index.html')); + }); +} + +app.use('/auth', authRoutes); app.use('/api', closetRouter); /** * configire express global error handler */ //catch all for errors// -app.use('*', (req,res,next) => { - return res.status(404).send('invalid end point') -}) - +app.use('*', (req, res, next) => { + return res.status(404).send('invalid end point'); +}); app.use(function (err, req, res, next) { - const defaultError = { - log: 'Express error handler caught unknown middleware error', - status: 400, - message: { err: 'An error occurred' }, - }; - const errorObj = Object.assign(defaultError, err); - console.log(errorObj.log); - res.status(errorObj.status).json(errorObj.message); + const defaultError = { + log: 'Express error handler caught unknown middleware error', + status: 400, + message: { err: 'An error occurred' }, + }; + const errorObj = Object.assign(defaultError, err); + console.log(errorObj.log); + res.status(errorObj.status).json(errorObj.message); }); +app.listen(3000, () => console.log('Listening on port 3000...')); -app.listen(3000, () => console.log('Listening on port 3000...')) - -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/src/App/App.js b/src/App/App.js new file mode 100644 index 0000000..65d6dab --- /dev/null +++ b/src/App/App.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import Store from './Store/Store'; +import Dashboard from './containers/Dashboard'; + +import '../stylesheets/styles.scss'; + +/* +App is abstracted, and wrapped with our Store +the Store consists of our Contexts: { UserContext, ItemContext, ClosetContext } +along with our CookieProvider which allows react-cookie to access +our browser's cookie storage. +*/ + +const App = () => { + return ( + + + + ); +}; + +export default App; diff --git a/src/App/App.jsx b/src/App/App.jsx deleted file mode 100644 index 72784d4..0000000 --- a/src/App/App.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable react/prefer-stateless-function */ -import React, { useState, useEffect } from 'react'; -import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'; -import Nav from './components/Nav'; -import ClosetContainer from './containers/ClosetContainer'; -import Landing from './components/Landing'; -import DonationPage from './components/DonationPage' -import "../stylesheets/styles.scss" - -const App = () => { - // return

Hello Jordan!

; - return ( - -
-
-
- ); -}; - -export default App; diff --git a/src/App/Store/ClosetContext.js b/src/App/Store/ClosetContext.js new file mode 100644 index 0000000..a2b386b --- /dev/null +++ b/src/App/Store/ClosetContext.js @@ -0,0 +1,19 @@ +import React, { createContext, useState } from 'react'; + +export const ClosetContext = createContext(); + +/* +This context was prepared to maintain a global state given a users closet +*/ + +const ClosetProvider = ({ children }) => { + const [closet, setCloset] = useState(); + + return ( + + {children} + + ); +}; + +export default ClosetProvider; diff --git a/src/App/Store/ItemContext.js b/src/App/Store/ItemContext.js new file mode 100644 index 0000000..19fec79 --- /dev/null +++ b/src/App/Store/ItemContext.js @@ -0,0 +1,19 @@ +import React, { createContext, useState } from 'react'; + +export const ItemContext = createContext(); + +/* +this context it unused in the application as of now +*/ + +const ItemProvider = ({ children }) => { + const [itemData, setItemData] = useState({}); + + return ( + + {children} + + ); +}; + +export default ItemProvider; diff --git a/src/App/Store/Store.js b/src/App/Store/Store.js new file mode 100644 index 0000000..24caada --- /dev/null +++ b/src/App/Store/Store.js @@ -0,0 +1,24 @@ +import React from 'react'; + +import { CookiesProvider } from 'react-cookie'; +import UserProvider from './UserContext'; +import ClosetProvider from './ClosetContext'; +import ItemProvider from './ItemContext'; + +/* +Our Store combines all of our Providers around their children components, +this allows us to abstract App.js as much as possible and pass Global State +as one, but still access each piece on its own. +*/ + +const Store = ({ children }) => ( + + + + {children} + + + +); + +export default Store; diff --git a/src/App/Store/UserContext.js b/src/App/Store/UserContext.js new file mode 100644 index 0000000..c32771f --- /dev/null +++ b/src/App/Store/UserContext.js @@ -0,0 +1,32 @@ +/* eslint-disable react/prop-types */ +import React, { createContext, useState, useEffect } from 'react'; +import { useCookies } from 'react-cookie'; + +/* +Our most important Global State is the user information. +It is initialized as null, and then stored with the response +from authentication under the property "verified". 'react-cookie' +allows us to check the browser for cookies. Our authentication route +assigns a cookie with the value of our users ID. Which can then allow +a user access. +*/ + +export const UserContext = createContext(); + +const UserProvider = ({ children }) => { + const [user, setUser] = useState(); + console.log('user: ', user); + const [cookies, setCookie] = useCookies(); + + useEffect(() => { + setUser({ ...user, verified: cookies.success }); + }, []); + + return ( + + {children} + + ); +}; + +export default UserProvider; diff --git a/src/App/components/Closet.js b/src/App/components/Closet.js new file mode 100644 index 0000000..de33cfd --- /dev/null +++ b/src/App/components/Closet.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import ItemCard from './ItemCard'; + +/* +Iterates through the closetData and returns an ItemCard +for each item in a users closet. +*/ + +const Closet = ({ closetData }) => { + const cards = closetData.map((item, i) => ( + + )); + return
{cards}
; +}; + +export default Closet; diff --git a/src/App/components/DonationPage.js b/src/App/components/DonationPage.js deleted file mode 100644 index 28d2b49..0000000 --- a/src/App/components/DonationPage.js +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import mapboxgl from 'mapbox-gl'; - -const DonationPage = (props) => { - // const [donateRow, setDonateRow] = useState([]); - const [userMap, setUserLocation] = useState([]); - - // useEffect(() => { - // fetch('/api') - // .then((data) => data.json()) - // .then((response) => { - // const rows = []; - // for (let i = 0; i < response.length; i++) { - // rows.push( - // - // ); - // } - // setDonateRow(rows); - // }); - // }, []); - - // useEffect( - // () => { - // if (window.navigator.geolocation) { - // window.navigator.geolocation.getCurrentPosition(showPosition); - // } else { - // console.log('give us your location NOW!'); - // } - // }, - // function showPosition(position) { - // userLocation = [position.coords.longitude, position.coords.latitude]; - // setUserLocation(userLocation); - // }, - // [] - // ); - - // mapboxgl.accessToken = - // 'pk.eyJ1Ijoic2dvbGRmYXJiMzM5MCIsImEiOiJjanlnODBvdjQwMGVoM2Jtc21nbGs3eWoxIn0.lLT4kAF2fiiv2Ey-h-5T-A'; - // let map = new mapboxgl.Map({ - // container: 'map', // Container ID - // style: 'mapbox://styles/mapbox/streets-v11', // Map style to use - // center: [userMap[0], userMap[1]], // Starting position [lng, lat] - // zoom: 12, // Starting zoom level - // }); - // let marker = new mapboxgl.Marker() // initialize a new marker - // // .setLngLat([-122.25948, 37.87221]) // Marker [lng, lat] coordinates - // .setLngLat([userMap[0], userMap[1]]) - // .addTo(map); // Add the marker to the map - - // let geocoder = new MapboxGeocoder({ - // // Initialize the geocoder - // accessToken: mapboxgl.accessToken, // Set the access token - // placeholder: 'Search for donation centers in NYC', - // mapboxgl: mapboxgl, // Set the mapbox-gl instance - // marker: false, // Do not use the default marker style - // // bbox: [-122.30937, 37.84214, -122.23715, 37.89838], // Boundary for Berkeley - // // proximity: { - // // longitude: -122.25948, - // // latitude: 37.87221 - // // } // Coordinates of UC Berkeley - // }); - - // // Add the geocoder to the map - // map.addControl(geocoder); - // // After the map style has loaded on the page, - // // add a source layer and default styling for a single point - // map.on('load', function () { - // map.addSource('single-point', { - // type: 'geojson', - // data: { - // type: 'FeatureCollection', - // features: [], - // }, - // }); - - // map.addLayer({ - // id: 'point', - // source: 'single-point', - // type: 'circle', - // paint: { - // 'circle-radius': 10, - // 'circle-color': '#448ee4', - // }, - // }); - - // // Listen for the `result` event from the Geocoder - // // `result` event is triggered when a user makes a selection - // // Add a marker at the result's coordinates - // geocoder.on('result', function (e) { - // map.getSource('single-point').setData(e.result.geometry); - // }); - // }); - - return ( -
- - These items Haven't Been Worn in The Last 30 Days -
-
- Item - Type - Color - Image - Active/Donate - Worn -
- {/*
{donateRow}
*/} -
- -
-
- ); -}; - -export default DonationPage; diff --git a/src/App/components/InputDisplay.js b/src/App/components/InputDisplay.js deleted file mode 100644 index fc496f8..0000000 --- a/src/App/components/InputDisplay.js +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useState } from 'react'; - -// when updating state, we are putting into the userInput state object -// key-value pairs. the four pairs are: -// itemName: String -> user types into texbar -// itemClothingType: String -> user selects from dropdown bar -// itemColor: String -> user selects from dropdown bar -// itemImage: File API integration -> user selects from file from device storage -const InputDisplay = () => { - const initialState = { - itemName: '', - itemClothingType: 'Tops/Shirts/Tees', - itemColor: 'Black', - itemImage: - 'https://res.cloudinary.com/dfu8r9blo/image/upload/v1605922447/HangerImages/no_uploaded_cu28uy.png', - }; - const [userInput, setUserInput] = useState(initialState); - const [loading, setLoading] = useState(false); - - const handleSubmit = async () => { - try { - setLoading(true); - // console.log(JSON.stringify(userInput)); - const response = await fetch('/api', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userInput), - }); - setLoading(false); - setUserInput(initialState); - } catch (error) { - console.log(error); - setLoading(false); - } - }; - - const handleImageUpload = async (event) => { - const [file] = event.target.files; - const data = new FormData(); - data.append('file', file); - data.append('upload_preset', 'hanger'); - setLoading(true); - try { - const response = await fetch( - 'https://api.cloudinary.com/v1_1/dfu8r9blo/image/upload', - { - method: 'POST', - body: data, - } - ); - const { url } = await response.json(); - setUserInput({ ...userInput, itemImage: url }); - setLoading(false); - } catch (error) { - console.log(error); - setLoading(false); - } - }; - - return ( -
-

Add to Closet

-

Upload Image

-
- - - setUserInput({ ...userInput, itemName: event.target.value }) - } - /> - -
- -
-

Select Color

- -

Select Type

- -
-
- ); -}; - -export default InputDisplay; diff --git a/src/App/components/ItemCard.js b/src/App/components/ItemCard.js new file mode 100644 index 0000000..4852926 --- /dev/null +++ b/src/App/components/ItemCard.js @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +import ItemDescribe from './ItemDescribe'; +import ItemView from './ItemView'; + +/* +Renders the container component for each Card +*/ + +const ItemCard = ({ id, item }) => { + const [visible, setVisibility] = useState(false); + const [sticky, setSticky] = useState(false); + const { itemimage } = item; + + return ( +
setVisibility(true)} + onMouseLeave={() => setVisibility(false)} + onClick={() => setSticky(!sticky)} + > + {visible || sticky ? ( + + ) : ( + + )} +
+ ); +}; + +export default ItemCard; diff --git a/src/App/components/ItemDescribe.js b/src/App/components/ItemDescribe.js new file mode 100644 index 0000000..3382cca --- /dev/null +++ b/src/App/components/ItemDescribe.js @@ -0,0 +1,135 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { useForm } from 'react-hook-form'; +import { Rating } from '@material-ui/lab'; +import { Typography, Box, Button, makeStyles } from '@material-ui/core'; +import { ItemContext } from '../Store/ItemContext'; + +/* +References the ItemContext for a default state +Forms created with 'react-hook-form' module +This was never linked wtih the backend +It is set up to fetch upon dismount which +may or may not fire too many times +*/ +const useStyles = makeStyles((theme) => ({ + root: { + '& > *': { + margin: theme.spacing(1), + }, + }, +})); +const ItemDescribe = ({ item }) => { + const classes = useStyles(); + + const [value, setValue] = React.useState(1); + + const [itemData, setItemData] = useContext(ItemContext); + const { register, handleSubmit } = useForm({ + mode: 'onChange', + defaultValues: item, + }); + + // 'api/delete/id' + useEffect(() => { + console.log('component did mounted'); + return () => { + const body = { ...item, ...itemData }; + console.log('bodyYODY', body); + /* + fetch('/api/updateItem', { + method: 'POST', + headers: { + Content-Type: 'application/json', + } + body: JSON.stringify(body) + }) + */ + }; + }, []); + + const onSubmit = (data) => { + console.log(data); + const { + itemname, + itemclothingtype, + itemcolor, + donation_status, + worn, + } = data; + // worn true or false ? + const update = { + ...item, + itemname, + itemclothingtype, + itemcolor, + worn, + donation_status: donation_status == '0' ? 'inactive' : 'active', + }; + + setItemData(update); + }; + + return ( +
+
+ {/* */} + + {/* */} + {/* */} +
+ + Rating + { + setValue(newValue); + }} + /> + +
+ +
+ {/* */} +
+ +
+ {/* */} +
+
+
+ ); +}; + +export default ItemDescribe; diff --git a/src/App/components/ItemView.js b/src/App/components/ItemView.js new file mode 100644 index 0000000..5985454 --- /dev/null +++ b/src/App/components/ItemView.js @@ -0,0 +1,15 @@ +import React from 'react'; + +/* +Renders the image side of the image card +*/ + +const ItemView = ({ image }) => { + return ( +
+ +
+ ); +}; + +export default ItemView; diff --git a/src/App/components/Landing.js b/src/App/components/Landing.js deleted file mode 100644 index b45a3b9..0000000 --- a/src/App/components/Landing.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - BrowserRouter as Router, - Route, - Link, - Switch, - NavLink, -} from 'react-router-dom'; - -const Landing = () => { - return ( -
- - -
- ); -}; - -export default Landing; diff --git a/src/App/components/Login.js b/src/App/components/Login.js new file mode 100644 index 0000000..8f4c49d --- /dev/null +++ b/src/App/components/Login.js @@ -0,0 +1,41 @@ +import React from 'react'; +import GoogleButton from 'react-google-button'; + +/* +No formatting, needs help and guidance and love +*/ +const Login = () => ( +
+ +

A new way to clean your closet

+ {/* google */} +
+ + + +
+
+ {/* facebook */} +
+); + +export default Login; + +/* + + +
+ + */ diff --git a/src/App/components/Logout.js b/src/App/components/Logout.js new file mode 100644 index 0000000..1900c45 --- /dev/null +++ b/src/App/components/Logout.js @@ -0,0 +1,13 @@ +import React from 'react'; + +/* +This was never used +*/ + +const Login = () => ( +
+

LOGIN OUT YO

+
+); + +export default Login; diff --git a/src/App/components/Market.js b/src/App/components/Market.js new file mode 100644 index 0000000..91033f5 --- /dev/null +++ b/src/App/components/Market.js @@ -0,0 +1,18 @@ +import React, { useState, useEffect } from 'react'; +import MarketCard from './MarketCard'; + +/* +Iterates through marketData and returns MarketCard components +for each item. + +This component is a little too abstracted, but it leaves room for scaling +*/ + +const Market = ({ marketData }) => { + const cards = marketData.map((item, i) => ( + + )); + return
{cards}
; +}; + +export default Market; diff --git a/src/App/components/MarketCard.js b/src/App/components/MarketCard.js new file mode 100644 index 0000000..fd411c0 --- /dev/null +++ b/src/App/components/MarketCard.js @@ -0,0 +1,32 @@ +import React, { useState, useEffect, useContext } from 'react'; + +import MarketDescribe from './MarketDescribe'; +import ItemView from './ItemView'; + +/* +MarketCard is the whole component that holds the information for +each item in the Market +*/ + +const MarketCard = ({ id, item }) => { + const [visible, setVisibility] = useState(false); + const [sticky, setSticky] = useState(false); + const { itemimage } = item; + + return ( +
setVisibility(true)} + onMouseLeave={() => setVisibility(false)} + onClick={() => setSticky(!sticky)} + > + {visible || sticky ? ( + + ) : ( + + )} +
+ ); +}; + +export default MarketCard; diff --git a/src/App/components/MarketDescribe.js b/src/App/components/MarketDescribe.js new file mode 100644 index 0000000..6ae196c --- /dev/null +++ b/src/App/components/MarketDescribe.js @@ -0,0 +1,28 @@ +import React, { useState, useEffect, useContext } from 'react'; + +/* +The back of each Market Card, has been neglected +is in need of item description, and a button to msg the user +*/ + +const MarketDescribe = ({ item }) => { + return ( +
+ {/*

{item.itemname}

+

{item.itemclothingtype}

+

{item.itemcolor}

+
+

Interested?

+ +
*/} +
+ ); +}; + +export default MarketDescribe; diff --git a/src/App/components/Nav.js b/src/App/components/Nav.js index 490b094..ffcaecc 100644 --- a/src/App/components/Nav.js +++ b/src/App/components/Nav.js @@ -1,24 +1,38 @@ import React from 'react'; import { Link } from 'react-router-dom'; +/* +Our Primary Nav bar uses React Router, +*/ + const Nav = () => { return ( -
- -
  • - Home +
    + {/* +
  • + Login
  • - + */} +
  • - Closet + Closet
  • - +
  • - Donation + Marketplace
  • + +
  • + Logout +
  • +
    ); }; diff --git a/src/App/components/NewItem.js b/src/App/components/NewItem.js new file mode 100644 index 0000000..448603f --- /dev/null +++ b/src/App/components/NewItem.js @@ -0,0 +1,145 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; + + +/* +unaddressed, needs reformating, could be refactored with react-hook-form + +notes from first phase: +when updating state, we are putting into the userInput state object +key-value pairs. the four pairs are: +itemName: String -> user types into texbar +itemClothingType: String -> user selects from dropdown bar +itemColor: String -> user selects from dropdown bar +itemImage: File API integration -> user selects from file from device storage +*/ + + +const NewItem = () => { + const initialState = { + itemName: '', + itemClothingType: 'Tops/Shirts/Tees', + itemColor: 'Black', + itemImage: + 'https://res.cloudinary.com/dfu8r9blo/image/upload/v1605922447/HangerImages/no_uploaded_cu28uy.png', + }; + const [closetItem, setClosetItem] = useState(initialState); + const [loading, setLoading] = useState(false); + const { register, handleSubmit } = useForm(); + const onSubmit = (data) => console.log(data, '<---- data'); + + // const handleSubmit = async () => { + // try { + // setLoading(true); + // // console.log(JSON.stringify(userInput)); + // const response = await fetch('/api', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify(closet), + // }); + // setLoading(false); + // setClosetItem(initialState); + // } catch (error) { + // console.log(error); + // setLoading(false); + // } + // }; + + const handleImageUpload = async (event) => { + const [file] = event.target.files; + const data = new FormData(); + data.append('file', file); + data.append('upload_preset', 'hanger'); + setLoading(true); + try { + const response = await fetch( + 'https://api.cloudinary.com/v1_1/dfu8r9blo/image/upload', + { + method: 'POST', + body: data, + } + ); + const { url } = await response.json(); + setClosetItem({ ...closetItem, itemImage: url }); + setLoading(false); + } catch (error) { + console.log(error); + setLoading(false); + } + }; + + return ( +
    +

    Add to Closet

    +

    Upload Image

    +
    +
    + + + setClosetItem({ ...closetItem, itemName: event.target.value }) + } + /> + +
    + +
    +

    Select Color

    + +

    Select Type

    + +
    +
    +
    + ); +}; + +export default NewItem; diff --git a/src/App/components/Row.js b/src/App/components/Row.js deleted file mode 100644 index 847f9df..0000000 --- a/src/App/components/Row.js +++ /dev/null @@ -1,11 +0,0 @@ -import React, { useState } from 'react'; - -const Row = ({ id, item, type, color, image }) => { - return ( -
    - {item}, {type}, {color}, -
    - ); -}; - -export default Row; diff --git a/src/App/components/TableDisplay.js b/src/App/components/TableDisplay.js deleted file mode 100644 index 3b702d1..0000000 --- a/src/App/components/TableDisplay.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState } from 'react'; -import Row from './Row'; - -const TableDisplay = ({ tableData }) => { - console.log(tableData); - const rows = []; - for (let i = 0; i < tableData.length; i++) { - console.log(tableData[i]); - rows.push( - - // potential last prop is the active/donate status for the article of clothing - ); - } - - console.log(rows); - - return ( -
    -
    - Item - Type - Color - Image - Active/Donate - {/* Worn */} -
    -
    {rows}
    -
    - ); -}; - -export default TableDisplay; diff --git a/src/App/containers/ClosetContainer.js b/src/App/containers/ClosetContainer.js index 241f675..439925a 100644 --- a/src/App/containers/ClosetContainer.js +++ b/src/App/containers/ClosetContainer.js @@ -1,29 +1,39 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; + import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'; -import InputDisplay from '../components/InputDisplay'; -import TableDisplay from '../components/TableDisplay'; +import { ClosetContext } from '../Store/ClosetContext'; +import { UserContext } from '../Store/UserContext'; +import NewItem from '../components/NewItem'; +import Closet from '../components/Closet'; +import { Typography, Box, Button, makeStyles } from '@material-ui/core'; +/* + ClosetContainer is the top-level component that holds all of + the data from a user's closet + + We access the UserContext in order to make a fetch request to the backend. + We save our response to the global state of ClosetContext + + The component passes the data to a single component with optional paths + default to closet, but the user is also allowed ot add items into their closet +*/ +const useStyles = makeStyles((theme) => ({ + root: { + '& > *': { + margin: theme.spacing(1), + background: '#FF8E53', + color: 'white', + }, + }, +})); const ClosetContainer = () => { - // invoke useEffect and make a fetch request to our backend endpoint - // to retrieve the table data and store it in state. this useEffect - // should only trigger once (and not each time the page re-renders - // (a la componentDidMount)) - - // inputDisplay does not need any props passed down to it - // pass down the appropriate props to the tableDisplay component - - // not sure on this part... - // create a custom hook (useFetch) to pass down as a prop to InputDisplay - // (EDIT: or maybe define in separate file and import) - // this will allow InputDisplay to trigger the useEffect hook in here - // to re-fetch the latest data from the backend, which will then update - // and re-render the TableDisplay component - - const [closet, setCloset] = useState([]); - const [hasLoaded, setHasLoaded] = useState(false); + const classes = useStyles(); + const [user] = useContext(UserContext); + const [closet, setCloset] = useContext(ClosetContext); + const [hasLoaded, setHasLoaded] = useState(false); useEffect(() => { - fetch('/api') + fetch(`/api/closet/${user.verified}`) .then((response) => response.json()) .then((data) => { setCloset(data); @@ -33,12 +43,43 @@ const ClosetContainer = () => { }, []); return hasLoaded ? ( -
    - - +
    + +
    +
    + +
    +
    + +
    +
    + + + + + + + + + +
    ) : ( -
    Still loading...
    +
    +

    Still loading...

    +
    ); }; diff --git a/src/App/containers/Dashboard.js b/src/App/containers/Dashboard.js new file mode 100644 index 0000000..d5d4894 --- /dev/null +++ b/src/App/containers/Dashboard.js @@ -0,0 +1,40 @@ +import React, { useState, useEffect, useContext } from 'react'; + +import UserContainer from './UserContainer'; +import MarketplaceContainer from './MarketplaceContainer'; +import ClosetContainer from './ClosetContainer'; +// import Logout from '../components/Logout'; +import Login from '../components/Login'; +import Nav from '../components/Nav'; +import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import { UserContext } from '../Store/UserContext'; + +/* +Our Dashboard is the view a user would see upon authentication +It checks our global state to see if the user has a property of verified +the value at this property would be the users id within the database. + +if this id is successfully retrieved we weill render the dashboard, +if not, the application will be directed to our Login component +*/ + +const Dashboard = () => { + const [user] = useContext(UserContext); + const id = user ? user.verified : null; + return id ? ( + +
    +
    +
    + ) : ( + + ); +}; + +export default Dashboard; diff --git a/src/App/containers/MarketplaceContainer.js b/src/App/containers/MarketplaceContainer.js new file mode 100644 index 0000000..0b2d0bf --- /dev/null +++ b/src/App/containers/MarketplaceContainer.js @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react'; +import Market from '../components/Market'; + +/* +MarketplaceContainer is the top-level component that holds +all of the components at the endpoint '/marketplace' + +we make a fetch request to our backend to receive +all of the items inside of the user's closet + +We pass the data to a single component 'Market' +*/ + +const MarketplaceContainer = () => { + const [market, setMarkets] = useState(); + const [hasLoaded, setHasLoaded] = useState(false); + + useEffect(() => { + fetch('/api/marketplace') + .then((response) => response.json()) + .then((data) => { + setMarkets(data); + setHasLoaded(true); + }) + .catch((error) => console.log(error)); + }, []); + + return hasLoaded ? ( +
    +
    +
    +

    Marketplace

    + +
    +
    + ) : ( +
    +

    Still loading...

    +
    + ); +}; +export default MarketplaceContainer; diff --git a/src/App/containers/UserContainer.js b/src/App/containers/UserContainer.js new file mode 100644 index 0000000..473c879 --- /dev/null +++ b/src/App/containers/UserContainer.js @@ -0,0 +1,47 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { ClosetContext } from '../Store/ClosetContext'; +import { UserContext } from '../Store/UserContext'; + +/* +UserContainer is the wrapper that holds the user's profile data... +includes Profile Picture and name + +Here we make a fetch to our Server with the ID we received +as a cookie which will return all of the user's information +that was saved in our database. +*/ + +const UserContainer = () => { + const [closet, setCloset] = useContext(ClosetContext); + const [user, setUser] = useContext(UserContext); + const [loaded, setLoaded] = useState(false); + console.log('user closet: ', closet); + useEffect(() => { + fetch(`/api/user/${user.verified}`) + .then((response) => response.json()) + .then((data) => { + setUser({ ...user, data }); + setLoaded(true); + }) + .catch((e) => console.log(e)); + return () => { + console.log('unmount'); + }; + }, []); + + return loaded ? ( +
    +

    {`Hey ${user.data[0].display_name_1}!`}

    +
    + +
    +

    Total Items in Closet:

    +

    {closet ? closet.length : 0}

    +

    Total Items :

    +
    + ) : ( +

    Loading...

    + ); +}; + +export default UserContainer; diff --git a/src/App/index.js b/src/App/index.js index 44293f4..e1b43e9 100644 --- a/src/App/index.js +++ b/src/App/index.js @@ -1,7 +1,6 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -// import style from './static/style.css'; -// eslint-disable-next-line import/extensions -import App from './App.jsx'; +import { render } from 'react-dom'; -ReactDOM.render(, document.getElementById('app')); +import App from './App'; + +render(, document.getElementById('app')); diff --git a/src/Images/HangerLogo.png b/src/Images/HangerLogo.png deleted file mode 100644 index abe47cc..0000000 Binary files a/src/Images/HangerLogo.png and /dev/null differ diff --git a/src/Images/HangerTextLogo.png b/src/Images/HangerTextLogo.png deleted file mode 100644 index f8b7465..0000000 Binary files a/src/Images/HangerTextLogo.png and /dev/null differ diff --git a/src/stylesheets/styles.scss b/src/stylesheets/styles.scss index 891cf1a..8a2e6ed 100644 --- a/src/stylesheets/styles.scss +++ b/src/stylesheets/styles.scss @@ -1,4 +1,154 @@ +// @import url('https://fonts.googleapis.com/css2?family=Rye&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Rye&display=swap'); +@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); + +html { + margin: 0; +} +body { + margin: 0; + font-family: 'Roboto', sans-serif; +} +#landing { + display: flex; + position: relative; + justify-content: center; + flex-direction: column; + align-items: center; +} +#temp { + width: 200px; + height: 200px; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + background-color: pink; + border: 2px solid black; +} +.dashboard { + background-color: rgb(255, 255, 255); + height: 100vh; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-rows: 10% 90%; + grid-template-areas: + 'nav nav nav nav' + 'profile closet closet closet'; +} +#user-pic { + border-radius: 58px; +} + +.user-container { + // background-color: rgba(0, 0, 0, 0.25); + border: 1px solid grey; + display: flex; + flex-flow: column; + align-items: center; + height: 386px; + justify-content: flex-start; + grid-area: profile; + margin: 16px 16px 0 54px; +} +.MuiButton-colorInherit { + background-color: white; +} +.content-container { + // background-color: rgba(0, 0, 0, 0.25); + color: white; + grid-area: closet; + margin: 16px; + padding: 16px; + overflow-y: scroll; +} +.navHeaders:hover { + color: #e8a832; +} +#navlogo { + // margin-right: 73rem; + // margin-bottom: 0.5rem; + // position: absolute; + width: 15rem; + height: auto; +} +.nav { + position: relative; + grid-area: nav; + margin-top: 7px; + margin-bottom: 24px; + display: flex; + align-items: center; + align-content: flex-start; + justify-content: space-around; + border-bottom: 1px solid black; +} +.closet { + display: flex; + flex-flow: row wrap; + align-items: flex-start; + justify-content: space-between; +} + +.item-card { + flex-direction: column; + width: 250px; + height: 300px; + background-color: white; + display: flex; + flex-flow: row; + align-items: flex-start; + justify-content: center; + margin: 16px; + + input { + width: 80%; + padding: 8px; + text-align: center; + font-size: 1.5em; + background: none; + border: none; + &:focus { + outline: 1px solid black; + background-color: rgba(0, 0, 0, 0.1); + } + } +} +#subheading { + font-family: Rye, cursive; + position: absolute; + margin-top: 10rem; +} +.item-describe, +.item-view { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + padding: 10px; + + form { + display: flex; + flex-flow: column; + align-items: center; + justify-content: space-around; + } +} + +img.item-image { + // border: 1px solid black; + max-width: 90%; + max-height: 90%; +} + +.content-nav { + color: white; + margin: 16px; + display: flex; + flex-direction: row; + justify-content: space-between; +} .InputDisplay { display: flex; @@ -28,7 +178,7 @@ .Nav { list-style-type: none; - font-family: 'Rye', cursive; + font-family: 'Roboto', sans-serif; } .navBar { @@ -81,13 +231,13 @@ } p, h3 { - font-family: 'Rye', cursive; + font-family: 'Roboto', sans-serif; margin-right: 0.5rem; } a { text-decoration: none; - color: black; + color: grey; } .Nav { @@ -101,7 +251,7 @@ a span:hover { #viewCloset { border-radius: 3px; - font-family: 'Rye', cursive; + font-family: 'Roboto', sans-serif; background-color: white; font-size: 3rem; } diff --git a/webpack.config.js b/webpack.config.js index c776f53..dbd5bfd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,20 +1,18 @@ const path = require('path'); -const config = { +module.exports = { entry: './src/App/index.js', output: { - path: path.resolve(`${__dirname}/build`), + path: path.resolve(__dirname, 'build'), filename: 'bundle.js', }, mode: process.env.NODE_ENV, devServer: { proxy: { '/api': 'http://localhost:3000/', - // '/signup': 'http://localhost:3000/', - // '/login': 'http://localhost:3000/', - // '/authorized': 'http://localhost:3000/', + '/auth': 'http://localhost:3000', }, - publicPath: '/', + publicPath: '/build/', }, module: { rules: [ @@ -37,6 +35,7 @@ const config = { resolve: { extensions: ['.js', '.jsx'], }, + performance: { + hints: false, + }, }; - -module.exports = config;