Weather app in React, Redux, Typescript, and Tailwind
Kenan SejmenoviΔ
Posted on August 2, 2020
Hello reader ππ,
In this article, you will learn how to make basic weather app in React, Redux, and Typescript.
The React part is written in Typescript.
The Redux part is written in plain Javascript for the sake of simplicity.
This article is meant for beginners in React. I have about a year's experience in Vue and Vuex before I started learning React and Redux. It's best suited for those readers.
Let me show you the app, then we will mix reverse engineering and going from step one to the final app.
Hope you liked it! If you don't have time to read, here is the source code. π
Let's start
Requirements: node.js and npm.
Step 1
Install packages
Let's first execute commands, then I will explain what each command does.
Open your terminal and execute commands:
npx create-react-app weather --template typescript
cd weather
npm install react-icons react-redux react-router-dom redux redux-thunk tailwindcss postcss-cli autoprefixer @fullhuman/postcss-purgecss @types/react-redux @types/react-router-dom
Take a look at why React does not put dependencies in devDependendencies.
The first command builds React template in Typescript. We named our app "weather".
The second command moves us into the application directory.
The third command installs packages:
β react-icons
- for fancy icons
β react-redux
- for connecting Redux with React
β react-router-dom
- for enabling many routes and SPA navigation (SPA - Single Page Application)
β redux
- for state management
β redux-thunk
- for enabling asynchronous behavior in redux
β tailwindcss
- CSS framework for easier styling
β postcss-cli
- for enabling minifying app for production (CSS file gzipped from ~140kb to ~3kb... WORTH IT π§)
β autoprefixer
- for parsing CSS and adding vendor prefixes to CSS rules
β @fullhuman/postcss-purgecss
- PostCSS plugin for PurgeCSS
β @types/react-redux
- type definitions for react-redux (for Typescript)
β @types/react-router-dom
- type definitions for React Router (for Typescript)
Let's start application:
npm start
Step 2
Remove auto-generated code
Let's remove minimal code that interferes with our goals, for now.
Go into ./src/App.tsx and remove code inside return statement to look like:
return <></>;
At the top you can see:
import logo from "./logo.svg";
import "./App.css";
Remove both imports and delete ./src/App.css.
If you see a white screen on your browser, you are good to go.
For now, it's good. Delete other useless code if you want, but to keep this post shorter, I will cut it here.
Step 3
Building structure
We need to make five new directories inside ./src.
Inside ./src make:
actions
assets
components
pages
reducers
Explanation:
- actions - for storing redux actions and action types
- assets - for static content, like images
- components - it's always a good thing to strive for the Single Responsibility Principle. In a bigger project, you will be able to use the same component multiple times and save time for everyone
- pages - a place of clean code and separate concerns where you connect routes to components
- reducers - place where dispatched redux actions changes the state of the application
Step 4
Enable Tailwind
Let's add Tailwind to the application.
Open ./src/index.tsx
and add:
import "./tailwind.output.css";
Also, add ./tailwind.config.js, so we learn how to add custom properties to Tailwind.
./tailwind.config.js
module.exports = {
theme: {
extend: {
width: {
"410px": "410px",
},
},
},
};
Before npm start
and npm run build
we want to build Tailwind also.
To solve this problem, in "scripts" tag in package.json add:
"build:tailwind": "tailwind build src/tailwind.css -o src/tailwind.output.css",
"prestart": "npm run build:tailwind",
"prebuild": "npm run build:tailwind"
Adding "pre" before the start and build, will run the desired command before every npm start
and npm run build
.
As you can see, there is src/tailwind.css, which is not created yet. So, let's do it.
./src/tailwind.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Stop watching changes in code by npm by hitting Ctrl + C on Windows in the terminal.
Again, run npm start
to compile code. You should now see something like in console:
npm run build:tailwind
And tailwind.output.css should appear in ./src.
Step 5
Prepare Redux
In ./src/reducers make:
./src/reducers/ajaxReducer.js:
const initialState = {
weather: {},
};
export default function (state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
We will fetch data from OpenWeatherMap, so we need a place to store data.
Data will be store in the weather, in the state.
For now, let's write the boilerplate code.
./src/reducers/index.js:
import { combineReducers } from "redux";
import ajaxReducer from "./ajaxReducer";
export default combineReducers({
weatherReducer: ajaxReducer,
});
At index.js combine all the reducers. We have only one - ajaxReducer in this project, but it won't be always the case.
At a large project, having index.js - a central place of Redux reducers is a good thing, "clean code".
Time for action.. actions!
Let's make types.js where we store all types of Redux actions. It's like ./src/reducers/index.js for actions.
In this simple project, we will only have one action.
./src/actions/types.js
export const FETCH_WEATHER = "FETCH_WEATHER";
And, let's make one and only ajax request/redux action. Before that, you need to go to the OpenWeatherMap and make a token.
A token is a requirement for using OpenWeatherMap, that is generous enough to give us a very high number of API calls for free.
./src/actions/ajaxActions.js
import { FETCH_WEATHER } from "./types";
export const fetchWeather = () => async (dispatch) => {
const ids = {
Munich: 2867714,
London: 2643743,
California: 4350049,
};
const fetches = await Promise.all(
Object.values(ids).map((e) =>
fetch(
`https://api.openweathermap.org/data/2.5/forecast?id=${e}&appid=` // here you put your token key
).then((e) => e.json())
)
);
dispatch({
type: FETCH_WEATHER,
payload: {
// iterating through object does not guarantee order, so I chose manually
Munich: fetches[0],
London: fetches[1],
California: fetches[2],
},
});
};
I chose those cities because I like them. You can pick the cities which you like. Here you can find IDs.
Explanation of ./src/actions/ajaxActions.js:
- Import type, so we can connect type with defined action
- Make an object of city names and IDs
- Store fetched and parsed JSON into constant fetches. Use Promise.all() for fetching data of cities concurrently. URL needs city ID and also Promise.all() expects argument of an array type. Do it by making an array from the object of cities and their ID with Object.values(). Iterate through it with a high-order function map, which returns the array. Fetch does not parse JSON, and it's asynchronous, so first wait for fetching data. Then "unpack" (parse) it by another asynchronous method: JSON. You could use await keyword again, but I prefer then, it seems like beautiful syntax.
- In the argument, you can see that we grabbed dispatch, so we can later dispatch an action to the store. If it isn't understandable, read about Javascript closures.
- In the end, we call dispatch and pass an object with two keys: type and payload. In type, we link type from ./src/actions/types.js, and in payload, we store data returned from API. There are a lot of ways to not duplicate yourself in this code, but I chose this way for simplicity's sake.
We left ajaxReducer.js unfinished. It's time to complete it.
./src/reducers/ajaxReducer.js
import { FETCH_WEATHER } from "../actions/types";
const initialState = {
weather: {},
};
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_WEATHER:
return {
...state,
weather: action.payload,
};
default:
return state;
}
}
As you can see, Redux does not allow us to change just one bit of a state from reducers. First, destructure the current state. Immediately after, overwrite weather key with action payload from ./src/actions/ajaxActions.js.
Step 6
Connect app to redux
Let's first make the main file of Redux. If you worked before with Vuex, you will recognize a pattern here. Vuex and Redux are very similar.
Both have the same purpose, but Vuex is a little easier to understand. Let's name the main Redux file.
./src/store.js
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
applyMiddleware(...middleware)
);
export default store;
Make it super clean. The code is self-explaining. Clean boilerplate for bigger projects.
In ./src/App.tsx it's time to make some changes.
./src/App.tsx
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from "./pages/Home";
function App() {
return (
<Provider store={store}>
<Router>
<Switch>
<Route path="/" component={Home} />
</Switch>
</Router>
</Provider>
);
}
export default App;
To make React application work with Redux, we need to wrap the app in , which receives ./src/store.js. It's possible to have multiple stores. Saw it before, but not a big fan //yet π€£.
You saw a couple of errors in your terminal if you saved your code, I'm sure. It's time to make a first page - Home.
Step 7
Naming assets
For background of cards on home page, I use gifs, so here are names (put whatever gifs you like):
./src/assets/clear.gif
./src/assets/clouds.gif
./src/assets/drizzle.gif
./src/assets/fog.gif
./src/assets/rain.gif
./src/assets/snow.gif
./src/assets/thunderstorm.gif
For Home page eight pictures are used. Four for phones, four for desktops.
For phones:
β ./src/assets/p_bg1.jpg
β ...
β ./src/assets/p_bg4.jpg
For desktops:
β ./src/assets/d_bg1.jpg
β ...
β ./src/assets/d_bg4.jpg
Step 8
Home and its components
./src/pages/Home.tsx
import React, { Component } from "react";
import Card from "../components/home/Card";
import { connect } from "react-redux";
import { fetchWeather } from "../actions/ajaxActions";
interface FormProps {
fetchWeather: Function;
weather: Record<string, any>;
}
interface FormState {
random: number;
imageSource: string;
}
class Home extends Component<FormProps, FormState> {
constructor(props: any) {
super(props);
const randomInt = (min: number, max: number) =>
Math.floor(Math.random() * (max - min)) + min; // generate random integer
this.state = {
random: randomInt(1, 5), // randomly select background, whose names ends with 1 | 2 | 3 | 4
imageSource: "",
};
}
// select randomly/change background on click
setBg = (type: "default" | "click"): void => {
if (type === "default") {
this.setState({
imageSource: require(`../assets/${
window.innerWidth < 768 ? "p" : "d"
}_bg${this.state.random}.jpg`),
});
} else if (type === "click") {
// increase random num, then call recursive callback
if (this.state.random === 4) {
return this.setState(
{
random: 1,
},
() => this.setBg("default")
);
}
return this.setState(
{
random: this.state.random + 1,
},
() => this.setBg("default")
);
}
};
componentDidMount() {
this.props.fetchWeather();
this.setBg("default");
window.addEventListener("resize", () => this.setBg("default"));
}
render() {
return (
<div
className="h-screen w-screen bg-cover bg-center"
style={{
backgroundImage: `url(${this.state.imageSource})`,
}}
onClick={() => this.setBg("click")}
>
<div
className="flex flex-col justify-center items-center w-screen"
style={{ height: "95%" }}
>
{Object.keys(this.props.weather).map((e, i) => {
return <Card city={e} key={i} weather={this.props.weather[e]} />;
})}
</div>
</div>
);
}
}
const mstp = (state: { weatherReducer: { weather: {} } }) => ({
weather: state.weatherReducer.weather,
});
export default connect(mstp, { fetchWeather })(Home);
Take advantage of Typescript, by predefining types of component props and state.
Define the component as a class component. The same thing can be done with React Hooks.
The thing to remember at expression setBg is that setState won't immediately set state, so take advantage of its second argument. It receives callback which will execute immediately after the state is updated. And then its time for the recursive call, to change background photo.
- The single argument of an arrow function you could write without parentheses. For clarity purposes, let's keep 'em
./src/components/home/Card.tsx
Name your components with a capital letter!
import LeftComponent from "./LeftComponent";
import { Link } from "react-router-dom";
import React from "react";
import { RiMapPinLine } from "react-icons/ri";
import RightComponent from "./RightComponent";
import Tomorrow from "./Tomorrow";
import { determineGif } from "../Utils";
interface FormProps {
city: string;
weather: any;
}
function Card(props: FormProps) {
// find min. and max. temperatures from all timestamps from today
const findMinAndMaxTemps = (list: any[]): [number, number] => {
const d = new Date();
const today = d.getDate() < 10 ? `0${d.getDate()}` : d.getDate();
let min: number[] = [],
max: number[] = [];
list.forEach((e) => {
if (`${e.dt_txt[8]}${e.dt_txt[9]}` === today.toString()) {
min.push(e.main.temp_min);
max.push(e.main.temp_max);
}
});
return [
Math.round(Math.min(...min) - 273.15),
Math.round(Math.max(...max) - 273.15),
];
};
let temperature = 0,
minTemperature = 0,
maxTemperature = 0,
stateOfWeather = "",
feelsLike = 0,
speed = 0,
deg = 0,
idOfWeather = 0,
day = true,
list = [];
if (props.weather?.list) {
temperature = Math.round(props.weather.list[0].main.temp - 273.15);
[minTemperature, maxTemperature] = findMinAndMaxTemps(props.weather.list);
stateOfWeather = props.weather.list[0].weather[0].main;
feelsLike = Math.round(props.weather.list[0].main.temp - 273.15);
speed = props.weather.list[0].wind.speed;
deg = props.weather.list[0].wind.deg;
idOfWeather = props.weather.list[0].weather[0].id;
day = props.weather.list[0].sys.pod === "d";
list = props.weather.list;
}
const [classes, url] = determineGif(idOfWeather);
return (
<Link to={`/${props.city}`} className="h-40 w-full sm:w-410px">
<div className="flex h-40 w-full sm:w-410px">
<div
className={`text-white m-2 rounded-lg flex-grow bg-left-bottom ${classes}`}
style={{
backgroundImage: `url(${url})`,
}}
>
<div className="flex w-full h-full divide-x divide-gray-400 ">
<div className="w-9/12">
<div
className="mt-2 ml-2 p-2 rounded-lg inline-block text-xs"
style={{
boxShadow: "0 0 15px 1px rgba(0, 0, 0, 0.75)",
backdropFilter: "blur(2px)",
}}
>
<div className="flex items-center">
<RiMapPinLine />
<div className="ml-2">{props.city}</div>
</div>
</div>
<div className="w-full flex justify-around items-center">
<LeftComponent
stateOfWeather={stateOfWeather}
idOfWeather={idOfWeather}
day={day}
/>
<div className="flex flex-col text-center">
<div className="text-5xl">{temperature}Β°</div>
<div className="text-lg">
{minTemperature}/{maxTemperature}Β°
</div>
</div>
<RightComponent speed={speed} deg={deg} feelsLike={feelsLike} />
</div>
</div>
<Tomorrow idOfWeather={idOfWeather} day={day} list={list} />
</div>
</div>
</div>
</Link>
);
}
export default Card;
If you are curious about determineGif, continue reading, we are almost there!
Take a look at an API response structure, so you can understand variable pairing.
The API response is in Kelvin, so to get Celsius you need to subtract 273.15.
You could do the same thing by passing units=metric at request URL, but its great to meet Javascript floating point number precision.
Remove Math.round() and time will tell you about it π€£.
As you can see, we get into Tailwind. Tailwind is nice, I would say 'micro' CSS framework, that almost doesn't let you write raw CSS. I don't like it like I do Vuetify, but if you need to manage style at a low and small level, it's great! The thing that I most like about it, it's great documentation.
This component could be separated into smaller parts. But to be time-friendly, I kept it relatively "big".
There are 3 more components, so let's explore π§.
./src/components/home/LeftComponent.tsx
import React from "react";
import { determineIcon } from "../Utils";
interface FormProps {
stateOfWeather: string;
idOfWeather: number;
day: boolean;
}
function LeftComponent(props: FormProps) {
return (
<div className="flex flex-col text-center">
{determineIcon(props.idOfWeather, props.day, "h-16 w-16")}
<div>{props.stateOfWeather}</div>
</div>
);
}
export default LeftComponent;
./src/components/home/RightComponent.tsx
import React from "react";
interface FormProps {
feelsLike: number;
deg: number;
speed: number;
}
function RightComponent(props: FormProps) {
const determineLevel = (temp: number): string[] => {
if (temp < 10 || temp > 29) {
return ["Bad", "bg-red-600"];
}
if ((temp > 9 && temp < 18) || (temp > 22 && temp < 30)) {
return ["ok", "bg-yellow-600"];
}
if (temp > 17 && temp < 23) {
return ["Good", "bg-green-600"];
}
return [];
};
const determineSide = (deg: number): string | undefined => {
if (deg < 30) return "N";
if (deg < 60) return "NE";
if (deg < 120) return "E";
if (deg < 150) return "ES";
if (deg < 210) return "S";
if (deg < 240) return "SW";
if (deg < 300) return "W";
if (deg < 330) return "NW";
if (deg < 360) return "N";
};
const feelsLikeProperties = determineLevel(props.feelsLike);
return (
<div className="self-end text-center">
<div
className={`${feelsLikeProperties[1]} rounded-lg text-xs sm:text-sm p-1`}
>
{props.feelsLike} {feelsLikeProperties[0]}
</div>
<div className="mt-1 text-xs md:text-sm">
{determineSide(props.deg)} {Math.round(props.speed * 3.6)} km/h
</div>
</div>
);
}
export default RightComponent;
determineLevel return could be better, but let's keep it simple.
Wind response is in m/s, so to convert it to km/h multiply by 3.6.
determineSide is there for determining if its north, east...
I have a challenge for you - after you make this application, try to make a feature to toggle wind speed between m/s, km/h, and km/s.
./src/components/home/Tomorrow.tsx
import React from "react";
import { RiArrowRightSLine } from "react-icons/ri";
import { determineIcon } from "../Utils";
interface FormProps {
idOfWeather: number;
day: boolean;
list: [];
}
function Tomorrow(props: FormProps) {
const determineNextDayAbb = (): string => {
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
let date = new Date();
let index: number;
if (date.getDay() === 6) {
index = 0;
} else {
index = date.getDay() + 1;
}
return weekdays[index];
};
const crawlNextDayTemps = (list: any[]): [number, number] | void => {
const d = new Date();
d.setDate(d.getDate() + 1); // tomorrow
const tomorrow = d.getDate() < 10 ? `0${d.getDate()}` : d.getDate();
let min: number[] = [],
max: number[] = [];
list.forEach((e) => {
if (`${e["dt_txt"][8]}${e["dt_txt"][9]}` === tomorrow.toString()) {
min.push(e.main.temp_min);
max.push(e.main.temp_max);
}
});
return [
Math.round(Math.min(...min) - 273.15),
Math.round(Math.max(...max) - 273.15),
];
};
const nextDayTemps = crawlNextDayTemps(props.list);
return (
<div className="w-3/12">
<div className="flex justify-between p-2">
<div className="text-xs">{determineNextDayAbb()}</div>
<div className="text-xs flex items-center">
<div>More</div>
<RiArrowRightSLine />
</div>
</div>
<div className="flex flex-col text-center">
<div className="w-full">
{determineIcon(props.idOfWeather, props.day, "h-16 w-16 mx-auto")}
</div>
<div className="text-lg">
{Array.isArray(nextDayTemps) ? nextDayTemps[0] : "?"}/
{Array.isArray(nextDayTemps) ? nextDayTemps[1] : "?"}Β°
</div>
</div>
</div>
);
}
export default Tomorrow;
Expression names are self-explaining. The classical example of a functional component.
Step 9
City and its components
It's a pretty long article. A lot longer than I expected to π
.
Let's first add the city route to React.
./src/App.tsx
Before
<Route path="/" component={Home} />
add:
<Route path="/:city" component={City} />
Add the "City" route before the "Home" route, or take advantage of exact
prop.
At the top of ./src/App.tsx add:
import City from "./pages/City";
./src/pages/City.tsx
import React, { Component } from "react";
import Desktop from "../components/city/Desktop";
import { connect } from "react-redux";
import { fetchWeather } from "../actions/ajaxActions";
// match.params.city is URL (react-router) variable
interface FormProps {
fetchWeather: Function;
match: {
params: {
city: string;
};
};
weather: Record<string, any>;
}
interface FormState {
imageSrc: string;
random: number;
}
class City extends Component<FormProps, FormState> {
constructor(props: any) {
super(props);
if (
this.props.match.params.city !== "Munich" &&
this.props.match.params.city !== "London" &&
this.props.match.params.city !== "California"
) {
window.location.replace("/404");
return;
}
if (!Object.keys(this.props.weather).length) {
// fetch from api, if city is accessed directly
this.props.fetchWeather();
}
const randomInt = (min: number, max: number) =>
Math.floor(Math.random() * (max - min)) + min;
this.state = {
imageSrc: "",
random: randomInt(1, 3), // choose random photo from 2 available photos
};
}
updateDimensions = () => {
// change background photo for phone/desktop
this.setState({
imageSrc: require(`../assets/${
window.innerWidth < 768 ? "p" : "d"
}_${this.props.match.params.city.toLowerCase()}${this.state.random}.jpg`),
});
};
componentDidMount() {
this.updateDimensions();
window.addEventListener("resize", this.updateDimensions);
}
render() {
return (
<div
className="h-screen w-screen bg-cover bg-center"
style={{
backgroundImage: `url(${this.state.imageSrc})`,
}}
>
<Desktop
city={this.props.match.params.city}
info={this.props.weather[this.props.match.params.city]}
/>
</div>
);
}
}
const mstp = (state: { weatherReducer: { weather: {} } }) => ({
weather: state.weatherReducer.weather,
});
export default connect(mstp, { fetchWeather })(City);
As you can see, if the URL is not these 3 cities, we redirect the user to the 404 pages. Challenge here for you is to make a good-looking 404 page.
The same pattern for changing background photo is used here.
In case the user enters URL directly, the application fetches data from API if there's no data in the state.
Here is the elephant of the code π
./src/components/city/Desktop.tsx
import React, { useState } from "react";
import { WiHumidity, WiStrongWind } from "react-icons/wi";
import { GiCrossedAirFlows } from "react-icons/gi";
import { MdVisibility } from "react-icons/md";
import { determineIcon } from "../Utils";
interface FormProps {
city: string;
info: any;
}
function Desktop(props: FormProps) {
const [day, setDay] = useState(0);
const [hour, setHour] = useState(0);
const blurredChip = {
boxShadow: "0 3px 5px rgba(0, 0, 0, 0.3)",
backdropFilter: "blur(2px)",
};
const determineNext5Days = (): string[] => {
const days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
let next5Days = [];
for (let i = 0; i < 4; i++) {
const d = new Date();
d.setDate(d.getDate() + i);
next5Days.push(days[d.getDay()]);
}
return next5Days;
};
interface Simplified {
time: string;
temp: number;
feelsLike: number;
weatherID: number;
weatherState: string;
day: boolean;
humidity: number;
pressure: number;
windSpeed: number;
visibility: number;
}
// pluck relevant info of todays timestamps
const determineTimestamps = (day: number, list: any[]): any[] => {
const d = new Date();
d.setDate(d.getDate() + day);
const timestamps: Simplified[] = [];
for (const e of list) {
if (parseInt(`${e["dt_txt"][8]}${e["dt_txt"][9]}`) === d.getDate()) {
timestamps.push({
time: e.dt_txt.slice(11, 16),
temp: Math.round(e.main.temp - 273.15),
feelsLike: Math.round(e.main.feels_like - 273.15),
weatherID: e.weather[0].id,
weatherState: e.weather[0].main,
day: e.sys.pod === "d",
humidity: e.main.humidity,
pressure: e.main.pressure,
windSpeed: Math.round(e.wind.speed * 3.6),
visibility: Math.round(e.visibility / 100),
});
}
}
return timestamps;
};
// rather return the last timestamps than earlier ones (e.g. 21:00 > 03:00)
const checkTerrain = (squares: number, tss: Simplified[]) => {
let cut: any[] = [];
const numberOfNeededRemoval = tss.length - squares;
if (numberOfNeededRemoval < 0) return tss;
for (let i = numberOfNeededRemoval; i < tss.length; i++) {
cut.push(tss[i]);
}
return cut;
};
const adaptToWidth = (tss: Simplified[]) => {
// show minimum four squares of timestamps to max 8
if (tss.length < 5) return tss;
if (window.innerWidth < 950) {
return checkTerrain(4, tss);
} else if (window.innerWidth < 1150) {
return checkTerrain(5, tss);
} else if (window.innerWidth < 1250) {
return checkTerrain(6, tss);
} else if (window.innerWidth < 1350) {
return checkTerrain(7, tss);
}
return checkTerrain(8, tss);
};
// until info from api is fetched
const timestamps = props.info?.list
? adaptToWidth(determineTimestamps(day, props.info?.list))
: [];
if (!timestamps.length) {
return <></>;
}
// after fetch
return (
<>
<div className="w-screen flex justify-between" style={{ height: "65%" }}>
<div className="text-white pt-8 pl-8">
<div className="text-6xl">
{determineIcon(timestamps[hour].weatherID, timestamps[hour].day)}
</div>
<div className="text-4xl my-1 sm:my-0">
{timestamps[hour].weatherState}
</div>
<div className="text-xl my-1 sm:my-0">{props.city}</div>
<div className="text-5xl font-bold">{timestamps[hour].temp}Β°C</div>
</div>
<div className="mt-20 mr-4 md:mr-20">
<div className="flex">
<div className="text-gray-200 pr-1">
<WiHumidity className="text-3xl" />
</div>
<div>
<div className="text-gray-200 text-sm sm:base">Humidity</div>
<div className="text-white text-2xl sm:text-3xl font-bold">
{timestamps[hour].humidity}%
</div>
</div>
</div>
<div className="flex my-4">
<div className="text-gray-200 pr-1">
<GiCrossedAirFlows className="text-2xl" />
</div>
<div>
<div className="text-gray-200 text-sm sm:base">Air Pressure</div>
<div className="text-white text-2xl sm:text-3xl font-bold">
{timestamps[hour].pressure} hPa
</div>
</div>
</div>
<div className="flex my-4">
<div className="text-gray-200 pr-1">
<WiStrongWind className="text-2xl" />
</div>
<div>
<div className="text-gray-200 text-sm sm:base">Wind speed</div>
<div className="text-white text-2xl sm:text-3xl font-bold">
{timestamps[hour].windSpeed} km/h
</div>
</div>
</div>
<div className="flex my-4">
<div className="text-gray-200 pr-1">
<MdVisibility className="text-2xl" />
</div>
<div>
<div className="text-gray-200 text-sm sm:base">Visibility</div>
<div className="text-white text-2xl sm:text-3xl font-bold">
{timestamps[hour].visibility}%
</div>
</div>
</div>
</div>
</div>
<div className="w-screen text-white" style={{ height: "35%" }}>
<div className="flex items-center pl-2 sm:pl-8">
{determineNext5Days().map((e, i) => {
return (
<div
className="px-2 py-1 mx-2 lg:mb-2 rounded-lg cursor-pointer"
style={day === i ? blurredChip : {}}
onClick={() => {
setHour(0);
setDay(i);
}}
key={i}
>
{e}
</div>
);
})}
</div>
<div className="flex justify-around px-8 pt-6 sm:pt-5">
{timestamps.map((e: any, index: number) => {
return (
<div
key={index}
className="h-40 w-40 flex flex-col cursor-pointer"
style={{
boxShadow: "0 0 15px 1px rgba(0, 0, 0, 0.75)",
backdropFilter: "blur(2px)",
transform: hour === index ? "scale(1.1)" : "",
zIndex: hour === index ? 2 : 1,
}}
onClick={() => setHour(index)}
>
<div className="pt-2 pl-2">{e.time}</div>
<div className="flex-grow"></div>
<div className="pl-1 sm:pl-2 pb-1 sm:pb-2">
<div className="text-2xl font-bold">{e.temp}Β°C</div>
{hour === index ? (
<div className="text-xs sm:text-base">
Feels like {e.feelsLike}Β°
</div>
) : null}
</div>
</div>
);
})}
</div>
</div>
</>
);
}
export default Desktop;
Challenge for you can be to separate this huge chunk of code into smaller components.
Welcome to React Hook. The hooks are amazing. I was wondering why the dev community makes all this drama about hooks. I didn't know anything about React back then. But after learning, I realised that it's a nice developer experience.
Here is the power of Javascript - callbacks.
Challenge for you could be to show the time of these cities. They are not in the same timezone, so its gonna be interesting.
Life without high-order functions would be painful.
Step 10
Utils.tsx
There is a lot of functionality that needs to be shared between components. Don't clutter code with duplications.
The functionality that we will adapt according to API is changing icons and gifs.
It's hardcoded. If the project was real-life, it will be through RegEx and loops. But for this purpose, the switch will do the job.
To not clutter already long post, here is the code of Utils.tsx. Path: ./src/components/Utils.tsx
Step 11
Prepare for production
./postcss.config.js
const purgecss = require("@fullhuman/postcss-purgecss")({
content: [
"./src/**/*.html",
"./src/**/*.ts",
"./src/**/*.tsx",
"./public/index.html",
],
defaultExtractor: (content) => {
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [];
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [];
return broadMatches.concat(innerMatches);
},
});
const cssnano = require("cssnano");
module.exports = {
plugins: [
require("tailwindcss"),
require("autoprefixer"),
cssnano({
preset: "default",
}),
purgecss,
],
};
./package.json
"build:tailwind": "tailwind build src/tailwind.css -o src/tailwind.output.css"
change to
"build:tailwind": "postcss src/tailwind.css -o src/tailwind.output.css"
Run npm run build
and you will get rid of the unused Tailwind classes and end up with ~3kb CSS file.
There's an option for passing ENV argument into npm build
and minimizing CSS only for production, but let's keep it simple here.
You may serve production build with the static server. You should receive a manual in the terminal after npm run build
.
Voila!
Backstory
Why I built this application?
- To get a taste of React, Redux, Typescript, and Tailwind. I've learned those in 3 days.
Why Redux in the ultra-small application?
- To find out why the whole Internet complains about Redux... but it's not that scary!
Why bother posting it?
- Someone is gonna find it useful. More content - better.
Can't wait to learn more about these web technologies. π§
The end
I hope you learned something from my first post. I thought that post would be much shorter. Even in a simple application is hard to cover all parts.
Thank you for reading. I hope I helped you. A well thought out critique is welcome.
Posted on August 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.