From cd077f1ed7161b9242ec558644f582a817fa6526 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Sat, 1 Aug 2020 00:15:54 +0000 Subject: [PATCH 01/22] Add more information to restaurant cards, including images, reviews, and website. --- static/absolute/css/searchPage.css | 72 +++++---- static/absolute/js/searchPage.js | 229 +++++++++++++++++++++++------ 2 files changed, 233 insertions(+), 68 deletions(-) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 75b7ede..947612a 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -2,17 +2,17 @@ body { font-family: Helvetica, sans-serif; margin: 0px; } - + /* margin left small */ .ml-s { margin-left: 15px; } - + /* margin left medium */ .ml-m { margin-left: 25px; } - + /* Banner at top of page contains logo */ #header-banner { display: flex; @@ -22,28 +22,28 @@ body { border: 1px solid #ccc; background-color: whitesmoke; } - + .search-bar { flex: 1; display: flex; align-items: center; justify-content: center; } - + .header-button-container { display: flex; } - + #logo { display: inline-block; width: 130px; height: 90px; } - + .search-padding { padding: 12px 25px; } - + .parent { display: grid; grid-template-columns: repeat(12, 1fr); @@ -52,7 +52,7 @@ body { grid-row-gap: 0px; height: 100vh; } - + .header { grid-area: 1 / 1 / 2 / 13; } @@ -66,7 +66,7 @@ body { .map { grid-area: 2 / 8 / 13 / 13; } - + .input { width: 300px; margin: 8px 0; @@ -76,18 +76,18 @@ body { transition: 0.5s; outline: none; } - + .input:focus { border: 3px solid #555; } - + .search-label { text-align: left; color: #444; font-weight: 600; font-size: 14px; } - + #search-btn { background-color: #fe5f55; border: 1px solid rgb(145, 145, 145); @@ -97,43 +97,65 @@ body { text-decoration: none; font-size: 16px; } - + #search-btn:hover { background-color: rgb(226, 55, 43); } - + .search-instructions { color: rgb(145, 145, 145); font-size: 1.5em; } - -.restaurant-card { + +.info-div { display: grid; - grid-template-columns: 50% auto; + grid-template-columns: 25% auto auto; +} + +.restaurant-card { border: 1px solid #d4ede0; border-radius: 0.5rem; margin-top: 25px; padding: 25px; } - + .restaurant-card:hover { box-shadow: 3px 3px 15px #d1d1d1; transition: box-shadow 0.3s ease-in-out; } - + +.restaurant-car:active { + box-shadow: 3px 3px 15px #d1d1d1; +} + .restaurant-name { color: #fe5f55; } - + .restaurant-info { color: #444; } - + .restaurant-basic-info { text-align: right; color: #444; } - + +.review-header { + color: #fe5f55; + font-weight: bold; + margin-top: 0px; +} + +.more-info-link { + color: #fe5f55; + margin-bottom: 0px; +} + +.more-info-link:hover { + cursor: pointer; +} + .banner-btn { padding: 12px 25px; text-decoration: none; @@ -143,11 +165,11 @@ body { border: 2px solid #fe5f55; border-radius: 0.5rem; } - + .banner-btn:hover { background-color: rgb(245, 156, 150); } - + #participants-btn { margin-right: 25px; } diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index e7158b4..809d28b 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -128,12 +128,25 @@ async function refreshUI() { * For now, this function deals with hard-coded data, but this * can be used as a template for when we get data the Places Library results. */ -function showRestaurants(restaurantsResponse) { - const allRestaurants = restaurantsResponse.data; +async function showRestaurants(restaurantResponse) { const restaurantContainer = document.getElementById("restaurant-container"); - restaurantContainer.innerHTML = ""; - + + if (restaurantResponse.status !== 200) { + let restaurantErrorMessage = document.createElement("p"); + restaurantErrorMessage.classList.add("search-instructions"); + let messageText = + restaurantResponse.data === "ZERO_RESULTS" + ? "We could not find any restaurants. Check to make sure you are using the correct address." + : "Something went wrong when searching for restaurants."; + restaurantErrorMessage.appendChild(document.createTextNode(messageText)); + + restaurantContainer.appendChild(restaurantErrorMessage); + return; + } + + const allRestaurants = restaurantResponse.data; + if (!allRestaurants.hasOwnProperty("results")) { let instructions = document.createElement("p"); instructions.classList.add("search-instructions"); @@ -142,62 +155,192 @@ function showRestaurants(restaurantsResponse) { "No one has added their information yet! Fill out the form above to start seeing some results." ) ); - + restaurantContainer.appendChild(instructions); return; } - + + //Used to get additional information about the restaurant results. + const fields = "url,formatted_phone_number,website,review"; + //This will be used to create a new div for every restaurant returned by the Places Library: - allRestaurants.results.forEach((restaurant) => { - let restaurantDiv = document.createElement("div"); - restaurantDiv.classList.add("restaurant-card"); - + let restaurantIndex = 1; + for (restaurant of allRestaurants.results) { + //Get additional details for every restaurant. + let placeDetailsResponse = await ( + await fetch( + `/api/placedetails?fields=${fields}&id=${restaurant.place_id}` + ) + ).json(); + + let additionalDetails = + placeDetailsResponse.status === 200 + ? placeDetailsResponse.data.result + : {}; + + //Create a restaurant card to hold information about each restaurant. + let infoDiv = document.createElement("div"); + infoDiv.classList.add("info-div"); + + //Create a section to hold the image + try { + let imageDiv = document.createElement("div"); + // if (placePhotosResponse.hasOwnProperty("data")) { + let image = document.createElement("img"); + image.src = + "https://maps.googleapis.com/maps/api/place/photo?photoreference=" + + restaurant.photos[0].photo_reference + + "&maxwidth=150&key=" + + "ApiKey"; + image.width = "150"; //px + imageDiv.appendChild(image); + infoDiv.appendChild(imageDiv); + } catch (err) { + console.log("Restaurant image could not be retrieved. Error: " + err); + } + // Add information to the left side of the restaurant card. This contains name and atmospheric information let leftDiv = document.createElement("div"); - + let name = document.createElement("h2"); name.classList.add("restaurant-name"); let restaurantName = restaurant.hasOwnProperty("name") ? restaurant.name : ""; - name.appendChild(document.createTextNode(restaurantName)); + name.appendChild( + document.createTextNode(restaurantIndex + ". " + restaurantName) + ); leftDiv.appendChild(name); - - let rating = document.createElement("p"); - rating.classList.add("restaurant-info"); - let restaurantRating = restaurant.hasOwnProperty("rating") - ? restaurant.rating - : "Unknown"; - rating.appendChild(document.createTextNode("Rating: " + restaurantRating)); - leftDiv.appendChild(rating); - - restaurantDiv.appendChild(leftDiv); - + + if (restaurant.hasOwnProperty("rating")) { + let rating = document.createElement("p"); + rating.classList.add("restaurant-info"); + rating.appendChild( + document.createTextNode("Rating: " + restaurant.rating) + ); + leftDiv.appendChild(rating); + } + + infoDiv.appendChild(leftDiv); + //Add information to the left side of the restaurant card. This contains contact information. let rightDiv = document.createElement("div"); - - let address = document.createElement("p"); - address.classList.add("restaurant-basic-info"); - let restaurantVicinity = restaurant.hasOwnProperty("vicinity") - ? restaurant.vicinity - : ""; - address.appendChild(document.createTextNode(restaurantVicinity)); - rightDiv.appendChild(address); - - let openingHours = document.createElement("p"); - openingHours.classList.add("restaurant-basic-info"); - let openNow = ""; + + if (restaurant.hasOwnProperty("vicinity")) { + let address = document.createElement("p"); + address.classList.add("restaurant-basic-info"); + address.appendChild(document.createTextNode(restaurant.vicinity)); + rightDiv.appendChild(address); + } + + if (additionalDetails.hasOwnProperty("formatted_phone_number")) { + let phoneNumber = document.createElement("p"); + phoneNumber.classList.add("restaurant-basic-info"); + phoneNumber.appendChild( + document.createTextNode(additionalDetails.formatted_phone_number) + ); + rightDiv.appendChild(phoneNumber); + } + + if (additionalDetails.hasOwnProperty("website")) { + let website = document.createElement("p"); + website.classList.add("restaurant-basic-info"); + + let link = document.createElement("a"); + link.appendChild(document.createTextNode("Website")); + link.href = additionalDetails.website; + + website.appendChild(link); + rightDiv.appendChild(website); + } + if (restaurant.hasOwnProperty("opening_hours")) { - openNow = Object.values(restaurant.opening_hours) + let openingHours = document.createElement("p"); + openingHours.classList.add("restaurant-basic-info"); + let openNow = Object.values(restaurant.opening_hours) ? "Open Now" - : "Closed now"; + : "Closed Now"; + + openingHours.appendChild(document.createTextNode(openNow)); + rightDiv.appendChild(openingHours); } - openingHours.appendChild(document.createTextNode(openNow)); - rightDiv.appendChild(openingHours); - - restaurantDiv.appendChild(rightDiv); - restaurantContainer.appendChild(restaurantDiv); - }); + + infoDiv.appendChild(rightDiv); + + //Add review to the card when clicked. + let moreInfoDiv = document.createElement("div"); + moreInfoDiv.classList.add("restaurant-info"); + + if (additionalDetails.hasOwnProperty("reviews")) { + let reviewContainerDiv = document.createElement("div"); + + //Create header for review section + let reviewDivHeader = document.createElement("h3"); + reviewDivHeader.appendChild(document.createTextNode("Reviews")); + reviewDivHeader.classList.add("review-header"); + reviewContainerDiv.appendChild(reviewDivHeader); + + let reviews = additionalDetails.reviews; + for (i = 0; i < reviews.length && i < 2; i++) { + let reviewerName = reviews[i].hasOwnProperty("author_name") + ? reviews[i].author_name + : ""; + let reviewTime = reviews[i].hasOwnProperty("relative_time_description") + ? reviews[i].relative_time_description + : ""; + + let reviewHeader = document.createElement("p"); + reviewHeader.appendChild( + document.createTextNode(reviewerName + " - " + reviewTime) + ); + let reviewText = document.createElement("p"); + reviewText.appendChild(document.createTextNode("\"" + reviews[i].text + "\"")); + + let reviewDiv = document.createElement("div"); + reviewDiv.appendChild(reviewHeader); + reviewDiv.appendChild(reviewText); + + reviewContainerDiv.appendChild(reviewDiv); + reviewContainerDiv.appendChild(document.createElement("br")); + } + moreInfoDiv.appendChild(reviewContainerDiv); + } + + if (additionalDetails.hasOwnProperty("url")) { + let listingLink = document.createElement("a"); + listingLink.appendChild(document.createTextNode("See Listing on Google")); + listingLink.href = additionalDetails.url; + + moreInfoDiv.appendChild(listingLink); + } + + moreInfoDiv.style.display = "none"; + + //Create a link to show moreInfoDiv + let moreInfoLink = document.createElement("p"); + moreInfoLink.classList.add("more-info-link", "restaurant-info"); + moreInfoLink.innerHTML = "Show More ↓";//With down arrow + + moreInfoLink.onclick = function () { + if (moreInfoDiv.style.display === "inline") { + moreInfoDiv.style.display = "none"; + moreInfoLink.innerHTML = "Show More ↓"//With down arrow + } else { + moreInfoDiv.style.display = "inline"; + moreInfoLink.innerHTML = "Show Less ↑"//With up arrow + } + }; + + //Add the two info sections to a restaurant card div. + let restaurantCardDiv = document.createElement("div"); + restaurantCardDiv.appendChild(infoDiv); + restaurantCardDiv.appendChild(moreInfoDiv); + restaurantCardDiv.appendChild(moreInfoLink) + restaurantCardDiv.classList.add("restaurant-card"); + + restaurantContainer.appendChild(restaurantCardDiv); + restaurantIndex++; + } } // Initializes a Map. From 62876fbd62bfd1ee4eb081f992bd1e557a2b4b14 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Sat, 1 Aug 2020 00:47:53 +0000 Subject: [PATCH 02/22] Add apis on the server to call Google's place photos and place details apis. --- src/server.js | 115 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/src/server.js b/src/server.js index 0a9264b..663fc74 100644 --- a/src/server.js +++ b/src/server.js @@ -7,46 +7,47 @@ const cookieSession = require("cookie-session"); const datastore = require("./datastore"); const fetch = require("node-fetch"); const { env } = process; - + const app = express(); - + const KIND_EVENT = "Event"; const URL_PARAM_EVENT_ID = `eventID`; - + // TODO(ved): There's definitely a cleaner way to do this. const PREFIX_API = "/api"; - + const ERROR_BAD_DB_INTERACTION = "BAD_DATABASE"; const ERROR_INVALID_EVENT_ID = "INVALID_EVENT_ID"; const ERROR_BAD_UUID = "BAD_UUID"; const ERROR_GEOCODING_FAILED = "GEOCODING_FAILED"; const ERROR_BAD_PLACES_API_INTERACTION = "BAD_PLACES_API"; - +const ERROR_PLACE_DETAILS_FAILED = "PLACE_DETAILS_FAILED"; + app.use(express.static("static/absolute")); - + function getAbsolutePath(htmlFileName) { return path.join(process.cwd(), "static", htmlFileName); } - + app.get("/", (_, response) => { response.redirect("/create"); }); - + app.get(`/create`, (_, response) => { response.sendFile(getAbsolutePath("createSession.html")); }); - + app.get(`/:${URL_PARAM_EVENT_ID}`, (_, response) => { response.sendFile(getAbsolutePath("searchPage.html")); }); - + app.get(`/:${URL_PARAM_EVENT_ID}/participants`, (_, response) => { response.sendFile(getAbsolutePath("participants.html")); }); - + // Parse request bodies with the json content header into JSON app.use(express.json()); - + app.use( cookieSession({ name: "session", @@ -54,7 +55,7 @@ app.use( maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year }) ); - + /** * Middleware that attaches to every request and checks if there * is a user id. If not, we assume this is a new user and give @@ -80,7 +81,7 @@ app.use((request, response, next) => { } next(); }); - + /** * Middleware that can be used on routes that match `/:${URL_PARAM_EVENT_ID}/...` * This will fetch the event from Datastore and attach it and its key to @@ -139,7 +140,7 @@ async function getEvent(request, response, next) { }); }); } - + app.post(`${PREFIX_API}/create`, async (request, response) => { const { name } = request.body; const key = datastore.key([KIND_EVENT]); @@ -154,7 +155,7 @@ app.post(`${PREFIX_API}/create`, async (request, response) => { eventID: result[0].mutationResults[0].key.path[0].id, }); }); - + app.post( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}`, getEvent, @@ -182,7 +183,7 @@ app.post( }); } ); - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/name`, getEvent, @@ -190,7 +191,7 @@ app.get( response.json({ status: 200, data: request.event.name }); } ); - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/me`, getEvent, @@ -204,7 +205,7 @@ app.get( }); } ); - + function averageGeolocation(coords) { if (coords.length === 0) { return {}; @@ -221,29 +222,29 @@ function averageGeolocation(coords) { for (let coord of coords) { let latitude = (coord.lat * Math.PI) / 180; let longitude = (coord.long * Math.PI) / 180; - + x += Math.cos(latitude) * Math.cos(longitude); y += Math.cos(latitude) * Math.sin(longitude); z += Math.sin(latitude); } - + let total = coords.length; - + x = x / total; y = y / total; z = z / total; - + let centralLongitude = Math.atan2(y, x); let centralSquareRoot = Math.sqrt(x * x + y * y); let centralLatitude = Math.atan2(z, centralSquareRoot); - + return { latitude: (centralLatitude * 180) / Math.PI, longitude: (centralLongitude * 180) / Math.PI, }; } } - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/restaurants`, getEvent, @@ -251,7 +252,7 @@ app.get( const { event } = request; const users = event.users || {}; const userData = Object.values(users); - + if (userData.length > 0) { const { latitude, longitude } = averageGeolocation(userData); const lat = latitude.toString(); @@ -260,7 +261,7 @@ app.get( const type = "restaurant"; const minprice = "0"; const maxprice = "4"; - + // Try for invalid Json Response. try { const placesApiResponse = await ( @@ -268,7 +269,7 @@ app.get( `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${long}&rankby=${rankby}&type=${type}&minprice=${minprice}&maxprice=${maxprice}&key=${env.API_KEY_PLACES}` ) ).json(); - + const { status } = placesApiResponse; if (status !== "OK") { console.error("Places API error. Status: " + status); @@ -301,7 +302,46 @@ app.get( } } ); - + +app.get(`${PREFIX_API}/placedetails`, async (request, response) => { + const placeId = request.query.id; + const fields = request.query.fields; + + const placeDetailsRequest = + "https://maps.googleapis.com/maps/api/place/details/json?place_id=" + + placeId + + "&fields=" + + fields + + "&key=" + + env.API_KEY_PLACES; + + try { + const placeDetailsResponse = await ( + await fetch(placeDetailsRequest) + ).json(); + const responseStatus = placeDetailsResponse.status; + + if (responseStatus !== "OK") { + console.error( + "Place Details error occured. Api response status: " + responseStatus + ); + response + .status(500) + .json({ status: 500, error: { type: responseStatus } }); + } else { + response.json({ + status: 200, + data: placeDetailsResponse, + }); + } + } catch (err) { + console.error(err); + response + .status(500) + .json({ status: 500, error: { type: ERROR_PLACE_DETAILS_FAILED } }); + } +}); + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/participants`, getEvent, @@ -314,20 +354,20 @@ app.get( }); } ); - + app.get(`${PREFIX_API}/geocode`, async (request, response) => { const address = encodeAddress(request.query.address); - + const geocodeRequest = "https://maps.googleapis.com/maps/api/geocode/json?address=" + address + "&key=" + env.API_KEY_GEOCODE; - + try { const geocodeResponse = await (await fetch(geocodeRequest)).json(); const geocodeResponseStatus = geocodeResponse.status; - + if (geocodeResponseStatus !== "OK") { console.error( "Geocoding error occured. Api response status: " + geocodeResponseStatus @@ -349,7 +389,7 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); } }); - + function encodeAddress(address) { const formattedAddress = encodeURIComponent(address) .replace("!", "%21") @@ -359,16 +399,16 @@ function encodeAddress(address) { .replace(")", "%29"); return formattedAddress; } - + // This number should be kept in sync with the port number in nodemon.json const port = 8080; const server = app.listen(port, () => console.log(`Server listening on http://localhost:${port}`) ); - + //Attach Socket.io to the existing express server. const io = require("socket.io")(server); - + //When socket.io is connected to the server, we can listen for events. io.on("connection", (socket) => { //Add client socket to a room based on the session ID. This will allow only clients with the same ID to communicate. @@ -376,3 +416,4 @@ io.on("connection", (socket) => { socket.join(eventId); }); }); + From 8a0297d430a85c29fe295a44a68db12c6a9866c8 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 14:08:49 +0000 Subject: [PATCH 03/22] Change styles for review section. Add price level to restaurant card. --- src/server.js | 8 ++++---- static/absolute/css/searchPage.css | 11 ++++++++++- static/absolute/js/searchPage.js | 27 +++++++++++++++++++-------- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/server.js b/src/server.js index 663fc74..3448c17 100644 --- a/src/server.js +++ b/src/server.js @@ -304,8 +304,8 @@ app.get( ); app.get(`${PREFIX_API}/placedetails`, async (request, response) => { - const placeId = request.query.id; - const fields = request.query.fields; + const placeId = encodeForURL(request.query.id); + const fields = encodeForURL(request.query.fields); const placeDetailsRequest = "https://maps.googleapis.com/maps/api/place/details/json?place_id=" + @@ -356,7 +356,7 @@ app.get( ); app.get(`${PREFIX_API}/geocode`, async (request, response) => { - const address = encodeAddress(request.query.address); + const address = encodeForURL(request.query.address); const geocodeRequest = "https://maps.googleapis.com/maps/api/geocode/json?address=" + @@ -390,7 +390,7 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { } }); -function encodeAddress(address) { +function encodeForURL(address) { const formattedAddress = encodeURIComponent(address) .replace("!", "%21") .replace("*", "%2A") diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 947612a..f29634d 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -2,6 +2,12 @@ body { font-family: Helvetica, sans-serif; margin: 0px; } + +hr { + height: 1px; + background-color: #d4ede0; + border: none; +} /* margin left small */ .ml-s { @@ -144,7 +150,10 @@ body { .review-header { color: #fe5f55; font-weight: bold; - margin-top: 0px; +} + +.individual-review { + padding: 25px; } .more-info-link { diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 809d28b..21136db 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -191,7 +191,7 @@ async function showRestaurants(restaurantResponse) { "https://maps.googleapis.com/maps/api/place/photo?photoreference=" + restaurant.photos[0].photo_reference + "&maxwidth=150&key=" + - "ApiKey"; + "APIKey"; image.width = "150"; //px imageDiv.appendChild(image); infoDiv.appendChild(imageDiv); @@ -220,6 +220,13 @@ async function showRestaurants(restaurantResponse) { ); leftDiv.appendChild(rating); } + + if (restaurant.hasOwnProperty("price_level")) { + let priceLevel = document.createElement("p"); + priceLevel.classList.add("restaurant-info"); + priceLevel.appendChild(document.createTextNode("$".repeat(restaurant.price_level))); + leftDiv.appendChild(priceLevel); + } infoDiv.appendChild(leftDiv); @@ -279,6 +286,7 @@ async function showRestaurants(restaurantResponse) { reviewDivHeader.appendChild(document.createTextNode("Reviews")); reviewDivHeader.classList.add("review-header"); reviewContainerDiv.appendChild(reviewDivHeader); + reviewContainerDiv.appendChild(document.createElement("hr")); let reviews = additionalDetails.reviews; for (i = 0; i < reviews.length && i < 2; i++) { @@ -289,22 +297,25 @@ async function showRestaurants(restaurantResponse) { ? reviews[i].relative_time_description : ""; - let reviewHeader = document.createElement("p"); - reviewHeader.appendChild( + let individualReviewHeader = document.createElement("p"); + individualReviewHeader.appendChild( document.createTextNode(reviewerName + " - " + reviewTime) ); let reviewText = document.createElement("p"); reviewText.appendChild(document.createTextNode("\"" + reviews[i].text + "\"")); - let reviewDiv = document.createElement("div"); - reviewDiv.appendChild(reviewHeader); - reviewDiv.appendChild(reviewText); + let individualReviewDiv = document.createElement("div"); + individualReviewDiv.classList.add("individual-review"); + individualReviewDiv.appendChild(individualReviewHeader); + individualReviewDiv.appendChild(reviewText); - reviewContainerDiv.appendChild(reviewDiv); - reviewContainerDiv.appendChild(document.createElement("br")); + reviewContainerDiv.appendChild(individualReviewDiv); + reviewContainerDiv.appendChild(document.createElement("hr")); } moreInfoDiv.appendChild(reviewContainerDiv); } + + moreInfoDiv.appendChild(document.createElement("br")); if (additionalDetails.hasOwnProperty("url")) { let listingLink = document.createElement("a"); From ae83434bd2762d8c9e0e1db242bd5816b4978894 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 14:40:26 +0000 Subject: [PATCH 04/22] Rearranged line for better organization. --- src/server.js | 83 +++++++++++++------------- static/absolute/css/searchPage.css | 56 +++++++++--------- static/absolute/js/searchPage.js | 93 ++++++++++++++++-------------- 3 files changed, 118 insertions(+), 114 deletions(-) diff --git a/src/server.js b/src/server.js index 3448c17..4d52bd7 100644 --- a/src/server.js +++ b/src/server.js @@ -7,47 +7,47 @@ const cookieSession = require("cookie-session"); const datastore = require("./datastore"); const fetch = require("node-fetch"); const { env } = process; - + const app = express(); - + const KIND_EVENT = "Event"; const URL_PARAM_EVENT_ID = `eventID`; - + // TODO(ved): There's definitely a cleaner way to do this. const PREFIX_API = "/api"; - + const ERROR_BAD_DB_INTERACTION = "BAD_DATABASE"; const ERROR_INVALID_EVENT_ID = "INVALID_EVENT_ID"; const ERROR_BAD_UUID = "BAD_UUID"; const ERROR_GEOCODING_FAILED = "GEOCODING_FAILED"; const ERROR_BAD_PLACES_API_INTERACTION = "BAD_PLACES_API"; const ERROR_PLACE_DETAILS_FAILED = "PLACE_DETAILS_FAILED"; - + app.use(express.static("static/absolute")); - + function getAbsolutePath(htmlFileName) { return path.join(process.cwd(), "static", htmlFileName); } - + app.get("/", (_, response) => { response.redirect("/create"); }); - + app.get(`/create`, (_, response) => { response.sendFile(getAbsolutePath("createSession.html")); }); - + app.get(`/:${URL_PARAM_EVENT_ID}`, (_, response) => { response.sendFile(getAbsolutePath("searchPage.html")); }); - + app.get(`/:${URL_PARAM_EVENT_ID}/participants`, (_, response) => { response.sendFile(getAbsolutePath("participants.html")); }); - + // Parse request bodies with the json content header into JSON app.use(express.json()); - + app.use( cookieSession({ name: "session", @@ -55,7 +55,7 @@ app.use( maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year }) ); - + /** * Middleware that attaches to every request and checks if there * is a user id. If not, we assume this is a new user and give @@ -81,7 +81,7 @@ app.use((request, response, next) => { } next(); }); - + /** * Middleware that can be used on routes that match `/:${URL_PARAM_EVENT_ID}/...` * This will fetch the event from Datastore and attach it and its key to @@ -140,7 +140,7 @@ async function getEvent(request, response, next) { }); }); } - + app.post(`${PREFIX_API}/create`, async (request, response) => { const { name } = request.body; const key = datastore.key([KIND_EVENT]); @@ -155,7 +155,7 @@ app.post(`${PREFIX_API}/create`, async (request, response) => { eventID: result[0].mutationResults[0].key.path[0].id, }); }); - + app.post( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}`, getEvent, @@ -183,7 +183,7 @@ app.post( }); } ); - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/name`, getEvent, @@ -191,7 +191,7 @@ app.get( response.json({ status: 200, data: request.event.name }); } ); - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/me`, getEvent, @@ -205,7 +205,7 @@ app.get( }); } ); - + function averageGeolocation(coords) { if (coords.length === 0) { return {}; @@ -222,29 +222,29 @@ function averageGeolocation(coords) { for (let coord of coords) { let latitude = (coord.lat * Math.PI) / 180; let longitude = (coord.long * Math.PI) / 180; - + x += Math.cos(latitude) * Math.cos(longitude); y += Math.cos(latitude) * Math.sin(longitude); z += Math.sin(latitude); } - + let total = coords.length; - + x = x / total; y = y / total; z = z / total; - + let centralLongitude = Math.atan2(y, x); let centralSquareRoot = Math.sqrt(x * x + y * y); let centralLatitude = Math.atan2(z, centralSquareRoot); - + return { latitude: (centralLatitude * 180) / Math.PI, longitude: (centralLongitude * 180) / Math.PI, }; } } - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/restaurants`, getEvent, @@ -252,7 +252,7 @@ app.get( const { event } = request; const users = event.users || {}; const userData = Object.values(users); - + if (userData.length > 0) { const { latitude, longitude } = averageGeolocation(userData); const lat = latitude.toString(); @@ -261,7 +261,7 @@ app.get( const type = "restaurant"; const minprice = "0"; const maxprice = "4"; - + // Try for invalid Json Response. try { const placesApiResponse = await ( @@ -269,7 +269,7 @@ app.get( `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${long}&rankby=${rankby}&type=${type}&minprice=${minprice}&maxprice=${maxprice}&key=${env.API_KEY_PLACES}` ) ).json(); - + const { status } = placesApiResponse; if (status !== "OK") { console.error("Places API error. Status: " + status); @@ -302,11 +302,11 @@ app.get( } } ); - + app.get(`${PREFIX_API}/placedetails`, async (request, response) => { const placeId = encodeForURL(request.query.id); const fields = encodeForURL(request.query.fields); - + const placeDetailsRequest = "https://maps.googleapis.com/maps/api/place/details/json?place_id=" + placeId + @@ -314,13 +314,13 @@ app.get(`${PREFIX_API}/placedetails`, async (request, response) => { fields + "&key=" + env.API_KEY_PLACES; - + try { const placeDetailsResponse = await ( await fetch(placeDetailsRequest) ).json(); const responseStatus = placeDetailsResponse.status; - + if (responseStatus !== "OK") { console.error( "Place Details error occured. Api response status: " + responseStatus @@ -341,7 +341,7 @@ app.get(`${PREFIX_API}/placedetails`, async (request, response) => { .json({ status: 500, error: { type: ERROR_PLACE_DETAILS_FAILED } }); } }); - + app.get( `${PREFIX_API}/:${URL_PARAM_EVENT_ID}/participants`, getEvent, @@ -354,20 +354,20 @@ app.get( }); } ); - + app.get(`${PREFIX_API}/geocode`, async (request, response) => { const address = encodeForURL(request.query.address); - + const geocodeRequest = "https://maps.googleapis.com/maps/api/geocode/json?address=" + address + "&key=" + env.API_KEY_GEOCODE; - + try { const geocodeResponse = await (await fetch(geocodeRequest)).json(); const geocodeResponseStatus = geocodeResponse.status; - + if (geocodeResponseStatus !== "OK") { console.error( "Geocoding error occured. Api response status: " + geocodeResponseStatus @@ -389,7 +389,7 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); } }); - + function encodeForURL(address) { const formattedAddress = encodeURIComponent(address) .replace("!", "%21") @@ -399,16 +399,16 @@ function encodeForURL(address) { .replace(")", "%29"); return formattedAddress; } - + // This number should be kept in sync with the port number in nodemon.json const port = 8080; const server = app.listen(port, () => console.log(`Server listening on http://localhost:${port}`) ); - + //Attach Socket.io to the existing express server. const io = require("socket.io")(server); - + //When socket.io is connected to the server, we can listen for events. io.on("connection", (socket) => { //Add client socket to a room based on the session ID. This will allow only clients with the same ID to communicate. @@ -416,4 +416,3 @@ io.on("connection", (socket) => { socket.join(eventId); }); }); - diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index f29634d..39a9b4a 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -8,17 +8,17 @@ hr { background-color: #d4ede0; border: none; } - + /* margin left small */ .ml-s { margin-left: 15px; } - + /* margin left medium */ .ml-m { margin-left: 25px; } - + /* Banner at top of page contains logo */ #header-banner { display: flex; @@ -28,28 +28,28 @@ hr { border: 1px solid #ccc; background-color: whitesmoke; } - + .search-bar { flex: 1; display: flex; align-items: center; justify-content: center; } - + .header-button-container { display: flex; } - + #logo { display: inline-block; width: 130px; height: 90px; } - + .search-padding { padding: 12px 25px; } - + .parent { display: grid; grid-template-columns: repeat(12, 1fr); @@ -58,7 +58,7 @@ hr { grid-row-gap: 0px; height: 100vh; } - + .header { grid-area: 1 / 1 / 2 / 13; } @@ -72,7 +72,7 @@ hr { .map { grid-area: 2 / 8 / 13 / 13; } - + .input { width: 300px; margin: 8px 0; @@ -82,18 +82,18 @@ hr { transition: 0.5s; outline: none; } - + .input:focus { border: 3px solid #555; } - + .search-label { text-align: left; color: #444; font-weight: 600; font-size: 14px; } - + #search-btn { background-color: #fe5f55; border: 1px solid rgb(145, 145, 145); @@ -103,50 +103,50 @@ hr { text-decoration: none; font-size: 16px; } - + #search-btn:hover { background-color: rgb(226, 55, 43); } - + .search-instructions { color: rgb(145, 145, 145); font-size: 1.5em; } - + .info-div { display: grid; grid-template-columns: 25% auto auto; } - + .restaurant-card { border: 1px solid #d4ede0; border-radius: 0.5rem; margin-top: 25px; padding: 25px; } - + .restaurant-card:hover { box-shadow: 3px 3px 15px #d1d1d1; transition: box-shadow 0.3s ease-in-out; } - + .restaurant-car:active { box-shadow: 3px 3px 15px #d1d1d1; } - + .restaurant-name { color: #fe5f55; } - + .restaurant-info { color: #444; } - + .restaurant-basic-info { text-align: right; color: #444; } - + .review-header { color: #fe5f55; font-weight: bold; @@ -155,16 +155,16 @@ hr { .individual-review { padding: 25px; } - + .more-info-link { color: #fe5f55; margin-bottom: 0px; } - + .more-info-link:hover { cursor: pointer; } - + .banner-btn { padding: 12px 25px; text-decoration: none; @@ -174,11 +174,11 @@ hr { border: 2px solid #fe5f55; border-radius: 0.5rem; } - + .banner-btn:hover { background-color: rgb(245, 156, 150); } - + #participants-btn { margin-right: 25px; } diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 21136db..73c2748 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -131,7 +131,7 @@ async function refreshUI() { async function showRestaurants(restaurantResponse) { const restaurantContainer = document.getElementById("restaurant-container"); restaurantContainer.innerHTML = ""; - + if (restaurantResponse.status !== 200) { let restaurantErrorMessage = document.createElement("p"); restaurantErrorMessage.classList.add("search-instructions"); @@ -140,13 +140,13 @@ async function showRestaurants(restaurantResponse) { ? "We could not find any restaurants. Check to make sure you are using the correct address." : "Something went wrong when searching for restaurants."; restaurantErrorMessage.appendChild(document.createTextNode(messageText)); - + restaurantContainer.appendChild(restaurantErrorMessage); return; } - + const allRestaurants = restaurantResponse.data; - + if (!allRestaurants.hasOwnProperty("results")) { let instructions = document.createElement("p"); instructions.classList.add("search-instructions"); @@ -155,14 +155,14 @@ async function showRestaurants(restaurantResponse) { "No one has added their information yet! Fill out the form above to start seeing some results." ) ); - + restaurantContainer.appendChild(instructions); return; } - + //Used to get additional information about the restaurant results. const fields = "url,formatted_phone_number,website,review"; - + //This will be used to create a new div for every restaurant returned by the Places Library: let restaurantIndex = 1; for (restaurant of allRestaurants.results) { @@ -172,16 +172,16 @@ async function showRestaurants(restaurantResponse) { `/api/placedetails?fields=${fields}&id=${restaurant.place_id}` ) ).json(); - + let additionalDetails = placeDetailsResponse.status === 200 ? placeDetailsResponse.data.result : {}; - + //Create a restaurant card to hold information about each restaurant. let infoDiv = document.createElement("div"); infoDiv.classList.add("info-div"); - + //Create a section to hold the image try { let imageDiv = document.createElement("div"); @@ -198,10 +198,10 @@ async function showRestaurants(restaurantResponse) { } catch (err) { console.log("Restaurant image could not be retrieved. Error: " + err); } - + // Add information to the left side of the restaurant card. This contains name and atmospheric information let leftDiv = document.createElement("div"); - + let name = document.createElement("h2"); name.classList.add("restaurant-name"); let restaurantName = restaurant.hasOwnProperty("name") @@ -211,7 +211,7 @@ async function showRestaurants(restaurantResponse) { document.createTextNode(restaurantIndex + ". " + restaurantName) ); leftDiv.appendChild(name); - + if (restaurant.hasOwnProperty("rating")) { let rating = document.createElement("p"); rating.classList.add("restaurant-info"); @@ -224,22 +224,24 @@ async function showRestaurants(restaurantResponse) { if (restaurant.hasOwnProperty("price_level")) { let priceLevel = document.createElement("p"); priceLevel.classList.add("restaurant-info"); - priceLevel.appendChild(document.createTextNode("$".repeat(restaurant.price_level))); + priceLevel.appendChild( + document.createTextNode("$".repeat(restaurant.price_level)) + ); leftDiv.appendChild(priceLevel); } - + infoDiv.appendChild(leftDiv); - + //Add information to the left side of the restaurant card. This contains contact information. let rightDiv = document.createElement("div"); - + if (restaurant.hasOwnProperty("vicinity")) { let address = document.createElement("p"); address.classList.add("restaurant-basic-info"); address.appendChild(document.createTextNode(restaurant.vicinity)); rightDiv.appendChild(address); } - + if (additionalDetails.hasOwnProperty("formatted_phone_number")) { let phoneNumber = document.createElement("p"); phoneNumber.classList.add("restaurant-basic-info"); @@ -248,46 +250,46 @@ async function showRestaurants(restaurantResponse) { ); rightDiv.appendChild(phoneNumber); } - + if (additionalDetails.hasOwnProperty("website")) { let website = document.createElement("p"); website.classList.add("restaurant-basic-info"); - + let link = document.createElement("a"); link.appendChild(document.createTextNode("Website")); link.href = additionalDetails.website; - + website.appendChild(link); rightDiv.appendChild(website); } - + if (restaurant.hasOwnProperty("opening_hours")) { let openingHours = document.createElement("p"); openingHours.classList.add("restaurant-basic-info"); let openNow = Object.values(restaurant.opening_hours) ? "Open Now" : "Closed Now"; - + openingHours.appendChild(document.createTextNode(openNow)); rightDiv.appendChild(openingHours); } - + infoDiv.appendChild(rightDiv); - + //Add review to the card when clicked. let moreInfoDiv = document.createElement("div"); moreInfoDiv.classList.add("restaurant-info"); - + if (additionalDetails.hasOwnProperty("reviews")) { let reviewContainerDiv = document.createElement("div"); - + //Create header for review section let reviewDivHeader = document.createElement("h3"); reviewDivHeader.appendChild(document.createTextNode("Reviews")); reviewDivHeader.classList.add("review-header"); reviewContainerDiv.appendChild(reviewDivHeader); reviewContainerDiv.appendChild(document.createElement("hr")); - + let reviews = additionalDetails.reviews; for (i = 0; i < reviews.length && i < 2; i++) { let reviewerName = reviews[i].hasOwnProperty("author_name") @@ -296,19 +298,21 @@ async function showRestaurants(restaurantResponse) { let reviewTime = reviews[i].hasOwnProperty("relative_time_description") ? reviews[i].relative_time_description : ""; - + let individualReviewHeader = document.createElement("p"); individualReviewHeader.appendChild( document.createTextNode(reviewerName + " - " + reviewTime) ); let reviewText = document.createElement("p"); - reviewText.appendChild(document.createTextNode("\"" + reviews[i].text + "\"")); - + reviewText.appendChild( + document.createTextNode('"' + reviews[i].text + '"') + ); + let individualReviewDiv = document.createElement("div"); individualReviewDiv.classList.add("individual-review"); individualReviewDiv.appendChild(individualReviewHeader); individualReviewDiv.appendChild(reviewText); - + reviewContainerDiv.appendChild(individualReviewDiv); reviewContainerDiv.appendChild(document.createElement("hr")); } @@ -316,39 +320,40 @@ async function showRestaurants(restaurantResponse) { } moreInfoDiv.appendChild(document.createElement("br")); - + if (additionalDetails.hasOwnProperty("url")) { let listingLink = document.createElement("a"); listingLink.appendChild(document.createTextNode("See Listing on Google")); listingLink.href = additionalDetails.url; - + moreInfoDiv.appendChild(listingLink); } - + moreInfoDiv.style.display = "none"; - + //Create a link to show moreInfoDiv let moreInfoLink = document.createElement("p"); moreInfoLink.classList.add("more-info-link", "restaurant-info"); - moreInfoLink.innerHTML = "Show More ↓";//With down arrow - + moreInfoLink.innerHTML = "Show More ↓"; //With down arrow + moreInfoLink.onclick = function () { if (moreInfoDiv.style.display === "inline") { moreInfoDiv.style.display = "none"; - moreInfoLink.innerHTML = "Show More ↓"//With down arrow + moreInfoLink.innerHTML = "Show More ↓"; //With down arrow } else { moreInfoDiv.style.display = "inline"; - moreInfoLink.innerHTML = "Show Less ↑"//With up arrow + moreInfoLink.innerHTML = "Show Less ↑"; //With up arrow } }; - + //Add the two info sections to a restaurant card div. let restaurantCardDiv = document.createElement("div"); + restaurantCardDiv.classList.add("restaurant-card"); + restaurantCardDiv.appendChild(infoDiv); restaurantCardDiv.appendChild(moreInfoDiv); - restaurantCardDiv.appendChild(moreInfoLink) - restaurantCardDiv.classList.add("restaurant-card"); - + restaurantCardDiv.appendChild(moreInfoLink); + restaurantContainer.appendChild(restaurantCardDiv); restaurantIndex++; } From 00beb1046a1a73b24955cc89b9160295696461e1 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 14:45:07 +0000 Subject: [PATCH 05/22] Remove unused and redundant css styling. --- static/absolute/css/searchPage.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 39a9b4a..7801f59 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -130,10 +130,6 @@ hr { transition: box-shadow 0.3s ease-in-out; } -.restaurant-car:active { - box-shadow: 3px 3px 15px #d1d1d1; -} - .restaurant-name { color: #fe5f55; } From 8a0302086a5fd1147d2fe55ea24b3aa4fe973036 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 14:50:45 +0000 Subject: [PATCH 06/22] Add styling to show more/show less button when hovering. --- static/absolute/css/searchPage.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 7801f59..f783a10 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -159,6 +159,7 @@ hr { .more-info-link:hover { cursor: pointer; + color: rgb(245, 156, 150); } .banner-btn { From 693f4f202689f94966dd5c492b671884ef0dc61b Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 14:55:43 +0000 Subject: [PATCH 07/22] Return more detailed error in restaurant api for special handling on the front-end --- src/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index 4d52bd7..af92825 100644 --- a/src/server.js +++ b/src/server.js @@ -275,7 +275,7 @@ app.get( console.error("Places API error. Status: " + status); response.status(500).json({ status: 500, - error: { type: ERROR_BAD_PLACES_API_INTERACTION }, + error: { type: status }, }); } else { response.json({ From a8b5e884c390bef0755bd8378aeb9715958dd067 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 15:22:33 +0000 Subject: [PATCH 08/22] Change variable names and comments. --- static/absolute/css/searchPage.css | 4 +- static/absolute/js/searchPage.js | 63 ++++++++++++++++-------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index f783a10..7543641 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -152,12 +152,12 @@ hr { padding: 25px; } -.more-info-link { +.show-more-link { color: #fe5f55; margin-bottom: 0px; } -.more-info-link:hover { +.show-more-link:hover { cursor: pointer; color: rgb(245, 156, 150); } diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 73c2748..52d932e 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -124,9 +124,6 @@ async function refreshUI() { /** * Creates HTML elements for the restaurant details and adds it * to a container in searchResults.html. - * - * For now, this function deals with hard-coded data, but this - * can be used as a template for when we get data the Places Library results. */ async function showRestaurants(restaurantResponse) { const restaurantContainer = document.getElementById("restaurant-container"); @@ -163,10 +160,10 @@ async function showRestaurants(restaurantResponse) { //Used to get additional information about the restaurant results. const fields = "url,formatted_phone_number,website,review"; - //This will be used to create a new div for every restaurant returned by the Places Library: + //Create a new div for every restaurant returned by the Places Library: let restaurantIndex = 1; for (restaurant of allRestaurants.results) { - //Get additional details for every restaurant. + //Get additional details for every restaurant using that restaurant's place id. let placeDetailsResponse = await ( await fetch( `/api/placedetails?fields=${fields}&id=${restaurant.place_id}` @@ -182,24 +179,28 @@ async function showRestaurants(restaurantResponse) { let infoDiv = document.createElement("div"); infoDiv.classList.add("info-div"); - //Create a section to hold the image + //Create a section to hold the image. try { let imageDiv = document.createElement("div"); - // if (placePhotosResponse.hasOwnProperty("data")) { let image = document.createElement("img"); + let width = "150"; //px + image.src = "https://maps.googleapis.com/maps/api/place/photo?photoreference=" + restaurant.photos[0].photo_reference + - "&maxwidth=150&key=" + + "&maxwidth=" + + width + + "&key=" + "APIKey"; - image.width = "150"; //px + image.width = width; + imageDiv.appendChild(image); infoDiv.appendChild(imageDiv); } catch (err) { console.log("Restaurant image could not be retrieved. Error: " + err); } - // Add information to the left side of the restaurant card. This contains name and atmospheric information + // Add information to the left side of the restaurant card. This contains name and atmospheric information. let leftDiv = document.createElement("div"); let name = document.createElement("h2"); @@ -232,7 +233,7 @@ async function showRestaurants(restaurantResponse) { infoDiv.appendChild(leftDiv); - //Add information to the left side of the restaurant card. This contains contact information. + //Add information to the left side of the restaurant card. This contains basic and contact information. let rightDiv = document.createElement("div"); if (restaurant.hasOwnProperty("vicinity")) { @@ -276,22 +277,25 @@ async function showRestaurants(restaurantResponse) { infoDiv.appendChild(rightDiv); - //Add review to the card when clicked. + //Show reviews and a link to restaurant listing on Google when 'show more' button is clicked. let moreInfoDiv = document.createElement("div"); moreInfoDiv.classList.add("restaurant-info"); + //Add review section to the card. if (additionalDetails.hasOwnProperty("reviews")) { - let reviewContainerDiv = document.createElement("div"); + let reviewContainer = document.createElement("div"); - //Create header for review section + //Create header for review section. let reviewDivHeader = document.createElement("h3"); reviewDivHeader.appendChild(document.createTextNode("Reviews")); reviewDivHeader.classList.add("review-header"); - reviewContainerDiv.appendChild(reviewDivHeader); - reviewContainerDiv.appendChild(document.createElement("hr")); + reviewContainer.appendChild(reviewDivHeader); + + reviewContainer.appendChild(document.createElement("hr")); let reviews = additionalDetails.reviews; for (i = 0; i < reviews.length && i < 2; i++) { + //Show only two results for simplicity. let reviewerName = reviews[i].hasOwnProperty("author_name") ? reviews[i].author_name : ""; @@ -313,14 +317,13 @@ async function showRestaurants(restaurantResponse) { individualReviewDiv.appendChild(individualReviewHeader); individualReviewDiv.appendChild(reviewText); - reviewContainerDiv.appendChild(individualReviewDiv); - reviewContainerDiv.appendChild(document.createElement("hr")); + reviewContainer.appendChild(individualReviewDiv); + reviewContainer.appendChild(document.createElement("hr")); } - moreInfoDiv.appendChild(reviewContainerDiv); + moreInfoDiv.appendChild(reviewContainer); + moreInfoDiv.appendChild(document.createElement("br")); } - moreInfoDiv.appendChild(document.createElement("br")); - if (additionalDetails.hasOwnProperty("url")) { let listingLink = document.createElement("a"); listingLink.appendChild(document.createTextNode("See Listing on Google")); @@ -331,28 +334,28 @@ async function showRestaurants(restaurantResponse) { moreInfoDiv.style.display = "none"; - //Create a link to show moreInfoDiv - let moreInfoLink = document.createElement("p"); - moreInfoLink.classList.add("more-info-link", "restaurant-info"); - moreInfoLink.innerHTML = "Show More ↓"; //With down arrow + //Create a link to show and hide the moreInfoDiv (reviews and restaurant listing). + let showMoreLink = document.createElement("p"); + showMoreLink.classList.add("show-more-link", "restaurant-info"); + showMoreLink.innerHTML = "Show More ↓"; //With down arrow - moreInfoLink.onclick = function () { + showMoreLink.onclick = function () { if (moreInfoDiv.style.display === "inline") { moreInfoDiv.style.display = "none"; - moreInfoLink.innerHTML = "Show More ↓"; //With down arrow + showMoreLink.innerHTML = "Show More ↓"; //With down arrow } else { moreInfoDiv.style.display = "inline"; - moreInfoLink.innerHTML = "Show Less ↑"; //With up arrow + showMoreLink.innerHTML = "Show Less ↑"; //With up arrow } }; - //Add the two info sections to a restaurant card div. + //Add the two info sections and show more/show less button to a restaurant card div. let restaurantCardDiv = document.createElement("div"); restaurantCardDiv.classList.add("restaurant-card"); restaurantCardDiv.appendChild(infoDiv); restaurantCardDiv.appendChild(moreInfoDiv); - restaurantCardDiv.appendChild(moreInfoLink); + restaurantCardDiv.appendChild(showMoreLink); restaurantContainer.appendChild(restaurantCardDiv); restaurantIndex++; From 1f6333890afb3453c8b0a53a176d240efdc1b300 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 15:25:01 +0000 Subject: [PATCH 09/22] Change hover styling of show more/show less button. --- static/absolute/css/searchPage.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 7543641..b653091 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -159,7 +159,7 @@ hr { .show-more-link:hover { cursor: pointer; - color: rgb(245, 156, 150); + color: rgb(226, 55, 43); } .banner-btn { From 88baa8a03c40f301780d79f2a8ca06b763e740f8 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 15:34:32 +0000 Subject: [PATCH 10/22] Change encode function in server to use broader terms. --- src/server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index af92825..a4a13a7 100644 --- a/src/server.js +++ b/src/server.js @@ -390,14 +390,14 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { } }); -function encodeForURL(address) { - const formattedAddress = encodeURIComponent(address) +function encodeForURL(string) { + const formattedString = encodeURIComponent(string) .replace("!", "%21") .replace("*", "%2A") .replace("'", "%27") .replace("(", "%28") .replace(")", "%29"); - return formattedAddress; + return formattedString; } // This number should be kept in sync with the port number in nodemon.json From c5c721827b2ab3719ba1ec4037ca69b767404424 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 15:41:31 +0000 Subject: [PATCH 11/22] Make place image request clearer by using descriptive variable. --- static/absolute/js/searchPage.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 52d932e..b5809d6 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -182,16 +182,18 @@ async function showRestaurants(restaurantResponse) { //Create a section to hold the image. try { let imageDiv = document.createElement("div"); - let image = document.createElement("img"); - let width = "150"; //px - image.src = + let width = "150"; //px + let placePhototsResponse = "https://maps.googleapis.com/maps/api/place/photo?photoreference=" + restaurant.photos[0].photo_reference + "&maxwidth=" + width + "&key=" + "APIKey"; + + let image = document.createElement("img"); + image.src = placePhototsResponse; image.width = width; imageDiv.appendChild(image); From 757dfc2b49a151001ea7ec3a79cb7e5c968f3789 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 15:43:42 +0000 Subject: [PATCH 12/22] Change variable name --- static/absolute/js/searchPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index b5809d6..e2f5ec2 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -184,7 +184,7 @@ async function showRestaurants(restaurantResponse) { let imageDiv = document.createElement("div"); let width = "150"; //px - let placePhototsResponse = + let placePhotosRequest = "https://maps.googleapis.com/maps/api/place/photo?photoreference=" + restaurant.photos[0].photo_reference + "&maxwidth=" + @@ -193,7 +193,7 @@ async function showRestaurants(restaurantResponse) { "APIKey"; let image = document.createElement("img"); - image.src = placePhototsResponse; + image.src = placePhotosRequest; image.width = width; imageDiv.appendChild(image); From 2e4e2205b230584bb824b41c838e840e97beafa8 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 16:57:59 +0000 Subject: [PATCH 13/22] Update check for error in restaurant api call to get correct error. --- static/absolute/js/searchPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index e2f5ec2..e846baf 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -133,7 +133,7 @@ async function showRestaurants(restaurantResponse) { let restaurantErrorMessage = document.createElement("p"); restaurantErrorMessage.classList.add("search-instructions"); let messageText = - restaurantResponse.data === "ZERO_RESULTS" + restaurantResponse.error.type === "ZERO_RESULTS" ? "We could not find any restaurants. Check to make sure you are using the correct address." : "Something went wrong when searching for restaurants."; restaurantErrorMessage.appendChild(document.createTextNode(messageText)); From e43cd36c37d73efa6d080cbad65a6b9e9977a82f Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 18:47:17 +0000 Subject: [PATCH 14/22] Maps marker numbering --- static/absolute/js/searchPage.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index e7158b4..0db8e9e 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -9,7 +9,7 @@ window.onload = function () { const nameInput = document.getElementById("name-input"); const name = nameInput.value; const addressInput = document.getElementById("address-input"); - const address = addressInput.value; + let address = addressInput.value; let lat = null; let long = null; @@ -22,6 +22,11 @@ window.onload = function () { } = await getPosition({ enableHighAccuracy: true }); lat = latitude; long = longitude; + latlng = lat.toString() + ',' + long.toString(); + geocodedPosition = await ( + await fetch(`/api/reverseGeocode?latlng=${latlng}`) + ).json(); + // address = reverseGeocodedPosition; } catch (err) { alert("Failed to get position, please enter address."); return; @@ -51,6 +56,7 @@ window.onload = function () { body: JSON.stringify({ name, location: [lat, long], + address: address, }), }); postResponse = await resp.json(); @@ -220,6 +226,7 @@ async function initMap(participantsResponse, restaurantsResponse) { }); // Add restaurant markers. const restaurants = restaurantsResponse.data.results; + let labelIndex = 1; restaurants.forEach((restaurant) => { new google.maps.Marker({ position: { @@ -227,6 +234,8 @@ async function initMap(participantsResponse, restaurantsResponse) { lng: restaurant.geometry.location.lng, }, map: map, + label: [labelIndex++].toString(), + labelClass: "mapIconLabel", // the CSS class for the label title: restaurant.name, }); }); From b993b69a1abd0218e377c5df76058fd0609eb749 Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 19:28:12 +0000 Subject: [PATCH 15/22] Show address on participant page --- src/server.js | 46 ++++++++++++++++++++++++++++-- static/absolute/js/participants.js | 9 +++--- static/absolute/js/searchPage.js | 7 +++-- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/server.js b/src/server.js index 81c5704..5a8f2ae 100644 --- a/src/server.js +++ b/src/server.js @@ -20,6 +20,7 @@ const ERROR_BAD_DB_INTERACTION = "BAD_DATABASE"; const ERROR_INVALID_EVENT_ID = "INVALID_EVENT_ID"; const ERROR_BAD_UUID = "BAD_UUID"; const ERROR_GEOCODING_FAILED = "GEOCODING_FAILED"; +const ERROR_REVERSE_GEOCODING_FAILED = "REVERSE_GEOCODING_FAILED"; const ERROR_BAD_PLACES_API_INTERACTION = "BAD_PLACES_API"; app.use(express.static("static/absolute")); @@ -162,9 +163,16 @@ app.post( const { body, datastoreKey: key, event } = request; const [lat, long] = body.location; const { name } = body; + const address = body.address; event.users = event.users || {}; const userInfo = event.users[request.session.userID] || {}; - event.users[request.session.userID] = { ...userInfo, name, lat, long }; + event.users[request.session.userID] = { + ...userInfo, + name, + lat, + long, + address, + }; datastore // Datastore attaches a "symbol" (https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) // to any entities returned from a query. We don't want to store this attached metadata back into the database @@ -346,7 +354,7 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { console.error(err); response .status(500) - .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); + .json({ status: 500, error: { type: ERROR_REVERSE_GEOCODING_FAILED } }); } }); @@ -360,6 +368,40 @@ function encodeAddress(address) { return formattedAddress; } +app.get(`${PREFIX_API}/reverseGeocode`, async (request, response) => { + const latlng = request.query.latlng; + + const reverseGeocodeRequest = + `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key= + ${env.API_KEY_GEOCODE}`; + + try { + const reverseGeocodeResponse = await (await fetch(reverseGeocodeRequest)).json(); + const {status} = reverseGeocodeResponse; + + if (status !== "OK") { + console.error( + "Geocoding error occured. Api response status: " + status + ); + response + .status(500) + .json({ status: 500, error: { type: status } }); + } else { + response.json({ + status: 200, + data: reverseGeocodeResponse.results[0].formatted_address, + }); + } + } catch (err) { + console.error(err); + response + .status(500) + .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); + } +}); + + + const port = 8080; const server = app.listen(port, () => console.log(`Server listening on http://localhost:${port}`) diff --git a/static/absolute/js/participants.js b/static/absolute/js/participants.js index 8f38e81..1841ddb 100644 --- a/static/absolute/js/participants.js +++ b/static/absolute/js/participants.js @@ -30,6 +30,7 @@ async function showParticipants() { participants .map((p) => ({ ...p, location: `${p.lat},${p.long}` })) .forEach((participant) => { + console.log(participant.address); let newDiv = document.createElement("div"); newDiv.classList.add("participant-card"); @@ -38,10 +39,10 @@ async function showParticipants() { name.appendChild(document.createTextNode(participant.name)); newDiv.appendChild(name); - let location = document.createElement("p"); - location.classList.add("participant-info"); - location.appendChild(document.createTextNode(participant.location)); - newDiv.appendChild(location); + let address = document.createElement("p"); + address.classList.add("participant-info"); + address.appendChild(document.createTextNode(participant.address)); + newDiv.appendChild(address); participantContainer.appendChild(newDiv); }); diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 0db8e9e..b3a4485 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -23,15 +23,17 @@ window.onload = function () { lat = latitude; long = longitude; latlng = lat.toString() + ',' + long.toString(); - geocodedPosition = await ( + const reverseGeocodedPosition = await ( await fetch(`/api/reverseGeocode?latlng=${latlng}`) ).json(); - // address = reverseGeocodedPosition; + // If address is empty, use reverse-geocoded HTML geolocation. + address = reverseGeocodedPosition.data; } catch (err) { alert("Failed to get position, please enter address."); return; } } else { + // Geocode address input for coordinates. const coords = await ( await fetch(`/api/geocode?address=${address}`) ).json(); @@ -46,7 +48,6 @@ window.onload = function () { return; } } - let postResponse; const eventId = getEventId(); try { From d0470f5444d6c04e0a7d8903ef4a33de3caaa7af Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 19:53:13 +0000 Subject: [PATCH 16/22] Edit reverse gecoding error message --- src/server.js | 27 +++++++++++---------------- static/absolute/js/searchPage.js | 12 ++++++------ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/server.js b/src/server.js index 5a8f2ae..d9402ee 100644 --- a/src/server.js +++ b/src/server.js @@ -163,7 +163,7 @@ app.post( const { body, datastoreKey: key, event } = request; const [lat, long] = body.location; const { name } = body; - const address = body.address; + const address = body.address; event.users = event.users || {}; const userInfo = event.users[request.session.userID] || {}; event.users[request.session.userID] = { @@ -354,7 +354,7 @@ app.get(`${PREFIX_API}/geocode`, async (request, response) => { console.error(err); response .status(500) - .json({ status: 500, error: { type: ERROR_REVERSE_GEOCODING_FAILED } }); + .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); } }); @@ -371,21 +371,18 @@ function encodeAddress(address) { app.get(`${PREFIX_API}/reverseGeocode`, async (request, response) => { const latlng = request.query.latlng; - const reverseGeocodeRequest = - `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key= + const reverseGeocodeRequest = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latlng}&key= ${env.API_KEY_GEOCODE}`; - + try { - const reverseGeocodeResponse = await (await fetch(reverseGeocodeRequest)).json(); - const {status} = reverseGeocodeResponse; + const reverseGeocodeResponse = await ( + await fetch(reverseGeocodeRequest) + ).json(); + const { status } = reverseGeocodeResponse; if (status !== "OK") { - console.error( - "Geocoding error occured. Api response status: " + status - ); - response - .status(500) - .json({ status: 500, error: { type: status } }); + console.error("Geocoding error occured. Api response status: " + status); + response.status(500).json({ status: 500, error: { type: status } }); } else { response.json({ status: 200, @@ -396,12 +393,10 @@ app.get(`${PREFIX_API}/reverseGeocode`, async (request, response) => { console.error(err); response .status(500) - .json({ status: 500, error: { type: ERROR_GEOCODING_FAILED } }); + .json({ status: 500, error: { type: ERROR_REVERSE_GEOCODING_FAILED } }); } }); - - const port = 8080; const server = app.listen(port, () => console.log(`Server listening on http://localhost:${port}`) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index b3a4485..ac5427e 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -22,18 +22,18 @@ window.onload = function () { } = await getPosition({ enableHighAccuracy: true }); lat = latitude; long = longitude; - latlng = lat.toString() + ',' + long.toString(); + latlng = lat.toString() + "," + long.toString(); const reverseGeocodedPosition = await ( - await fetch(`/api/reverseGeocode?latlng=${latlng}`) - ).json(); - // If address is empty, use reverse-geocoded HTML geolocation. - address = reverseGeocodedPosition.data; + await fetch(`/api/reverseGeocode?latlng=${latlng}`) + ).json(); + // If address is empty, use reverse-geocoded HTML geolocation. + address = reverseGeocodedPosition.data; } catch (err) { alert("Failed to get position, please enter address."); return; } } else { - // Geocode address input for coordinates. + // Geocode address input for coordinates. const coords = await ( await fetch(`/api/geocode?address=${address}`) ).json(); From a4edcc9a8d2513cc9c2c7f1fad33b1ab77605f5d Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 20:03:55 +0000 Subject: [PATCH 17/22] Search button margin adjustments --- static/absolute/css/searchPage.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index 75b7ede..f2071ab 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -5,12 +5,12 @@ body { /* margin left small */ .ml-s { - margin-left: 15px; + margin-left: 5px; } /* margin left medium */ .ml-m { - margin-left: 25px; + margin-left: 20px; } /* Banner at top of page contains logo */ From 5f8be4b52feb0b332d46754efda3d84e8c577150 Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 20:07:42 +0000 Subject: [PATCH 18/22] format changes --- static/absolute/js/participants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/static/absolute/js/participants.js b/static/absolute/js/participants.js index 1841ddb..6fa45cb 100644 --- a/static/absolute/js/participants.js +++ b/static/absolute/js/participants.js @@ -30,7 +30,6 @@ async function showParticipants() { participants .map((p) => ({ ...p, location: `${p.lat},${p.long}` })) .forEach((participant) => { - console.log(participant.address); let newDiv = document.createElement("div"); newDiv.classList.add("participant-card"); From f0fe299b7e05b73b6ac72b3399acfffe6ce924b5 Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Mon, 3 Aug 2020 20:26:48 +0000 Subject: [PATCH 19/22] Edit error message for reverse geocoding API --- src/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index d9402ee..f95575d 100644 --- a/src/server.js +++ b/src/server.js @@ -381,7 +381,7 @@ app.get(`${PREFIX_API}/reverseGeocode`, async (request, response) => { const { status } = reverseGeocodeResponse; if (status !== "OK") { - console.error("Geocoding error occured. Api response status: " + status); + console.error("Reverse Geocoding error occured. Api response status: " + status); response.status(500).json({ status: 500, error: { type: status } }); } else { response.json({ From c943054b4a1d9c3f9856a6a68be2faf697ea173e Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Mon, 3 Aug 2020 21:23:05 +0000 Subject: [PATCH 20/22] Add white space to the bottom of the search results. --- static/absolute/css/searchPage.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/absolute/css/searchPage.css b/static/absolute/css/searchPage.css index b653091..4147c72 100644 --- a/static/absolute/css/searchPage.css +++ b/static/absolute/css/searchPage.css @@ -122,6 +122,7 @@ hr { border: 1px solid #d4ede0; border-radius: 0.5rem; margin-top: 25px; + margin-bottom: 25px; padding: 25px; } From 69262e59fc2728e59065dc195b3b069d70132e41 Mon Sep 17 00:00:00 2001 From: Chisom Okwor Date: Tue, 4 Aug 2020 01:33:59 +0000 Subject: [PATCH 21/22] Numbering markers --- static/absolute/js/searchPage.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/absolute/js/searchPage.js b/static/absolute/js/searchPage.js index 39f105a..ec194c8 100644 --- a/static/absolute/js/searchPage.js +++ b/static/absolute/js/searchPage.js @@ -266,6 +266,12 @@ function addRestaurants(map, restaurants, restaurantCards) { }, map: map, title: restaurant.name, + label: { + text: [i + 1].toString(), + color: "white", + fontWeight: "22px", + fontSize: "18px", + }, }); marker.addListener("click", () => { card.scrollIntoView({ behavior: "smooth", alignToTop: true }); From e73176974a0aea2d8b502e1a076415a05a149c87 Mon Sep 17 00:00:00 2001 From: Asha Ivey Date: Tue, 4 Aug 2020 19:23:18 +0000 Subject: [PATCH 22/22] Fix issue on participants page. --- static/absolute/js/participants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/absolute/js/participants.js b/static/absolute/js/participants.js index 8f38e81..da6f1aa 100644 --- a/static/absolute/js/participants.js +++ b/static/absolute/js/participants.js @@ -26,7 +26,7 @@ async function showParticipants() { ); participantContainer.innerHTML = ""; - const participants = response.data; + const participants = response.data.participants; participants .map((p) => ({ ...p, location: `${p.lat},${p.long}` })) .forEach((participant) => {