Wanderlust project - walkthrough final challenge with code snippets

Jokes aside, I spent more time figuring out the correct api call than the actual time spent coding.

This post is for those who had/are having issues with the final challenge of Wanderlust project.
What I did:

  • Stored in a global variable the venues ids
let venuesIds = [];
//get data from Foursquare
async function getVenues(){
  const city = $input.val();
  let limit = 10;
  const and = "&";
  const toFetch = url.toString()+city.toString()+and+"limit="+limit.toString()+and+"client_id="+clientId.toString()+and+"client_secret="+clientSecret.toString()+and+"v=20210421"; 
  try{
    const response = await fetch(toFetch);
    if(response.ok){
      const jsonResponse = await response.json();
      //obtaining array of venues from API response
      const venues = jsonResponse.response.groups[0].items.map(
        (item) => item.venue
      );
      //storing the id of each venue for a later request
      venues.forEach(
        venue => venuesIds.push(venue.id)
      );      
      // console.log(venues);
      // console.log(venuesIds);
      return venues;
    }
  }catch(error){
    console.log(error)
  }
};
  • Created an async function that would loop through the venues ids previously stored and store in a global variable the urls of the best photos for the various venues
let venuesImgs = [];
//get bestPhoto urls for each venue obtained with getVenues()
async function venuesPhotosUrl() {
  const and = "&";
  for await (const id of venuesIds) {
    const toFetch =  "https://api.foursquare.com/v2/venues/"+id.toString()+"?"+"client_id="+clientId.toString()+and+"client_secret="+clientSecret.toString()+and+"v=20210421";
    try{
      const response = await fetch(toFetch);
      if(response.ok){
        const jsonResponse = await response.json();
        console.log(jsonResponse);
        //obtain bestphoto
        const pathTobestPhoto = jsonResponse.response.venue.bestPhoto;
        console.log(pathTobestPhoto);
        const bestPhotoUrl = pathTobestPhoto.prefix.toString()+pathTobestPhoto.width.toString()+"x"+pathTobestPhoto.height.toString()+pathTobestPhoto.suffix.toString();
        console.log(bestPhotoUrl);
        venuesImgs.push(bestPhotoUrl);
      }    
    }catch(error){
      console.log(error)
    }      
  }
};

bestPhoto is a property available from the Premium API Endpoints details, as per their docs:

Photo we have determined to be the best photo for the venue based on user upvotes and our internal algorithm.

Because it’s a premium API it’s capped at 50 requests per day, so if you suddenly start getting an error in browser console with code 429, is because you reached the limit.

The weird const pathTobestPhoto = jsonResponse.response.venue.bestPhoto; instead of a simple and more obvious jsonResponse.bestPhoto is because the actual response received from the server is not the list of response fields as the documentation suggests, but instead:

{
  "meta": {
    //some stuff
  },
  "response": {
    //list of response properties as per https://developer.foursquare.com/docs/api-reference/venues/details/#response-fields
  }
}

So do not confuse the variable const response = await fetch(toFetch); with the property response of the object above (that is jsonResponse).

  • Finally, render the photos with renderVenues(venues) when is called from executeSearch()
// Render venues from getVenues()
const renderVenues = (venues) => {
  $venueDivs.forEach(($venue, index) => {
    const venue = venues[index];
    const venueIcon = Object.values(venue.categories[0].icon).join("bg_64");
    const bestPhotoUrl = venuesImgs[index];
    // console.log(venueIcon);
    let venueContent = createVenueHTML(venue.name, venue.location, venueIcon, bestPhotoUrl);
    $venue.append(venueContent);
  });
  $destination.append(`<h2>${venues[0].location.city}</h2>`);
};
//listen for submitted button
const executeSearch = () => {
  $venueDivs.forEach(venue => venue.empty());
  $weatherDiv.empty();
  $destination.empty();
  $container.css("visibility", "visible");
  Promise.all([getVenues(), venuesPhotosUrl()]).then(
    venues => renderVenues(venues[0])
  , error => console.log(error));
  getForecast().then(
    forecast => renderForecast(forecast)
  );
  return false;
};

Promise.all() returns an array of the values returned by each Promise it resolves, if all are fulfilled. Because venues in this case is an array we need to pass to renderVenues(passTheOutputOfGetVenues) only the first element of this array, which corresponds to the returned value of the first promise: getVenues().

  • Of course in helpers.js we also need to edit createVenueHTML()
    createVenueHTML = (name, location, iconSource, bestPhotoUrl)

What I learnt from this challenge is that working with APIs is stressful and that jQuery should not to be used anymore.

Hopefully this will be useful for someone, if you have any questions feel free to ask.
If anyone else got different ideas on how to solve this challenge share them! It’ll make great feedback :slight_smile:

Hi,

Thank you for having posted this. I also struggle with the final challenge.

I have copy/pasted your code, however I do not even get into the try block of the second call in the Promise.all.

I have also tried to use the photos endpoint. When manually the endpoint I get the response with the URL I am creating.

const venuesPhotosUrl = async() => {
  console.log(venuesIds);
  for await (const id of venuesIds) {
    const urlToFetch = `${url}${id}/photos?client_id=${clientId}&client_secret=${clientSecret}&v=${versionDate}`;
    console.log(urlToFetch);
    try {
      const response = await fetch(urlToFetch);
      console.log(response);
      if (response.ok) {
        const jsonResponse = await response.json();
        console.log(jsonResponse);
      }
    } catch (error) {
      console.log(error)
    }
  }
}

The first console.log of the venueIds gets executed, so it should be correct.

Maybe you have another idea?

This is the link to my main.js on GitHub: Node-JS/main.js at main · Klaynie/Node-JS · GitHub

Cheers,
Christoph

Hey Christoph! How is it going? :slight_smile:

This is what I get when rendering in localhost:

So for me it works, however I see that sometimes the images do not load, other times they do, I don’t understand why honestly.

I think it may be something related to asynchronicity. Because executeSearch() run synchronously with the rest of the code, but I am not sure really, because theoretically it should wait for the Promise.all() to be either fulfilled or rejected before calling renderVenues(). But somehow despite promises are fulfilled, the images are sometimes not loaded and the global variable venuesImgs, where the urls of the images are stored, returns empty.

Now I cannot even do a true debugging, because after just a few trials I get the 429 error for capping the API rate limit:

Hope you can find another way to solve it!

Edit:
I modified the console screenshot to hide my API key :wink:

Hey,
Thank you for coming back to me. I really like how you modified the button and search bar.

Unfortunately, I have not found a solution that works for me. I have replaced the functions with your code 1 for 1.

However, I now get a TypeError when I run the search:

Would it be possible to share your complete code? Of course without the API keys :smiley:

Cheers,
Christoph

Thank you for coming back to me. I really like how you modified the button and search bar.

No prob. Yes I did style it a bit, I don’t like it really haha but maybe is better than before, there was too much stuff to fix, like making it responsive and such, so I left it unfinished. There was much more to style there in my opinion.

Would it be possible to share your complete code? Of course without the API keys :smiley:

Yes no prob. Hopefully I don’t get flagged again for no reason.

main.js

// Foursquare API Info const clientId = 'BIEE3CRS'; const clientSecret = 'PFATRIP'; const url = 'https://api.foursquare.com/v2/venues/explore?near='; let venuesIds = []; let venuesImgs = []; // const todayDate = new Date(); // const Day = todayDate.getDate(); // const Month = todayDate.getMonth(); // const Year = todayDate.getFullYear(); //get today date as a string formatted as YYYYMMDD // OpenWeather Info const openWeatherKey = 'd7b151f3999cb4a558b158674d5b2c60'; const weatherUrl = 'https://api.openweathermap.org/data/2.5/weather'; // Page Elements const $input = $('#city'); const $submit = $('#button'); const $destination = $('#destination'); const $container = $('.container'); const $venueDivs = [$("#venue1"), $("#venue2"), $("#venue3"), $("#venue4"), $("#venue5"), $("#venue6"), $("#venue7")]; const $weatherDiv = $("#weather1"); const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; //get data from Foursquare async function getVenues(){ const city = $input.val(); let limit = 10; const and = "&"; const urlToFetch = url.toString()+city.toString()+and+"limit="+limit.toString()+and+"client_id="+clientId.toString()+and+"client_secret="+clientSecret.toString()+and+"v=20210421"; try{ const response = await fetch(urlToFetch); if(response.ok){ const jsonResponse = await response.json(); //obtaining array of venues from API response const venues = jsonResponse.response.groups[0].items.map( item => item.venue ); //storing the id of each venue for a later request venues.forEach( venue => venuesIds.push(venue.id) ); // console.log(venues); // console.log(venuesIds); return venues; } }catch(error){ console.log(error) } }; //get bestPhoto urls for each venue obtained with getVenues() async function venuesPhotosUrl() { const and = "&"; for await (const id of venuesIds) { const toFetch = "https://api.foursquare.com/v2/venues/"+id.toString()+"?"+"client_id="+clientId.toString()+and+"client_secret="+clientSecret.toString()+and+"v=20210421"; try{ const response = await fetch(toFetch); if(response.ok){ const jsonResponse = await response.json(); console.log(jsonResponse); //obtain bestphoto const pathTobestPhoto = jsonResponse.response.venue.bestPhoto; console.log(pathTobestPhoto); const bestPhotoUrl = pathTobestPhoto.prefix.toString()+pathTobestPhoto.width.toString()+"x"+pathTobestPhoto.height.toString()+pathTobestPhoto.suffix.toString(); console.log(bestPhotoUrl); venuesImgs.push(bestPhotoUrl); } }catch(error){ console.log(error) } } }; //get data from OpenWeather async function getForecast(){ const city = $input.val(); const and = "&"; const urlToFetch = weatherUrl.toString()+"?q="+city.toString()+and+"APPID="+openWeatherKey.toString(); try{ const response = await fetch(urlToFetch); if(response.ok){ const jsonResponse = await response.json(); // console.log(jsonResponse) return jsonResponse } }catch(error){ console.log(error) } }; // Render venues from getVenues() const renderVenues = (venues) => { $venueDivs.forEach(($venue, index) => { const venue = venues[index]; const venueIcon = Object.values(venue.categories[0].icon).join("bg_64"); const bestPhotoUrl = venuesImgs[index]; // console.log(venueIcon); let venueContent = createVenueHTML(venue.name, venue.location, venueIcon, bestPhotoUrl); $venue.append(venueContent); }); $destination.append(`<h2>${venues[0].location.city}</h2>`); }; // Render forecast from getForecast() const renderForecast = (day) => { let weatherContent = createWeatherHTML(day); $weatherDiv.append(weatherContent); }; //listen for submitted button const executeSearch = () => { $venueDivs.forEach(venue => venue.empty()); $weatherDiv.empty(); $destination.empty(); $container.css("visibility", "visible"); Promise.all([getVenues(), venuesPhotosUrl()]).then( venues => renderVenues(venues[0]), error => console.log(error) ); getForecast().then( forecast => renderForecast(forecast) ); return false; }; $submit.click(executeSearch);

helpers.js

const createVenueHTML = (name, location, iconSource, bestPhotoUrl) => { return `<h2>${name}</h2> <img class="venueimage" src="${iconSource}"/> <h3>Address:</h3> <p>${location.address}</p> <p>${location.city}</p> <p>${location.country}</p> <img class="bestphoto" src="${bestPhotoUrl}"/>` } const createWeatherHTML = (currentDay) => { console.log(currentDay) return ( ` <ul> <li><span>Day:</span>${weekDays[(new Date()).getDay()]}</li> <li><span>Temperature:</span>${kelvinToFahrenheit(currentDay.main.temp)}&deg;F</li> <li><span>Sky condition:</span>${currentDay.weather[0].description}</li> </ul> <img src="https://openweathermap.org/img/wn/${currentDay.weather[0].icon}@2x.png"> ` ) } const kelvinToFahrenheit = k => ((k - 273.15) * 9 / 5 + 32).toFixed(0);

style.css

*{margin: 0; padding: 0; line-height: 1; font-family: Montserrat;}

:focus{outline: none;}

html{font-size: 1vh; width: 100%; overflow-x: hidden;}

header{width: 100%; min-height: 10rem; height: 20px; position: relative;}

  .logo {vertical-align: middle; max-height: 50px; height: 10rem; position: absolute; top: 0; left: 0; margin-top: calc(5rem - 25px); margin-left: 1rem;}

main {

  width: 100%; min-height: 250px; height: 50rem;

  text-align: center;

  background-image: url('https://content.codecademy.com/courses/intermediate-javascript-requests/wanderlust/background_photo_desktop.jpg');

  background-size: cover;

  background-position: top;

}

  h1 {

    padding: 5rem 0;

    font-size: 10rem;

    font-weight: 300;

    letter-spacing: 1px;

    color: #ffffff;

    text-shadow: 1px 1px 2px darkblue;

    margin: auto;

  }

  form{

    padding: 2rem; width: 50%; min-height: 1rem; height: 50px; margin: 0 auto; font-size: 2rem;

    display: flex; flex-flow: row wrap; justify-content: center; align-items: center; flex: 1;

  }

    form>input{

      height: 100%;

      width: 70%;

      border-radius: 50px;

      border: solid 1px darkblue;

      background: linear-gradient(90deg, hsla(234, 94%, 40%, 0.50), rgba(6, 89, 198, 0.50), rgba(6, 104, 196, 0.50));

      text-align: left;

      color: white;

      padding-left: 3rem;

      font-size: 3rem;

    }

    form>button{

      height: 100%;

      width: 20%;

      border-radius: 10px;

      background: radial-gradient(#0668c4, #0658c4, #0619c4);

      font-family: Source Sans Pro;

      font-size: 18px;

      font-weight: 600;

      text-align: center;

      color: #ffffff;

      margin-left: 1rem;

    }

.container {

  width: 100%;

  margin: 0 auto;

  visibility: hidden;

  text-align: center;

  margin-top: 10rem;

}

h2{font-size: 4rem; width: auto; margin: 0 auto; text-align: center; padding: 2rem}

#weather {

  display: flex; flex-direction: column; justify-content: center; align-items: center;

  min-height: 250px; height: 33rem;

}

div.weather {

  min-width: 200px;

  width: 50%;

  min-height: 250x;

  height: 20rem;

  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);

  /* font-size: 1rem; */

}

div.weather>ul{

  width: 90%;

  list-style: none;

  font-size: 2.5rem;

  margin: 0 auto;

}

div.weather>ul>li{

  display: flex; flex-direction: row; justify-content: flex-start; flex: 1; margin-left: 33%;

}

div.weather>ul>li>span{

  margin-right: 1rem;

  flex-basis: 30%;

  text-align: left;

}

/* .gallery{} */

h3 {

  font-family: Work Sans;

  font-weight: 500;

  line-height: 1.29;

  color: #27c19e;

  padding-left: 5px

}

p {

  font-family: Work Sans;

  font-size: 14px;

  font-weight: 300;

  line-height: 1.36;

  text-align: left;

  color: #4a4a4a;

  padding-left: 5px;

}

footer {

  display: flex;

  align-items: center;

  height: 50px;

}

.weathericon {

  width: 150px;

  height: 150px;

}

.venue {

  min-width: 400px;

  width: 25%;

  padding: 20px;

  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);

}

.venueimage {

  margin-top: 20px;

}

.bestphoto{

  object-fit: cover;

  object-position: center;

  width: 100%;

}

#venues h2 {

  font-family: Work Sans;

  font-size: 22px;

  font-weight: 600;

  line-height: 0.77;

  text-align: center;

  padding-top: 5px;

}

#destination {

  font-family: Montserrat;

  font-size: 30px;

  font-weight: 600;

  letter-spacing: 0.9px;

  text-align: left;

  color: #27b7c1;

}

#venues {

  display: flex;

  justify-content: space-around;

  min-height: 250px;

}

/* #city {

  width: 286px;

  height: 46px;

  border-radius: 4px;

  border: solid 2.5px #ffffff;

  background-color: inherit;

  opacity: 0.73;

  font-family: Source Sans Pro;

  font-size: 18px;

  line-height: 1;

  letter-spacing: 0.4px;

  text-align: left;

  color: #ffffff;

}

#button {

  width: 140px;

  height: 46px;

  border-radius: 4px;

  background-color: #27b7c1;

  font-family: Source Sans Pro;

  font-size: 18px;

  font-weight: 600;

  line-height: 0.94;

  letter-spacing: 0.5px;

  text-align: center;

  color: #ffffff;

} */

@media (max-width: 860px) {

  main {

    background-image: url('https://content.codecademy.com/courses/intermediate-javascript-requests/wanderlust/background_photo_moble.jpg');

  }

  h1 {

    padding-top: 80px;

    font-size: 32px;

  }

  form {

    margin-top: 30px;

    padding-bottom: 10px;

  }

  #venues, #weather {

    display: flex;

    flex-direction: column;

    align-items: center;

  }

  .venue, .weather {

    margin: 10px auto;

    display: flex;

    flex-direction: column;

    width: 80%;

    align-items: center;

  }

  .weather {

    padding: 10px 20px;

    height: 100%;

  }

  .sectiontitle, .sectiontitle h2, #destination {

    margin: auto;

    text-align: center;

  }

}

Hey,

Thank you again :smiley:

No prob. Yes I did style it a bit, I don’t like it really haha but maybe is better than before, there was too much stuff to fix, like making it responsive and such, so I left it unfinished. There was much more to style there in my opinion.

Yeah, I understand what you mean. I think I am not much of a Front-End Developer and will leave that task to somebody else and will rather use templates like Bootstrap or other ready-made frameworks.

Yes no prob. Hopefully I don’t get flagged again for no reason.

Wow, how did that happen?

Thanks for sharing your code. I have tried it again. At least now it is a different error (Same error happens in both Firefox and Chrome). Do you have any idea what this could mean? I am a bit lost here.

Cheers,
Christoph

404 means that is not finding the resource.
If you are now using only my code, go in console and type venuesIds, it’s the global variable storing the array with all the ids for the venues.

Do the same thing again for venuesImgs, it’s the global variable storing the array of images URLs.

Do you mean like this?

2021-05-28 23_16_09-Wanderlust

Did not know that you could use the console like this, you learn something new every day :smiley:

Somehow venuesImgs is empty. I guess it has to do with the 404 error. Would not know how to fix this to be honest.

Cheers,
Christoph

Yes, that’s the same problem that I get. I don’t understand why honestly.
Anyhow now always from console try to run the last part of the code, so executeSearch() or directly the Promise.all().

Wow, not it worked with manually running it. I wonder if there actually is another approach that works reliably.

Thank you very much for your help. Would like to invite you to one of those places :smiley:

Cheers,
Christoph

1 Like