Skip to content

ironhack-labs/lab-typescript-weather-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

logo_ironhack_blue 7

LAB | TypeScript Weather App

Learning Goals

This exercise allows you to practice and apply the concepts and techniques taught in class.

Upon completion of this exercise, you will be able to:

  • Send HTTP requests to an external API using Axios and TypeScript
  • Create type aliases to describe responses from an API
  • Use type annotations for Promises
  • Use type assertions to interact with DOM elements


Introduction


In this lab, you will create a weather app that allows the user to type the name of a location and get the current weather for that location.

For this application, we will use some APIs from Open-Meteo and the axios library for HTTP requests. In particular, we will interact with these two APIs from Open-Meteo:

Geocoding API (docs):

  • This API will allow us to search for a specific location by name.
  • For example, we can get a list of 3 locations that match the name "Berlin" by sending a GET request to this URL: https://geocoding-api.open-meteo.com/v1/search?name=Berlin&count=3.
  • It will return a list of locations matching that name and some information for each location (country, latitude, longitude, etc.).

Weather Forecast API (docs):


Initial setup

To begin, follow these steps:

  • Fork this repo
  • Clone this repo
  • Open the project folder in VSCode

Then, run the following commands in your terminal 👇👇

  • npm install (this will install all the dependencies)
  • npm run dev

Finally, open the URL http://localhost:5173 in your browser. You should see a page with a form (the functionality for the form is not working yet, that's what we will implement during this lab 😉).


Submission

  • Upon completion, run the following commands:

    git add .
    git commit -m "Completed lab"
    git push origin main
  • Create a Pull Request and submit your assignment.


Iteration 0 | Understanding the initial code

The initial code provided for this lab is a Vite application with TypeScript already configured.


To help you get started quickly, we have created the initial structure and added some initial code. For example, you'll find these HTML and CSS files:

  • index.html: this is the HTML file that will be rendered in the browser. If you open it, you will see that we've added the HTML code for the form and some containers that we will use to display the info about the location and weather.
  • src/style.css: we have also included some CSS, so that you can focus on the functionality.

Our TypeScript code will be organized in 3 different files:

  • src/types.ts: this is the file where we'll include all TypeScript type definitions.
  • src/utils.ts: in this file we will create several utility functions (reusable functions that perform a specific task and can be used in other parts of our application).
  • src/main.ts: in this file, we will add the main logic of our application.

If you open src/types.ts, you will see that we have already added 2 type definitions:

  • Location: a type alias that defines the structure of an object with information about a given location.
  • LocationResponse: a type alias that defines the structure of the response that we get from the API when we send a request to get the details of a specific location.

If you open src/utils.ts, you will find this initial code:

  • We are importing axios (which is already installed as a dependency)
  • We also import some type definitions from src/types.ts.
  • And, we have already created a function getLocation(). This function takes the name of a location as an argument and will send a request to the API to get the details of that location. It returns a promise that resolves to the type LocationResponse

Iteration 1 | Functionality to get the weather in a specific location

1.1 - Create the type alias WeatherResponse

In src/types.ts, create a new type alias with the name WeatherResponse. This type alias should describe the structure that we get from the API when we send a request to get the weather for a specific location.

To get an example of a response, you can send a GET request to this URL: https://api.open-meteo.com/v1/forecast?latitude=52.52437&longitude=13.41053&current_weather=true&models=icon_global. Then, create a type alias that follows that pattern and export it so that it can be used in other files.


Tip 1

To see the response in a human-friendly format, you can use Postman:

Postman

Once you can see the response in human-friendly format, it will be much easier to create the type alias.

Tip 2

Another option, when you have to create a type alias for complex data is to use ChatGPT or a similar AI tool to generate the type alias for you:

ChatGPT

Note: there's some cases in which ChatGPT will not be able to give you the right answer. For example, if there's optional properties and they're not included in the example provided, or when some properties can have different types (i.e., union types).

Solution
// src/types.ts

// ...

export type WeatherResponse = {
    latitude: number;
    longitude: number;
    generationtime_ms: number;
    utc_offset_seconds: number;
    timezone: string;
    timezone_abbreviation: string;
    elevation: number;
    current_weather_units: {
        time: string;
        interval: string;
        temperature: string;
        windspeed: string;
        winddirection: string;
        is_day: string;
        weathercode: string;
    };
    current_weather: {
        time: string;
        interval: number;
        temperature: number;
        windspeed: number;
        winddirection: number;
        is_day: number;
        weathercode: number;
    };
};

Note: don't forget to export it, so that you can use this type definition in other files.


1.2 - Create the signature for the function getCurrentWeather()

In this step, you will create the signature for the function getCurrentWeather(). This function will send a request to the Weather Forecast API and return the API response.

For now, we will just define the signature of our function (ie. we will declare the function, specifying which parameters it takes and what it returns but we will not implement the logic of the function).

Open the file src/utils.ts and declare a function with this signature:

  • Name of the function: getCurrentWeather()
  • Parameters:
    • locationDetails (an object of type Location)
  • Return value:
    • Our function should return a Promise that resolves to the type WeatherResponse
    • Example: Promise<WeatherResponse>
  • Note: for now, don't worry about the code inside the function (we will do that in the next step)

Solution
// src/utils.ts

import { LocationResponse, Location, WeatherResponse } from "./types.ts";

// ...


export function getCurrentWeather(locationDetails: Location): Promise<WeatherResponse> {
  // ...
}

1.3 - Implement the function getCurrentWeather()

Next, you will implement the logic of the function getCurrentWeather(). Your function needs to do the following:

  1. Send the API Request:
  • Use axios to send a GET request to this URL: https://api.open-meteo.com/v1/forecast?latitude=${locationDetails.latitude}&longitude=${locationDetails.longitude}&current_weather=true&models=icon_global (where locationDetails.latitude and locationDetails.longitude are the coordinates that we get from the argument locationDetails).
  1. Handle the Response:
  • Return a promise that resolves with the data from the API response.
  • Note: you can use the .then() method to extract the data property from the response object.

Solution
// src/utils.ts

import { LocationResponse, Location, WeatherResponse } from "./types.ts";

// ...

export function getCurrentWeather(locationDetails: Location): Promise<WeatherResponse> {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${locationDetails.latitude}&longitude=${locationDetails.longitude}&current_weather=true&models=icon_global`;
    return axios.get(url).then((response) => response.data);
}

Iteration 2 | Functionality to display the location in the UI

2.1 - Create the signature for the function displayLocation()

In this step, we will define a function that will receive the details of a location and display them in the user interface.

In src/utils.ts, create a function with this signature:

  • Name of the function: displayLocation()
  • Parameters:
    • locationDetails (an object of type Location)
  • Return value:
    • For this function, we're not interested in the return value.

Also, make sure to export your function so that it can be used in other files.


Solution
// src/utils.ts
// ...

export function displayLocation(locationDetails: Location) {
    // ...
}

Note: you can also explicitly state that we're not interested in the return value using void:

// src/utils.ts
// ...

export function displayLocation(locationDetails: Location): void {
    // ...
}

2.2 - Implement the function displayLocation()

Next, implement the logic for the function displayLocation(). This function should do some DOM manipulation to display in the user interface the details of a location that we receive as an argument. In particular, it should do the following:

  1. In the HTML element with the id location-name, you should display the name of the location.
  2. In the HTML element with the id country, you should display the country.

Hint

You can get a reference to an HTML elements using the method getElementById() and a type assertion. Once you have a reference, you can modify the content using innerText.

Example:

const myCoolHtmlElm = document.getElementById('my-cool-id') as HTMLElement;
myCoolHtmlElm.innerText = "Hello World";
Solution
// src/utils.ts
// ...

export function displayLocation(locationDetails: Location) {
    // display location name
    const locationNameElm = document.getElementById('location-name') as HTMLElement;
    locationNameElm.innerText = "" + locationDetails.name;

    // display country
    const countryElm = document.getElementById('country') as HTMLElement;
    countryElm.innerText = "(" + locationDetails.country + ")";
}

Iteration 3 | Functionality to display the weather info in the UI

3.1 - Create the signature for the function displayWeatherData()

In this step, we will define a function that will receive the data about about the weather and display that info in the user interface.

In src/utils.ts, create a function with this signature:

  • Name of the function: displayWeatherData()
  • Parameters:
    • obj (an object of type WeatherResponse)
  • Return value:
    • For this function, we're not interested in the return value.

Solution
// src/utils.ts
// ...

export function displayWeatherData(obj: WeatherResponse) {
  // ...
}

Note: you can also explicitly state that we're not interested in the return value using void:

// src/utils.ts
// ...

export function displayWeatherData(obj: WeatherResponse): void {
  // ...
}

3.2 - Implement the function displayWeatherData()

Next, implement the logic for the function displayWeatherData(). This function should do some DOM manipulation to display in the user interface the info about the weather. In particular, it should do the following:

  1. In the HTML element with the id temperature, you should display the temperature, including the units (e.g. 20.6 °C).
  2. In the HTML element with the id windspeed, you should display the wind speed (e.g. 3.4 km/h).
  3. In the HTML element with the id winddirection, you should display the wind direction (e.g. 32 °).

Solution
// src/utils.ts
// ...

export function displayWeatherData(obj: WeatherResponse) {
    // display temperature  
    const temperatureElm = document.getElementById('temperature') as HTMLElement;
    const temperature = obj.current_weather.temperature;
    const temperatureUnits = obj.current_weather_units.temperature;
    temperatureElm.innerText = `Temperature: ${temperature} ${temperatureUnits}`;
    
    // display wind speed
    const windspeedElm = document.getElementById('windspeed') as HTMLElement;
    const windspeed = obj.current_weather.windspeed;
    const windspeedUnits = obj.current_weather_units.windspeed;
    windspeedElm.innerText = `Wind Speed: ${windspeed} ${windspeedUnits}`;

    // display wind direction
    const winddirectionElm = document.getElementById('winddirection') as HTMLElement;
    const winddirection = obj.current_weather.winddirection;
    const winddirectionUnits = obj.current_weather_units.winddirection;
    winddirectionElm.innerText = `Wind Direction: ${winddirection} ${winddirectionUnits}`;
}

Iteration 4 | Display weather from users' input

Now that we have all our type definitions and utility functions ready, we will implement the functionality so that, when the user types the name of a location, we can display the weather for that location.


4.1 - Implement Event Listener for Form Submission

In this step, we will add an event listener to detect when the user submits the form.

In src/main.ts, create the code to do the following:

  1. Add an event listener to detect if the user submits the form (note: the form has the id weather-form).
  2. Inside the code for that event listener, invoke the method event.preventDefault(), so that the page does not reload when the user submits the form.
  3. Also inside the event listener, do a console.log() with the message "The user has submitted the form".

Once you have completed these steps, when the user submits the form, you should see a message "The user has submitted the form" in the console.


Solution
// src/main.ts

const form = document.getElementById("weather-form") as HTMLFormElement;

form.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent the default form submission behavior
  console.log("The user has submitted the form");
});

4.2 - Get the name of the location provided by the user

Next, modify the event listener you created in the previous step so that, when the user submits, you display in the console the name of the location that they have entered (example: "The user has submitted the form and is searching for a location with this name... [Berlin]").


Solution
// src/main.ts

const form = document.getElementById("weather-form") as HTMLFormElement;

form.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent the default form submission behavior

  const locationInput = document.getElementById("location") as HTMLInputElement;
  const locationName = locationInput.value;
  
  console.log(`The user has submitted the form and is searching for a location with this name... ${locationName} `);

  locationInput.value = ""; // Clear the form
});

4.3 - Display weather when the user submits the form

Now, you will need to put all the pieces together! Modify the code in src/main.ts so that, when the user submits the form, we display info about the location and the current weather.


Hints

You will need to do the following:

  • When the user submits, invoke the function getLocation() to get the details of the desired location from the API. This function returns a promise, so you can handle the response with .then().catch().
  • Once you have the response from the API (i.e., inside the .then()), use the first result (the API returns an array, so you can get the element with index zero) and invoke the functions displayLocation() (to display those details in the UI) and getCurrentWeather() (to get the current weather for that location).
  • Once you have the info about the current weather, invoke the function displayWeatherData()

These steps are more complex and we're putting many pieces together. Remember that you can add a console.log() to see what data you have (for example, you can add a console.log() to see what data you get from the API) 😉

Solution
// src/main.ts

import { getLocation, getCurrentWeather, displayLocation, displayWeatherData } from './utils.ts';

const form = document.getElementById("weather-form") as HTMLFormElement;

form.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent the default form submission behavior

  const locationInput = document.getElementById("location") as HTMLInputElement;
  const locationName = locationInput.value;
  locationInput.value = ""; // Clear the form

  getLocation(locationName)
    .then((response) => {
      if(response.results){

        // Get the first result (the api may provide multiple results if there's multiple locations with the same or similar names, we will just use the first one for simplicity)
        const location = response.results[0];

        // Display info about the location
        displayLocation(location);

        // Get info about the weather for that location
        return getCurrentWeather(location);
      } else {
        // If there's no results, throw an error
        throw new Error('Location not found');
      }
    })
    .then((weatherData) => {

      // Display info about the weather
      displayWeatherData(weatherData);
      
    })
    .catch((error) => {
      console.log("Error getting weather data");
      console.log(error);
    });

});



Bonus: Iteration 5 | Update background

Congratulations, if you've reached this point, the main functionality will be working! In this iteration, we will improve the user experience by adding a background image that reflects the weather in each location (for example, if you search for the weather in Berlin and it's cloudy, we will display a background image with clouds).

See Expected Result

So that you can focus on the functionality, we have already included all the images you will need (in the directory /public/images/background) and some CSS rules (in src/style.css).


5.1 - Create the signature for the function updateBackground()

In this step, we will define a function that will receive some details about the weather and, based on those details, it will do some DOM manipulation so that those CSS rules are applied.

In src/utils.ts, create a function with this signature:

  • Name of the function: updateBackground()
  • Parameters:
    • weatherCode (a number)
    • isDay (also a number)
  • Return value:
    • For this function, we're not interested in the return value.

Solution
// src/utils.ts
// ...

export function updateBackground(weatherCode: number, isDay: number) {
  // ...
}

Note: you can also explicitly state that we're not interested in the return value using void:

// src/utils.ts
// ...

export function updateBackground(weatherCode: number, isDay: number): void {
  // ...
}

5.2 - Implement the function updateBackground()

At the end of the file src/style.css, we have included some CSS rules that modify the background based on the class of the <body> tag (for example, if the <body> tag has the class sunny, a sunny background will be applied).

Implement the logic for the function updateBackground() so that it changes the class of the body tag based on weatherCode and isDay, following the table below:

First character of weatherCode isDay Class name
0 or 1 0 sunny-night
1 sunny
2 0 partly-cloudy-night
1 partly-cloudy
3 any value cloudy
4 any value foggy
5 any value drizzle
6 any value rain
7 any value snow
8 any value showers
9 any value thunderstorm

For example:

  • If the first character of weatherCode is 0 and isDay is also 0, your function should change the class of the HTML body tag to sunny-night.
  • If the first character of weatherCode is 3, your function should change the class of the HTML body tag to cloudy.

Hint

You can change the class using the property className. For example:

document.body.className = "sunny-night";
Solution
  // src/utils.ts
  // ...

  export function updateBackground(weatherCode: number, isDay: number) {

      const firstCharacter = weatherCode.toString().charAt(0);

      switch(firstCharacter){
          case "0":
          case "1":
              if(isDay === 0){
                  document.body.className = "sunny-night";
              } else {
                  document.body.className = "sunny";
              }
              break;
          case "2":
              if(isDay === 0){
                  document.body.className = "partly-cloudy-night";
              } else {
                  document.body.className = "partly-cloudy";
              }
              break;
          case "3":
              document.body.className = "cloudy";
              break;
          case "4":
              document.body.className = "foggy";
              break;
          case "5":
              document.body.className = "drizzle";
              break;
          case "6":
              document.body.className = "rain";
              break;
          case "7":
              document.body.className = "snow";
              break;
          case "8":
              document.body.className = "showers";
              break;
          case "9":
              document.body.className = "thunderstorm";
              break;
          default:
              document.body.className = "";
              break;
      }
  }

5.3 - Invoke the function updateBackground()

Finally, you will need to update the file src/main.ts and invoke the function updateBackground() once you have the details about the weather.


Hint

You can invoke updateBackground(), right after invoking displayWeatherData() (in src/main.ts).

When you invoke updateBackground(), make sure to pass the expected arguments.

Solution

First, make sure to import the function updateBackground() in src/main.ts:

// src/main.ts

import { getLocation, getCurrentWeather, displayLocation, displayWeatherData, updateBackground } from './utils';

Then, modify the code to invoke it once we have the details about the weather:

// src/main.ts

// ...

form.addEventListener('submit', (event) => {
  // ...

  getLocation(locationName)
    .then((response) => {
      // ...
    })
    .then((weatherData) => {

      // Display info about the weather
      displayWeatherData(weatherData);

      // Update background
      updateBackground(weatherData.current_weather.weathercode, weatherData.current_weather.is_day);  // <== ADD THIS
      
    })
    .catch((error) => {
      // ...
    });

});



Happy coding! ❤️


Acknowledgments

This project uses weather data from Open-Meteo, licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0), and images from Pixabay.


FAQs

I am stuck and don't know how to solve the problem or where to start. What should I do?

If you are stuck in your code and don't know how to solve the problem or where to start, you should take a step back and try to form a clear question about the specific issue you are facing. This will help you narrow down the problem and come up with potential solutions.

For example, is it a concept that you don't understand, or are you receiving an error message that you don't know how to fix? It is usually helpful to try to state the problem as clearly as possible, including any error messages you are receiving. This can help you communicate the issue to others and potentially get help from classmates or online resources.

Once you have a clear understanding of the problem, you will be able to start working toward the solution.


Back to top

How do I run the app?

Check the section "Initial setup".


Back to top

When I run the app, I get an error "vite: not found"

Make sure to install all the dependencies running this command in your terminal:

npm install

Back to top

How can I run the app on a different port?

By default, Vite will run on the port 5173. If you need to run the app on a different port, you can use this command:

npm run dev -- --port=3001

Back to top

Do I need to add type annotations to everything?

No. In many cases, TypeScript can infer the types from the context and using implicit types can make your code more clear and readable.

For example:

function calcTotal(numberOfProducts: number, price: number): number {

    const total = numberOfProducts * price; // implicit (TypeScript can infer that "total" will be a number)

    return total;
}

Back to top

I get an error "Cannot find name 'abc'"

If you get an error "Cannot find name 'abc'" (for example, Cannot find name 'WeatherResponse'), it can be because your code is not exported/imported correctly.

Make sure to export your type definitions and functions, so that they can be used in other files. For example:

// src/types.ts

// ...

export type WeatherResponse = {
  // ...
}

Also, when you use a type definition or a function from another file, make sure to import it. For example:

// src/utils.ts

// ...

import { LocationResponse, Location, WeatherResponse } from "./types.ts";

Back to top

I am unable to push changes to the repository. What should I do?

There are a couple of possible reasons why you may be unable to push changes to a Git repository:

  1. You have not committed your changes: Before you can push your changes to the repository, you need to commit them using the git commit command. Make sure you have committed your changes and try pushing again. To do this, run the following terminal commands from the project folder:

    git add .
    git commit -m "Your commit message"
    git push

  2. You do not have permission to push to the repository: If you have cloned the repository directly from the main Ironhack repository without making a Fork first, you do not have write access to the repository. To check which remote repository you have cloned, run the following terminal command from the project folder:

    git remote -v

If the link shown is the same as the main Ironhack repository, you will need to fork the repository to your GitHub account first, and then clone your fork to your local machine to be able to push the changes.

Note: You may want to make a copy of the code you have locally, to avoid losing it in the process.


Back to top

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published