Build a Weather App with React.js, TypeScript, and Tailwind CSS
Tags: typescript, react, tutorial
Published: 2023-07-28 10:58:06

One of the most effective ways to enhance your coding skills is to work on practical projects that challenge you and allow you to apply your knowledge in real-world scenarios.

In this tutorial, we will create a Weather App using **[React.js](https://react.dev/learn)**, **[TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)**, and **[Tailwind Css](https://tailwindui.com/?ref=top)**. The purpose of this app is to provide users with up-to-date weather information for their desired locations. You can play with the **[live demo](https://weather-app-gray-delta-51.vercel.app/)** to see what we want to build and you can also look into the **[source code](https://github.com/Olaleye-Blessing/weather_app)**.


![Weather app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qjxw3xn0iiez0qrnizfl.png)

## ****Prerequisites****

This tutorial assumes that you have a basic knowledge of HTML, CSS, JavaScript, and React.js. You don’t need to be a TypeScript expert to follow this tutorial.

## Setting up the Development Environment

Setting up the development environment is the first step in building our weather app. We will use [**CRA**](https://create-react-app.dev/docs/getting-started#creating-a-typescript-app) to create our react project.

> Make sure you have **[nodejs and npm](https://nodejs.org/en)** installed.
> 

Run the following command to initialize a new project:

```bash
npx create-react-app weather_app --template typescript
```

After the above is done, then you can run the following:

```bash
cd weather_app
npm start
```

Your default browser will be opened at [**http://localhost:3000/**](http://localhost:3000/) with the following content:


![Localhost](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fe3ccheavpj6b9re4v03.png)

## Set up TailwindCSS

We first need to add Tailwind CSS as a dev dependency in our project:

```bash
npm install -D tailwindcss
```

Then we create a `tailwind.config.js` file with the following command:

```bash
npx tailwindcss init
```

Add the paths to all of our template files in our `tailwind.config.js` file by replacing the content of our file with the below:

```jsx
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Add the Tailwind directives to our CSS by updating `index.css` file with the following code:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

We can now test that Tailwind is working by updating the content of our `App.tsx` with the below code:

```tsx
function App() {
  return (
    <h1 className="text-3xl font-bold underline">
      Hello world!
    </h1>
  );
}

export default App;
```

`App.css` should be deleted since its styling is no longer needed.

Our [**browser**](http://localhost:3000/) should have the following content at this point:


![Hello world in React](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4q7q3echc3t1c5z8t1s.png)

## Other dependencies

Go ahead and install some other dependencies we will be using:

```bash
npm install @heroicons/react @tailwindcss/forms axios
```

- `axios` to make asynchronous requests.
- `@tailwindcss/forms` to style our form.
- `@heroicons/react` to display icons.

Our `tailwind.config.js` needs to be updated to make use of `@tailwindcss/forms` plugin.

```tsx
/** @type {import('tailwindcss').Config} */
module.exports = {
	content: ["./src/**/*.{js,jsx,ts,tsx}"],
	theme: {
		extend: {},
	},
	plugins: [require("@tailwindcss/forms")],
};
```

Finally, add the colors we will be using to the `theme` key in our `tailwind.config.js`.

```tsx
module.exports = {
...
theme: {
		extend: {
			colors: {
				black: {
					1: "#323544",
					2: "#0000001a",
					3: "#262936",
				},
				white: {
					DEFAULT: "#fff",
					1: "#bfc1c8",
				},
				blue: {
					"1": "#009ad8",
				},
			},
		},
	},
...
}
```

## Get the OpenWeatherMap API Key

OpenWeatherMap provides minutely forecast, historical data, current state, and from short-term to annual forecasted weather data. We can get our weather forecasts from OpenWeatherMap by getting an API key.

Head over to [**https://openweathermap.org/api](https://openweathermap.org/api)** to create an account and generate an API Key.

It’s a good idea to save secret keys like our API key in a `.env` file so as to reduce the risk of unauthorized access.

> Note: Variables saved in `.env` in our React application are not entirely hidden. This is because environment variables are embedded into the build, meaning anyone can view them by inspecting your app's files.
> 

Create a `.env` file at the root level and store your weather key there:

```tsx
// /.env
REACT_WEATHER_KEY=your_weather_key
```

Notice our key name starts with `REACT_APP`. The reason for this naming convention is to avoid conflicts and ensure that the environment variables are only applied to your React application.

Also, add `.env` to your `.gitignore` so as not to push your secret key(s) to the public.

## Get Sample data

We can now use our weather key to get sample data of what we would be expecting when we create a function to fetch our weather forecasts.

If you go through openweathermap’s documentation, you will notice we can use the:

- **[Current Weather Data](https://openweathermap.org/current#name)** to get today’s weather data
- [5 day weather forecast](https://openweathermap.org/forecast5#name5) to get 5 days forecast

You can test these endpoints to see the sample data we will be using:

- [https://api.openweathermap.org/data/2.5/weather?q=lagos&units=metric&appid=YOUR_KEY](https://api.openweathermap.org/data/2.5/weather?q=lagos&units=metric&appid=0e0f3dc606259100c818f1679185a738)
    
    
    ![Openweather today weather forecast for Lagos state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xntcqmq2d1aaao6xpah.png)
    
- [https://api.openweathermap.org/data/2.5/forecast?q=lagos&units=metric&appid=YOUR_KEY](https://api.openweathermap.org/data/2.5/forecast?q=lagos&units=metric&appid=0e0f3dc606259100c818f1679185a738)
    
    
     ![Openweather 7 days forecast for Lagos state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ub0e7oaidw1h30m0k7o.png)
    

These sample data are going to increase our development process as we won’t need to request data each time we make changes to our code.

We should create `interfaces` for our sample data(**[source code](https://github.com/Olaleye-Blessing/weather_app/commit/7ffd471c35e9f44de9e906a21283164a3762ec66#diff-f79922ca98805f65525a5d998419a7d456b5e17882c9f5e8898ea0458d53aa10)**):

```tsx
// src/interfaces/weather.ts
export interface IToday {...}
export interface IForecast {..}
```

You can go ahead and save the sample data as constants in `src/data/weather.ts` as `today` and `forecasts` respectively. The `list` array will stand for our `forecasts`(**[source code](https://github.com/Olaleye-Blessing/weather_app/commit/7ffd471c35e9f44de9e906a21283164a3762ec66#diff-de1db84a1734ed7fa27f246548093ebeb4d53333209a493211c80b1e0e31825e)**):

```tsx
// src/data/weather.ts

import { IForecast, IToday } from "../interfaces/weather";

export const today: IToday = {...}
export const forecasts: IForecast[] = [...]
```

## Building our Components

The best way to have a maintainable and readable react project is to have different components for different purposes. Our weather app is going to make use of different components:

- Navbar: contains children's components:
    - Search: enables users to search for different locations.
    - Histories: saves and lists searched locations.
- Forecasts: contains the following children components:
    - Today: displays weather data for the current day.
    - Other: displays other day's forecasts.

### Icons

We will be using 3 SVG icons to illustrate different weather conditions, the SVGs code can be found in the source code(**[Direction](https://github.com/Olaleye-Blessing/weather_app/commit/3d0dedd8b385027a58d1498e1a3eb5109ac6dc3c#diff-cfbefd8f7cbe2e1ebecb1fe254b347a6213e8b54dfb0b28a0eafa9aae02fba1a)**, **[Umbrella](https://github.com/Olaleye-Blessing/weather_app/commit/3d0dedd8b385027a58d1498e1a3eb5109ac6dc3c#diff-f014d73de4ee2506350fd7f811912272a2db9f967e2d2904b0c01a06e8bc645a)**, **[Wind](https://github.com/Olaleye-Blessing/weather_app/commit/3d0dedd8b385027a58d1498e1a3eb5109ac6dc3c#diff-909272994bbc8660bf232c07b9e7fa94bb5ab50845b0c185d7e065054b8b9c07)**):

```tsx
// src/components/icons/Direction.tsx

const Direction = () => {}

export default Direction;

// src/components/icons/Umbrella.tsx

const Umbrella = () => {}

export default Umbrella;

// src/components/icons/Wind.tsx

const Wind = () => {}

export default Wind;
```

### Search.tsx

```tsx
// src/components/Search.tsx
import { useState } from "react";

const Search = () => {
	const [query, setQuery] = useState("");

  return (
    <form
      className="w-full max-w-xs"
      onSubmit={(e) => {
        e.preventDefault();
        console.log("Get data");
      }}
    >
      <input
        type="search"
        name="search"
        id="search"
        aria-label="Search for city/country forecast"
        placeholder="Search for city/country forecast"
        className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </form>
  );
};

export default Search;
```

### Histories.tsx

```tsx
// src/components/Histories.tsx

import { useEffect, useRef } from "react";

// Comment 1
const modalHeight = "!h-[calc(100vh-2.75rem)]"

const Histories = () => {
  // Comment 2
  const locations: string[] = JSON.parse(localStorage.getItem("locations") || "[]");
  // Comment 3
  const ulRef = useRef<HTMLDivElement>(null);

  const toggleUl = () => {
    const { current: container } = ulRef;

    if (!container) return;

    container.classList.toggle("!w-screen");
    container.classList.toggle(modalHeight);
  };

  useEffect(() => {
    const closeUIModal = () => {
      const { current: container } = ulRef;

      if (!container) return;

      container.classList.remove("!w-screen");
      container.classList.remove(modalHeight);
    }

    window.addEventListener("click", closeUIModal);

    return () => window.removeEventListener("click", closeUIModal)
  }, [])

  return (
    <div className="relative">
      <button type="button" onClick={(e) => {
        e.stopPropagation();
        toggleUl();
      }}>
        <ClockIcon className="h-6 w-6" />
      </button>
      <div
        ref={ulRef}
        className="absolute top-8 right-0 z-10 overflow-hidden h-0 w-0 bg-white transition-all duration-300 ease-in-out flex items-start justify-end pl-3 bg-opacity-5"
      >
        <div className="w-full max-w-[15rem]" onClick={e => {
          e.stopPropagation();
        }}>
          {locations.length === 0 ? (
            <p className="text-center text-gray-700 font-extrabold">
              No history
            </p>
          ) : (
            <ul className="bg-white shadow-xl rounded-lg overflow-y-auto h-full max-w-[15rem] max-h-60">
              {locations.map((location) => (
                <li
                  key={location}
                  className="flex items-center justify-between"
                >
                  <button
                    className="text-ellipsis text-left overflow-hidden whitespace-nowrap px-4 py-2 hover:bg-gray-500 transition-colors duration-150 w-full"
                    type="button"
                    onClick={() => { console.log("Get Data") }}
                  >
                    {location}
                  </button>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
};

export default Histories;
```

`Comment 1`: The height of the modal is the height of the screen minus the height of the navbar. This is to allow users to have the opportunity to search even if the modal is opened.

`Comment 2`: We are getting our location history from local storage. The components re-render each time a new location is searched. This re-rendering gives us the chance to grab the new history from the local storage.

`Comment 3`: A transparent modal is displayed on the page when the history icon is pressed. The useRef is used to reference the modal so that it will be easy for us to close the modal without any re-rendering.

### Navbar.tsx

```tsx
// src/components/Navbar.tsx

import Histories from "./Histories"
import Search from "./Search"

const Navbar = () => {
  return (
    <nav className="flex item-center justify-between">
      <div className="w-full max-w-5xl mx-auto flex item-center justify-between">
        <div>
          <a href="/" className="text-2xl font-semibold">
            Forecast
          </a>
        </div>
        <Search />
        <Histories />
      </div>
    </nav>
  )
}

export default Navbar
```

### Today.tsx

```tsx
// src/components/forecasts/Today.tsx

import { today as data } from "../../data/weather";
import { windDirection } from "../../utils/weather";
import WeatherCondition from "./WeatherCondition";

const dateOptions: Intl.DateTimeFormatOptions = {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
};

const timeOptions: Intl.DateTimeFormatOptions = {
  hour: "2-digit",
  minute: "2-digit",
};

const Today = () => {
  const date = new Date(data.dt * 1000);

  return (
    <div className="bg-black-1">
      <time
        dateTime={date.toString()}
        className="flex items-center justify-between bg-black-2 p-4"
      >
        <span>{date.toLocaleString(undefined, dateOptions)}</span>
        <span>{date.toLocaleTimeString(undefined, timeOptions)}</span>
      </time>
      <div className="px-4 py-12 lg:flex lg:items-center lg:justify-center lg:flex-col lg:h-[90%]">
        <h3 className="text-4xl mb-4">{data.name}</h3>
        <p>{data.weather[0].main}</p>
        <div className="mt-8 flex items-center justify-start">
          <p className="text-7xl">
            {data.main.temp}
            <sup className="relative text-[1.8rem] -top-8 font-semibold">o</sup>
            C
          </p>
          <figure className="w-16 h-16">
            <img
              src={`https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`}
              alt="sun icon"
            />
          </figure>
        </div>
        <div className="flex items-center justify-start space-x-2 mt-4">
          <WeatherCondition icon="umbrella" value={data.main.humidity} />
          <WeatherCondition icon="wind" value={`${data.wind.speed}m/sec`} />
          <WeatherCondition
            icon="direction"
            value={windDirection(data.wind.deg)} // Comment 1
          />
        </div>
      </div>
    </div>
  );
};

export default Today;
```

- `Comment 1`: `windDirection` is a function that is used to determine the direction of the wind

```tsx
// src/utils/weather.ts
export const windDirection = (deg: number) => {
	if (deg === 0) return "N";

	if (deg > 0 && deg < 90) return "NE";

	if (deg === 90) return "E";

	if (deg > 90 && deg < 180) return "SE";

	if (deg === 180) return "S";

	if (deg > 180 && deg < 270) return "SW";

	if (deg === 270) return "W";

	if (deg > 270 && deg < 360) return "NW";

	return "N";
};
```

### WeatherCondition.tsx

```tsx
// src/components/forecasts/WeatherCondition.tsx

import { FC } from "react";
import Umbrella from "../icons/Umbrella";
import Wind from "../icons/Wind";
import Direction from "../icons/Direction";

const Icons = {
  "umbrella": <Umbrella />,
  "wind": <Wind />,
  "direction": <Direction />
}

interface Props {
  value: string | number;
  icon: keyof typeof Icons;
}

const WeatherCondition: FC<Props> = ({ value, icon }) => {
  return (
    <div className="flex items-center justify-start">
      <figure className="w-5 h-5 mr-2">
        {Icons[icon]}
      </figure>
      <p className="font-semibold">{value}</p>
    </div>
  )
}

export default WeatherCondition
```

### Other.tsx

```tsx
// src/components/forecasts/Other.tsx

import { forecasts } from "../../data/weather";

const dateOptions: Intl.DateTimeFormatOptions = {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
};

const Other = () => {
  return (
    <ul className="grid grid-cols-[repeat(auto-fit,minmax(20rem,1fr))] lg:flex-grow lg:flex-shrink">
      {forecasts.map((forecast, key) => {
        const date = new Date(forecast.dt * 1000);
        const day = date.toLocaleDateString(undefined, dateOptions).split(", ")[0];

        return <li key={key} className="bg-black-3 even:bg-black-1 flex flex-col items-center justify-center text-center">
          <time className="bg-black-2 py-4 block w-full">{day}</time>
          <figure className="mt-8">
            <img
              src={`https://openweathermap.org/img/wn/${forecast.weather[0].icon}@2x.png`}
              alt="sun icon"
            />
          </figure>
          <p className="text-4xl mb-8 mt-4">
            {forecast.main.temp}
            <sup className="relative text-[1.8rem] -top-8 font-semibold">o</sup>
            C
          </p>
        </li>
      })}
    </ul>
  )
}

export default Other
```

### Forecasts.tsx

```tsx
// src/components/forecasts/Index.tsx

import Today from "./Today";
import Other from "./Other";

const Index = () => {
  return (
    <section className="text-white-1 lg:flex">
      <Today />
      <Other />
    </section>
  );
};

export default Index;
```

Now that our components are ready, we can go ahead and update our `App.tsx`:

```tsx
// src/App.tsx

import Navbar from "./components/Navbar";
import Forecasts from "./components/forecasts/Index";

const App = () => {
  return (
    <div className="px-4 py-3 min-h-screen bg-gray-200">
      <Navbar />
      <main className="mt-8 max-w-5xl mx-auto">
        <Forecasts />
      </main>
    </div>
  );
}

export default App
```

Our page should look like this:


![Static weather forecast](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxa67caarca3wqt397ke.png)

## Implement Functionalities

We need to replace our static data with real data. From our current page, you would notice we have more forecasts than we need. We have different forecasts for each day. We need to filter the forecasts in such a way that we would only display the forecasts at 00:00 of each day.

```tsx
// src/utils/weather.ts
import { IForecast } from "../interfaces/weather";

export const filterForecasts = (data: IForecast[]) =>
	data.filter((list) => list.dt_txt.indexOf("00:00:00") != -1);
```

Next, we would implement the function that fetches the real data.

```tsx
// src/services/weather.ts

import axios, { isAxiosError } from "axios";
import { IForecast, IToday } from "../interfaces/weather";
import { filterForecasts } from "../utils/weather";

const WEATHER_KEY = import.meta.env.VITE_WEATHER_KEY;

interface Data {
	today: IToday;
	forecasts: IForecast[];
}

export const getForecasts = async (query: string) => {
	const baseUrl = "https://api.openweathermap.org/data/2.5";
	// Comment 1
	const searchParams = new URLSearchParams({
		q: query,
		units: "metric",
		appid: WEATHER_KEY,
	}).toString();

	const urls = [
		`${baseUrl}/weather?${searchParams}`,
		`${baseUrl}/forecast?${searchParams}`,
	];

	try {
		const requests = urls.map((url) => axios.get(url));

		// Comment 2
		const responses = await Promise.all(requests);

		const [today, forecasts] = responses.map((response) => response.data);

		const data: Data = {
			today,
			forecasts: filterForecasts(forecasts.list),
		};

		return data;
	} catch (error) {
		let message = "Unknown error";

		if (isAxiosError(error)) {
			message =
				error.message === "Network Error"
					? "Please! Check your internet connection"
					: "Location not found";
		}

		throw new Error(message);
	}
};
```

- `Comment 1` URLSearchParams encodes the `query` before adding it to the `URL`. You can read more about it in my article: **Create Dynamic URLs with URL Constructor in JavaScript.**
- `Comment 2` [**Promise.all**](https://javascript.info/promise-api#promise-all) executes our requests in parallel and returns the response of each request. It returns an error for the whole request if any of the requests fails. We used Promise.all because the result of one doesn’t depend on the other.

We will start updating our components from the base, `App.tsx`

### App.tsx

```tsx
// src/App.tsx

import { useState } from "react";
import Navbar from "./components/Navbar";
import Forecasts from "./components/forecasts/Index";
import { IFetchWeather } from "./interfaces/weather";

const defaultWeather: IFetchWeather = {
  loading: false,
  data: null,
  error: null,
};

const App = () => {
  const [forecasts, setForecasts] = useState<IFetchWeather>(defaultWeather);

	// Comment 1
  const handleSetForecasts = (forecasts: Partial<IFetchWeather>) => {
    setForecasts((prev) => ({ ...prev, ...forecasts }));
  }

  return (
    <div className="...">
      <Navbar handleSetForecasts={handleSetForecasts} />
      <main className="...">
        <Forecasts {...forecasts} />
      </main>
    </div>
  );
}

export default App
```

- `Comment 1` Typescript provides the [**Partial utility type**](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) to make all fields of an interface optional. We are using `Partial` to make sure not all fields are compulsory as we won’t be updating all keys at once.

Our interfaces need to be updated to have `IFetchWeather`

```tsx
// src/interfaces/weather.ts

export interface IFetchWeather {
	loading: boolean;
	data: { today: IToday; forecasts: IForecast[] } | null;
	error: null | string;
}
```

### Navbar.tsx

```tsx
// src/components/Navbar.tsx

import { FC } from "react";
import { getForecasts } from "../services/weather";
import { IFetchWeather } from "../interfaces/weather";
import Histories from "./Histories"
import Search from "./Search"

interface Props {
  handleSetForecasts: (forecasts: Partial<IFetchWeather>) => void;
}

const Navbar: FC<Props> = ({ handleSetForecasts }) => {
	// Comment 1
  const fetchForecasts = async (query: string) => {
    try {
      handleSetForecasts({ loading: true, data: null, error: null });
      const data = await getForecasts(query);
      handleSetForecasts({ data });
    } catch (error: any) {
      handleSetForecasts({ error: error.message });
    } finally {
			// Commnet 2
      handleSetForecasts({ loading: false });
    }
  };

  return (
    <nav className="...">
      <div className="...">
        ...
				...
        <Search fetchForecasts={fetchForecasts} />
        <Histories fetchForecasts={fetchForecasts} />
      </div>
    </nav>
  )
}

export default Navbar
```

- `Comment 1` fetchForecasts resets the forecasts state before fetching new data and then updates the forecasts state depending on the result.
- `Comment 2` Finally runs whether there is an error or data. This is the best place to have our cleanup instead of repeating the same code in the `try` and `catch` blocks.

### Search.tsx

```tsx
// src/components/Search.tsx

import { FC, useState } from "react";
import { persistLocation } from "../utils/weather";

interface Props {
  fetchForecasts: (query: string) => Promise<void>;
}

const Search: FC<Props> = ({ fetchForecasts }) => {
  const [query, setQuery] = useState("");

  return (
    <form
      className="w-full max-w-xs"
      onSubmit={(e) => {
        e.preventDefault();
				// Comment 1
        fetchForecasts(query);
				persistLocation(query);
      }}
    >
      ...
    </form>
  );
};

export default Search;
```

- `Comment 1` We fetch new data each time the user presses enter.

`persistLocation` function updates the local storage each time a new location is searched:

```tsx
// src/utils/weather.tsx

export const persistLocation = (location: string) => {
	const locations = localStorage.getItem("locations");

	if (!locations) {
		localStorage.setItem("locations", JSON.stringify([location]));
		return;
	}

	const parsedLocations: string[] = JSON.parse(locations);

	if (parsedLocations.includes(location)) return;

	localStorage.setItem(
		"locations",
		JSON.stringify([...parsedLocations, location])
	);
};
```

### Histories.tsx

```tsx
// src/components/Histories.tsx

import { FC, useEffect, useRef } from "react";
import { ClockIcon } from "@heroicons/react/20/solid";

const modalHeight = "!h-[calc(100vh-2.75rem)]"

interface Props {
  fetchForecasts: (query: string) => Promise<void>;
}

const Histories: FC<Props> = ({ fetchForecasts }) => {
  // ...

  return (
    <div className="relative">
      // ....
    </div>
  );
};

export default Histories;
```

In the same file, change the location button to fetch new data when a location is clicked:

```tsx
// src/components/Histories.tsx

<button
	className="..."
  type="button"
  onClick={() => fetchForecasts(location)}
 >
  {location}
 </button>
```

### Forecasts.tsx

```tsx
import Today from "./Today";
import Other from "./Other";
import { IFetchWeather } from "../../interfaces/weather";
import { FC } from "react";

const Index: FC<IFetchWeather> = ({ data, error, loading }) => {
  if (loading) return <p>Loading...</p>;

  if (error) return <p className="font-semibold text-lg text-red-600">{error}</p>;

  if (!data) return null;

  return (
    <section className="text-white-1 lg:flex">
      <Today data={data.today} />
      <Other forecasts={data.forecasts} />
    </section>
  );
};

export default Index;
```

- This component has been updated to display different items based on if the data is still loading if there is an error or if data is available.

### Today.tsx

```tsx
// src/components/forecasts/Today.tsx

import { FC } from "react";
import { windDirection } from "../../utils/weather";
import WeatherCondition from "./WeatherCondition";
import { IToday } from "../../interfaces/weather";

const dateOptions: Intl.DateTimeFormatOptions = {
  weekday: "long",
  minute: "2-digit",
};

const Today: FC<{ data: IToday }> = ({ data }) => {
  const date = new Date(data.dt * 1000);

  return (
    <div className="bg-black-1">
			...
		</div>
	)
}
```

### Other.tsx

```tsx
// src/components/forecasts/Other.tsx

import { FC } from "react";
import { IForecast } from "../../interfaces/weather";

interface Props {
  forecasts: IForecast[]
}

const dateOptions: Intl.DateTimeFormatOptions = {
  weekday: "long",
  day: "numeric",
};

const Other: FC<Props> = ({ forecasts }) => {
  return (
    <ul className="..">
			...
		</ul>
	)
}
```

We can check our browser now to see how this works.

## Conclusion

This comes to the end of our weather app. The app could still be improved by:

- preventing the user from searching for an empty query.
- making sure persisted/saved locations are case-insensitive.
- deleting history.