How to Build a Weather Forecast App with React, TypeScript, Tolgee and OpenWeather API
Anietie Brownson
Posted on October 22, 2024
Introduction
Weather apps are a great way to practice building real-world applications, and so in the spirit of Hacktoberfest and with the power of Vite, TypeScript, and Tolgee, we will create a beautiful and multilingual weather app. Vite provides us with a lightning-fast development environment, TypeScript ensures our app is type-safe, and Tolgee simplifies internationalization (i18n), allowing our app to support multiple languages with ease.
In this tutorial, we'll walk through the process of building the weather app using:
- Vite: For a fast, modern development experience.
- TypeScript: For type safety.
- Tolgee: To effortlessly manage translations.
- OpenWeather API: To fetch real-time weather data.
Let's dive in!
Project Setup
Before we can start coding, let's get our project set up.
Install Vite:
Start by creating a new Vite project. Run the command below:
npm create vite@latest weather-app --template react-ts
cd weather-app
npm install
This sets up a Vite project using React and TypeScript.
Install TailwindCSS
Next, let's install TailwindCSS, a utility-first CSS framework that provides us with a set of classes to style our app. Run:
npm install -D tailwindcss postcss autoprefixer
Initialize Tailwind CSS:
After installation, initialize Tailwind CSS. This will create a tailwind.config.js file in your project:
npx tailwindcss init -p
Open the tailwind.config.js
file add the following code:
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
Add Tailwind’s directives to your CSS:
Open the index.css file and add the following lines to include Tailwind’s base, components, and utilities:
@tailwind base;
@tailwind components;
@tailwind utilities;
Install React Icons
To add icons to our app, we'll install React Icons. Run:
npm install react-icons
Install Tolgee:
Next, we'll install Tolgee. Tolgee makes it easy to implement in-context translation and edit translations directly in your app. Install it by running:
npm install @tolgee/react
Now visit the Tolgee website to create an account and get your API key.
Get your OpenWeather API key:
Sign up for an API key at OpenWeather.
Go to the root of the project and create a .env
file to store your API keys gotten from the Tolgee and OpenWeather websites:
VITE_APP_TOLGEE_API_URL=https://app.tolgee.io
VITE_APP_TOLGEE_API_KEY=your-api-key
WEATHER_API_KEY=your-openweather-api-key
Quickly install type definitions for our app
npm i --save-dev @types/node
Open your vite.config.ts file and paste the code below
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return {
define: {
"process.env.VITE_APP_TOLGEE_API_URL": JSON.stringify(
env.VITE_APP_TOLGEE_API_URL
),
"process.env.VITE_APP_TOLGEE_API_KEY": JSON.stringify(
env.VITE_APP_TOLGEE_API_KEY
),
"process.env.WEATHER_API_KEY": JSON.stringify(env.WEATHER_API_KEY),
},
plugins: [react()],
};
});
Building the App
With our project set up, it's time to dive into building the core functionality. Copy the following code into main.tsx
:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Tolgee, DevTools, TolgeeProvider, FormatSimple } from "@tolgee/react";
import App from "./App.tsx";
import "./index.css";
const tolgee = Tolgee().use(DevTools()).use(FormatSimple()).init({
language: "en",
// for development
apiUrl: process.env.VITE_APP_TOLGEE_API_URL,
apiKey: process.env.VITE_APP_TOLGEE_API_KEY,
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<TolgeeProvider tolgee={tolgee} fallback="Loading...">
<App />
</TolgeeProvider>
</StrictMode>
);
Now, in the root of your project, create a components
folder and two files inside it - LanguageSelect.tsx
and Loader.tsx
Inside the LanguageSelect.tsx
file, add the following code:
import { T, useTolgee } from "@tolgee/react";
export default function LanguageSelect() {
const tolgee = useTolgee(["language"]);
return (
<div className="flex items-center space-x-2">
<label
htmlFor="language-select"
className="block text-sm font-medium text-gray-700 mb-1"
>
<T keyName="label">Change Language</T>
</label>
<select
value={tolgee.getLanguage()}
onChange={(e) => tolgee.changeLanguage(e.target.value)}
className="border border-slate-500 z-50"
>
<option value="en">English</option>
<option value="es">Español</option>
<option value="ar">العربية</option>
<option value="zh-Hans">中文</option>
</select>
</div>
);
}
Inside the Loader.tsx
file, add the following code:
// import React from 'react'
const RippleLoader = () => {
return (
<div className="lds-ripple relative inline-block w-20 h-20">
<div className="absolute border-4 border-current rounded-full opacity-100 animate-ripple"></div>
<div className="absolute border-4 border-current rounded-full opacity-100 animate-ripple delay-100"></div>
<style>{`
.lds-ripple div {
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 0;
}
4.9% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 0;
}
5% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 1;
}
100% {
top: 0;
left: 0;
width: 80px;
height: 80px;
opacity: 0;
}
}
`}</style>
</div>
);
};
export default RippleLoader;
Now open your App.tsx
file and add the following code:
import { useState, useEffect } from "react";
import { T, useTranslate } from "@tolgee/react"; // Importing translation functions from Tolgee
import "./App.css";
import { FaMapMarkerAlt } from "react-icons/fa";
import { FaDroplet, FaWind } from "react-icons/fa6";
import { BiSearch } from "react-icons/bi";
import LanguageSelect from "./components/LanguageSelect";
import RippleLoader from "./components/Loader";
// Defining types for weather data
interface WeatherData {
name: string;
main: {
temp: number;
humidity: number;
};
weather: {
description: string;
icon: string;
}[];
wind: {
speed: number;
};
}
// Defining types for forecast data
interface ForecastData {
list: {
dt: number;
main: {
temp: number;
};
weather: {
icon: string;
}[];
}[];
}
function App() {
// State variables to store city name, weather data, forecast data, loading state, and errors
const [city, setCity] = useState<string>(""); // City input value
const [weatherData, setWeatherData] = useState<WeatherData | null>(null); // Current weather data
const [forecastData, setForecastData] = useState<ForecastData | null>(null); // Forecast data
const [loading, setLoading] = useState<boolean>(false); // Loading state
const [error, setError] = useState<string | null>(null); // Error message
const { t } = useTranslate(); // Tolgee translation hook
// Function to fetch weather and forecast data from OpenWeather API
const fetchWeatherData = async (cityName: string) => {
const apiKey = process.env.WEATHER_API_KEY; // Fetching the API key from environment variables
const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${apiKey}&units=metric`; // API URL for current weather
const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?q=${cityName}&appid=${apiKey}&units=metric`; // API URL for 3-day forecast
try {
setLoading(true); // Setting loading state
setError(null); // Resetting error state
// Fetching current weather data
const weatherResponse = await fetch(currentWeatherUrl);
if (!weatherResponse.ok) {
throw new Error("City not found! Try another one"); // Error if city not found
}
const weatherData: WeatherData = await weatherResponse.json(); // Parsing weather data
setWeatherData(weatherData); // Setting the fetched weather data
// Fetching forecast data
const forecastResponse = await fetch(forecastUrl);
if (!forecastResponse.ok) {
throw new Error("Unable to fetch forecast data"); // Error if forecast data not available
}
const forecastData: ForecastData = await forecastResponse.json(); // Parsing forecast data
setForecastData(forecastData); // Setting the fetched forecast data
} catch (error) {
// Handling any errors that occur during the fetch
if (error instanceof Error) {
setError(error.message); // Displaying error message
}
setWeatherData(null); // Resetting weather data
setForecastData(null); // Resetting forecast data
} finally {
setLoading(false); // Resetting loading state
}
};
// Handling form submission to fetch weather data based on city input
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
fetchWeatherData(city);
setCity("");
};
// useEffect to fetch the user's location and display the weather for the current location
useEffect(() => {
const getUserLocation = async () => {
if (navigator.geolocation) {
// Checking if geolocation is available
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords; // Getting user's device location
const apiKey = process.env.WEATHER_API_KEY;
const reverseGeocodeUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=metric`; // API URL for reverse geocoding (get location by lat/lon
try {
setLoading(true);
const response = await fetch(reverseGeocodeUrl); // Fetching weather data by geolocation
if (!response.ok) {
throw new Error("Unable to fetch location data"); // Error if location data fetch fails
}
const data: WeatherData = await response.json(); // Parsing weather data
setWeatherData(data); // Setting the fetched weather data
setCity(data.name); // Setting the city based on fetched location
await fetchWeatherData(data.name); // Fetching weather data for the fetched city
} catch (error) {
// Handling any errors that occur during the fetch
if (error instanceof Error) {
setError(error.message); // Displaying error message
}
setWeatherData(null); // Resetting weather data
setForecastData(null); // Resetting forecast data
} finally {
setLoading(false); // Resetting loading state
}
},
() => {
// Handling geolocation errors (e.g., if user denies location access)
setError("Unable to retrieve your location");
setWeatherData(null);
setForecastData(null);
}
);
} else {
// Handling case where geolocation is not supported by the browser
setError("Geolocation is not supported by this browser.");
setWeatherData(null);
setForecastData(null);
}
};
getUserLocation(); // Invoking the function to get user location on page load
}, []); // Empty dependency array to run effect once on component mount
return (
<>
<section className="flex flex-col items-center justify-center min-h-screen px-3">
<LanguageSelect />
<div className="mt-3 bg-slate-400/50 rounded shadow-lg border border-white/30 p-5 w-full md:w-[350px]">
<div>
<form className="relative" onSubmit={handleSubmit}>
<input
type="text"
name="search"
placeholder={t("search_city", "Search City")} // Translation for placeholder text
className="w-full px-4 py-2 border rounded-full text-white/70 bg-[#668ba0] focus:outline-none border-transparent focus:border-[#668ba0]"
value={city}
onChange={(e) => setCity(e.target.value)}
/>
<button type="submit" className="absolute top-3 right-3">
<BiSearch className="text-white/70" size={20} />
</button>
</form>
</div>
{loading && (
<div className="flex justify-center items-center">
<RippleLoader />
</div>
)}
{error && (
<p className="text-red-600 font-bold flex justify-center items-center">
{error}
</p>
)}
{!error && weatherData && (
<div>
{/* Displaying current weather data */}
<div className="flex justify-between items-center text-white font-bold">
<span className="flex items-center gap-x-2">
<FaMapMarkerAlt size={20} />
<p className="text-xl font-serif">{weatherData.name}</p>
</span>
<div className="flex flex-col items-center">
<img
src={`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`}
alt="weather icon"
/>{" "}
{/* Weather icon */}
<span>
<p className="text-6xl font-bold text-white">
{Math.round(weatherData.main.temp)} °C
</p>
</span>
</div>
</div>
<div className="my-5 flex justify-between items-center">
{/* Humidity */}
<div className="flex items-center gap-x-3">
<FaDroplet size={30} className="text-white/90" />
<span>
<p className="text-lg font-serif text-white font-bold">
<T keyName="humidity">Humidity</T>
</p>
<p className="text-lg font-medium text-white/90">
{weatherData.main.humidity}%
</p>
</span>
</div>
{/* Wind Speed */}
<div className="flex w-1/2 items-center gap-x-3">
<FaWind size={30} className="text-white/90" />
<span>
<p className="text-lg font-serif text-white font-bold">
<T keyName="wind_speed">Wind Speed</T>
</p>
<p className="text-lg font-medium text-white/90">
{weatherData.wind.speed} km/h
</p>
</span>
</div>
</div>
</div>
)}
</div>
</section>
</>
);
}
export default App;
In our code above, we wrapped some of the tags with the T
tag from Tolgee to be able to provide multilingual support. We also used the useTolgee
hook to get the current language and change the language when the user selects a different option.
Finally, open your App.css
file and add the following styles:
body {
background-color: #98c7da; /* Replace with your desired background color */
background-image: url("./assets/mountain-view.jpg"); /* Replace with the file path to your background image */
background-repeat: no-repeat;
background-position: center;
background-size: cover;
position: relative;
overflow: hidden;
}
body::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: inherit;
background-repeat: no-repeat;
background-position: inherit;
background-size: inherit;
filter: blur(5px);
z-index: -1;
}
Now open the app in your browser http://localhost:5173/
, you should see something like this:
Add translation support to our app
On your browser, hold down your ALT
key and hover over the "Forecast for the next 3 days" text or any other text we wrapped with the T
tag and tap on it. A popup should appear like so:
Write down the text you want to be translated in the English text box and scroll down and click the create button
Go to your Tolgee app dashboard, click on the translations tab and locate the new key you just created
On the right as in the picture above, select the language and choose the translation and then hit save. Now do the same for the other text wrapped with the T tag and there we've successfully added translation support to our app with Tolgee.
Conclusion
We've successfully built a weather app using Vite, TypeScript and TailwindCSS and integrated with the OpenWeather API to provide real-time weather data. With Tolgee, we added multilingual support, making the app accessible to users in different regions. This project demonstrates how modern tools can help us quickly build and scale applications while ensuring internationalization.
Got any questions? Ask in the comments
Happy Hacking!
Posted on October 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 22, 2024