Stop Copying Tutorials — Build a Real React App With Me
By Austin Davis
Summary
Topics Covered
- Build a Real Modern React App From Scratch
- Create React Context Outside Components to Prevent Rerenders
- Store API Keys in Environment Variables for Security
Full Transcript
All over YouTube, there are thousands and thousands of React tutorials, but very rarely do you see a genuine modern React app built completely from scratch.
So, in this video, we're going to start from absolutely nothing, meaning a completely empty terminal to a productionready React app. We'll use
technologies like React Query, Tailwind, skeleton suspense loaders, API fetching, interactive maps, light mode and dark mode, responsive design, so it looks great on this device, this device, and
this device, and basically everything you need in order to build a modern React app that looks and works great.
I'm going to have very minimal editing of this video. It'll pretty much be a raw cut of me building out the entire project. It'll be split up into
project. It'll be split up into segments. So, if there's certain parts
segments. So, if there's certain parts you don't care about or certain parts where you want to see exactly what I do, please feel free to skip around. You're
not going to offend me. In the
description, I'll link the GitHub to the project so you can clone the repo and check the whole thing out for yourself.
Without wasting any more time, let's get right into it. Here's a quick snippet of the project that we're going to be building. It's a real-time weather
building. It's a real-time weather dashboard where you can access the weather data at any latitude and longitude coordinates across the entire planet. We'll be using the open weather
planet. We'll be using the open weather API to get all this data and combine everything we know about React to make this a buttery smooth and highly functional React app. Let's get it. To
create a new React app, go into your terminal, making sure you're in whichever folder you want this project to be in, and run npm create beat at latest.
V is a build tool that takes all of your React files and builds them into the actual app that you see on a web page.
This create V at latest command will create a V project that gives us a great starting point. Once we've run this
starting point. Once we've run this command, it'll ask us a couple of things. First, okay, to proceed. We'll
things. First, okay, to proceed. We'll
say y for yes. It'll do that. Ask for a project name. We'll just call this
project name. We'll just call this weather- project. Something super
weather- project. Something super simple. It'll ask for a framework. We'll
simple. It'll ask for a framework. We'll
say, of course, React. And for variance, we're going to choose TypeScript. Uh
roll down V for for now, we'll say no.
We don't really need that. And then with npm, we'll go ahead and say yes. And
there we go. It just created our V app and it actually just launched it. If we
control-click the link that it gave us, it'll take us to this page here. And
this is our web app. At least it's the starting point. To actually get into
starting point. To actually get into your code, go to open folder and open the folder that you just created. So
whatever project name you gave it, go ahead and open that folder. And this
will actually open up your code. Let's
zoom this out so we can see a little better. And also just collapse this
better. And also just collapse this agent window on the right. This
extremely simple process of running mpmreate v at latest is my preferred way of getting your React app up and running super quickly. So, it's always what I
super quickly. So, it's always what I recommend to anyone watching. V is
pretty much the industry standard now as it's a fast and super efficient build tool. So, setting up this V project with
tool. So, setting up this V project with this boilerplate code is a perfect way to start a React app. All projects that you create with Vit are going to look something like this. I'll zoom in so you can see a little better. We look in our file explorer over here to the left.
You'll see we have a couple of files and folders. Most of these config files down
folders. Most of these config files down here you don't have to worry about too much right now. They're just settings for the overall project. What we really care about overall is the source folder, which is where we have all of our React
code. React apps built with V will
code. React apps built with V will always have main.tsx as the entry point to the React code. So it all starts here in this file. We render out this app component, which if we go into it, is
the code that we actually see on this page over here on the right. So main.tsx
is the entry point for our React app, but app.tsx is the primary component
but app.tsx is the primary component that we're going to be working in. beat
has hot module reloading, meaning if I change anything in the JSX here. I add a bunch of characters and I save the file, it'll update in real time on the page.
Got a couple other random things on here like some anchors, images, divs, whatever. But as promised, we're going
whatever. But as promised, we're going to start from a completely clean slate.
So, let's actually take all of this, completely wipe it out. Crl S, and we'll start completely from scratch with nothing but a blank page over here on the right, which also means I don't really care about this count and set
count state. So, I'll get rid of that as
count state. So, I'll get rid of that as well. If we look, we have this app.css
well. If we look, we have this app.css
file, which if we go ahead and open it, you can see we have some boilerplate CSS, but honestly, I really don't care about it. So, let's go in here, app.css,
about it. So, let's go in here, app.css,
and let's just completely delete it. So,
we'll also remove this import so there's no weird import errors. Go ahead and save it. And we've completely
save it. And we've completely obliterated the app CSS file. Lastly, we
have this index.css, which again just has some more boiler plate. There's a
bunch of random anchor, body, button, and some animation stuff here at the bottom in this CSS file that again, we don't really care about. So, let's
actually delete everything from here up to here. I don't want to completely get
to here. I don't want to completely get rid of root just yet cuz we might use some of this, but let's get rid of all that and go ahead and save it. And now,
we've also wiped out that CSS file.
We'll still use some of this basic stuff like this background color for now and some of this font stuff because this index CSS file is imported directly into main.tsx, tsx, meaning anywhere we have
main.tsx, tsx, meaning anywhere we have any React components in our code, we'll use the CSS file. So, this is fine for now. Let's go ahead and leave it alone.
now. Let's go ahead and leave it alone.
We'll close it and let's go back to app.tsx, which again is the component
app.tsx, which again is the component that we actually care about. Right now,
our app on the right is running out this app component, which obviously is nothing cuz this app component is nothing. And we have really no CSS
nothing. And we have really no CSS besides a few things that I just saved from index CSS. So, we're starting completely from a blank slate. We don't
have a template, no weird stylesheet stuff going on, not a bunch of stuff we're copying, just a completely blank page. Now is where the fun begins. Let's
page. Now is where the fun begins. Let's
start by talking about some of the infrastructure for this app. There are
two technologies or packages aside from React itself that are going to make up the core of this project. One for
styling and the other one for data fetching. For styling, we're going to
fetching. For styling, we're going to use Tailwind and for data fetching, we're going to use Tanstack query, which is the same thing as React Query. So
before we get too far into our coding, I want to get the infrastructure for these two things set up right now so that we don't have to worry about it later. So
let's start with Tailwind. If we
navigate to Tailwind's website and go to the installation tab, the default installation is 4v. So we can easily just follow this guide. We've already
created our project using the mpm create vatus command. So we can go ahead and
vatus command. So we can go ahead and skip this first step. Next, it tells us to install tail and css and at tail and css/vit. We'll go ahead and take this
css/vit. We'll go ahead and take this command, copy it, paste it over into our terminal, and run that. Next, it tells us to add this tailwind CSS to our V
plugins. So, if we go to our files, we
plugins. So, if we go to our files, we go over to our v.config.ts.
We go ahead and look in here. We already
have React as a plugin. So, let's just add T and CSS as a plugin, just like this. And go ahead and save this file.
this. And go ahead and save this file.
Next, it'll tell us to import Tailwind CSS into our index CSS file. So, we'll
take this import here, go back to index.css
index.css right here, and go ahead and just add this import at the very top of the file, and go ahead and save this file. And
then all we have to do is restart our build server just in case. So, we'll
terminate this. Make sure to run npm rundev again, which again is the run command. That restarts our build server.
command. That restarts our build server.
And now we are actually free to use Tailwind in our app. So, we can go ahead and close these. And that is all we need to install Tailwind. It's a super simple setup process to test this. and make
sure it works. If we go into app here, let's just make this a div. I'm going to give it a class name of size 32 and then bg red 500. This is the whole point of Tailwind. You can write styles in line
Tailwind. You can write styles in line like this instead of having to have dedicated CSS files. If I go ahead and save it, you can see we have this red square up here on our screen. So,
Tailwind is working perfectly. I can
change these in Tailwind, start messing with it, and everything I do will be Tailwind valid because we set it up really easily. Cool. So, we know
really easily. Cool. So, we know Tailwind works exactly like it should.
If you're not super familiar with Tailwind, just know it's a way of writing super nice and convenient styles inside the class name of your JSX elements. Instead of just spamming CSS
elements. Instead of just spamming CSS files and random classes everywhere and getting messy, we'll be using it throughout this entire video. Now that
we have Tailwind added for the data fetching part, we're going to add Tanstack query, which some people still call React Query, into our project. I
have two whole videos on Tanstack Query if you don't know how it works that I'd suggest checking out. But otherwise,
I'll assume you have at least a foundational understanding of it as we work through this video. This library is also very, very easy to add. Just like
Tailwind, if we go to the installation docs for tanstackquery.com, we can look at the npmi command to install it. So, let's go ahead and copy
install it. So, let's go ahead and copy this, go into our terminal, and install tanstack query into our project. With
that done, there's only one more thing that we need to do. If we go back to main.tsx, tsx which again is the entry
main.tsx, tsx which again is the entry point for our app. We need to set up our query client in this file because tanstack needs that in order to have context to the queries that we're going to be making. So in main.tsx, we'll
first want to make a new query client.
To do that, we'll just say const query client equals new query client just like this.
That is from anstack. And we're
intentionally doing this outside of any sort of React component because we only want it to be created once. We don't
want it to have any sort of rerendering or nothing involved in the React life cycle. And then what we'll do is
cycle. And then what we'll do is highlight our app here. Control shiftp
type wrap. And then we're going to wrap this right here with a query client provider just like this. We'll import
this also from tanstack like this. And
this component takes in a single prop which is just called client where we can pass in our query client. This query
client provider right here works just like any other provider you would use with context in React. It is just what tanstack query requires to have a functional query client. Now that's
done, we can close out of this file and we've just got two huge foundational steps out of the way. We have our styling component installed and our data fetching component installed. So we're
ready to go and use both. Speaking of
data fetching, how are we actually going to do that? I mean, I know how to fetch with Tanstack query, but what data are we actually fetching? We want this weather dashboard to show realtime
up-to-date factual data on our page, not just some mumbo jumbo hard-coded garbage. This is where open weather
garbage. This is where open weather comes in. This whole entire project is
comes in. This whole entire project is not possible without them. Open weather
is a giant API that allows us to get all sorts of accurate real-time and historical weather information.
Openweathermap.org is where we can find all the information about them, including their API docs. If we open the menu over here and go to the API page, I can start scrolling and see all sorts of different types of information that we
can get that's from weather. So, current
weather, hourly, daily. You can see there's bulk downloads for stuff, solar radiance, uh, history, weather maps, air pollution, and all sorts of other APIs that we can use from this site. Some of
these APIs are locked behind pay walls, but the ones that we're going to be using are completely free. The primary
one that we're going to be looking at is this one call API 3.0, which is their main one. Basically, what it will do is
main one. Basically, what it will do is it will give us the current weather at any location, the hourly forecast, daily forecast, minutely forecast, and a bunch of other stuff. We're not going to worry about minutely in this video, but we
will be using the current, daily, and hourly weather information to populate stuff on our page. If we click into the API docs right here, I can show you exactly how it works. If we scroll down,
it'll tell us exactly how to make an API call with this link right here. I can
zoom in so it's a bit easier to see, but it's just this URL that it gives us. All
we need to do is call this, pass in latitude and longitude coordinates, and give them our API key. But basically,
how we'll be able to click on different areas of the globe and get the weather information for anywhere in the entire planet Earth is through these latitude and longitude arguments that you can pass into this URL. And we'll have some
other customization options like exclude or whatnot that we'll also take a look at. But for right now, let's focus on
at. But for right now, let's focus on this API key right here. If you try to hit this whole URL right here, this API with no API key, we're going to get an error 400 telling us that we're forbidden from accessing it. If you've
worked with APIs in the past and you're familiar with them, you know this is pretty much how all APIs work. If you
want to use the service, you have to get a key through the service first that you can pass through to each API call. To
get an API key for Open Weather, all you have to do is click on this API key tab right here. If you have not made an
right here. If you have not made an account with Open Weather or you've never used them before, go ahead and create an account right now because you can't get to this page without an account. Before you create an API key,
account. Before you create an API key, it'll most likely ask for payment information as well. But don't worry, you don't have to pay a single dime.
Everything that I'm doing in this video is completely on the free tier up to 2,000 calls per day. So, as long as you're not spamming 2,000 plus calls every single day, which you definitely shouldn't be, it's completely free. They
just want the payment information in case you go over. Once you've gotten through all the logistics, like creating your account and you get to this API keys tab, you should be able to generate an API key. My API key is right here.
So, what I'll do is go ahead and copy this entire key because we'll be needing it in just a second. What I will usually do for storing API keys or other sensitive information is go to my file
explorer and at the root level create a new file that I usually call env.local
just like this where I'm going to be storing my private environment variables. You don't want API keys
variables. You don't want API keys leaking out to the public because then people could spam with your key and end up running up your credit card. So, it's
usually something that you want to hide.
And before anybody tries to get funny and run at my credit card with this key, I will be destroying it before I post the video. So don't think about it. If
the video. So don't think about it. If
we take a quick look at our git ignore file, you can see that any file that has this.local on it is going to be hidden
this.local on it is going to be hidden from git. So this env.local is not going
from git. So this env.local is not going to be pushed up. So we don't have to worry about it being public. Luckily for
us, vit has a very simple way of doing environment variables. We just have to
environment variables. We just have to start the name with vit and then v will recognize it. So for this we'll just say
recognize it. So for this we'll just say v_api_key equals quotes and then we'll paste in the API key that we copied from open weather and then go ahead and save this
file. Now anywhere we need to access our
file. Now anywhere we need to access our API key we can use vit's environment variable system to use this value in a safe and protected way. Okay, now that we have that let's take a second to
think about what we should do next.
We've created the basis of our React app installing Tailwind and Tanstack query in the process. So, we've got a blank app page that we can start working on.
We also got an API key from Open Weather, which permits us to call their weather API to get the information that we'll be populating our page with. In my
opinion, the next logical step here would be to set up some functions that we can easily call in our React components that will fetch us the API data from this link right here from Open Weather. So, let's go ahead and do that.
Weather. So, let's go ahead and do that.
We'll start with this API call here.
We'll write a function that will take in latitude and longitude as arguments. And
then what it will return will be the actual weather data. If we want to see what the return value is going to look like, we can scroll down to here where it gives us the example of an API response. And we know that this function
response. And we know that this function that we call is going to return something of this format. So again,
it'll have stuff like the latitude, longitude, time zone, current weather data, minutely, hourly, and a bunch of other stuff. So, just to reiterate, this
other stuff. So, just to reiterate, this function will take in latitude and longitude as arguments, and it will spit back out the weather data for whatever we pass into it. Let's go ahead and do that. Now, normally I would want to
that. Now, normally I would want to split up API functions into different files and folders, but since we'll only use a handful of different Open Weather API calls, I'm just going to make a single file inside of source here that I
will just call API.ts.
And in here is where we'll make our functions that interact with Open Weather. The first one will be the one
Weather. The first one will be the one that we just talked about. So let's just say it's going to be a function. We'll
say export async function and I will call it get weather. Let's look back over at our page over here and take this exact link right here. So we'll go ahead and copy this. Then we'll go back over
to our code. Let's say const results and we're going to say equals await fetch quotes and we'll pass in this link. And
actually instead of using quotes, we're going to use template quotes. So we can inject some stuff in there just like this. And this will be our basic setup.
this. And this will be our basic setup.
If you've interacted with any API or backend before, you know that fetch is the standard way in JavaScript of interacting with HTTP to get responses like that. If you want to, you can also
like that. If you want to, you can also use something like Axios, but for this video, we're going to keep things simple and just use fetch. So, we're going to fetch and we're going to give it the open weather API link right here. Like
most fetch requests, we'll want to parse this into JSON format. So, let's go up here and let's just say const data equals await res.json to parse it into
JSON format. Again, our API response, if
JSON format. Again, our API response, if we look over here, is going to be an object with a bunch of stuff. And if you look closely, this actually is in JSON format, which makes our life a lot
easier. Last thing we'll do is return
easier. Last thing we'll do is return data. And this function is essentially
data. And this function is essentially done for the most part. Very, very
simple. All we're doing is fetching the open weather link, parsing that as JSON, and then returning it. What this means is if all goes well, I took this exact link right here and pasted in here. So,
if I ran this in my app, I should get this exact response object. Obviously,
the numbers would be a bit different because it's probably a different point of time that I'm doing this, but you get the picture. There's a few things we
the picture. There's a few things we need to make our get weather function actually work properly. The first
obvious problem is that this is just a hard-coded link with hard-coded latitude and longitude. In our case, we want to
and longitude. In our case, we want to be able to pass in latitude and longitude, not just hardcode the value.
So, let's give this function some arguments to take these in. You can do this in two separate arguments if you want, but I'm just going to put them inside of a single object. So, I only need to pass in one thing. I'll just say
an object expecting latitude and longitude. And to make TypeScript happy,
longitude. And to make TypeScript happy, we'll say its type is going to be latitude, which is a number, and longitude, which is also a number. All
that we're saying here is forget weather. We're expecting a single
weather. We're expecting a single argument, an object, where we are destructuring latitude and longitude. if
we want to use it in our API call because we use the template quotes here we can inject JavaScript directly into the string. So right here where it says
the string. So right here where it says latitude let's take this replace it with dollar sign brackets and we can put in latitude like this and for longitude we
will do the exact same thing. Now the
latitude and longitude that this API calls will be directly decided by whatever latitude and longitude we pass into this function. For the last thing we need to do this exact same thing for our API key. And this is where the
environment variable that we just made comes into play. At the very top of this file, but outside of this function, let's make a variable for our environment variable. All I will do is
environment variable. All I will do is say const API key equals to properly use vit environment variable system. All we have to do is
variable system. All we have to do is import meta.env
import meta.env and then whatever the name of our environment variable is. We named ours v_i_key
just like this. So now this variable has our API key. If we go back real quick to our env.local, you can see this is v_i
our env.local, you can see this is v_i key. So we just want to make sure this
key. So we just want to make sure this matches one to one right here. So now
anywhere where we want to use our API key, we can just use this const right here. What that means is in our fetch
here. What that means is in our fetch over here, instead of doing this random app ID thing, all we have to do is get rid of this, make this also dollar sign
curly brackets, and pass in API key just like this. Now we're not only passing
like this. Now we're not only passing through latitude and longitude from our function arguments, but we're also passing in our API key that we're storing in our environment variables.
And now this function is good to go. Now
that we have the essentials, this function get weather will actually work now and return to us weather data from open weather, assuming we pass in valid latitude and longitude coordinates.
However, there's one more quick thing I want to change. First of all, I prefer to have my weather units in Fahrenheit.
So after longitude here, I'm actually going to make another argument. I'm
going to say ampers units equals imperial just like this. If we scroll up in the API, it mentions this. If you
look here in units, you can pass in standard, metric, or imperial. So, I'm
going to use imperial. But for your own app, feel free to use whatever you like.
There's also this exclude right here. I
mentioned that we're not going to be using the minutely data, so we might as well exclude that. So, in addition to doing the units imperial, I'm going to add another one. I'll say amperand
exclude equals minutely just like this.
And we're also not going to use alerts.
So let's do comma separated and go ahead and exclude that as well. So now we've customize this function to do what we want and have all the essential things like passing in latitude and longitude and returning the data that open weather
gives us back. And now that that's done, we should actually be good to go for this function. We can take in latitude
this function. We can take in latitude and longitude. We fetch the open weather
and longitude. We fetch the open weather using those coordinates by passing those in to the fetch call while also passing in our API key. We parse that data as JSON and then we return it so that we
can use it in our React components.
Let's make sure this file is saved and now close it and head back over to our app and take this function for a test drive. In our app component, we're going
drive. In our app component, we're going to make a use query for this API call to see if it's actually working. So up here at the top, how we normally do use queries with tanstack query is we'll say const we'll extract data out equals use
query and then we need to pass in a query key and a query function. We'll
make sure this is imported from tanstack. So for our query key, we don't
tanstack. So for our query key, we don't really care for right now. I'm just
going to say weather. And then for our query function, this is just going to be the function that we just wrote. So
we'll say parenthesis like this call get weather. And like we just did with get
weather. And like we just did with get weather, we'll import it. Remember that
it is expecting a single argument that's an object that contains a lat property and a lawn property. So right here we'll just say lat I don't know 50 and also
lawn 50 just like this. We're just hard coding them for right now to make sure it works as expected. And then in our JSX to make this super simple. Let's get
rid of this div right here and let's just go in here and let's just json.stringify
json.stringify data. That way we can at least see it on
data. That way we can at least see it on the page. Let's go ahead and save this
the page. Let's go ahead and save this now and head over to our page. And we
can see boom, we have this huge wall of text that now has a bunch of weather data. I'm not going to individually
data. I'm not going to individually parse through everything in this massive text wall. But if you look at the very
text wall. But if you look at the very top right here, you can see the response open weather gives us at least says lat 50 and lawn 50. So we know it's the right coordinates, which gives us a time
zone in an area of somewhere in Asia. If
we go back into our code here and instead of doing 5050, let's do 1025 for example. Go ahead and save it. Refresh
example. Go ahead and save it. Refresh
this page and you can see now we have lat 10 and lawn 25 which is somewhere in Africa. This stringified data is
Africa. This stringified data is obviously super super hard to read because this is not really, you know, the intended way of printing all this out. But if we go over here, rightclick,
out. But if we go over here, rightclick, inspect, and at least open up our network tab. Oops. At least open up the
network tab. Oops. At least open up the network tab here. Go ahead and refresh this and look at this. You can see we get it all in a nice JSON format. So
it's a bit easier to read if you want to see what's going on. If I just zoom this in a little bit, you can see we have again the lat lawn, the time zone, we have the current weather data. So stuff
like the sunrise time, sunset time, we have the temperature, we have the due point, visibility, all this other stuff that pertains to the current weather information. Then we have hourly, which
information. Then we have hourly, which is an array of all the hourly forecast data. So this would be the forecast for
data. So this would be the forecast for the first hour, forecast for the second hour. You get the picture. And then if
hour. You get the picture. And then if we actually let's say collapse this here, you can see next we have our daily data which is pretty much the same thing. It's an array where each object
thing. It's an array where each object represents one day. This is day one of our forecast. Here is the second day.
our forecast. Here is the second day.
Same thing. And I think you kind of get the picture on what all this data is.
This is a hugely important part of our app that we just got out of the way. Our
React app is now interfacing with an external API, grabbing the appropriate weather data depending on what we pass into the get weather function. This is
super cool. It's a huge part of a React app that we just dealt with. Before we
go any further though, however, we should fix something. If we go over to data here and we hover it, it's going to say the type is of type any. And this,
for one, is completely expected.
Whenever you make an API call and then you parse it as JSON and then just return it because you're parsing just pretty much a raw response string as JSON and returning it. TypeScript really
has zero way of inferring what the actual data type looks like. Working
with data that has type any is generally a big no no with modern TypeScript apps primarily because it makes it so easy to try and access things that don't actually exist and it just leads to a lot of potential errors. If you've used
quite a bit of TypeScript, you know that having smart types where you can get IntelliSense and you can know what properties exist and what you can and can't do is super super helpful as a developer and lacking it makes
developing 10 times harder. So, let's
fix this type any and make sure data is the actual response type that matches what we're seeing over here in the network tab. The way that I prefer to do
network tab. The way that I prefer to do this is by using ZOD. If you don't know about ZOD, it's basically a type validation library that will ensure all of your API calls are typed properly, so you don't have to work with type any
everywhere in your code. It'll also
throw errors if your API response doesn't match the type that you give it to ensure 100% type safety. This isn't
hard to set up at all. If we go to the Zod docs, it tells us to install it. All
we have to do is run npm install zod.
So, if we go into our terminal, let's just run that here and install Zod into our project. And then what I will
our project. And then what I will typically do is in our source folder, I will create another folder that I will call schemas. And in here, I can put all
call schemas. And in here, I can put all of my ZOD schemas. If you want to know how Zod works, let me show you real quick. In this file, I'll make a new
quick. In this file, I'll make a new schema. I will just say weather
schema. I will just say weather schema.ts just like this. Basically, to make a ZOD schema, all you have to do if you want to make an object is say something like
Z.Object. If you want to make an array,
Z.Object. If you want to make an array, Zarray string Z dotstring. You get the picture. Basically, what we'll have to
picture. Basically, what we'll have to do with ZOD is go to our API and find what the response is going to look like.
So, for open weather, it's going to look something like this. And then we'll have to make a schema that matches all the types. So, it'll be an object that has,
types. So, it'll be an object that has, for example, latitude, which is a number. So, Z dot number in Zod.
number. So, Z dot number in Zod.
Longitude is also a number. America/
Chicago is a string. So, Z.String. And
you get the picture. Basically, we're
just building out a ZOD schema to match the exact shape of the open weather response. I'll be honest, I'm super
response. I'll be honest, I'm super lazy. So, instead of going hand by hand
lazy. So, instead of going hand by hand and going every single one of these properties and then making a schema thing for it, what would be easier is just to use something like chatbt to generate a schema real quick. What I'm
going to do is actually copy this entire example response here. So, we'll go ahead and just copy this entire sample response object. Go to chatbt,
response object. Go to chatbt, paste that in here. I'm going to say please make me a zod schema that matches
this exact shape minus I don't want minutely and alerts. So I'll do this and it should spit out a schema that pretty much matches the exact shape. So we're
going to take this actually exactly at face value. Let's see we got all this.
face value. Let's see we got all this.
Yeah. So we'll take this copy it and in weather schema let's just paste that entire thing in. So, it's 100 lines, a lot of stuff, but again, doing this by hand is super tedious, and I don't recommend doing it. I'd rather just use
something like JPT to do it for me. So,
if we go ahead and save this now, we have a weather schema that is hopefully valid. If it's not valid, this will
valid. If it's not valid, this will throw errors. So, a good test would be
throw errors. So, a good test would be to just plug it in and see if it works.
Again, like I said, just quickly going over Zod. For each field, we'll just
over Zod. For each field, we'll just want to map to the Z type or the Zod type. So, like Z.tring for time zone, Z.
type. So, like Z.tring for time zone, Z.
for the offset. DT is the time. So Z dot number and you get the picture. Almost
all of these are just numbers. So having
Z dot number is super super simple. So
this actually isn't a crazy ZOD schema even though it looks a little bit complicated. So how to actually use a
complicated. So how to actually use a ZOD schema is once we have our schema defined, we can go back to our API function which is get weather and instead of just returning data like we
are here, what we can actually do is we can return the weather schema. So we'll
say weather schema. Make sure we import that from the schema we just made.
Parsse and then we pass in our data just like this. Now, if we go ahead and save
like this. Now, if we go ahead and save this, we're going to parse through our schema. And we're assuming that data is
schema. And we're assuming that data is going to match this schema shape. So, if
we go over to app now and we hover over data now, data should be the exact correct type. If we go ahead and head
correct type. If we go ahead and head over to our page here and just refresh this, it looks like we're not getting any breaking errors. So, I think it's safe to assume for right now that this schema is right. Hopefully chat GBT
didn't just sell a short and it gave us a correct schema. If it's wrong for whatever reason, we'll have to correct it going forward. We're not going to get any sort of weird type any. And by doing this, we're likely going to avoid a bunch of future type issues. Anywhere
now in React land that we want to use this data variable, we can use IntelliSense to know what properties we can and can't access. For example, if I want to access the current weather data, I try and do something like data do
whatever. IntelliSense now knows that I
whatever. IntelliSense now knows that I can access current, daily, hourly, or any of these things because data is typed properly. That's something that's
typed properly. That's something that's going to be really helpful going forward. Sweet. Now that that's out of
forward. Sweet. Now that that's out of the way, what do we do next? Now that we have the current, hourly, and daily weather information fetching the way that we want it to, I think the next logical step would be to create and style the components that are going to
be holding this information. We could
handle all the data fetching at once and then save all the styling till the very end, but sometimes doing that creates a monstrous amount of styling. if you save it all to the end that I tend to not like very much. So, I'll break down each
section of the page with functionality and styling before moving on to the next section. Here's what I kind of figured
section. Here's what I kind of figured and envisioned for our end product. You
want to have a dashboard that has multiple cards on it, each of which holds weather information like hourly forecast, daily forecast, current weather, and you get the picture. It
would make sense logically if we made this a single card component, and then the inner content of each card would just be the card's children. That way we have a sharable and reusable component that we can use for all of our stuff on
our dashboard instead of having to rewrite a bunch of code. With that being said, let's go over to our file explorer and in our source folder, let's create a new folder called components where we're going to make all of our primary React
components. And because we'll end up
components. And because we'll end up having different types of components, I'll make yet another folder inside of components that I will just call cards just like this. And inside of cards,
let's make a component called card.tsx.
If you have the React Shortcuts extension in VS Code, you can just type TSR RFC, which stands for TypeScript React Functional Component. Press enter
and it actually makes a skeleton for the component to avoid you having to write some of the boiler plate because we want each card on the dashboard to have the same outer styling, but be able to change the inner contents of it. We'll
give this card a children prop just like this. And instead of rendering our card
this. And instead of rendering our card right here, we will just render out children. And to make TypeScript happy,
children. And to make TypeScript happy, let's make sure we add it to our props.
So we'll say children which is of type react node just in case we need to add more customizable styling later. I want
to make this div that holds the children different from the outermost div of this whole component. So if I highlight this
whole component. So if I highlight this whole div right here, control shiftp and type in wrap. I'm going to wrap it in a second div. And before we get any
second div. And before we get any farther styling this component, let's see what these cards look like if we pass in some data that we already have.
So let's go ahead and save this card right here. Let's go into app.tsx.
right here. Let's go into app.tsx.
Instead of rendering out data just being stringified, let's actually do something else. For now, what we're going to do is
else. For now, what we're going to do is wrap this all in parenthesis to make sure it is valid. And let's actually break this into three separate chunks here. So, we'll render out card just
here. So, we'll render out card just like this.
And then let's copy and paste this twice. So, we have three cards on our
twice. So, we have three cards on our page. And then for each one of these
page. And then for each one of these cards, I'll pass in data that we're already getting from Open Weather. So,
for this first one, what I'll do is I'll just pass in JSON.stringify stringify
and I will pass in the current weather data which if we hover data is just data current. So I'll do data.curren and then
current. So I'll do data.curren and then for these next two cards I want to pass in hourly and daily. So I'll take this copy and paste it. This will be data
hourly because again if we hover data here we can see hourly is this array and daily is the exact same thing. So we'll
do data daily. Typescript will say this can possibly be undefined. So, let's
just add a question mark here to each of these to make TypeScript happy. And if
we go ahead and save our page, now we'll have three cards for each type of data, both the current, hourly, and daily weather. Nothing really looks different
weather. Nothing really looks different right now since we haven't added any styles, obviously, but at least now our data is broken out into cards. So, let's
now go back to the car component we're working on and get some styling for this. So, using Tailwind on this
this. So, using Tailwind on this outermost div, let's give it a class name. And generally the first thing I
name. And generally the first thing I like to add to new components, especially ones that have a background color and some borders, is padding. So
we'll add a class name, we'll have P4 for padding, just to create some separation between the content and the outer edges. And then we'll also give it
outer edges. And then we'll also give it rounded XL to make a little bit rounded.
And we'll say BG zinc 900. Go ahead and save it. And it still looks pretty
save it. And it still looks pretty horrible because you still can't really see any sort of separation, but we're at least getting somewhere. It still looks pretty horrible because there's just this massive text wall. So let's go to
app and instead of stringifying the entire thing like this, let's actually take that string and let's just take the first 100 characters for each one of these. So I'll add this slice 0 to 100
these. So I'll add this slice 0 to 100 for each one of our cards. And now it's a lot easier to see. We can at least see where each card begins and ends. This
makes it a bit better, but let's actually make this fragment right here into a div. And also give this a class name. And let's give each of the cards
name. And let's give each of the cards some separation. So we'll say flex flex
some separation. So we'll say flex flex column layout. and we'll give gap eight to
layout. and we'll give gap eight to create even more separation between the cards. Go ahead and save it. And now
cards. Go ahead and save it. And now
that's even better. If we now go back to our card component, let's add a couple of more things. First and foremost, the majority of the time you have an element that is kind of close to the color of its background, like these cards here,
adding shadow can generally be good because it helps create a layered look.
It makes the cards pop out against the background. So on these cards, we'll
background. So on these cards, we'll also just throw a shadow. We'll try MD for now. Save it. And that creates just
for now. Save it. And that creates just a slight separation against the background. Next up, let's think for a
background. Next up, let's think for a second about how we want these cards to actually be used. We know each of these cards is going to represent one portion of our data. So, I'll have one card for current weather, one for hourly, and one
for daily. How do we know which card is
for daily. How do we know which card is which just by looking at it? Well, the
most clear way to show which card is which would be to give each card a title. That way, the user knows which
title. That way, the user knows which card they are actually looking at. If
all the cards are going to have a title, it would make sense just to put that title in this card component here and have each render of this card pass down a title prop. That way we know what to display. So what I mean by that is
display. So what I mean by that is inside card above the children here, let's make a header. We'll say header 2 for right now. And let's say it's going to display title. Title will come from props. So we'll add title to our props
props. So we'll add title to our props here. And to make TypeScript happy,
here. And to make TypeScript happy, we'll say that title is going to be a string. To make this title actually
string. To make this title actually stand out like a header, let's go on to this. Give it a class name and let's
this. Give it a class name and let's give it text 2XL and then font semibold to give it a sort of titleish look and go ahead and save this card. If we now navigate back to our app, TypeScript is
going to complain because we said title is a required prop, but we're not passing it in here. So for each one of these cards, let's actually pass in the title. For the current weather card,
title. For the current weather card, let's give it a title prop. And we'll
just make these all strings. This will
just say current weather. And then for other two, let's do a similar thing for the hourly card. Instead of saying current weather, this will just say
hourly forecast and we'll specify that it is 48 hours. And then for our daily information, we will just put this as
daily forecast.
Go ahead and save it. And now our cards have a title on them, which we've just done through this title prop. Super
simple. We need a bit more separation between the title and the actual information of the card. So let's go back to our card component here. And on
this class name here, let's give it flex, flex call, and gap 4. That'll make
sure the title always appears above the children with a decent gap between them.
Save it. And it looks a little bit better. Some more separation. We may
better. Some more separation. We may
come back to this shared card component in a bit to change or add some things to it. But for now, let's actually work on
it. But for now, let's actually work on getting the specific card set up. So,
the daily card, hourly card, and the current card. So, we'll go ahead and
current card. So, we'll go ahead and close out of these files, go back to our app, and let's ask ourselves, starting from daily forecast, what do we actually want this card to look like? Well,
before we figure that out, preferably, I wouldn't have to build out the entirely daily card within this app component because then that would get a bit bloated. So, let's actually make a new
bloated. So, let's actually make a new component for this card specifically.
So, inside of this cards folder that we already made, let's make a new file.
We're just going to call it daily forecast.tsx
forecast.tsx like this. We will ts RFC the same
like this. We will ts RFC the same shortcut we used before to make the component like this. And now we have our daily forecast card. Let's go in here, take what we already have for this card,
like this. Let's cut it, paste it into
like this. Let's cut it, paste it into this component. Make sure to import
this component. Make sure to import card. Go ahead and save it. And instead
card. Go ahead and save it. And instead
of rendering out that, we will render out this component. We just made an app daily forecast just like this. So we
just componentize the card. We'll do the same for the other two, but for right now, let's stay focused on daily forecast. However, having this in a
forecast. However, having this in a separate component only introduces one problem in this component. I have zero context to any of the weather information that I'm fetching in the app component up here in the use query. I'm
trying to access this data right here, but I haven't fetched data in this component. So, I have no idea what data
component. So, I have no idea what data even is. To fix this, I'm going to do
even is. To fix this, I'm going to do something that might seem a little bit strange at first. I'm going to take this entire use query that we already have in app and I'm going to copy and paste it verbatim into this component right here.
Making sure to import use query and also to import get weather. And now if we save it, it should at least fix the error. So now we can see our page again.
error. So now we can see our page again.
The reason I took the same exact use query and just copy and pasted it into this component is that as long as these two use queries have the exact same query key, which they both do, they're
both weather. then it should only query
both weather. then it should only query once because these app and daily forecast components will mount at the exact same time. Even though it's two separate use query calls, it's smart enough to still only fetch the data
once. Here in a little bit, we'll be
once. Here in a little bit, we'll be refactoring it and taking this use query out of the app completely. But still,
it's not a problem for right now. Like I
said, we're not going to be double querying because we have the exact same query key in both of them. And actually,
instead of using use query, I'm actually going to change this to a use suspense query instead. Later on in this video,
query instead. Later on in this video, we're going to add skeleton loaders. So,
I want suspense boundaries around each one. You'll see what I mean later by
one. You'll see what I mean later by this, but in order to do that, we'll need to have this be a use suspense query rather than a regular use query.
More on this later. All right. Now,
let's put our creative caps on for a second. From a very rough standpoint,
second. From a very rough standpoint, what do we want this card to look like?
Keep in mind before you roast me, I'm not a graphic designer, so this drawing is not going to be super pretty. What I
was thinking is we would have our card here in this rough little box like this.
And open weather in its doc says that the daily forecast is an 8-day forecast.
So in my mind, this card would be broken out into rows like this. One row for each day. For each row, we'd have a
each day. For each row, we'd have a couple of things from left to right. The
first thing we would have on the very left side for each row is the day of the week like this. So Monday, Tuesday, Wednesday, whatever. Then next to it, we
Wednesday, whatever. Then next to it, we could have an icon like for whatever the weather's going to be for the day, whether that's sunny, cloudy, rainy, whatever. Here, I'll draw a little sun
whatever. Here, I'll draw a little sun in here. is not super amazing, but you
in here. is not super amazing, but you get the idea. And then we would have three more numbers for the rest of the row. We would have the average temp, so
row. We would have the average temp, so for example, like 73 degrees. We would
have the min temp, so something like 61.
And then the max temp, so something like, I don't know, 77. And each row would roughly, for the most part, look like this. So we have the day of the
like this. So we have the day of the week, followed by an icon, followed by the average, then the min, then the max, and this would be eight times because there are eight items in our daily
forecast. Again, show me a bit of mercy.
forecast. Again, show me a bit of mercy.
I'm not a graphic designer, so I know this, you know, doesn't look fantastic, but I think you kind of get the idea.
So, let's take that very rough draft design and translate it into JSX using Tailwind. So, the first thing I'll do is
Tailwind. So, the first thing I'll do is just wipe out our data here entirely. We
don't care about it for right now cuz we're going to format it. Let's make a div inside of here first. And let's give this div a class name of flex, flex call, and gap 4. That will create each
of our individual rows. Now inside of here is where we want to map over each row. So we create a new item in the
row. So we create a new item in the flexbox for each day. But basically for each day we want to render something out. The way we normally do that if we
out. The way we normally do that if we have an array for example when we hover over data we want to look at daily information. You can see daily right
information. You can see daily right here is going to be an array of objects.
So what we can do is we can say data daily map just like this. And then for each day we're going to map something.
For right now, we'll say each day is just mapping over a div.
Just so we don't forget, let's throw a key on this div. And something that will be unique with each day is actually the timestamp, which is just this dt. We can
say that the key is just dayd just like this. Now the question is how do we want to style each row? Because
this div right here is representing each row for each day that we have. Well, we
want to space everything out evenly. So
let's give it a class name of flex justify between. That'll make it in a
justify between. That'll make it in a flex row layout and space everything out evenly. Now, what do we want to have
evenly. Now, what do we want to have inside each row? If we look back at our design, remember that we wanted five things. First, we want the day, then the
things. First, we want the day, then the icon, and then the three numbers, the average, the min, and the max. So, let's
make an element for each one of these five inside this div right here. For the
first one, the day of the week, let's just make it a p tag for right now. And
we're just going to say date. For the
icon, let's make an image tag just like this. And let's give it a source. Now,
this. And let's give it a source. Now,
how we can find the icon for each day is if we go to our weather schema here and we look at our daily information, you can see that we have this weather
array at the very bottom that has an object that has this icon on it. So,
this icon is what we can actually use to display an image. If we head back to the open weather API and we control F on this page just for icon and we go down a little bit, you can see we have this
weather.on icon field, which matches
weather.on icon field, which matches exactly what we're looking at in our schema. Right here, it says how to get
schema. Right here, it says how to get icons. If we just click on it, it takes
icons. If we just click on it, it takes us to this page, and it tells us to get icons, we just need to use basically this URL right here. Here's a quick look at all the different icons we can get.
So, obviously, this would be something like sunny or clear sky, daytime versus night. We have cloudy, we have rainy,
night. We have cloudy, we have rainy, stormy, snowing, you get the idea. So,
let's actually take this exact link that it gives us and go ahead and copy it.
And if we go back to our code here, let's go to the source, make this a string with back to quotes, and go ahead and paste it in. However, if we just keep it hardcoded to be this 10D at 2x,
it's going to be this image right here, this 10d.png. So, this little rainy
this 10d.png. So, this little rainy icon, and that's not ideal. Obviously,
it shouldn't be hard-coded. We want this to be dynamic. So, let's actually replace this entire 10D at2x with dollar sign quote to inject our own expression into here. And let's pass in the actual
into here. And let's pass in the actual icon that we're getting from the response. Like I just showed you a
response. Like I just showed you a second ago. If we look at the daily
second ago. If we look at the daily information and we look at weather, we have this array and then an object inside the array that has icon. So it's
pretty deeply nested in here. Now, one
thing I will point out is that weather, like I said, is an array because technically over the course of the day, there can be multiple weather conditions. We're not going to worry
conditions. We're not going to worry about this for right now. Let's just
assume we'll take whatever the first weather condition is. In other words, index zero in the array of weather. So
what that means is that day that we have right here represents this Z.Object. So
to access the icon, we need to do day dot weather at the zero index dot icon.
So let's do that. So for the source here, we will say day do weather. And
it's nice that we have this type with zod because now we have intellisense.
Say weather. take the zero index and we'll do do icicon just like I said. And
just to be safe, we'll give this an alt here. That will just say weather icon.
here. That will just say weather icon.
Go ahead and go back to our app and now save this. You can see we have date,
save this. You can see we have date, date, date, all this. And we actually have the weather icons already on this card. I want these icons to be just a
card. I want these icons to be just a little bit smaller. So I'm going to give it a class name for the image. And let's
just say size eight. Go ahead and save it. And now they're a bit smaller, which
it. And now they're a bit smaller, which makes our card not so huge. Now, let's
get the average temp, the min temp, and the max temp. If we look back at our schema here and we look at this temp object, you can see we have day, min, and max. So, we're going to use these
and max. So, we're going to use these three numbers right here. So, let's
actually make P tags for all three of these. So, after the image here, let's
these. So, after the image here, let's make a P tag right here. And the way we can access that is we can say day.temp
day for the average for the whole day.
Day.temp.day
just like this. We'll go ahead and copy this. Paste it twice. This will be
this. Paste it twice. This will be day.temp.min
day.temp.min
and this will be day.temp.mmax.
Now if we go ahead and save it for each day, we have the average for the day, the min, and the max in that exact order. I think it might look better if
order. I think it might look better if the min and max fields are slightly less visible and pop out less than the actual average for the day because the average for the day is what's more important. So
for the min and max, let's actually give these a class name. And for each of them, let's just say text gray 500 slash75 to make it slightly transparent.
And we'll give this actually to both of them. Go ahead and save it. And that
them. Go ahead and save it. And that
just grays them out a little bit. With
this pretty simple component that we just made in this daily forecast, our daily weather information is now actually looking pretty nice and presentable with the obvious problem right now being that we're just putting this date string here instead of the
actual day of the week. So let's address that. So let's replace this date here
that. So let's replace this date here with a JavaScript expression. And again
inside of here if we hover day the actual date will be given to us by this DT. Now DT is going to be in Unix time
DT. Now DT is going to be in Unix time which is why it's a number and not a date object. So what we can do to
date object. So what we can do to actually make this a date is inside this expression we can say new date just like this. And inside of here we want to pass
this. And inside of here we want to pass in day.dt dt* 1,00 and that's because
in day.dt dt* 1,00 and that's because this date constructor that we're using in JavaScript expects dates to be in milliseconds not seconds but the dt that
we get from open weather is in seconds once we've done this we can say to locale date string just like this if we go ahead and save this now this will
convert each one to a date string so we can see right now 10:23 10:24 1025 you get the picture each row has its own respective day now however I don't want this to be day of the month. I'd prefer
for it to be the actual day of the week.
So like Monday, Tuesday, Wednesday. So
how can we get that? Well, all we have to do is change the arguments here that we have in this two local date string function. We can pass in undefined for
function. We can pass in undefined for this first argument because we don't want to pass in any local time zone information. But for the second
information. But for the second argument, we'll make it an object. And
one of the first things we'll do is just give it weekday short just like this. for date strings.
This second argument that we're making this object right here has a couple of different customization options. One of
them is weekday, which can either be long or short. Long means something like Monday, Tuesday, Wednesday, and short is just the abbreviation. So, M O N TU and so forth. So, if I save it now, instead
so forth. So, if I save it now, instead of having the day of the month, we just have the day of the week, but abbreviated. The day I'm filming this on
abbreviated. The day I'm filming this on is Wednesday, so it makes sense the first day in the forecast would be Thursday because that's tomorrow. And
again, we have 8 days. So, it's Thursday going all the way to the next Thursday.
Let's now clean this component up just a little bit more. First things first, I don't really want fractional temperatures. So, I don't want 96.24.
temperatures. So, I don't want 96.24.
I'd rather just have it say 96. To do
that, what we can do is for each of these temperatures, we can just wrap each of them in math. Just like this. Go
ahead and copy this for each one.
Wrap them all. And now, if we save it, we have integers for temperatures and no more decimals. I also want to put the
more decimals. I also want to put the units for each one. And I actually just learned that if you're on Windows, something cool that you can do is you can actually do alt and then 0176
on your numpad to actually do the degrees symbol. That's pretty cool. And
degrees symbol. That's pretty cool. And
then I'm using Fahrenheit, so we'll do degrees Fahrenheit. We'll take this and
degrees Fahrenheit. We'll take this and we'll add this to the other temperatures as well. That way for each one, we are
as well. That way for each one, we are saying degrees Fahrenheit. Now, if we look at our rows, you can see that not everything is aligned perfectly. For
example, the start of this 98 degrees Fahrenheit is not left aligned with this 91° Fahrenheit. They're a little bit
91° Fahrenheit. They're a little bit off. You can see that even more so with
off. You can see that even more so with this 94 right here. It's jutting out to the right when compared to 91 and 96.
This most likely stems from the fact that our weekday names on the left side are not all the same length. For
example, Wednesday or WED is longer in width than Tuesday is. So, we create this weird kind of offset look. One
quick workaround to this is that all the entries in the left column here like Thursday, Friday, Saturday are all a fixed width. So to fix that, let's go to
fixed width. So to fix that, let's go to our P tag for our day of the week. Throw
a class name on it and give it width nine. Now if we save it, everything
nine. Now if we save it, everything should now be aligned because we're ensuring that each of these has a fixed width. So the other ones are starting
width. So the other ones are starting all at the same point. Nice. So
everything is aligned great now. And
that's actually going to be it for our daily weather card. Just to do a quick recap so we're all on the same page and understand what's going on in this daily weather card. We're first just
weather card. We're first just inheriting from the from the shared card component that we're going to use for all of our cards. Again, this is pretty basic. Just has a title plus children
basic. Just has a title plus children and some stuff like roundedness and padding. But the daily forecast card
padding. But the daily forecast card specifically just has one div that is flex flex call. So we map out each one of our rows like this in a flex column layout. To get each day, all we have to
layout. To get each day, all we have to do is data. map that actually gives us each day and then for each day we can just map out the information. Flex
justify between is to make each row in a actual row layout making sure that everything is spaced evenly apart. And
then we just have elements for each of our items like the day of the week, the icon, and then our three temperatures, the average, min, and the max. However,
one thing I'm actually seeing real quick that can be a super fast optimization thing is getting rid of this div right here because it's a little bit redundant. I'm a pretty big hater of
redundant. I'm a pretty big hater of having divs that are unnecessary because I think it makes the code a bit harder to decipher and fix later on. When I say this div is redundant, I mean that if we look at our card component here, we
already have this div right here that is wrapping the children. Then when we inherit it like this, we're wrapping another div around children as well. So
it's like we're essentially double wrapping it, which doesn't really cause problems, but also it's not the best way of doing it. The easiest way to fix this and a way that I like to handle this is go into our car component here and let's
give it another prop here. It'll be an optional prop though. We are going to call children class name just like this.
Capitalize the N. And it'll be an optional string. We'll also dstructure
optional string. We'll also dstructure it out of here.
And then on this div, let's give it a class name. And this class name is going
class name. And this class name is going to be, you guessed it, children class name. Now, if we go ahead and save this
name. Now, if we go ahead and save this file, instead of having this redundant div, we can actually take the style exactly, which is just this flex flex call gap 4. Let's paste it up here so we
don't lose it. And we can actually wipe this div out completely. And instead, we can give it another prop, the children class name, where we just paste this in.
Now, if we save it, it'll behave the exact same. We did just add a prop to
exact same. We did just add a prop to the card component, but we completely removed a redundant div, which in my opinion makes it easier to read in the future. Now that we have the daily
future. Now that we have the daily forecast done, let's move on to the hourly forecast. Like the daily
hourly forecast. Like the daily forecast, let's go over to our file explorer and inside cars, we'll make a new file, call it hourly forecast.tsx.
We'll do tsrfc to create the component.
And just like daily, we'll make this be a card that inherits from our shared card component. If we go back to the
card component. If we go back to the drawing board, here's what I was thinking for our hourly card. We'll have
a card that is quite a bit longer than it is tall just because for hourly we're going to have 48 hours. So, there's
going to be a lot of stuff. So, it's
going to have to be able to scroll side to side. I was thinking for each hour we
to side. I was thinking for each hour we could have something like the actual hour number. So, like I don't know,
hour number. So, like I don't know, might not do military time, but let's say we do military time. We'll have the number. We'll have the icon for what's
number. We'll have the icon for what's going to be during the hour. So whether
that's, you know, sunny, cloudy, whatever. And at the bottom, we'll just
whatever. And at the bottom, we'll just put the actual temperature we expect it to be. So for example, 74° Fahrenheit.
to be. So for example, 74° Fahrenheit.
And this will be for every single hour.
So basically, it's the opposite of what we're doing for daily weather. Instead
of having a bunch of rows that kind of go in a vertical format, we're going to have a bunch of different columns that go in a horizontal format. So we'll have hour one right here, hour two right here, three, four, and you get the
picture. And it's going to scroll side
picture. And it's going to scroll side to side. That should be honestly super
to side. That should be honestly super easy to do. So let's go ahead and get started. We will do the exact same thing
started. We will do the exact same thing we did for daily forecast by taking the same query from app. So let's take this exact same suspense query from daily forecast and let's copy and paste it in
here. Making sure to import the suspense
here. Making sure to import the suspense query and importing our API function.
Again, I want to emphasize that because all of these components are mounting at the same time and share the same query key, we're not actually going to make multiple network requests. It's only
going to make one. even though we have multiple use queries. I mentioned just a second ago that we want this hourly card to be in a horizontal format, meaning we want it to be in a flex row layout. So
to do that, we can kind of copy the similar thing we did for our daily forecast with the children class name, but instead of doing flex flex call, we'll just do flex flex row. So if we go
back here, let's give it a class name.
And we will just say flex and let's do gap six. I could add flex row here, but
gap six. I could add flex row here, but flex row is the default. So technically
it's the same as just not having it at all inside this card. Now just like we mapped over the daily data using this data.map,
data.map, let's do the exact same for the hourly.
Again, if we look at data, you can see it's an object and we have this hourly that's an array kind of in the same vein as daily. So what we can do is we can
as daily. So what we can do is we can say in our card here, we're going to do data.ourly hourly dom and then for each
data.ourly hourly dom and then for each hour we're going to map out a div here.
Art is going to complain because we're missing the title prop. So let's
actually go to app, take the title that we already had for the hourly forecast, take this and we will just put it back into here. So now we can make card happy
into here. So now we can make card happy again. Now let's ask ourselves for each
again. Now let's ask ourselves for each hour that we're mapping over, what do we want to display? Well, in the little rough draft design that I just drew out, each hour just have the time, the icon, and then the actual temperature all
stacked on top of each other. So, in a vertical layout. To get this vertical
vertical layout. To get this vertical layout, we can give this div a class name. Let's give it flex flex call. And
name. Let's give it flex flex call. And
we'll give it a gap of two. The gap
isn't going to be huge, just enough great separation. But this should at
great separation. But this should at least set up a reasonable column layout for each individual hour. Just like the daily forecast, we'll first display the date, except instead of being an actual date like Monday, Tuesday, Wednesday, it
will be the time of day. It only makes sense if we're doing hourly forecast to have this be the actual hour in question. So, we'll make this a P tag
question. So, we'll make this a P tag right here. And similar to daily, we
right here. And similar to daily, we will make this new date calling the date constructor. And just like day, the hour
constructor. And just like day, the hour also has a DT. So, we can say hourdt times 1,00 to get the actual date. And
then to get the time, all that we have to do is call to local time string just like this. Before we go any further,
like this. Before we go any further, just so you can see how this looks, let's actually save this file real quick. And in our app, instead of
quick. And in our app, instead of rendering out this card, just like we're rendering out daily forecast, we're also going to render out hourly forecast. And
now, if we look at it, we can see right here on this hourly forecast card, we have a bunch of times. We have 8:00 p.m., 9:00 p.m., 10 p.m., and you get
p.m., 9:00 p.m., 10 p.m., and you get the idea. Back to our hourly forecast
the idea. Back to our hourly forecast component. right underneath this timer
component. right underneath this timer here. We want to have an icon for the
here. We want to have an icon for the weather just like we have for daily. So,
if we go to daily, you can see we have this image right here. And I think because we're going to reuse this, it might be a good idea to actually make this its own component. So, to do that, it'll be super easy. Let's just take
this image tag. I'm going to copy it exactly. Go into our file explorer and
exactly. Go into our file explorer and in the components folder, let's make a new file just called weather icon.tsx
tsrfc again to make this component. And
then in here, let's just return what I just copied, the image element. And now
what we can actually do is instead of hard- coding this dayweather icicon that we're doing, we can just pass in source, which we will just make a prop to this component. So we'll say source and
component. So we'll say source and source is going to be a string. So
essentially now we have a sharable component that we can use between both our hourly forecast and our daily forecast. We can implement it in both.
forecast. We can implement it in both.
If we go to daily forecast, let's get that real quick. We'll just take this source right here, which is the dayweather zero icon. Let's copy this.
Let's actually get rid of this whole thing and just render out weather icon in its place, giving it a source of this right here. Now, if we save it, it
right here. Now, if we save it, it should behave in the exact same way with just being a reusable component. So now
that we have it in the daily forecast, let's copy it and let's paste it verbatim inside of the hourly forecast right here, right underneath our date.
Go ahead and import it. And the only major difference is that instead of being data weather icon, it'll actually be hour because the format is going to be similar. We have this hour object.
be similar. We have this hour object.
Inside of that, we have this weather array. We're going to take the first
array. We're going to take the first index of it and then grab icon. So now
if we save it, boom. Now, each of our hours in our forecast has its own icon.
And that, my friends, is how you speedrun shared components in React.
Anyways, let's get back to the hourly forecast. The last thing we're missing
forecast. The last thing we're missing in here is our actual temperature, which should just go right underneath the weather icon. Let's make another P tag
weather icon. Let's make another P tag right here. And for the temperature, all
right here. And for the temperature, all we have to do for each hour, we can access it with this temp right here. So,
it's super simple. All we have to do is say hour.te. However, just like the
say hour.te. However, just like the daily information, I want to have a unit on it. So, I'll take this degrees
on it. So, I'll take this degrees Fahrenheit right here, add it to the end, and I also want this to be rounded.
So, I'll do math.round just to make sure it's an integer and not a decimal. Now,
if I go ahead and save it, we also have the temperature for each item. The next
obvious problem you might notice is that now our page has a huge scroll bar because shocker, our content is just way overflowing all this. We don't actually have a proper scrolling container set
up. So, all the hours are literally just
up. So, all the hours are literally just overflowing the side. Clearly, this is not something that we want. A super easy fix for that is on this children class name here. All we have to add is
name here. All we have to add is overflow xc scroll just like this. Now
if we save it, this is going to scroll within the container. So now the actual container itself is scrolling. The page
itself will still actually scroll because this top card is still messing us up. However, this component itself,
us up. However, this component itself, the hourly forecast, is no longer causing the entire window to scroll.
It's going to be a super long list because like I said, there's 48 hours of forecast, which means there's 48 of these objects. So, yeah, we got a lot of
these objects. So, yeah, we got a lot of horizontal space here. Let's quickly
improve some of the styling to make it look a little bit sharper. First of all, I don't love that the time right here is overflowing or wrapping down. I wouldn't
want the 8:00 to be on a different line than the PM. Doesn't really look very good. So, what we can do is go to the P
good. So, what we can do is go to the P tag where we're doing our time. Let's
put a class name on here. And all we have to do is give it a white space. No
wrap. Just like this. And now it's not going to wrap anymore. For each hour, I want all the items to be centered, not left aligned. As you can see here,
left aligned. As you can see here, obviously this weather icon is not centered directly under the time. So
that's something we can super quickly fix. If we just go onto this outer div
fix. If we just go onto this outer div right here, flex flex call, and we give item center. That'll make sure
item center. That'll make sure everything is centered within the column. And each of the hours feels a
column. And each of the hours feels a little bit cramped right now. So I'm
actually going to add P2 to create a little bit more spacing for each hour.
And this is already looking quite a bit better. The very last thing is I
better. The very last thing is I honestly don't like this time format right here. I think having the 800 0
right here. I think having the 800 0 just seems super redundant. These are
just extra zeros right here. We don't
need them. So to format our time, we could do something pretty similar to how we handled the dates on daily forecast.
Inside of our two local time string, we'll again make the first argument undefined so that this stays relative to your local time zone. And for the second argument, we'll again pass in an object.
And for this one, all we have to do for the options is give it our numeric just like this. We'll also give it minute
like this. We'll also give it minute two digit and then we'll give hour 12 true. If we save our file now we can see
true. If we save our file now we can see our times are just formatted like this without the two extra zeros. And that
ladies and gentlemen is our hourly forecast card done. There's a chance we could come back to it later to make some improvements or changes. But with what we have right now, this is working great. We can see the forecast for all
great. We can see the forecast for all the next hours. So, we have 8:00 p.m.,
9:00 p.m., 10 p.m., and I can scroll if I'm curious about any hour in the next 48 hours. Super crisp, super smooth.
48 hours. Super crisp, super smooth.
Now, on our page, or other words, in the app component, we're displaying both the hourly forecast and the daily forecast in the exact way that we want it to.
Theoretically, given any latitude and longitude coordinates, I can now see the hourly forecast for that exact area and what the next 8 days are going to look like. So, we've already made tons and
like. So, we've already made tons and tons of progress from where we started.
But, let's keep going. The last big chunk for this dashboard is getting the current weather information. We already
have hourly and data. Let's get the current weather component set up. Just
like the other two, same process we're getting familiar with. If we go to our cards, make a new file. Let's call this one current weather.tsx
tsrfc to generate the component. And
same pattern as before. We're going to take the exact same suspense query and add it to current weather. I know I keep saying this and reiterating it. Don't
mean to beat a dead horse, but as long as they have the same query key, it's not going to be making multiple network requests. They're going to share the
requests. They're going to share the exact same query. Let's go ahead and just save this component now. And let's
render out current weather. And here we finally have individual card components for all three. And just to make sure actually we have the same title. Let's
not forget this real quick. Let's do
title. And for here, make sure before you forget to render out the card, the same share card you've been using this whole time. And we will pass in this
whole time. And we will pass in this title. Oops.
title. Oops.
Pass in this title right here. Just like
this. Now, if we go ahead and save it, go back here.
And we can just make this current weather. And now it's happy. And we have
weather. And now it's happy. And we have it on our page. The main question is now, what do we want this current weather card to actually look like?
Let's once again go back to the drawing board for this current weather card.
Here's kind of what I was thinking. This
card, unlike the hourly forecast, but similar to the daily forecast, is probably going to be a bit taller than it is wide. What we could do for this card is right at the top center of the
card, have the temperature in big bold letters. So, for example, something like
letters. So, for example, something like this, right in the middle, just so it's very obvious and sticks out to the user.
Immediately under that, we could put the current weather condition. So something
like I don't know this can say cloudy and then we'll have the icon that corresponds to that exact condition either to the side of it or directly under it. So for cloudy you know it'll
under it. So for cloudy you know it'll be some cloud like this and an icon and then right under this to fill up more space. We'll probably put what the time
space. We'll probably put what the time it is at the actual location that we're looking at. Again with their app we're
looking at. Again with their app we're going to be able to see the weather for anywhere in the entire world. So it's
probably nice to know what the actual local time is there. So, let's say I don't know, it's 14:52:00.
That can be a time under it. And it
won't be quite as big as the temperature. This should be a little bit
temperature. This should be a little bit smaller. So, this is not quite to scale.
smaller. So, this is not quite to scale.
And then at the bottom, maybe we just have a few stats kind of in a row. So,
we could say like right here, maybe what the temperature feels like. We could
say, you know, maybe humidity is in this box. And then we could say, I don't
box. And then we could say, I don't know, something like wind speed or wind degrees or something like that. And this
could be just a very rough outline. So,
we'll have all this in a flex flex call starting from the top because it's all kind of in a vertical layout. Since we
want all this to be in that vertical layout, let's go into our code here.
Make sure we have our card. It's
annoying sometimes how prettier your formats these. And we'll give it a
formats these. And we'll give it a children class name of flex flex call again just to make it all vertical. I
also want to make sure that all this stuff is centered. So, I'm going to give item center just like this. Now that we have this, let's get the rest out of the way. For the first section, like I just
way. For the first section, like I just mentioned, we'll have the temperature in big bold letters. So, let's replace this current weather right here with an H2, not H1 because I'm going to reserve that for bigger headers later down the road.
And here, we need the current temperature. So, how do we get that?
temperature. So, how do we get that?
Well, if we look at data, we can see in the current object here, we have this temp number. That is the current
temp number. That is the current temperature. So, to get it, all we have
temperature. So, to get it, all we have to do is say data.curren.temp
just like that. Same as all the other ones, I also want to use degrees Fahrenheit. So, we'll take this exact
Fahrenheit. So, we'll take this exact same unit. And just to keep things
same unit. And just to keep things consistent, we'll also do math.round
just like this. So, now if we save it, we'll have the current temperature of wherever we are. Right now, it says 74°.
So, wherever in the world is 10 latitude, 25 longitude. It's apparently
74° F. This temperature right now looks way too tiny and out of place. So, on
this H2, let's give it a class name.
Let's give it some pretty fat text.
We'll say text 6xL because we want it to be quite big. And I'll also give it font semi-bold just to make it pop out a little more. And text center just to
little more. And text center just to make sure it's centered. So now that looks quite a bit better, way easier to see. Next up, let's get the weather
see. Next up, let's get the weather description and the weather icon. I'm
going to want the big bold temperature and the weather icon and description all in the same div just for organization purposes. So let's actually wrap this H2
purposes. So let's actually wrap this H2 here. Wrap it in a div. And for this
here. Wrap it in a div. And for this div, just to create some separation between them, let's make sure this is all vertical. So we'll say flex flex
all vertical. So we'll say flex flex call. And we'll also give it gap 2 for
call. And we'll also give it gap 2 for just a little bit of separation. So if
we go ahead and save it, it now has its own div that we can also put the icon and the description inside of. We
already made a share component for the weather icon, which again is called weather icon. So let's take this, copy
weather icon. So let's take this, copy it, and just paste it right here, making sure to import it. And then for the actual icon, if we go into data, look at the current information. It's pretty
much the exact same thing as the other two. We have this weather object or
two. We have this weather object or sorry, weather array of objects. We'll
just take the first index, the icon. So
instead of doing dayweather, all we have to do is data.curren.weather
just like this. And now we're good to go. If I go ahead and save it, boom. Now
go. If I go ahead and save it, boom. Now
we have a weather icon. I want this weather icon to be centered. So I'm
going to actually add item center on this div right here. And that'll make sure it's centered directly below the temperature. However, right now, this
temperature. However, right now, this icon honestly looks really tiny compared to the actual temperature with how big the text is. Let's make it a bit bigger.
One problem we're going to run into with that though is if we try to modify the size of the weather icon directly, we're going to modify it every single place that it's used. And I don't want to change the size of how it is in the
hourly and daily forecast. So, what I can easily do to make this more customizable is add a class name prop that can be optional. that will just be a string and we'll also pull class name
out of the props here meaning dstructure them that we can use to pass in our own custom styles. The way I typically
custom styles. The way I typically handle custom class names like this is with a tiny package called clsx which is a super tiny utility function that just resolves conditional class names like
this instead of having to do some ugly string interpolation because let's be honest string interpolation looks super scuffed. To get CLSX, all you have to do
scuffed. To get CLSX, all you have to do is go into your terminal. Make sure you have a new one open and just run npmi clsx just like this and it'll get it added to your project. Again, it's a
super super tiny utility. It's not a big package we're installing. Once you've
done that, literally all you have to do is just make your class name an expression like this. And you're going to wrap it in clsx as if you're calling it like a function. You just want to
make sure you import it from clsx. Now
what we can do in addition to having size 8 which will be our default style is any custom class that we pass in we can just put on top of it and this CLSX function will actually resolve this. So
if we pass in a class name it'll just get appended to the same class name and if we try to override the size it will override it. Meaning it's size 8 by
override it. Meaning it's size 8 by default but if I want to pass in something bigger or smaller I can. So
now if I go ahead and save this file and I go back to current weather I can give this weather icon our own custom class name. And instead of defaulting to size
name. And instead of defaulting to size eight, let's actually say, I don't know, size 14. Go ahead and save it. And now
size 14. Go ahead and save it. And now
our icon is quite a bit bigger without impacting the size of the icon on hourly forecast and daily forecast. So that's a quick sneak peek into how I handle conditional styles for when I need to do
my own custom class names where it's maybe the same in a lot of spots, but has slight variations in other spots.
Now, if we go back to our current weather component, right underneath the icon, I want to put the actual weather condition that we're in, like cloudy, sunny, rainy, you get the picture. To do
that, all we need to do is add, we'll do an H3 for this one cuz it should be slightly smaller than the uh temperature. And to get the description
temperature. And to get the description for the current weather, it's actually pretty similar to how we get the icon.
We hover over data, look at current, and go to weather. You can see we have this description right here. What we can do, we can actually let's just take this exact thing right here and we'll say
data.curren.weather at
data.curren.weather at
index0ero.escription
just like this. Now, if we save it, we have the weather description. This one
in particular says scattered clouds.
Let's make this look a little bit nicer by adding a class name here. Let's add
capitaliz so it capitalizes the first letter of each word. Then we'll also make it text XL. So, it's still relatively big, but not nearly as big as the actual temperature. Save it. And I
think that looks quite a bit better.
Underneath the temperature, the icon, and the actual weather description, let's get the time figured out. When I
say I want to put a time on this card, I think it would be good to put the actual local time of wherever we are. Because
to me, that would make sense in the context of the current weather information. So, if I want to look at
information. So, if I want to look at somewhere in Germany, the time it gives me for this card would be German time zone, not an American time zone like my own. I like to have it so that we have
own. I like to have it so that we have in big letters saying local time and then right underneath it have the actual time itself. So to do that we can
time itself. So to do that we can actually go under this main div we have right now and let's actually make another div just like this and we will give it a class name of flex flex call
and gap 2 to create this sort of layout.
We'll make the first thing in here a p tag and this is just going to say local time right underneath it. Let's make
another header. So it's going to be similar size to our description. So
we'll say H3 and this is going to have the local time. There's a couple of different ways that we could do this.
However, the way I'm going to do it using the time zone that is actually returned from data. So if we hover over data, you can see we have this time zone string right here. This is actually very useful if we want to get the local time.
A way we can do that is by inside of here making a JavaScript expression and we can say new int l just like this dot date time format. For the first argument, we're going to pass in a
string that is en- us since this is my preferred format. And then for the
preferred format. And then for the second argument here, just like all the other dates that we've worked with, we'll make an object that we can customize. In here, we'll say hour is
customize. In here, we'll say hour is going to be twodigit. We'll also say that minute is the same thing. We also
want it to be twodigit. And just like hourly, we'll make our 12 true. And then
to get the actual local time zone of wherever we are and not my time zone, what we can do is give it a final option that is just time zone just like this.
And that will just be data time zone because again we hover data, we have the time zone right here. So we pass that in as another option to our date and this will actually create our date for us
completely using the local time of whatever the time zone is that comes back from our call. You'll notice this whole thing right now is going to be highlighted red because a date itself can't be a React node. We need it to be a string. So to actually format it as a
a string. So to actually format it as a string, what we can do is after this big chunk right here, we can just say format and inside here can say new date and you
want to pass in the actual time of the date. So we'll say for the time it is
date. So we'll say for the time it is stored in data.curren.dt
that is the current time. So we'll do that data.curren.dt dt* 1,000 like
that data.curren.dt dt* 1,000 like always. And now if we go ahead and save
always. And now if we go ahead and save it, you can see the local time of the coordinates 10:25 or whatever we have in our query right here is 4:26 a.m. It
looks a little bad right now like this, like a bunch of just kind of jumbled text. So, let's style it up. We go down
text. So, let's style it up. We go down here for our local time for this P tag.
Let's go ahead and give it a class name.
We'll say text XL. And then for the actual time itself in this H3, we'll also give it a class name. and we will say text 4XL and also give it font semibold just so it sticks out quite a
bit. Now if we go ahead and save it, the
bit. Now if we go ahead and save it, the text is quite a bit more readable.
However, now this time just got way bigger. This local time is offc center.
bigger. This local time is offc center.
It looks kind of weird. So all we have to do is just add text center to this.
Just like this. And now it's back to looking good. The only last thing now to
looking good. The only last thing now to do on this current weather card would be to add the three quick sections at the very bottom. getting the feels like
very bottom. getting the feels like temperature, the humidity, and the wind speed. These will all share the same
speed. These will all share the same style. So, this will be super simple.
style. So, this will be super simple.
We'll go down here below this div and make yet another div. We will give it a class name of flex justify between so that each of these three little sections spaces evenly from each other. And then
for each little section, we'll have another div. For each of these, we'll
another div. For each of these, we'll give it a class name of flex flex call gap 2. All we're going to do for each
gap 2. All we're going to do for each one of these divs is just give it a p tag that will say, for example, for the feels like temperature, we'll just say feels like. And then for what it
feels like. And then for what it actually feels like, meaning the temperature, all we have to do is data.
Oops, crap is an expression here.
Data.curren dot. You can see we have feels like right here. So, we can just use that. We want to keep the same
use that. We want to keep the same consistency with the units. So, we'll do degrees Fahrenheit. And we will also
degrees Fahrenheit. And we will also math.round around this whole thing. Now,
math.round around this whole thing. Now,
if we do this for our first thing, we'll see feels like 72°. Clearly, it doesn't look very good yet, especially because there's only one element. So, let's make some styling to fix this in this flex flex call div right here. Let's make
sure we also throw item center to make sure they are vertically aligned with each other. Still does not look very
each other. Still does not look very good because there's only still one item there. So, let's actually take this div
there. So, let's actually take this div chunk here. I'm going to copy it and
chunk here. I'm going to copy it and we're going to paste it twice because each section is going to look very similar to each other. The second one instead of saying feels like is going to just be humidity and instead of doing
the data.curren.slike,
the data.curren.slike,
what we can do instead is just replace this with data.curren
dohumidity. This is not going to be degrees Fahrenheit. This is going to be
degrees Fahrenheit. This is going to be a percentage. So we'll use the percent
a percentage. So we'll use the percent symbol. And then for this one, this also
symbol. And then for this one, this also is not going to be feels like. This is
going to be the wind speed, which we will just call wind. And then we can replace this whole thing with just data
current windspeed just like this. Wind
speed in imperial units is going to be miles per hour. So we will just say m.
Now if we go ahead and save it, we have three different things. The feels like temperature, the humidity, and the wind speed. Obviously still looks pretty
speed. Obviously still looks pretty horrible, but at least we have all of our data here. To fix this weird styling, let's make sure that this flex justify between div right here is width
full to make sure it takes up the entire space of the card. We save this now spaces it out so it takes up the whole card. Next, I want these numbers in the
card. Next, I want these numbers in the bottom section like the feels like number, the humidity percentage, and whatnot to stand out against their labels. So, on each label to make it
labels. So, on each label to make it slightly darker and less in your face, I'm going to throw a class name on it and I'm just going to give it text gray 500. Just like this. And we'll actually
500. Just like this. And we'll actually copy and paste this class name on the other labels as well. Go ahead and save it. And now they don't stand out against
it. And now they don't stand out against the numbers so much. Makes the numbers pop a little bit more. And lastly, it's pretty clear that each of these items, so like this whole item at the bottom next to this time, next to the stuff
right here is not separated out with any space at all. So all is kind of just cluttered. So if we go back up here to
cluttered. So if we go back up here to our card where we have the children class name, let's actually throw a gap on here. Let's just give it, I don't
on here. Let's just give it, I don't know, gap four for right now, just to make everything a bit more spaced out and a little bit nicer. Actually, maybe
let's do gap six. Honestly, now our card is looking pretty good. It doesn't look perfect yet, so we may also revisit this in a bit with the rest of our cards. It
looks a little bit awkward right now because these cards are so wide, but for the actual finished dashboard, these cards aren't going to be taking up the whole width of the page, so it's not going to look so stretched out. It'll
look quite a bit better. So, for now, let's move on. The only last card that I want to make for this dashboard that's going to be consuming information from the API is just a card that displays some of the extra information that we're
getting back that we're not currently using. If we go to the weather schema
using. If we go to the weather schema and we look at it, we obviously have the daily and the current and the hourly.
However, there's a lot of things here that we're not actually going to be using. So, for example, we have like the
using. So, for example, we have like the sunrise and sunset time. We have, I don't know, the clouds. We have the wind degrees. Basically, just a bunch of
degrees. Basically, just a bunch of fields that aren't being used at all.
We've got plenty of real estate to work with on this dashboard. So, let's
actually use these other pieces of information on a final card that we'll just call additional info. So, going
back to our cards folder, we'll make a final component in here that we're just going to call additional info.tsx
tsrfc to make the component. And we'll
go ahead and save this file. Close this.
And just like the other ones, we'll make it inherit from the same card. And for
this title, we're going to give it a title of additional weather info, just like this. This card is just going to
like this. This card is just going to have some additional weather information. So, it's not going to be
information. So, it's not going to be anything crazy. We're not going to go
anything crazy. We're not going to go really weird with the styles. We'll keep
it pretty basic. So, let's actually wrap this back in parenthesis here. Fix this
annoying formatting that prettier always does. And then in here, what do we want
does. And then in here, what do we want to do? Well, in terms of design, I was
to do? Well, in terms of design, I was thinking about just making it something super simple. We'll just have all of our
super simple. We'll just have all of our additional fields laid out into columns.
So, we'll have a card like this, and we'll have some columns that are just going vertical like this. For each
individual column, what we can do is we can just have the label. So, I don't know, like, you know, wind speed here on the left, wind speed, and then on the right would be the actual number. So,
this is not anything crazy, just super simple. for like cloudiness, we could do
simple. for like cloudiness, we could do cloudiness, whatever, and then have some number here. And we would do that for
number here. And we would do that for each of the different pieces of information that we have. Super super
basic layout. So to create this vertical layout here, let's give our card some children class name. We're just going to say flex flex call. Let's make the gap a little bit bigger this time. We'll say
gap 8. Now the question is, how do we actually want to render out the information inside of our component right here? I could write each field
right here? I could write each field individually since they're all going to be a bit different, but then I'd have to spam a bunch of the same JSX and this component would look a little bit messy.
I prefer being able to map over something. That way, we only have to
something. That way, we only have to write the core JSX in one spot. I mean
by that is at the bottom of this file, I'll make a new variable. I'm just going to say const rows and it'll be an array.
And in here for each of these rows, I think we should make an object with two properties. We'll have label and then we
properties. We'll have label and then we will also have value. Label will be what the user actually sees and value will be what it's actually called from the API.
So for example, let me show you what I'm talking about. If we take the same exact
talking about. If we take the same exact suspense query. Let's go ahead and get
suspense query. Let's go ahead and get it into this card as well.
Making sure to import our stuff. And if
we now hover data, we can see that current has something for example like clouds. In this case, the label will say
clouds. In this case, the label will say something like cloudiness and value would be clouds. So it maps to this property. So what I can say for this is
property. So what I can say for this is for label I will say cloudiness and we'll do percentage just so we know the unit and then the actual value itself is
just going to be clouds. We just want to make sure this value matches exactly how it's going to look like in the response.
And we basically just want to do this for all of our fields. But let's pick out a few important fields that we want aside from cloudiness. We look at data here. We can see that in addition to
here. We can see that in addition to clouds we have UVI. So I think we can include UVI. Let's take the wind degrees
include UVI. Let's take the wind degrees because that'll be the wind direction.
We'll also take uh let's take pressure up here. And then we'll also get sunrise
up here. And then we'll also get sunrise and sunset. So that would make six total
and sunset. So that would make six total properties that we're getting, which I think should be plenty. I'm lazy and don't feel like writing these out individually. So I'll ask Chad GBT to do
individually. So I'll ask Chad GBT to do it for me. And here is what it cooked up. So for each of these, we'd have the
up. So for each of these, we'd have the same thing where we just have the label, which is what the user sees, and then what it actually is on the endpoint. So
this should be good. I pretty much just gave it the response structure and asked it to just take those six fields into account. And now we have a label and
account. And now we have a label and value for each thing. Now that we have this rows array, what we can do is we can go back to our car component here.
We can make an expression and we can say rows.m map. And then for each row, we
rows.m map. And then for each row, we want to oops for each row like everything else just map out a div here.
For each row, what do we want it to look like? Well, I mentioned we wanted to
like? Well, I mentioned we wanted to have it be flex just to between. Let's
go ahead and do that. And all it's going to be is something super basic. We'll
have two spans inside of here. Span one
is just going to be the actual label that the user sees. So, row.
And then the second span that we have is going to be the actual value from the endpoint. If we make sure that our
endpoint. If we make sure that our values match up one to one with how we have them declared in our schema and we look at it, I can just access data.curren.clouds
data.curren.clouds
for example to get that particular number. So the way that we can do this
number. So the way that we can do this in our map, we can say data.curren
at value. And it's really that simple.
Actually, it's not that simple. It needs
to be row.value, not just value.
However, we're going to get a TypeScript error here saying that it has any type and it's going to complain with the polong thing that is basically just caused by the fact that this array is not declared as const. So, if we just
say as const, it'll actually just get rid of that error completely. We're just
telling Typescript that this array is never going to have anything pushed or popped from it. It'll always be this.
So, it can safely assume the type is never going to be anything different.
And just to clean things up a little bit, I like to dstructure things wherever I can. So instead of doing row just like this, let's actually dstructure both label and value out of
here. And instead of doing row label and
here. And instead of doing row label and row value, we can just use them directly after dstructuring. And then before we
after dstructuring. And then before we forget, like all mapping, we need to make sure we throw a key on this. So
let's throw a key. And we'll say our key is just equal to, I don't know, the value because that's always going to be unique. So let's go ahead and save it.
unique. So let's go ahead and save it.
And now let's actually render out this card under dashboard so we can actually see what we're working with. So let's go back to our app here. And right under daily forecast, let's render out
additional info just like this. Now if
we save it, scroll down, we have this additional weather info card, which is the component that we just made. So this
is nothing crazy. It's actually a pretty basic component. Let's style it up just
basic component. Let's style it up just a little bit. For the label, let's give the label a class name. And I'm just going to give it a class name of text gray 500 just to make it a little bit less obvious than the value itself.
We'll also want to convert this sunrise and sunset numbers you see down here into actual dates. Having them as Unix time doesn't really do anything for us.
But basically, we need to format these as actual dates and the other ones as numbers. So, that's going to be a little
numbers. So, that's going to be a little bit of an issue with the way that we currently have it. What I like to do when it comes to something like this is I make a separate component that just formats the values. Instead of doing a nasty turnary or conditional inside of
this expression, right below this component, I'll actually just make a super simple new one. We'll say function format component just like this. This is just going to be
another React component. To format each thing properly, we'll need to know the value and the actual number that we have for that value. So, we'll make two props. We'll say value and number. And
props. We'll say value and number. And
to make TypeScript happy, we'll say value is going to be a number. Sorry,
string. And then number is obviously just going to be a number. For most of these fields, we just want the raw number. So by default, we can just say
number. So by default, we can just say return number like this. However, above
this return, specifically, if it's a date, we want to do something different.
So what we can do is we can say if the value is equal to sunrise or the value is equal to sunset then we
want to return a formatted date. So
we'll say return new date and we will do number times 1. Again just converting the actual date time to a real date
object. And then just like before we
object. And then just like before we will call to local time string. We go
ahead and save it. Just get a little bit of formatting here. makes it a bit nicer to read. We'll do the same thing as
to read. We'll do the same thing as before. Undefined for the initial
before. Undefined for the initial options. Pass in the customization for
options. Pass in the customization for the second argument here. Oops.
Sometimes this formatting gets a little bit uh bit funky. I want to map exactly what we have for our hourly forecast.
So, it's the exact same. So, if we go in here, see what the options are for this.
Let's actually just take everything we have inside of here, copy it, and we will actually just put it exactly verbatim inside of here. Now, if we go
ahead and save this, this component should be good to go. If I was using this customization options right here in a bunch of different spots, I'd probably extract it out into its own utility. But
because we're only using in two spots, I don't really think it's that big of a deal to just copy and paste it. Again,
essentially all this component is doing is normally we're just going to return the number to format that number directly. However, if it's a date,
directly. However, if it's a date, meaning if it's sunrise or sunset, then we're going to actually format it as a date. So now what we can do is we can go
date. So now what we can do is we can go up here and inside of this span right here, we can actually just completely replace this by rendering out format
component just like this. For our props, we obviously need value and number. For
value, it's going to be just a direct one one. It's going to be the actual
one one. It's going to be the actual value and then number is going to be what we had before which was data.curren
at value just like this. That's the
actual corresponding number from the endpoint. Now if we save it, boom, we
endpoint. Now if we save it, boom, we have the rest of them formatted as numbers, but the sunrise and sunset are now formatted as dates just the way that we wanted it. Before we call this card good, there's one more thing that I want
to add to it. All of our other cards have some sort of icons in some capacity like the weather icons. It makes them look a lot less bland and a bit more, I don't know, sophisticated or styled. But
this card right here is just displaying text. So, it looks a little bit boring,
text. So, it looks a little bit boring, at least when compared to the other cards. What we can do is we can actually
cards. What we can do is we can actually add some quick SVGs to make these look a little bit cooler. If I head over to svgroup.com, I can see all sorts of SVGs that I can just get completely for free. For
example, for cloudiness, I can search, I don't know, cloud in here, and I can take any one of these icons and use them as a cloud SVG in my app. So, real
quick, offscreen to not waste your time, I'm going to find an SVG for each one of our six categories and get them added to our repo. All right, so I found an icon
our repo. All right, so I found an icon for each one of our things. We have this cloud for cloudiness, pressure for pressure, sunrise, and sunset. We have
UV, and then we have wind. These will be super easy to add to our project. So,
basically, all that I'm wanting to do is in this additional info card, for each little field that we have, I'm wanting to have an icon next to the label. So,
for cloudiness, we'll have little cloud.
for UV index will have a little UV symbol and you get the idea. Just a
little icon for each one to make it look a little cooler. How do we render out SVGs as React components? There's a
couple of different ways we can do it, but by far the simplest way and the way that I like the most with VIT is using a super tiny package called SVGR. If you
look up SVGR, you can find this page called Vit plugin SVGR. Let's get this added to our project real quick by taking this command right here. Go ahead
and copy it. Open up our terminal. Paste
it in, run it, and now we have SVGR and we're ready to use it in a super simple way. All we have to do is do exactly
way. All we have to do is do exactly what it says right here. Basically, just
add it to our plugins like how we have React and Tailwind. If we go to our V config here, we already have React and Tailwind. Let's now just add SVGR like
Tailwind. Let's now just add SVGR like this. This will be imported from the
this. This will be imported from the plugin. Looks like TypeScript isn't
plugin. Looks like TypeScript isn't recognizing it for the import, but it should be imported just like this. What
this tiny little package allows us to do now is actually import SVGs as React components like it says over here right in the docs. You basically just import it and you add question mark React and
it'll actually import it as a React component that is already ready to use.
So what I mean by that is if we close out of these, go back to additional info, I can now import these SVGs as components by doing something like for
sunrise for example, I can say import sunrise from I'll say file path is /source slassets
sunrise.svg
sunrise.svg just like this and then add question mark react. And this will make the
mark react. And this will make the sunrise actually a react component. And
I can actually do this for all six of our icons. So I'll take this here. Make
our icons. So I'll take this here. Make
it six times. We'll make this sunset.
We'll make this cloud UV.
Do lowercase V there. And then what were the other two that we had? We had uh oh yeah wind.
And then we also have pressure just like this. And then we'll change each of
this. And then we'll change each of these. This one's going to be pressure
these. This one's going to be pressure SVG.
This is wind SVG UV cloud and then sunrise and sunset. Now we can use each of these. Let's plug each of
these icons into our rows array. What we
can do is like how we have label and value for each one is also add an icon field and this will just be a component.
So the icon for clouds is just going to be the cloud component. And then we'll take this actually copy and paste it for each one
just as such. And then this will be UV.
This will be wind pressure.
And then we have sunrise and sunset.
So these right here are actually all React components. By default, TypeScript
React components. By default, TypeScript is actually not going to recognize these as React components because the way it's importing it is a little bit unconventional. So it actually gives us
unconventional. So it actually gives us this super nice thing we can use right here. This line, if we just copy it, it
here. This line, if we just copy it, it says to add it to our venv.d.ts.
So let's take this. Let's go to that exact file, which we don't actually have. So let's create it. We'll just
have. So let's create it. We'll just
make a new file in here. We'll say b- env.ts just like this. And let's paste this line in here. And now we go back to
additional info. And we save it. It
additional info. And we save it. It
should resolve all these correctly. Oh,
and also this venv.ts file should I don't think be at this level. I think it should actually be
level. I think it should actually be inside of source which may be the issue.
Move that. Yeah. Okay. If we move that, it looks like it resolves those imports.
So no more annoying TypeScript errors.
Uh, looks like oops, we forgot the slash on this. So now it should import all of
on this. So now it should import all of these icons correctly. Now that we have them in here, let's actually get them displaying on our guard. We already have each of the icons and the rows now. So
what we can do is we're mapping over our rows here. We can also dstructure the
rows here. We can also dstructure the icon out of here. And actually, we'll make sure it's capital I because it is a React component. So we'll make sure
React component. So we'll make sure icon here. Select all of them. Capital I
icon here. Select all of them. Capital I
for component. just so we have uh proper conventions here. And then we want to
conventions here. And then we want to render out this as a component. So let's
take our label span here. Let's wrap it in another div. So we'll wrap div here.
Create some more spacing. And we'll just give it a simple class name of flex gap 4. And then right after our label, let's
4. And then right after our label, let's just render out icon as a component just like this. What's really cool about SVGR
like this. What's really cool about SVGR is that you can actually pass in custom class names to give your icon certain sizes. So, we can say, let's give this
sizes. So, we can say, let's give this one size, I don't know, let's do size eight for now and see how that looks. We
go ahead and save it. Now, we scroll down and look at our card. We have SVG icons for each label. I definitely think that having these SVGs add some more personality to this card. Only thing we
need to fix real quick is that all of these are black against a dark background, so it looks kind of bad. So,
let's actually throw the invert class on this, too. And that will make sure they
this, too. And that will make sure they appear white instead of black. And the
very last thing I want to do for this card is really quickly just adjust this wind direction. This number right here
wind direction. This number right here doesn't really make a whole lot of sense. The other numbers kind of do
sense. The other numbers kind of do because we have the units next to them, but having just a raw number for the wind direction doesn't really mean anything. Now, technically, it's in
anything. Now, technically, it's in degrees, but I think one cool way to show this would just be to show an arrow pointing which direction the wind is going. I found another SVG on SVG repo
going. I found another SVG on SVG repo called up arrow that just looks like this that we can use to point the proper direction. way we can super easily do
direction. way we can super easily do that is just how we have the formatting for sunrise and sunset we can do a similar thing for the wind degree. So
instead of having this return as a raw number can say if the value is equal to wind degree just like this then what do we want to return? Well all we want to
return is just the up arrow SVG. So we
can actually import that like other components. We can say
components. We can say take this line. We can call this the up arrow component and it'll be from up arrow.svg just like this. And now for
arrow.svg just like this. And now for right here all we want to do is if we're on wind degree, we want to return up arrow. And to get it pointing the proper
arrow. And to get it pointing the proper direction, we just want to rotate it by whatever the wind direction is. An easy
way that we can do that is to actually just give this up arrow a style tag, which again is a nice thing about SVGR.
We can give custom class names, styles, everything like that. And inside of here, what we're going to do is just say transform. And you want to transform and
transform. And you want to transform and a rotate. So we'll say this, we'll do
a rotate. So we'll say this, we'll do the back to quotes here. We'll say
rotate. And the amount of degrees we want to rotate it is going to be just whatever the number is. So we'll say dollar sign curly brackets to inject something in here. say number and you
want it to be exactly that many degrees.
Because the arrow is an up arrow, that means the starting point is at 0 degrees. So if our wind direction is,
degrees. So if our wind direction is, for example, 180, that'll make it go to a down arrow. I hope you see what I'm getting at here. If I go ahead and save this now, you can see it's actually just
this massive icon. So we need to fix the sizing on it real quick. So let's go to the class name. Actually, we'll make a class name for it because there's not already one on here. Let's just give it
a class name of size eight just to be consistent with the other icons. And
we'll also invert it to make it white.
Now, if we save it, you can see we have a cool little arrow right here that points in the direction of the wind. So,
our arrow is pointing not quite perfectly downwards, but a little bit to the left of that. So, it's probably around the 180 or 190° mark. If we
quickly just go into our network tab, we can verify this. We just refresh the page. Look at this call. Let's go to the
page. Look at this call. Let's go to the current weather information and we can see the wind degree is 184. So yeah,
184. So that's why it looks like it's almost straight down, but not quite.
It's a little bit past. That's actually
really cool. Now we have the arrow that points in the direction of the wind.
Having this arrow point to 184° is way cooler than just putting 184. With that
done, let's take a step back for a second. We've just successfully gotten
second. We've just successfully gotten API information from open weather fetch through tanstack in four separate card components that each have their own unique styling and each subscribe to the
exact same query that grabs this data.
We've still got quite a ways to go before this app is finished. But we've
made great progress so far and arguably we've just knocked the four most important portions of our app out of the way. That being these four cards. So,
way. That being these four cards. So,
what's next? Now that we have our cards out of the way, I think it would be a great time to get the interactive map added to our app. All we really have on our page right now is just these four cards, which is cool, but by themselves, they don't really do anything. And we
don't actually have a way to change the latitude and longitude coordinates that we're getting our data from. Like this
10 and 25 here is just hard-coded. So,
it doesn't really mean anything if we can't change this and have it be dynamic. So, what I'll go ahead and do
dynamic. So, what I'll go ahead and do is go into our components folder here, and let's make a new component. And we
will just call it map.tsx here. And
then, as usual, tsrfc to create the component. And for this map, we've got
component. And for this map, we've got quite a few different options for actually creating it. The library that I think is probably the easiest, at least for our use case, to implement is Leaflet. It's one of the most, if not
Leaflet. It's one of the most, if not the most widely used library in JavaScript for creating maps. And there
is actually a nice package called React Leaflet that essentially wraps this entire Leaflet library and gives us super easy to use React components. So,
we'll use this for our app. If we're on the React Leaflet site and we go over to this menu, can go to getting started and there should be a step for installation and it tells us how to get it added to
our app. If we scroll down here, it
our app. If we scroll down here, it should give us an npm command right here. So, we'll go ahead and install
here. So, we'll go ahead and install this. The first command it tells us to
this. The first command it tells us to run is this command right here. And we
actually already have React and React DOM. So, we don't need to run those
DOM. So, we don't need to run those again. What we will do is open up our
again. What we will do is open up our terminal, a new one here, and we will say mpm install leaflet. So we can get Leaflet added to our project because Leaflet is a dependency of React Leaflet
since it uses it under the hood. And
then next it says to install React Leaflet add next. So we'll go ahead and install this. Copy that. Paste into our
install this. Copy that. Paste into our terminal. And now we should have both
terminal. And now we should have both leaflet and react leaflet. The last
thing I'll do just for convenience is to install the types so TypeScript can be happy with some of this. So we'll take the leaflet types and get that. And once
we've run those three commands, we should be good to go. And then at the very bottom of this page, there should be this next setup button here. So,
we'll go to this page and it actually gives us an example of what a basic map looks like. For the sake of just of just
looks like. For the sake of just of just showing you what it looks like and how it works, I'm going to take this entire example. Actually, I'm going to copy it
example. Actually, I'm going to copy it and I'm going to paste it into our map component. So, we just have it in here.
component. So, we just have it in here.
Then, I'll make sure to import these components from React leaflet. Get map
container tile layer. Let's get a marker and then popup. We will go ahead and save this file. I also think for right now, we actually don't need this pop-up on Second Thought. So, I'm actually
going to get rid of this pop-up and I'm just going to make marker a self-closing tag here. So, I'll just make it a
tag here. So, I'll just make it a standalone marker with no children.
Right now, going back to our app real quick, the coordinates that we've been fetching for for different components are just latitude 10 and longitude 25.
So, if we go to our map here, that's actually what these are right here, the center, this array right here that we use for both center and position. So,
I'm actually instead of having these hard-coded numbers, just going to put in our 10 and 25 coordinates. So this
position of the map matches exactly what we're already hard coding. 13 zoom is a lot. So I'm going to change this down to
lot. So I'm going to change this down to something like five, something way less zoomed in. And I also don't care about
zoomed in. And I also don't care about this scroll wheel zoom prop. So we're
going to go ahead and get rid of it and save this file again. And until we actually get proper setup for our dashboard styling, the map that we have on the page is not going to have any actual width or height. Let's just give
us something hardcoded for now just so we can see it. So I'll give this a Oops.
Wait. Give this a width of 500 pixels.
And I'll also give it a height of 500 pixels as well. Just hardcode it. And
now let's take this map component and let's go over to our app here. And let's
put it above the current weather card.
It's going to render out map just like this. Looks like the import isn't quite
this. Looks like the import isn't quite recognized. So I'm going to take uh this
recognized. So I'm going to take uh this here. Copy it. This will be import map.
here. Copy it. This will be import map.
And it should be export default. So it
can be whatever name you want it. And
then it should just be slashcomponents slash map just like this. And now it should be happy. And if we go ahead and save it, uh, yeah, it's going to look pretty horrible. And this is likely
pretty horrible. And this is likely because we haven't actually imported any of the CSS from Leaflet. So to import the CSS and make it actually look not like whatever the heck this is right
here. Let's go into the map component.
here. Let's go into the map component.
And at the very top here, we're importing everything else. We're just
going to add import leafletist/leaflet.css.
leafletist/leaflet.css.
Just like this. Go ahead and save it.
And now this map should actually be somewhat functional and not whatever the heck we were just looking at. And this
500 pixel width is actually a bit small.
So let's change this to be I don't know just 1,000 pixels just so it's a bit wider. Go ahead and save it. And now we
wider. Go ahead and save it. And now we have a little bit more real estate to work with. So yeah, here's really what
work with. So yeah, here's really what the map is. And this is again powered through Leaflet, the very popular library. The map doesn't really do
library. The map doesn't really do anything crazy by itself, but what it does allow for is you can drag it. You
can look around the map. You can zoom in, zoom out, see different city names, and it's actually actually supports a very high level of zoom. So, I can go on individual streets here, and I can go out to the whole global level. It's
actually a really, really cool mapping library. I've used in a few things
library. I've used in a few things before, and I never really had any problems with it, but I think it's quite a nice library to use. By default, it honestly looks and behaves very similarly to Google Maps, minus obviously a bunch of the complex Google
functionality. So, it's pretty cool that
functionality. So, it's pretty cool that actually with super minimal setup and just pasting in a command and changing some stuff around we're able to actually get a functional map like this. It's
nice when these libraries and packages can do a lot of the heavy lifting.
Something that is actually pretty cool now is this marker is positioned at latitude 10, longitude 25. So now when we're looking at our weather information and we see like the current weather and the forecast, we at least actually have
a reference now to where in the world we're actually looking at, which is part of the point of even having this map in the first place. Here's the point in the video where we need to step up our game and start connecting everything
together. Because right now, aside from
together. Because right now, aside from being kind of cool, this map doesn't actually do anything. Nothing happens
when you click it, it has no connection to any of our data or anything, aside from the hard-coded coordinates we're giving it. So, a big piece that we're
giving it. So, a big piece that we're missing is connecting this map somehow to the cards so that when we click on the map, it'll update the latitude and longitude to update the information on our cards. What I think we need to do
our cards. What I think we need to do next is hook into the actual click event of the map somehow set some latitude and longitude state and then connect that state to the query that we're making to
actually get weather information for those coordinates because all we've done so far every time we use this use query and call the get weather function, we're just hard coding 1025 in there which you know is cool for testing but it's not
functional. So, if we actually found a
functional. So, if we actually found a way to hook into the click event of this map, update some state, and get our query to reflect that actual weather information, that's the next big step that we're missing. So, without wasting
any more time, let's go ahead and build out that functionality. React leaflet
has a hook called use map that allows us to get access to whatever map container that we're referencing. Once we have the map from use map, we can hook into all sorts of events. Control camera panning and zooming and all sorts of other
stuff. This use map is what we'll use to
stuff. This use map is what we'll use to hook into the click event. However, the
main problem we're going to run into right away is that use map can only be called if we're inside a map container.
Otherwise, it's not going to work. The
reason why we can't do that in this map component is because this map component, if we were to call, you know, use map up here, this map component is not wrapped inside of map container. Only the things
that are actually inside of it would be.
So the way I've typically handled this in the past is just creating a brand new component beneath this that I would just call usually call map click just like this. And then this component right here
this. And then this component right here is where we would actually use the use map hook. So we'd call it down here. We
map hook. So we'd call it down here. We
would get this imported from React leaflet. And then because this component
leaflet. And then because this component is purely for functionality for getting this map, we actually don't care about returning anything. So we're going to
returning anything. So we're going to return null for this. And then inside of here, we're just going to render map click just like any other regular React component. Now, because we're calling
component. Now, because we're calling the use map hook inside of a map container, this object that we're going to get back here, this map object, will directly reference the map container that it is inside. And basically, all we
have to do now to actually hook into the click event to do what we want it to do is go down here and we would just say map on and we can hook into the click
event just like this. And then our event handler would be the second argument which is a function. So we'll take an e and make it a function like this that will get called whenever we click the
map. In other words, if I go in here in
map. In other words, if I go in here in the map and I start clicking around, whatever function we have right here is going to get invoked on each click.
Before we worry about the actual click functionality and setting any sort of latitude or longitude state, it would be cool if we added a pan effect. Meaning
if I click somewhere on the map, we should make that click point the new map center. That way, if I click somewhere
center. That way, if I click somewhere on the edge of the map, like over here in France, the map will actually move over and shift to center the area that I just clicked. And there's actually a
just clicked. And there's actually a super super simple way we can do that in this on click. All we have to do inside of this function is type map.pan to just like this. And the first argument that
like this. And the first argument that this panor takes in is an array that is just the latitude and longitude. That
way, the map knows which coordinates to actually pan to. How do we actually get the latitude and longitude out of our click event? All we have to do directly
click event? All we have to do directly above this panoo is we can actually get it from this E event here. It exposes
the coordinates that we clicked on. If
just to show you, I go ahead and console log out E here. I'm going to comment out this panoo. We'll go ahead and save
this panoo. We'll go ahead and save this. I'll open up our console right
this. I'll open up our console right here. And the map's super tiny, but I'll
here. And the map's super tiny, but I'll go ahead and click somewhere. And if I click here is the event, and you'll notice we have this property right here
called lat longitude or lat LNG. and it
has the latitude and longitude coordinates that we just clicked on right here. So basically what that means
right here. So basically what that means is what we can do in the code is to get the latitude and longitude we can actually dstructure them. We can say
const lat and also LNG equals e dol lat long just like this and it's really that simple. Now this will be the latitude
simple. Now this will be the latitude and this will be the longitude. So we
can uncomment our pan to and we can say lat longitude just like this. And now
it'll pan to wherever we click. If I go ahead and save this file, go ahead and refresh just in case. Now, if I zoom out and I click somewhere, notice how when I click, it is panning to where I'm
clicking. And it doesn't quite look
clicking. And it doesn't quite look centered, actually, because this map is way too huge. It's kind of overflowing the bounds here. So, let's take it from 1,000 pixels. Let's do let's do 700
1,000 pixels. Let's do let's do 700 pixels. Just slightly wider than it was
pixels. Just slightly wider than it was before at 500. And that's a little better. So, now if I click, it's a bit
better. So, now if I click, it's a bit easier to see that it's centering where I click. So, when I want to jump around
I click. So, when I want to jump around the map, say I'm in Africa and I want to go to Italy, I can click here and it'll pan everywhere I click. And it's really just that simple to create a pan effect.
Obviously, this marker that we have right here isn't changing yet because this marker is still hardcoded to this 1025, but we'll fix that in just a second. But now that we have this pan
second. But now that we have this pan effect working, what's next? Let's get
down to what we really came here to do.
Inside of this click event on the map, we don't just want to pan to wherever we clicked. What we really want to do is
clicked. What we really want to do is set some latitude and longitude state of wherever we clicked. That way the other cards on the page can update their information to reflect those actual coordinates. The question is now how do
coordinates. The question is now how do we even start by going about doing something like that? Well, the simplest way I can think of is by keeping track of latitude and longitude in the app component. So the most parent component
component. So the most parent component because it happens to be the direct parent of the map but also all of our cards. Basically, what I'm saying is
cards. Basically, what I'm saying is that if we're getting the latitude, longitude in the parent and every component that needs it is just a direct child, it's actually super easy to do because we can just store it in app and
then we can pass it down as props to each of the children. If you don't fully understand where I'm going with this yet, let me just show you firsthand and we'll work through it. First things
first, we're not using this use query up here anymore. So, let's just completely
here anymore. So, let's just completely get rid of it. Then, what we're going to do is make a new state for our coordinates. We'll say const chords just
coordinates. We'll say const chords just short for coordinates and set chords equals use state just like this. And in
terms of the type for these coordinates, we'll just make it an object. We'll just
give it a lat property which you know we've been using this 1025 number. So
we'll keep using that. So we'll say lat and lawn just like this. Making sure to import you state. So basically we have this coordinate state and a setter function for coordinates. We're making a state in React and we're just defaulting
it to an object with default values 10 and 25. If we go to any of our cards
and 25. If we go to any of our cards like current weather for example, we still have this hard-coded 1025 number.
But now that we have this chord state up here, let's use that instead so that can at least all share the same state. So
what I'm saying with this is we should take coordinates and pass it down as props to all of our cards down here.
Now, if I had a ton of components and the complexity was a bit more in-depth than it's going to be here, I would most likely use a provider and pass the coordinates down with a context. That
way, we're not drilling props everywhere. However, because these cards
everywhere. However, because these cards are the direct children of the app component, and we're not doing huge amounts of nesting and, you know, huge component complexity, I think it's best for simplicity if we just do them as
props. Because we're going to be seeing
props. Because we're going to be seeing this type structure right here a lot of the object that has the lat longitude property. What I'm actually going to do
property. What I'm actually going to do real quick is just go into source here.
I'm going to make just a new file in here. I'm just going to call it types.
here. I'm just going to call it types.
It'll just be like a helper function that we can export types from. I'm going
to say export type chords and we'll make it match the structure that we were just talking about. We'll have lat be a
talking about. We'll have lat be a number and lawn oops and longitude also be a number. And
we don't really have to do this right now, but let's just go into app and just to make sure let's make sure the state is typed with type chords just so we're being explicit. Now that we have this
being explicit. Now that we have this chords type, let's pass down chords to each of our cards. All we have to do for that for each one is we'll say chords equals chords just like this. And
because they're all going to be the same, let's take this, copy it, and paste it and all four of them. Again,
like I said, if we were dealing with more complexity, I'd probably do this in a provider to prevent these props just being passed on everywhere. But I think this keeps it pretty simple for our use case. Now, each of these will have a
case. Now, each of these will have a type error because they don't expect any actual props. They don't expect the
actual props. They don't expect the scores yet. Let's go into current
scores yet. Let's go into current weather, for example. Let's go into the props here and we'll just make sure to add chords as type chords just like this. And we can dstructure chords from
this. And we can dstructure chords from the props just like this. Now for our suspense query, instead of hard coding 10 and 25, what we can do now is just
say chords.lat and chords.lawn. So we
say chords.lat and chords.lawn. So we
can be assured that the coordinates that we're getting down in this component, this current weather card, are the same coordinates that are set in the parent.
Then we'll have to make the same changes to our other components to make TypeScript happy and to make sure this all works the way that we want it to. So
let's actually take the suspense query.
I'm going to go each of our cars here.
Let's replace the suspense queries. Do
an hourly. We'll do it in daily. And we
will also do it in additional info. And
then these will need the exact same prop structure as current weather. So we'll
just take props here. Also doing the same thing. Make sure these are all type
same thing. Make sure these are all type the exact same because again they're only all just taking in the coordinates.
So we'll have this and for each one make sure we import chords here and dstructure chords. So there should be
dstructure chords. So there should be additional info good to go. We'll make
sure we import chords here in the daily forecast also destructure from here.
Save this. Now this is good to go. Get
chords out of here. Import the type.
Hourly forecast should be good to go.
And now that should be all four of them taken care of. Now all four of our cards are using a shared state from the parent. So we can actually go ahead and
parent. So we can actually go ahead and just, you know, close out of these cards here. Let's go back to our app real
here. Let's go back to our app real quick. And we can see we've suppressed
quick. And we can see we've suppressed all of the type errors. Now just to see what happens if I go ahead and refresh this page here with our 1025 coordinates. Actually, here real quick,
coordinates. Actually, here real quick, let me save this. Refresh it. You can
see it says we're at 76°, you know, 1246, whatever. So, let's change this
1246, whatever. So, let's change this 1025 to something else. Let's make it 4055 instead. And now, if we do this, go
4055 instead. And now, if we do this, go ahead and save it. All of these cards just changed. I can do it again. Let me
just changed. I can do it again. Let me
just show you. I'll do this 50 45. And
now, every single card just shifted because now they're all using the exact same coordinate state. They're not just a bunch of hard-coded numbers coming from nowhere. Now that we did this, step
from nowhere. Now that we did this, step one is complete for connecting the map and these cards. The next step would actually be logically to get this same coordinate state passed down to our actual map because right now we have
this marker that's on the map but the marker doesn't mean anything. It's not
really coming from anywhere. So ideally
the map actually needs the same coordinate state cuz it needs to know where to place this little blue marker at. So what we can do in map is the same
at. So what we can do in map is the same thing as the other ones. We can give it a type chords.
And here in our props, dstructure it.
And instead of using center 1025 and position 1025, going to actually dstructure here to make it a bit easier.
We'll say lat long equals coordinates.
Get these out of here. The 10 will just be latitude. The 25 will be the
be latitude. The 25 will be the longitude. So now the map is also using
longitude. So now the map is also using our shared state. And we'll just be sure to actually pass this down in the same way we're passing it down for our other cards. We'll go ahead and save both of
cards. We'll go ahead and save both of these now. And now if we look over at
these now. And now if we look over at our map, it looks like our marker has moved over here to somewhere in Russia.
Now that that's taken care of and the map also has respect to the same latitude and longitude coordinates, let's take care of the important part and that's setting up the latitude and longitude coordinates once we actually
click the map. So basically utilizing the set coordinates function. So what
we'll do in our app here is let's make a new function. We're going to say const
new function. We're going to say const on map click make it a function and we'll just make it accept two arguments latitude which is a number and longitude which is also of course a number. What
we're going to do is we're going to define this on mapap click function up here in the parent. We're going to pass it down to the map and then we're going to call it in the map whenever we do the click event. The reason for that is the
click event. The reason for that is the state that we want to update is up here on the app level. But whenever we call it we can just pass in the appropriate latitude and longitude from the map click event. So that's where these
click event. So that's where these arguments will get passed in. So inside
of this on mapap click, all we need to do for right now is just say set chords calling the setter function and we're going to set it to lat and lawn just like this. But basically whatever we
like this. But basically whatever we pass in for our latitude and longitude will be what the new coordinate state gets set to. Now the question is how do we actually get this on map click function to get called in the click event of the map? Well, like all the
other things, we're going to use props.
So we're going to go to our map here.
We're going to add an on map click prop and we will just pass in this on map click function. Go ahead and go to the
click function. Go ahead and go to the map here and let's add on map click. We
know it takes in two things. It's just
latitude and longitude both numbers.
Doesn't return anything. So we'll just say void. Now if we save both of these,
say void. Now if we save both of these, we're passing on map click down. And now
in map, we have the ability to access it from our props. So let's go ahead and dstructure it just like this. And then
we'll also have to drill this on map click down down to our map click component so we can actually use it in the click event. Normally I would say prop drilling is a bad thing, but if it's only two layers and it's only for a
single thing that you're doing, it's really not that big of a deal. If I was going deeper than two layers, definitely I'd use a provider and pass it down through context. But for now, we'll just
through context. But for now, we'll just make things simple. And we'll also give the same exact prop to our on map click.
So, we'll drill it down to this component here. And make sure that our
component here. And make sure that our map click expects on map click, which is just of type. That's actually the exact same type up here. So, I'll just go ahead and copy this. It'll be on map
click, just like this. And now we finally done it. We've actually arrived at the click event with our on map click function. So now all we need to do is
function. So now all we need to do is actually just call it in here right below our panoo. All we'll do is call on map click. And again, we know the
map click. And again, we know the signature is taking in a lat and a longitude number as two separate arguments. So easy enough, we already
arguments. So easy enough, we already have those two. We'll just pass in lat, lawn to get latitude and longitude passed in. And now we've actually done
passed in. And now we've actually done it. Our map is fully connected to the
it. Our map is fully connected to the click event where we will propagate this data back up to the parent, which will update the coordinates. So let's test it out. If I actually save it, refresh just
out. If I actually save it, refresh just to be sure, and click it, we're going to notice something. It looks like nothing
notice something. It looks like nothing is actually happening for our cards. The
state actually is getting set properly, but nothing seems to really be happening in terms of the state update. So, let's
test something really quick. Let's go
into app here and let's just add a console log and let's log out our coordinates here. Go ahead and open up
coordinates here. Go ahead and open up our console. Close out of all this.
our console. Close out of all this.
Oops, not the console itself. I just
wanted to shrink this down. So, let's
close this. Let's just click somewhere in the map. You can see we have our latitude longitude coordinates right here. If I just I don't know scrolling
here. If I just I don't know scrolling this little tiny window over here.
Expand it a tiny bit so it's easier to see. I click here. You can see if you
see. I click here. You can see if you look in our console, it actually is updating the coordinates. So it looks like the state update is going through, but for whatever reason, our cards don't actually seem to be updating. And
there's actually a very simple reason for this and super easy fix. Right now,
all of our cards like current weather, hourly forecast, whatever, all have the same query key. It's just the string weather. However, generally a huge no no
weather. However, generally a huge no no with tanstack query is that whenever you're passing in dynamic values to your query function, like we are for example with the coordinates because you know these can change. You want to make sure
these coordinate values are included as part of your query key because what's happening right now since these all just have this hard-coded weather query key is when we click somewhere in the map, it adds that weather key to our cache
and then we click somewhere else and it sees the exact same key even though our coordinates change. So it just uses the
coordinates change. So it just uses the data that is already there. Meaning
we're always going to be stuck on whatever the first cache value is.
That's why you usually add these dynamic values like coordinates to our query key. That way it knows to update with
key. That way it knows to update with each one. In other words, if the query
each one. In other words, if the query key is always static, the cache value in the tanstack cache is going to be treated as static as well. When I say this fix is super simple, it is. All we
have to do is in our query key, add accordance to it like this, a fresh query key for each time we click somewhere. So, we'll go ahead and take
somewhere. So, we'll go ahead and take this, we'll actually save it, and we'll go into each of our cards and make sure that all of them expect that exact same
query key. That way they're all not
query key. That way they're all not static anymore. Go ahead and get this
static anymore. Go ahead and get this one. And we will get additional info for
one. And we will get additional info for the final one right here. And now if we go ahead and just close out of all these cards and we go back to our map and I
click somewhere, boom, all these cards are going to update. Now I can kind of scroll to the bottom here. So you can see a bit of the current and the hourly.
So you can see if I'm clicking somewhere on the map here, these cards are updating in real time. So that's super sick because honestly in a really short amount of time, we just got our map hooked up to all of the cards. So now
all of the components that we're seeing on this entire page all share the same value context. No matter where I click
value context. No matter where I click on the map, every time I go somewhere, looks like it lags a little bit sometimes, but every time I click somewhere, these cards are going to update completely. So we actually
update completely. So we actually already have a huge, huge chunk of functionality of this app knocked out of the way. If I want to know the weather
the way. If I want to know the weather information for I don't know somewhere in Japan let's say Tokyo I can go here and I actually forgive my geography here
I don't know I think this is Tokyo here can select this and you can see this is the weather information for that particular city if I want to go I don't know I'm curious to see what the weather
looks like in London and scroll over here go to London click here like it's uh getting a bit of lag there but we have 44 degrees and we can see the
hourly forecast, the daily forecast for the next week, and all sorts of additional weather information. So,
that's super super neat. Now that this functionality is out of the way, what do we want to tackle next? The next thing that I like to do is add a location picker drop down. It's super nice being able to use this map and click anywhere
you want to get the exact weather. But,
as you saw, sometimes I want to know the weather for a particular location or city like London or Tokyo. So, what I think would be neat is above this map here, having a dropown that we could click that would have a list of
pre-populated popular cities. So,
instead of having to zoom in the map and find it, I can just open the drop down, click wherever, and I'll get the weather information for that particular city.
However, creating a fully functional select component that is styled great and works exactly like we want it to may take quite a bit of time building from scratch. Which is why right now, instead
scratch. Which is why right now, instead of building our own custom select, we're instead going to use Shad CN for some of their pre-built components. If you're
not familiar with Shad CN, essentially it's just a component library that allows you to install components as needed, not some library where we install 10 GB of packages or anything crazy. We can just import some pre-made
crazy. We can just import some pre-made components like these ones into our app.
If you want to build your own custom components just for the sake of learning, that's completely fine. More
power to you. But for this video, to save time, we're going to use these pre-built Shad CN components. For
example, if I go to the view components here, I can find the select component.
And if I look at this, it's a drop down just like this. Pretty much already coded for us. Obviously, we can put our own data in there, but the component is already essentially made. This is going to save us a lot of time as opposed to
trying to code all this logic inhouse.
While there obviously can be unique use cases where you need to do your own thing, I think it's important to not reinvent the wheel, especially if you want to keep things consistent. So, in
our use case, we'd have a drop down like this. We would click it and there'd be a
this. We would click it and there'd be a list of cities here that we can click and when we click, it would update our map and all of our cards. The way that shad CNN works is for each component that you want to add to your app.
Basically, all you have to do is just run this command right here, this npx shaden at latest, and you just add whatever component you want to add. And
then from there, you can actually just render out the component like this. They
have a bunch of examples, but essentially you just import the stuff from the component, and you can put your own content inside of it. However,
before we do that, we need to make sure our project is set up to add shad components because there is some setup required before we can actually run this command. If we go back to the menu up
command. If we go back to the menu up here and we go to the installation page, go to VIT should be the default there.
And there's already some commands here.
You know, create the V project, which we've already done. Tail and CSS. We
already have it. So, let's move on to this step down here. First things first, it tells us to edit our tsconfig.json
file. So, let's go into our file explorer here. Go to tsconfig.json.
explorer here. Go to tsconfig.json.
And it wants us to add these compiler options right here, right underneath the references array. So, we'll take this
references array. So, we'll take this compiler options, take it like this, and add it right underneath references. Go
ahead and save it. And next, it says to also edit the tsconfig.app.json.
So, we'll go to that file as well. And
we already have the compiler options here. We just need to add this base URL
here. We just need to add this base URL and paths here. Let's copy this. Let's
actually go to the very bottom here.
Actually, see, compiler options ends here. Okay. So, let's go inside here and
here. Okay. So, let's go inside here and just add it right here. Make sure to have a comma so it doesn't get mad. And
now we have this added. So, we'll go ahead and save this file here. And next,
all we got to do is get these types installed so we can actually resolve our file path whenever we try to import the components. So, we'll take this command,
components. So, we'll take this command, open up our terminal, copy paste it in there, go ahead and run that. And then
the very last thing I think that we need to do before we can start adding components and we can initialize the project is just to add some stuff here to our V config. So if we go to our V
config file, it says that in addition to having the plugins here that we have, so we already have, you know, React Tailwind and SVGR, we need to add this resolve object right here. So let's just
copy that, go here, paste it, and then this path should just be imported directly from path just like this. And
now if we save it, we should be all good to go in terms of our own files. Now
that we have all these to actually get shad CN in our project here, all we need to do is run this command right here.
This mpx shad cn latest in it. So what
we'll do is we'll take this command here, copy it. Go into our terminal and we'll go ahead and paste it in here.
Didn't quite copy and paste right. So
let's uh make sure it just runs this and it'll ask if we're okay to proceed to install shad CN. We'll say yes. Of
course, it's going to ask for a color palette. For this one, we'll say zinc. I
palette. For this one, we'll say zinc. I like zinc quite a bit. So, I'll go ahead and select that. And then it should install the dependencies and get the rest of it set up. And now, we should be good to go. Now, let's actually go back to our page here. Let's go ahead and
refresh it. And just to make sure
refresh it. And just to make sure everything is actually fresh here. Let's
restart our beat server. So, I'm going to go control C this npm rundev again.
And now, if we save it, go back to our page here. Notice how all the styling
page here. Notice how all the styling suddenly looks pretty bad. These cars
have a bunch of dark text now. And now
the background of our app is white for some reason. And that's because if we go
some reason. And that's because if we go to our index.css file, when we initialize Shad CN in our repo, Shad CN added a bunch of different variables to support the zinc color palette. So we
have things like background, foreground, cards, popovers, all this stuff that it added some of this predetermined styling to our page. So that's why it looks a little funky. Let's fix this real quick
little funky. Let's fix this real quick before we add the select in our index CSS here. Notice how we have this card
CSS here. Notice how we have this card variable. This card variable is actually
variable. This card variable is actually exactly what you think it is. If we have cards on a dashboard, we want them to use this color instead of just some random other hard-coded color. So, what
I mean by that is if we let's just close out of all these tabs here, more room.
We open up car.tsx.
Go to here. Right now, it is just background zinc 900. But if I instead change this to BG card and save it, now this is actually going to match this
card variable that we defined in here.
Now, however, what we've just kind of done is switch from working in an app that was pretty much in a dark mode configuration to a super bright and just white app that's I mean essentially just light mode. We're going to add a toggle
light mode. We're going to add a toggle later in the video to go back and forth between light mode and dark mode. But
for right now, let's go back to the dark scheme that we had before. All we have to do is to go into our index.html HTML.
And if we go to this body tag right here, all we have to do is add class equals dark, just like this. And if we go ahead and save it, now we're back to the dark theme. And this is something that's actually really cool about Shad
CN that I'll explain in way more detail when we actually do the light mode and dark mode switcher. But basically, what shad CN does is we have this variant here for dark, which checks the body for
having this class dark on it. And if
that's the case, what it will actually do is instead of using all these variables, which these variables are are defined for light mode, it will actually use variables defined down here,
specifically if we have dark mode. So
now instead of using this card right here, which this card right here is just white, it'll actually use the dark card right here, which is more of a zinc color, which is why we see this color
over here of the dark color, not the light color, because this dark selector is a dark class selector. And on the index html, we do have the dark class.
Again, I'll explain this more once we get into our light mode and dark mode switcher. But for now, we're just going
switcher. But for now, we're just going to have it hardcoded to dark mode cuz I think it's easier to work in. I just
prefer it. I don't want to get too sidetracked from adding the Shad CN components to our app. But real quick, something that I think would be super cool to add is to make these cards look a little bit more reflective. So, right
now they're just this solid color. But
one thing you can do that can kind of make stuff pop out a little more in my opinion is instead of just doing a solid color background, if you instead do a
gradient, so I'll do BG gradient to BR, meaning a gradient going from the top left to the bottom right. And then a neat little trick I like to do is when you do gradients to start it from the
card color and actually have it go to the same card color but with slightly less opacity. So we'll say I don't know
less opacity. So we'll say I don't know /60 for this. And it looks like if you hover this giving us a warning. I'm
curious what this says. Uh can be rewritten as BG linear to BR. So okay,
let's try that. Must be a new talent thing. So I'll go ahead and save that.
thing. So I'll go ahead and save that.
And now our cards I think look a little bit nicer. It's a very, very subtle
bit nicer. It's a very, very subtle change, but it makes the cards, in my opinion, look a bit cooler. It just
looks a little bit brighter at the top left than it does in the bottom right.
Now that we've got these colors looking good, let's get back to the whole reason why we added Shaden to our app in the first place. To add the select box for
first place. To add the select box for particular locations on the map. So, if
we go back to Shad Cen and we go to the component menu, which we can go here, go components, we can find select again.
Let's scroll down to where the command is to add the select. Looks like it's this command right here. What we're
going to do is we're going to take this, we're going to copy it, open up our terminal, and just run that exact command. Not sure why for some reason
command. Not sure why for some reason this copy and paste seems to be acting up, but do that. And once you run this command, it'll actually install the select component and just the select
component from shad CN into our repo.
What it does that's actually pretty cool is it will go into your repo and it'll look for a file or sorry, a folder called components, which we already have. And if you have that already
have. And if you have that already existing, it'll make a new folder called UI. And this is where all the shad CN
UI. And this is where all the shad CN components go. So now you can see we
components go. So now you can see we have this component select.tsx in here.
That is just the shad CN select component. Now that we have this, we're
component. Now that we have this, we're free to use it however we please. And if
we look over here at shad CN, it gives us a perfect example of how to actually use the select just like this. What
we're going to do is we're actually going to take this right here. We're
going to copy this to our clipboard. I'm
going to go over to our file explorer and inside of our components folder, I'm going to make a new folder and I'm going to call it dropdowns because we're going to have I think two dropdowns in this app, not just one, which I'll explain in
a second. But I'll make a new file in
a second. But I'll make a new file in here that I'm just going to call location dropdown.tsx.
location dropdown.tsx.
This is going to be for our map tsrfc as usual to create this component. And now
that we have this example over here from shad cn, let's again we have it copied.
So let's now paste it into this component and get all these imported from UI/ select. Now it's kind of annoying because you'll see the add import from radics UI, but all these are
going to be imported directly from our own UI folder. So we'll do here select content. Oops.
content. Oops.
Import select content here. And then
just import select item. And now this component should be good to go to at least test. Now what this is is this
least test. Now what this is is this select trigger component right here is essentially just the button that triggers the dropdown to open and close and select content is the dropdown
itself. So for example over here we go
itself. So for example over here we go to this this button you're seeing that says select a fruit over here is the select trigger. Now the select content
select trigger. Now the select content is what actually gets dropped down right here. And we have all this just wrapped
here. And we have all this just wrapped in the select component. Now that we have this file saved and we have this component let's go back to app.tsx tsx
here and directly above our map. Let's
actually render out location dropdown over to our page and let's go ahead and save it and give it a good old refresh.
And now you can see up here at the very top left as our drop down and in its raw form it looks something like this. And
actually looks like it's getting cut off quite a bit by the map cuz the map's just covering it. One thing I've noticed for some reason with leaflet is that sometimes the maps that you use have a z-index of around a thousand for whatever reason. So to have the drop
whatever reason. So to have the drop down just show over the map for right now like we would expect. Let's just go onto the select content here. Here and
let's just give this a class name. And
we'll just say Z1.
I generally like to stay away from spamming Zindexes everywhere. But
sometimes you got to do what you got to do. So let's save this. Go back. And now
do. So let's save this. Go back. And now
you can see it's opening up over the map. And this is what it looks like in
map. And this is what it looks like in its raw form. Now let's get some real content into this dropdown. Obviously,
we don't want light, dark, and system cuz that's has nothing to do with what our dropown actually is. So, let's get some cities added to this. I just went over to chat JBT real quick and asked it to give me an array of really popular
and well-known cities. And this is what it gave me back. So, instead of just having some random values in here, let's actually use these cities to put inside of our dropdown. The way that we can do that is just to wipe out what we already
have here for the select items, let's make a JavaScript expression in here.
And we're just going to say locations.m
map. And then for each location, actually what's called a city. For each
city, we want to map over something. And
you want to return the same select items we were using before. We'll just do it like this. And you want to make sure
like this. And you want to make sure because we're mapping that we have a key here. So we'll say key. Key will just be
here. So we'll say key. Key will just be the city name because these should always be unique. And then all these need a value as well. And the value will also be the city, which also will be what we're putting inside the select
item here. So that's what the user sees.
item here. So that's what the user sees.
Now, if we go ahead and save this, go back to our page, click on this. Now,
our drop down is full of our cities instead of whatever random stuff we had before. So, I can click on Tokyo, Dubai,
before. So, I can click on Tokyo, Dubai, London, whatever I want to click on. And
our drop down is actually looking pretty cool. Now, what's next? Well,
cool. Now, what's next? Well,
unfortunately, when we're making these calls to open weather to get the weather information, it only accepts coordinates in latitude and longitude. So if I select Tokyo for example, how do I have
any idea what the latitude and longitude coordinates of Tokyo actually are? I
need to have some way to convert city names directly into latitude and longitude coordinates. Luckily, that's
longitude coordinates. Luckily, that's exactly something that Open Weather has support for. It's called the geocoding
support for. It's called the geocoding API, which you can just get to at / API- geocoding API. Basically, what this
geocoding API. Basically, what this endpoint does is it takes in the names of countries, cities, states, or whatever, and can return latitude and longitude coordinates that match. If we
look at the way to call the API, it's actually pretty simple. All it really needs is for us to pass this Q parameter right here, that can be the city or state name. We go to the actual API
state name. We go to the actual API response fields. You can see here once
response fields. You can see here once we pass in, for example, a city name, here's what we'll get in return. We'll
get some we'll get a name for the local name, you know, in those particular languages. And then what we actually
languages. And then what we actually want, the latitude and longitude. So, we
just spit in the city name and we get out the coordinates. So what this implies is much like how we're doing the fetching from the one call API for the weather information, we'll do the exact
same for a function that calls this geocoding API. Where we did that
geocoding API. Where we did that originally was in the API.ts file. So
let's go back to this and all we'll have to do here is make another function just like get weather that calls this particular geocoding endpoint instead of the one call endpoint. So what it will
call me, we scroll up past this. Let me
actually minimize this here. What it
will actually call is this URL right here. What we'll do here is we'll go
here. What we'll do here is we'll go into API.ts. We'll make a new function.
into API.ts. We'll make a new function.
We'll say export async function. We're
going to call it get geocode to keep our naming conventions pretty similar. And
inside of here, just like the other one up there, let's actually take this.
Let's copy and paste it in here. But
instead of using this link, the same one we're using for get weather, let's take this exact link right here. So, we'll go ahead copy it and let's paste this in to our string instead of that. We'll do
this, paste it in here. And now, let's modify this to be what we want. As you
can see here in the docs, it says this Q can be a city name, state code, country code, whatever. We don't really care
code, whatever. We don't really care about all that. All that we're going to do is a city name. So, that's all we really need. So basically in this call
really need. So basically in this call here where we have that we're going to replace this whole chunk here this whole q equals chunk we're just going to replace this with dollar sign brackets
like this so we can inject something and we're just going to say location which will be an argument that we pass into this function that will just be a string. So for example, if I go into our
string. So for example, if I go into our dropdown and I click something like Tokyo, that will pass Tokyo as a location into this function, which will then get the latitude and longitude for
Tokyo by passing it in here. For this
limit right here, let's just hardcode it to one. Reason being is you can get
to one. Reason being is you can get multiple in the form of an array. I
don't really want that. I just want one match cuz I think it's sensible enough to assume there'll only be one source of information for any given city. And then
just like the other one where we have this app ID, we're just going to plug in our API key just like this. So we'll add the dollar here and we'll do API
key. And now we're passing our API key
key. And now we're passing our API key through to this API. The only last thing to do is we're already converting it to JSON. But what we're not doing is
JSON. But what we're not doing is parsing it through a correct schema.
We're still using the same schema as the weather information, which obviously we don't want cuz the response structure is different. So let's make a new schema.
different. So let's make a new schema.
Luckily, the schema that we're going to have for this is actually super simple and easy. I mean, there's only a couple
and easy. I mean, there's only a couple of fields here to actually take into account. So, what we'll do is we'll go
account. So, what we'll do is we'll go back to our file explorer here. We have
the schemas folder. Let's make a new one. We're just going to call it, I
one. We're just going to call it, I don't know, geocode schema.ts, just like this. And then we'll need a zod schema
this. And then we'll need a zod schema that matches this return shape from Open Weather. I'm again trusting my good
Weather. I'm again trusting my good friend Chad GBT here to generate me the schemas. Basically, all I did is just
schemas. Basically, all I did is just took this exact response shape from open weather and asked it to make me a ZOD schema. And this is what it spit out.
schema. And this is what it spit out.
And if we just quickly look at this, it looks like it's correct. I mean, we have name, which is a string. Lat and lawn are both numbers. Obviously, country is going to be a string. And state's also going to be an optional string. And then
local names we're not really going to use, so it doesn't really matter, but this does look to be correct. Now that
we have the schema, much like we did for our weather information, we'll parse that schema. So, if we go here, all we
that schema. So, if we go here, all we have to do is say geocode schema.parse
parse the data and we actually should be good to go. Now, if we save this, this function should be ready to take for a test drive. If we hover over get geood,
test drive. If we hover over get geood, you can see the response type is a promise because the function's async and it just has this structure. So, we know we're going in the right direction. All
right. Now, let's hook up this API function into tanstack query and get this ball rolling. If we just close out a few of these files real quick just to free up some space here, we head back
into app. We already have a Ustate that
into app. We already have a Ustate that stores our coordinates. Let's make
another ustate that stores the location that we've selected in the drop down. So
if I go here and I select Dubai, we should have a state that now holds Dubai. So what I'll do is I'll say const
Dubai. So what I'll do is I'll say const set location equals new state and it'll just be a string.
Let's just default it to Tokyo. We'll
assume Tokyo is always the default starting point. And then to geocode
starting point. And then to geocode using that function we just wrote, all we have to do is write a use query in here. So we'll say const get data out of
here. So we'll say const get data out of here equals use query. And like any other use query, we need a query key and a query function for the query key.
Let's say it is an array. We'll just say it's the string geocode. And we'll also pass in the location because again like I showed you with the weather information, we want to make sure if our query function is going to consume
location which it will. We need to have that be part of our query key. Now we
just need the query function which is the function that we just wrote which we called believe it was get geood. So we
will call it like this so that we can pass in arguments and we go back to this. Remember this just expects one
this. Remember this just expects one argument a location string. So we'll go here pass in the location because it is the string just like this. Now this
query should be good to go. Now if I go ahead and save this we can actually see what's going to happen. If I go over to our console here let's go to our network tab so we can see the request. Let's
just go ahead and refresh this. You
should see that we have this one call which is from the actual you know the weather information we've been working with. But we also have this other call
with. But we also have this other call right here direct. And this is the response that we're getting from the geocoding. So we can see if we look at
geocoding. So we can see if we look at the headers, it is the geocoding one.
We're passing Tokyo in the uh request URL here. So if we go to response, we go
URL here. So if we go to response, we go down to latitude and longitude. Boom,
there they are. Jackpot. So now we know that we can pass in any city name, at least any city name that Open Weather recognizes, which it definitely should recognize all the major ones. And we
know the response from this geocoding API will give us the coordinates of wherever we want. And that is going to be huge because as we know our weather information requires numerical coordinates, not just names. So now we
can use these coordinates that we're getting back from the geocoding. Now
that we have the latitude and longitude from the geocoding, that means there's two possible spots these coordinates could now come from. Either they come from the geocode API if we click on a city from the drop down or they're
coming from the map click if we're looking for our own custom location.
What that means is that this chord state right here needs to accommodate for both. How do we know whether we should
both. How do we know whether we should use the coordinates from the geocoding, so from the drop down, or whether we should use it from the map click? An
easy way to do this would be to check and see if this location string right here is actually a city name. My thought
is what we can do is in the on map click, we can set location just to custom like this. Because every time we click on the map, we know we're not finding the information for a city from
the drop down. So, let's just set it to custom each time we click it. What this
will allow us to do is define which coordinates we want to pick. If it's
custom, use the map coordinates. If it's
not custom, use the geocoded coordinates. And that's how we're going
coordinates. And that's how we're going to differentiate them. So down here, what we can do is let's actually rename this chords to just be coordinates instead because we're going to redefine
chords down here as a derived value. So
we're going to say con chords and we're going to make this a turnary. We're
going to check the location and we're going to say, okay, if the location is equal to custom, then what? If it's
custom, we know that we're coming from a map click and not the drop down.
Therefore, we should just use this coordinates value up here. So, let's say if we're on custom, use coordinate.
Oops, use coordinates just like this.
Otherwise, if we're not on custom, that means we click somewhere on the drop down. So, let's use those instead. And I
down. So, let's use those instead. And I
just showed you if we go to the network request again just so we can double check this. If I just close out of this
check this. If I just close out of this real quick, refresh and I look at one of the geocoding calls, we have the object and then we have the latitude and longitude. So if we click somewhere from
longitude. So if we click somewhere from the drop down instead of using coordinates, which is for the map, we can actually do is set this equal to an object and just say we want this to be
latitude and the lat comes from data.
It's an array so it'll be the zero index there and then latin lawn. So that's
going to be data at the zero index dot latitude. And then the longitude is
latitude. And then the longitude is going to be data at the zero index.
Just like this. And because data can be undefined, we'll technically have a type error here saying possibly undefined.
Let's just add a question mark dot to this just so we suppress that error. And
now what we're saying with this coordinates, the coordinates that we're passing down to our components is that we're either going to use the coordinates from the map or we're going to use the coordinates in the geocoding which is from the drop down. However,
now because we added this question mark dot here, it's going to say that our coordinates can possibly be undefined.
So it's going to throw a little type error here. So what we can do in that
error here. So what we can do in that case is we can say if these are for whatever reason undefined, let's just make them both fall back to zero. This
way they're at least always going to be a number. And that should make
a number. And that should make everything happy. So if we save it and
everything happy. So if we save it and refresh, that should make our map return. And it looks like we keep
return. And it looks like we keep getting this error kind of crashing our page here. And it's a Zod error saying
page here. And it's a Zod error saying that for the weather information, it's expecting wind gust to be a number, but it's undefined. So let's go to that
it's undefined. So let's go to that schema real quick. We go to weather schema. Look at wind gust. It looks like
schema. Look at wind gust. It looks like this can maybe be optional because we're getting undefined for some of them. So
let's just add optional to this. Go
ahead and save it. And hopefully that should fix our page crashing issue. Just
make sure it won't freak out for a second. Hopefully it shouldn't. And
second. Hopefully it shouldn't. And
there we go. Seems to be fixed. And
before we do anything else, just so this code is a little bit more clear on what we're doing, let's not just have this be data. Let's actually rename this to uh
data. Let's actually rename this to uh we'll say geocode data just so it's obvious that whenever we're getting this latitude and longitude, it's coming from the geo code in case you're reading it over. Now, we have just one more thing
over. Now, we have just one more thing to add. We're setting location to Tokyo
to add. We're setting location to Tokyo by default. And we're setting it to
by default. And we're setting it to custom whenever we click anywhere on the map. But one thing we're not doing,
map. But one thing we're not doing, which is arguably the most important part of this dropdown, is actually setting any state when we click one of these. So if for example, I go in here
these. So if for example, I go in here and click London, nothing actually happens on any of our cards or anything because, well, I'm not setting any state on the clicks. To give context to our dropdown on what the current location is
and how to set it, let's actually pass down both the state itself and the setter function as props. Meaning the
location and set location into the location dropdown. So what we'll do is
location dropdown. So what we'll do is we'll say location equals location and then set location equals set location just like this. Our location dropdown
doesn't expect those props right now. So
let's go into that component and we will go ahead and add them. So we'll say location is going to be a string and then set location is just going to be a
type of dispatch set state action. This
is how setter functions are normally typed in react and it is setting a type of string. Import dispatch from react
of string. Import dispatch from react and then these should be good. Now we
can pull both the location and set location out of here as props. To make
sure our select knows what value it should be displaying is, we can just go into the select. We can give it a value and make sure the value is just set to whatever our current location is. So,
it's either going to be a city name or it's going to be custom. And then we can take a step further and on this same select, we can give another prop that is just on value change, meaning each time we click one of the options, what we can
do is we can get the value out of here.
So, basically whatever we clicked on and this value right here is going to be the value of whatever is clicked. So for
example, if we click London because we have the value equal city here, this value will be London. So what we can do now is use the set location prop we just passed down and we can say set location
with whatever value we just clicked on.
Meaning if I set for example Dubai in this dropdown, now that will set the location state in app to Dubai. If I now go ahead and save it, we can actually see this directly in action. You can see
first of all now we're actually hovering right over Tokyo. If I let's just zoom out a little bit. I want to go to Dubai.
Boom. Our information updates. If I want to go to Rome again, our information updates. So each time I'm doing it, you
updates. So each time I'm doing it, you can see the data changes. And that's
because we're switching which city we're fetching data for by clicking an item in this drop down since we're now setting the state. However, there's immediately
the state. However, there's immediately one problem that I'm noticing. The main
problem being that whenever I select a new city from this dropdown, nothing happens with the map. It looks like the data updates right away down here, but the map just stays exactly where it was.
it never changes. And I think the way we can fix this is actually super easy. We
can just do it in a oneliner. We know
that whenever we click a location from the drop down, the coordinates are changing no matter what. So if I have Dubai selected and I click London, we know the coordinates are going to change. And we know that when the
change. And we know that when the coordinates change, the map component is going to rerender to reflect the coordinate changes. So why isn't this
coordinate changes. So why isn't this map updating? Well, what I would presume
map updating? Well, what I would presume is that if we go into the map component, I would guess that because this map container is kind of just a generic wrapper around the vanilla JavaScript
library of Leaflet, it might not consider this center right here changing to be a valid enough reason to trigger a rerender. So, we can actually do it
rerender. So, we can actually do it manually. A little hack I like to use to
manually. A little hack I like to use to force component rers is to throw a key on it. If the key of a component
on it. If the key of a component changes, React is 100% going to rerender the component. What we can do is on this
the component. What we can do is on this map container, we can actually throw a key on it. And let's just make this a string. Let's just do um I don't know
string. Let's just do um I don't know chords.lat,
chords.lat, chords.lon, just like this. Just so it's
chords.lon, just like this. Just so it's a string. This way we can be 100%
a string. This way we can be 100% confident that whenever coordinates change, this string right here, this keystring is going to change. Therefore,
the map should rerender. So, if I save that now and I refresh just to start clean here and I click on one of these items, now our map should go to wherever we're wanting to go to. And it looks
like now it's working. Our map is rerendering. However, I'm sure you're
rerendering. However, I'm sure you're noticing this little problem here. This
flickering thing. Anytime we go somewhere, it flickers to looks like the middle of the ocean first before it goes to wherever we're actually wanting to go to. I'm noticing a potential problem
to. I'm noticing a potential problem actually now with this key approach because if I click a location on the map, it's always going to rerender the entire component no matter what, which is going to force that little flickering
that we're seeing. And this happens really no matter where we go because this key that we have is actually forcing the rerender. So now we're always just going to be flickering, which is honestly super annoying and we don't want that. So let's rethink this
key approach. I think I've got an idea.
key approach. I think I've got an idea.
Let's remove this key from here and instead of rerendering the entire component, let's just call the map.panto
function that we have right here directly in the component body of mapclick. After all, we know the only
mapclick. After all, we know the only way that mapclick can rerender itself is if coordinates change because coordinates would be a prop. So, what
I'm saying is what we can instead do to fix this little issue that we're having is we are already getting the coordinates passed down here. What we
can do is we can actually also pass coordinates down to the map click. So
let's pass chords like this. Let's make
sure we get chords out of this component. And we'll say the chords RF
component. And we'll say the chords RF type chords here. And let's actually take this map top pan out of the click.
So we'll move it up here. And instead of using this lat and longitude, we don't have those anymore. Let's just use the coordinates directly. So we'll do
coordinates directly. So we'll do chords.lat. And we'll also do chords.
chords.lat. And we'll also do chords.
itude and actually it's not longitude it should be um believe it's just lo. Now
what I've just done with this is I have made it so that whenever the coordinates change now mapclick is actually going to rerender itself because coordinates are a prop and we know in React whenever
props change a component rerenders. And
the reason why it's fine to have this map.panto pan to and just the component
map.panto pan to and just the component body and not actually in the click event or in like a use effect or anything is because this map should only ever
rerender when the coordinates change.
And if that's the case, map click will also only ever rerender when coordinates change. Therefore, we assume that
change. Therefore, we assume that anytime that coordinates change, we need to pan to a new location anyways. So,
it's fine to just run this code in the component body. In fact, that makes more
component body. In fact, that makes more sense and makes it less complicated than having to do something like a use effect, which I generally cautioned against. Now, I can show you because I
against. Now, I can show you because I just saved it. If I uh refresh this, we start in Tokyo. Let's go to somewhere on the map. Click, and boom. Now, we get
the map. Click, and boom. Now, we get the exact behavior you want. No more
weird flickering thing happening on this page. If I go here, select something
page. If I go here, select something like, I don't know, Manila. Notice how
this case, we actually are still having a bit of a flicker. only when we select something from the dropdown, but not when we select a custom location. The
reason this is actually happening and we're getting this weird flicker effect is that because when I click on one of these locations, it is immediately setting the location to the new string.
So when I click somewhere, it's immediately setting location and this updates right away. However, the problem is this get geocode function is asynchronous, meaning it takes some time
to actually resolve it. So basically for a split second after we set location this is still running. It's still in flight. So the actual geocode data that
flight. So the actual geocode data that has latitude and longitude is undefined.
So basically for a split second we fall back to 0 0 for our coordinates because we have this fallback here. And then it becomes defined. So that's why it's
becomes defined. So that's why it's causing the flicker. So if I click somewhere here, let's refresh it. I go
here like London, it's going to flash for a second because it falls back to 0 0 and then it resolves and then goes to London. So, it's kind of this really
London. So, it's kind of this really weird flashing issue, but it makes sense why it's happening. However, we're not going to fix this quite yet. We're going
to address this once we take a look at the skeleton loaders. Once we set up proper suspense boundaries and skeleton loaders, this shouldn't be a problem anymore. Let's move on to the next
anymore. Let's move on to the next thing. There's one more thing I want to
thing. There's one more thing I want to add to this map real quick to allow it to give us way more information than it currently is. If we go back to Open
currently is. If we go back to Open Weather, one of the APIs they have you can access for free is called weather maps. If we go into the dock for weather
maps. If we go into the dock for weather maps here, there's something actually really cool that we can do. So over
here, you know, you have the map of the world and they have different types of map you can layer on top of your existing map. So for example, a cloud
existing map. So for example, a cloud layer would look something like this.
Precipitation layer, sea level, wind speed. There's a few different layers I
speed. There's a few different layers I think that if we added to our app could make it really cool. And this actually is not nearly as hard as it might look.
This is super super easy and we can get added to our app very quickly.
Basically, all we have to do is in here, it gives us a URL that we can use. It's
this URL right here. We can actually just copy this URL directly. And what we can do is if we go to our map here, we can add a new component in here. See, we
have this tile layer here that's doing our actual map. We can actually just make another one of those. We can say tile layer just like this. And in the same way this has a URL, let's take same
thing. URL equals this. We'll use backit
thing. URL equals this. We'll use backit characters and go ahead and paste this in. All we need to do now is to pass
in. All we need to do now is to pass something in here for our actual layer and then pass in our API key. And it's
really that simple. So if we go to the API.ts file, we have this up here.
That's how we got our API key. Let's
actually take this exact thing and add it to the top of this file. We'll say
API key. And in this tile layer, we'll use that same API key just like we have for everything else. say API key just like this. And now we just need to
look at this layer right here. If we go back to the docs, it says that layer is the layer name. By layer name, what that is is this. So, for example, the cloud
layer is just clouds new. Precipitation
layer is precipitation new. So, those
are the layer names we need to pass in to this layer right here to get the actual layer that we want. So, now the question is, how do we know what to actually pass in for this layer here?
After all, we're inside of our map component, but like we have no context of knowing what layer the user wants to see, especially if we want to support all these different layers. Well, what I was thinking is we already have a drop
down to select the city for different cities across the world. What if we just use the exact same component and do the same for map type? What I'll actually do for this is I'll go to our dropowns folder. And when I said there'd be
folder. And when I said there'd be another dropdown, this is what I was meaning. We'll make a new dropdown. We
meaning. We'll make a new dropdown. We
will just call this map type dropdown.tsx.
dropdown.tsx.
And I'm actually going to take all the code pretty much verbatim from location dropdown. I'm just going to copy it. I'm
dropdown. I'm just going to copy it. I'm
going to paste it in here. And we'll
just make sure to rename this one to map type dropdown. And then instead of
type dropdown. And then instead of taking in location and set location, we'll just say map type. And then this one here is just going to be set map type as such. And we'll make sure these
just get those out directly. Let's make
sure this is also map type here. Rename
everything. This is set map type. And we
can actually get rid of these. These are
now redundant.
And now we essentially have functionally the same dropdown as we have for the cities, but now just for map types. If I
go and save this for right now, I'm going to go and add it to our app here.
We'll just go pass in map type dropdown. Now let's go down here. And we have locations. But
down here. And we have locations. But
obviously in here, we're not calling locations anymore. Let's just call this
locations anymore. Let's just call this just call this new array types. Instead
of having a bunch of city names here, let's just put the types that we're getting from open weather. So again, I think there should be uh five types in here. Let's just copy each of these
here. Let's just copy each of these down. So we have clouds new. Actually,
down. So we have clouds new. Actually,
we'll just uh this whole thing here, copy it, paste it. So we have clouds, new, we have precipitation, new,
we have pressure, new, wind new.
And I think this is the last one, temp new. So now here's the types that we are
new. So now here's the types that we are mapping over. And it's as easy as that.
mapping over. And it's as easy as that.
Now we have a functional dropdown that can set a map type state on each click just like we're doing for the map location. What I'm going to do here is
location. What I'm going to do here is I'm going to go ahead and save this file. Let's go back to our app here and
file. Let's go back to our app here and just so we can render out both the map type and the location dropdown. We're
going to control shiftp around this div here around this uh dropdown. wrap it in a div and we're going to give it a class name of let's say flex gap 8. That way
we can render out both the location dropdown and the map type dropdown side byside. The only thing left to do now is
byside. The only thing left to do now is to create a state to hold our map type just like we're doing to hold our location. So what we'll do is actually
location. So what we'll do is actually we'll take kind of this same format here. Copy it, paste it. We're going to
here. Copy it, paste it. We're going to call this map type right here.
Map type. And then this will be set map type as such because the very first map type that we see in here I believe is clouds or should be clouds new. Let's
make that our default state. So let's
say instead of Tokyo, we'll make this clouds new. And then because we made our
clouds new. And then because we made our dropdown the very same format as we did our location dropdown. We can just pass in map type like this. That will be map
type. And then set map type is the exact
type. And then set map type is the exact same thing. And now our dropdown is
same thing. And now our dropdown is happy. Let's go ahead and save this file
happy. Let's go ahead and save this file here and look over. And now we should have both the location dropdown and the map type dropdown. What's nice about using these reusable shaden components
is that once you get the component set up for one use case, getting it set up for the next is super super simple. As
you can see, getting this map type thing was pretty self-explanatory. So, because
we copied the exact same format, now we know that whenever we click one of these items in the dropdown, we are setting this map type. And this map type is what we need to pass down to our map to plug
in to this layer right here. So, let's
go into map here and we'll actually add that to our props. We'll say we want to pass in the map type, which is going to be of type string. And then we will dstructure that from our props like
everything else. And then instead of
everything else. And then instead of passing in layer right here, we can add a dollar sign and inject the map type directly. And now we're all set. It's
directly. And now we're all set. It's
really that simple. So I'll go ahead and save it. And I'll just be sure to now
save it. And I'll just be sure to now pass in the map type to our map. Oops,
auto formatting is a little weird. Pass
in map type like this. Save it. And look
at this. We just got a layering over our actual map that has for right now the precipitation. If I want to do it as the
precipitation. If I want to do it as the clouds, boom, here's what the clouds look like. We can do pressure. This is
look like. We can do pressure. This is
what pressure looks like. And we have super easily just created another layer on our map to show some more information. Super super cool. Every
information. Super super cool. Every
single one of these gives me a new unique map. And what's really cool is
unique map. And what's really cool is you'll notice because both the tile layers that we have in map, so meaning the actual map itself that's right here and the layer for the clouds or precipitation pressure whatever
because they share the exact same parent and the way they're positioned. If I
zoom in, notice how everything still stays in the correct spot. It's not like when I zoom in or zoom out, the actual, you know, layering of the wind is going to change and shift around. They all
stay relative to each other. So whether
I'm grabbing and moving around or I'm scrolling in and out, they should always stay in the same spot, which is exactly what we want cuz that's how map layering should work. One thing, however, we
should work. One thing, however, we should fix real quick is the names in this drop down. The open weather API needs the names in this format right here. But if I were a user and this is
here. But if I were a user and this is the format of the strings I saw, I definitely think it's a bit weird design, right? Because if I want to see,
design, right? Because if I want to see, you know, a wind map, I don't want to just click on win new. It doesn't really make a whole lot of sense. To make these say their actual names and not this
whole, you know, API format of names with an s new, whatever. We can actually let's close out of these tabs here and let's go back to our map type dropdown.
We can actually do a little bit of a trick here. First of all, we can throw a
trick here. First of all, we can throw a class name on select item here. We'll
throw a class name and we'll add capitalize. That way, we just capitalize
capitalize. That way, we just capitalize the first letter of each thing here. So
now if we go into this dropdown, the first letter should be capitalized of each one. And now for this trick I was
each one. And now for this trick I was talking about, the names we want to display to the user should ideally just be the part of the strings that come before the underscore. For example,
precipitation new should just say precipitation. Pressure underscore new
precipitation. Pressure underscore new should just say pressure. Basically on
each one, just get rid of the underscore new. So for what we're actually
new. So for what we're actually displaying to the user here, which is this value right here, we can do something super super simple. We can
saysplit and we want to split the string on the underscore character. That way it will
underscore character. That way it will split it into an array. So for each thing if we have for example clouds new this city.split at the underscore we'll
this city.split at the underscore we'll make it an array with the first string being clouds and the second one being new. And since we only want the first
new. And since we only want the first part of that string, we can just take it at index zero. Meaning if we're take meaning if clouds new is what this city is, this will split it and we'll only
take clouds. So now if we go ahead and
take clouds. So now if we go ahead and save it, look at here. It just says clouds precipitation pressure wind and temp. Just the way that we want it.
and temp. Just the way that we want it.
What's cool with this method of doing it is that the user is going to see the actual names here, but whenever we click one of these, the value is still just the city. You know, we're not doing any
the city. You know, we're not doing any sort of split on this. So the API still has the correct value for it. The user
just sees something that's a little bit more formatted. And now what this means
more formatted. And now what this means is that we have a fully functional map layer dropdown that allows us to stack different map layers on top of the existing map. I can see where any clouds
existing map. I can see where any clouds in the entire world are, what the pressure is like, what the precipitation is like, and you get the picture. The
last thing I want to do with these dropowns before we go hands off is just a slight modification to the location dropdown. I noticed a second ago that
dropdown. I noticed a second ago that whenever we click a custom location on the map that it just blanks out the location dropown up here. And that's
because whenever we do a custom location, this doesn't match any city name. Because whenever we set custom
name. Because whenever we set custom location, we're setting it like this.
And because we're plugging location into our location dropdown, just like this as the value, it's going to see custom. And
custom doesn't match any of these. So
therefore, it just goes blank. A super
quick fix for this is inside the location drop down here outside of where mapping over locations, we can just make a one-off select item. So we'll make a one-off just specifically for custom. So
we'll say select item just like this.
And actually, no, it can't be self-closing. It'll have to be like
self-closing. It'll have to be like this. It'll say custom
this. It'll say custom and then the value itself will actually be custom. And this doesn't need to be
be custom. And this doesn't need to be an expression. We can just have it be a
an expression. We can just have it be a raw string. And basically, we'll only
raw string. And basically, we'll only render this out if we're on custom. So,
we'll say if location equals custom, then we render this. Otherwise, don't
show it at all because we don't want it in there. So, now if I go ahead and save
in there. So, now if I go ahead and save it, now it should say custom in here. If
I select an actual name like London, there's no custom in here. But as soon as I click somewhere in the map now, boom, it shows custom in the drop down.
I think that's a bit more helpful to the user than it just blinking out cuz it looks like an error happened or something. Lastly, just so the user
something. Lastly, just so the user knows which dropown is which, let's put a label by each one. So, let's go close out of these, go into our app component here, and we have this div surrounding both dropdowns. What we can do is take
both dropdowns. What we can do is take each one of these drop downs and let's wrap each of them in their own div. So
we'll say wrap. We'll do div here. And
for this one, we will say the class name is going to be flex gap 4. And right
above the dropdown, let's just make an h1 for the actual label itself. And this
will just say location. And we'll do the exact same thing for the map type dropdown. So we'll go here control
dropdown. So we'll go here control shiftp wrap wrap it a div give it the exact same class name
as such and we'll take this same header and instead of saying location it'll just say map type just like this. Now if
we go ahead and save it boom we have some labels for each one. Let's do a bit more styling on these headers. Let's say
for these we'll give it a class name of I don't know say text 2XL and then font semibold. That looks quite a bit better.
semibold. That looks quite a bit better.
Let's just copy that and put the exact same thing on the other one. So we'll
take class name put it on here and boom.
Now we have this for both. And I'm
actually zoomed in a bit on my browser right now. So let's zoom out so it
right now. So let's zoom out so it doesn't do that weird wrapping thing.
Let's zoom out so it looks more like this which puts other cars a bit more in perspective here. And then now that we
perspective here. And then now that we zoomed out a little bit, actually I want to change our map to be a bit bigger than it was. So let's go back to our map. And now let's see if we change it
map. And now let's see if we change it back to a thousand and see if it looks good again. Go ahead and refresh this.
good again. Go ahead and refresh this.
And yeah, there we go. This looks quite a bit better. We're going to get to some more detailed styling later on. But
until then, we're going to leave these drop downs alone because they're actually working great right now. I can
select any city I want, get the location and everything for that city and all the weather information. And on top of that,
weather information. And on top of that, I can see the different map types you want for any sort of map layering. So, I
have the freedom to do quite a bit, which means we've honestly come a super long way since the beginning, but we're not quite done yet. The next thing I want to address also involves the map.
And for right now, it's pretty clear that this default looking map, like this kind of Google Maps looking thing, I don't think looks very good considering the color scheme of our site. Like this
is all kind of a dark mode. you know,
the zinc color palette that Shad Cien selected and having this like Google maps like greenish blue just does not blend in with the rest of our app at all. This map right here is just the
all. This map right here is just the default map from Open Street Map. But if
you want to get a fancier looking one or one that matches our theme a little bit more, we can use a service called Maptyler. If we head over to
Maptyler. If we head over to openmaptiles.org and then we scroll down, there's this dark mode map we can use right here that I think would look really cool in our
app. It looks something like this. So,
app. It looks something like this. So,
it's this dark mode theme that I think definitely matches kind of this color scheme way more than this, you know, this default looking one does. So, let's
get this added to our app. To add a map like this, we'll first have to install the SDK for map tyler. We can actually do that pretty simply by going into our terminal here and running the command
mpmi and it's going to be at mapaptyler/leaflet.
That's a leaflet extension. Maptyler SDK
just like this. If we install this, oops, looks like we forgot the uh the in on mpm. Let's uh let's run that back.
on mpm. Let's uh let's run that back.
Add the in here. And now it should actually install the map tyler SDK. Now
we can close this and go back to our map component to add this custom cooler looking map to our map component. It's
going to be made easier if we have access to the use map hook just like we do within the map click component. So
instead of rendering out this tile layer right here that's just doing the default tile layer. Let's make a new component
tile layer. Let's make a new component down here that we're just going to call say function map tile layer just like this. It won't take in any props or
this. It won't take in any props or anything. This component will be super
anything. This component will be super simple just like map click. We're going
to make it return null because it's purely for functionality not for actually rendering JSX. We'll do the same thing that we're doing up here by using the use map hook. I'll say con map
equals use map. And to mount this tile layer properly, we can use a use effect.
So we'll say use effect just like this.
Make sure we import use effect into our code. And I generally caution against
code. And I generally caution against using use effects. But when interfacing with straight JavaScript like in the case of this tile layer here or interfacing with external libraries, it's generally okay to have a use
effect. The way we can display this new
effect. The way we can display this new cooler looking map is if we make a variable inside of this use effect. say
const tile layer equals new map tile layer just like this. That's going to be from the SDK that we just installed.
This will be a function that we can call inside. We pass in an options argument
inside. We pass in an options argument and it needs two options on here. The
first one is style. So the actual type of map that we have and an easy one that actually looks really similar to this this map you're seeing right here, but is actually quite a bit more lightweight than it and will be a little bit faster
is just one called basic dark just like this. So, you'll see what it looks like
this. So, you'll see what it looks like in a second, but we'll use this for our style. And the second argument it needs
style. And the second argument it needs is an API key. Just like Open Weather, Map Tyler wants you to use an API key if you're accessing its custom maps. And
don't worry, you don't need to enter in any payment information or anything.
It's completely free. You just need an account to get an API key on Map Tyler's website. So, over here, all you need to
website. So, over here, all you need to do is log in, which I will just do now with one of my Google accounts. Once
you're logged in, you can just go to this menu here, go to API keys, and you can generate an API key. Once you have this key here, just go ahead and copy it. And then we go back to our options
it. And then we go back to our options here for map tile layer. All we have to do is say API key just like this and pass in the string you just copied. Now
that we have this, all we need to do is use this new tile layer component. We'll
just say tile layer and call this method called add to. And this is where we pass in our map. So I'll just pass in map just like this. It'll just add this layer on top of our map making it look
like this basic dark style. Let's save
this. Get a little bit of formatting here. And working with use effects,
here. And working with use effects, especially that involve external libraries, it's almost always a smart idea to have a cleanup. So, what we'll do at the very bottom of this use effect
is we'll just say return.
And on return, you want to say map.ove
layer. And we want to remove the tile layer as such. That way, we're just being safe with how we're adding this tile layer to the map. That also means the dependencies of this use effect
would simply be map. Now that we have this, let's go ahead and save this.
And instead of rendering out tile layer up here, like I said before, like this is this is the default tile layer that we're seeing that's getting the Google Maps kind of look. We're going to replace it now with map tile layer,
which like I just said, will now supplant this map tile on top of it to give it that new look. So if I go back here, save it. Boom. Now our map is
dark. As you can see, this matches our
dark. As you can see, this matches our actual theme a lot better. I am noticing though on this particular layering. So,
the clouds layering these uh these pretty bright white clouds are looking kind of kind of harsh against the map.
It's kind of covering up a lot of space here. Like I can't really see anywhere
here. Like I can't really see anywhere in Europe right now. What I'll do is actually go to this tile layer that we have here that we're doing for the map type. And we can actually add an opacity
type. And we can actually add an opacity on it. One of the props it supports. And
on it. One of the props it supports. And
let's just do 0.7. Maybe that makes it a bit better just so it's easier to see.
not quite so bright. And just for the sake of experimentation, if we click through some of our other map types, this is what they now look like against this dark backdrop. So, it still looks really, really clean. And like I said, I
think it just looks way better and way more kind of in place than the other map that we had was. Okay. Now, right now at this point, this is pretty much for the most part a fully functioning app on its
own. We have map interaction that's
own. We have map interaction that's hooked up with data queries. We've got
map layering with different weather types, the ability to see all sorts of different information, and a few other things. A lot of the hard work is done.
things. A lot of the hard work is done.
There's just a few more things I think we can add to this app to make it even better, along with polishing up what we've already got. With that being said, what's the next thing I think we should do? I think we should do one final thing
do? I think we should do one final thing with this map before we just stop touching it forever. To finish it out, I think one thing that would be good is adding a legend showing what each of these colors mean. Because for example,
if I go in here and I go to the pressure map, I really have no idea what this pressure map even means. I mean, I can assume that the darker colors probably mean higher pressure and the lighter colors mean, you know, lower pressure,
but I really don't have context as to any sort of units or what I'm actually looking at. Most maps that look like
looking at. Most maps that look like this with some layering generally have a legend to show what numbers each color represents. If we go back to the
represents. If we go back to the documentation for the weather maps 1.0 I know that we're using up here there's this map styles legend and it says we have default styles for weather layers.
So if we click on it, it'll actually take us to this page here that I can zoom out a little bit, but basically it gives us the actual legend values for each different map type. So for rain,
snow, clouds, whatever. These are the actual numbers you could use for the colors. What this does is gives us an
colors. What this does is gives us an actual quantitative way to make a legitimate legend. It's essentially just
legitimate legend. It's essentially just giving us the color spectrums that we can use for each different map type. Not
going to lie, I'm too lazy to do this on my own. So, I'm going to take all of
my own. So, I'm going to take all of these numbers here, each of these objects, plug them into chat GBT, and ask it to make me one big record that has all this data in it that we can use to make our legend. So, let's go ahead
open up our file explorer here. Go to
components, and let's make a new one. We
will just call it map legend.tsx for our legend. tsrfc to make the component. And
legend. tsrfc to make the component. And
I'm going to paste in what chatbt actually generated, which is this kind of massive object right here. This might
look like a ton of complicated data at first, but it's really not too bad. It's
basically just one big object here or a record where the key for each thing is just a string, which is the map type.
And then the value is just another object that has some more information like the title of whatever we're looking at, the unit, and what the stopping points are for the colors. And we have this for each map type. So we have one
for precipitation. We got one for
for precipitation. We got one for temperature, clouds, and you get the idea. The title we'll use to just
idea. The title we'll use to just display something for each legend. The
units so it can actually have a unit number. And then the stops are how we're
number. And then the stops are how we're going to generate the gradient for what the legend actually shows. And of
course, I could have done this by hand, but I promise you that Chad GBT can make this map type data record way faster than I ever could, so I'm happy with it.
The first important thing that we'll need in this new map legend component is the actual map type. Since we have this big record of map type data, we need to know which map type we're actually looking at for our legend. So just like
we're doing for the map itself, let's pass the map type down as props. So we
go in here, we'll say map type just like this. And that will of course be a
this. And that will of course be a string and then we will extract map type out of the props. And now to get the corresponding data for the legend because we have this big record already,
all we have to do is say const data equals map type data at any given map type. So for example, if we pass down a
type. So for example, if we pass down a map type of precipitation new, then this data is going to be equal to this whole object here because we're referencing this map type data at that key. So we
get the value. Now we just need to build out our gradient for the legend. One of
the ways that we can do this is to map each stop point to its color and also exactly where it should be on the legend. So to do that, what I mean by
legend. So to do that, what I mean by that is down here below data, let's make a new variable. Let's say const gradient stops. is going to be equal to data dos
stops. is going to be equal to data dos stops. So we're just grabbing this array
stops. So we're just grabbing this array here and for each one we want to map. So
we're going to say mapap and then we'll say for each stop what we want to do is we want to make a template string since we're plugging this into CSS and we're going to say stop.c color just like
this. However, for gradients to work
this. However, for gradients to work right in CSS when we have a bunch of them together, we need to give each gradient a position that is percentagebased. Meaning the bottom
percentagebased. Meaning the bottom value should be at 0% positioning in the gradient and the top value for example 140 for precipitation should be at 100%.
And that goes for all these different types. This first entry here will always
types. This first entry here will always be the 0% point for the gradient and the last one will always be the 100% point for the gradient. Setting the percentage position for each stop is how CSS knows where each color is actually going to
go. The way that we can do that in here
go. The way that we can do that in here where we're mapping is right after we're doing our stop. We can actually separate this by a space and we'll do another expression here. And the way that we can
expression here. And the way that we can actually get the percentage is by doing the current value divided by the max value. That will give us exactly where
value. That will give us exactly where we are. Let's first find the max value.
we are. Let's first find the max value.
We know the max value for any given set of stop data is always the value of the very last array item. For example, for this stops here, we know the highest value is always going to be 140. For
temp new, we know the highest value is always going to be 30. Luckily, the way chat gbt generated it and the way it was in the open weather docs goes from lowest value to highest value. So that
makes our job quite a bit easier. What
that implies then is that the max data we can say const we'll call it const max value equals data.s stops and it's going to be data.s stops.length minus one to
get the very last one and it's going to be that value. So exactly what I was just saying. And now what we can do in
just saying. And now what we can do in this expression is in here we can simply say stop.value value divided by max
say stop.value value divided by max value. We're going to multiply that by
value. We're going to multiply that by 100 and that whole thing is going to be a percent. So this might look a little
a percent. So this might look a little bit wackier, this whole gradient stop thing we're doing, but basically all it is is for each stop, we're just generating valid CSS that we can use for the gradient, which is this this whole
string here. This first expression, this
string here. This first expression, this first point is the actual color of the gradient, which we have here with each of the stops. And the second value is at what point in the gradient it should actually be, which is this. And that's
always going to be a percentage. So if
we take, let's say, for example, we go down here to clouds. This is easy because they're all, you know, 0 to 100 here. Let's say we take this 50 value.
here. Let's say we take this 50 value.
Well, for this particular gradient, we're going to say the color is this because we're just mapping over stop.c
color. And then we're going to take going back to 50 here. We're going to take that value, which is 50 divided by
100. So we do 50 / 100 which is 0.5 time
100. So we do 50 / 100 which is 0.5 time 100. So that's 50%. So essentially that
100. So that's 50%. So essentially that particular value is this RGB code here this 247247 255 and it's going to be at position 50%. That's what this thing is
position 50%. That's what this thing is doing here for every single gradient.
You'll see how I can use this whole string in just a second. The only last thing that I need is to convert this entire thing to a commaepparated string.
I can't give CSS just a JavaScript array of strings. It won't do anything. It
of strings. It won't do anything. It
needs to be commaepparated. So to turn this whole mapping thing, that whole array that I just made essentially into a string that is commaepparated, all we have to do is dojoin.
And we're going to join it at the commas. And now we should be good to go.
commas. And now we should be good to go.
Let's finally use this and get it implemented in our JSX. Let's scroll
down here till we have our JSX. And
right now, actually, it's up here. Right
now, we don't have anything. It's just
this div. So let's wrap this in parenthesis here. And for this outermost
parenthesis here. And for this outermost class name, let's give it just a couple of things. First, we'll make it be
of things. First, we'll make it be positioned absolute. And then we're
positioned absolute. And then we're going to say top four and right four.
Point of having this positioning is that when we render this out, it'll be in the top right corner of the map. We'll also
give it the same z-index of our map. So
we'll say Z1000 and also just width 48 to make sure it has a quantitative defined width. Let's now render this out
defined width. Let's now render this out and just see how it looks. So, we'll go ahead and save this file here. Go to our app. And what we'll do is we'll take our
app. And what we'll do is we'll take our map here, and let's wrap this whole map in a div. That way, the map and the map legend can actually share the exact same parent. We map this in a div here. The
parent. We map this in a div here. The
space for the uh the formatting. And
then let's give this div a class name of relative. That way, whenever we position
relative. That way, whenever we position our map legend absolutely, it positioned itself relative to the parent. And now,
right below our map, we'll just render out map legend as well, just like this.
Make sure we get it imported. And we
still need to pass in the map type. So,
we'll do the same thing there. We'll say
map type equals map type, just like this. And now, if we save it, you can
this. And now, if we save it, you can see obviously our map legend doesn't really have a whole lot going on because there's, you know, no styling here besides saying map legend. But we do see it say map legend here at the top right
of the map. So, it is there. Now that we know it's showing, let's get this JSX in order. So again, I wrap this in a
order. So again, I wrap this in a parenthesis. It's kind of annoying that
parenthesis. It's kind of annoying that Prettier seems to like want to reduce this to one line and you do a save, which is of a complaint I have with it, but is what it is. So what we'll do
first on this div here is we'll throw a couple of things on it. First, we'll do let's do rounded XL. Let's also give it a shadow and some padding. And let's
give it a background color. Luckily with
Shad CNN, when it exposed some of those CSS variables, one of the ones it uses is background. So, let's actually do BG
is background. So, let's actually do BG background. And I don't want it to be
background. And I don't want it to be 100% opaque because I don't want it to necessarily block the map behind it. So,
we're going to do /50 so it's 50% transparent. Go ahead and save this.
transparent. Go ahead and save this.
Now, you can see now we have this little view. Lastly, let's give it a tiny bit
view. Lastly, let's give it a tiny bit of a border for even more separation.
So, we're going to say border. And then
for the border, for the actual border color, there's also another variable that's that Shad Cian created called accent that I think will be good for this. So we'll say border accent. We'll
this. So we'll say border accent. We'll
also make this not fully opaque. We'll
make it slightly transparent. So we'll
say /70. Now save it. And it sticks out a bit more. This will be more noticeable when you get to light mode, but that creates just a tiny bit more of a separation from the actual map itself.
And now instead of doing 48, I actually want to change this to width 96. And now
that we have that, I think this will be good to go for this outer div. Now, in
terms of what we want to put in the JSX here, it should actually be pretty simple. First thing we'll do is we'll
simple. First thing we'll do is we'll put a title at the top, then the gradient bar below it, and then the values at the very bottom. Just three
simple things. We want this in a column layout. So for this outer div, we'll
layout. So for this outer div, we'll make this flex flex call gap 3 to give it that vertical layout. First things
first, we'll do the title. We'll make
this an H3 here. And this will just be the title. So we'll say class name. We
the title. So we'll say class name. We
want it to stick out. We'll say text SM font semibold. And then let's do text
font semibold. And then let's do text foreground, which should just be the opposite of background. So if the background is black, the foreground should be white. And for the title for each stop, we have this title right here, which makes everything super
simple. So all we have to do is in here,
simple. So all we have to do is in here, we have already theta. We're going to say theta.title. Now that we have the title,
theta.title. Now that we have the title, we'll make the div for the gradient bar.
So, we'll say div. And actually, this div is not going to be an open and closing tag. It'll just be a
closing tag. It'll just be a self-closing element. This div is where
self-closing element. This div is where we're going to use the gradients that we just calculated up here. To do that, all we have to do is say style, just like this, double brackets, make some space
for it, and we're going to say background. And we want this background
background. And we want this background to be the actual gradient. To do that in raw CSS, we can say something like linear dash gradient parenthesis. We'll
make it go from left to right. So we'll
say to right and then here we can actually pass in our gradient stops. All
this is saying is make the background color of this div a linear gradient that goes from left to right. And here we give it the calculated gradients. And
for some more styling here, let's throw a class name on this div. And we will just give it let's say width full so it takes up the entire width of the legend rounded XL. Give it a bit of a border.
rounded XL. Give it a bit of a border.
We'll do the same border that we're doing for the actual outer. So we'll do a border and border accent 70. And now
this should be good to go. If I go ahead and save it, it looks like it's not quite showing up. Let's give it a defined height first. So let's say I don't know height six. And now if we save it, we should see the gradient up
here. It's a bit hard to see it on this
here. It's a bit hard to see it on this white on black here. So, let's change it to something like pressure. And here you can see the gradient much more obviously. If I go to precipitation,
obviously. If I go to precipitation, same thing temperature. This one's
probably the best looking gradient in my opinion. But this is essentially what it
opinion. But this is essentially what it will look like. The very last thing is just to get the values at the very bottom here underneath this gradient div. For the sake of simplicity, we'll
div. For the sake of simplicity, we'll just use the first and last values. So,
underneath the gradient here, for example, for precipitation, we would just show zero on the far left side and then 140 on the far right side. To do
that, all we have to do is make a new div here. Let's give it a class name of
div here. Let's give it a class name of flex justify between to space them out between the left and the right sides.
And then for the text of these, let's just make it text SS. We want it to be pretty small. And then we'll say text
pretty small. And then we'll say text foreground. Same thing. If it's against
foreground. Same thing. If it's against a black background, the text right here will be this white foreground color. And
inside of here for both our first and our last, we'll make each one a span. So
we'll say span for the first one, span for the last one. First one is obviously just going to be data.s stops at zero.
That's the very first one. And we'll get that value. And then the other one will
that value. And then the other one will just be a very similar thing, but we'll get the very last one. So it's data at data.stops.length
data.stops.length
minus one. And now this span should have the very first value and this span should have the very last value. And
after each one, we'll just also have another expression here that just does data.unit separated by a space here.
data.unit separated by a space here.
We'll add that to both of these. Oops.
add that to both. And if we go ahead and save it here, now we have the units on the legend. So we can see the far left
the legend. So we can see the far left is -65 and the far right is 30. And now
our legend is good to go. We're
completely done with this component.
Little bit of calculation up here, little bit of styling, but nothing too bad. And now we got a nice looking
bad. And now we got a nice looking legend that applies to each different map type that we can use. Not only to make our map look a little cooler, but also to make it make a bit more sense.
Now that we have this nicel looking map and the nicel looking layers for the different map types and we have the map legend, let's finally put this map away for a second. I think we're done working on it for now. Let's move on to
something else. I think at this point in
something else. I think at this point in the video and at this point in the app building process, the next thing we should probably take care of is the skeleton loaders. Right now, when we go
skeleton loaders. Right now, when we go to anywhere on our map and we just click, notice how the data kind of sits at what it is already before it just flashes to the new data. So if I'm, you know, somewhere in India and I go and
click somewhere in Saudi Arabia here, the data stays still for a second and then just shifts. There's not really any sort of visual indication that we're fetching new data. If you're not familiar with what a skeleton loader is,
they're basically just some indication that your data is loading or fetching from some API or backend instead of using a loading spinner. If I go to the Shad CN documentation here, they actually have a skeleton component and
it looks something like this. And I'm
sure you've seen this before on other websites. This dimming and flashing
websites. This dimming and flashing behavior that we're seeing is what we actually call the skeleton loader. And
it should generally match the shape that your data will be in. And it looks way cleaner than doing a loading spinner.
Because if, for example, on our app, we went somewhere here and we clicked on some, I don't know, some area of the map and then we had a loading spinner on each card and then the data suddenly flashed in, that still doesn't look very
clean. However, having skeleton loaders
clean. However, having skeleton loaders and transitioning from a skeleton loader to the actual data should look way smoother. With that being said, there's
smoother. With that being said, there's a reason why in all of our cards on the dashboard, like for example, the current weather, hourly forecast, whenever, I made these all suspense queries instead
of regular use queries. If you have a component that uses use suspense query, you can wrap that entire component in the built-in suspense component in React, which will allow you to have a fallback component, meaning the
component that is rendered while we're waiting on the data to fetch. If we do our skeleton loaders the correct way, our skeleton components would be our fallback. And then once the data is
fallback. And then once the data is loaded in, it uses the actual component.
What this means is that the skeleton component should effectively match exactly the actual component in terms of shape and the way it looks. But wherever
there should be information, we would just have that pulse animation going to show that it is loading like how we have here. For example, if we go to our
here. For example, if we go to our current weather card, the skeleton loader for this card would probably still say current weather here. But
where we actually have text for the actual card like this that comes from the API, we would replace it with a skeleton loader until the data is actually there. The easiest way to do
actually there. The easiest way to do this, in my opinion, without completely muddying the components that we already have, would be to create brand new skeleton components that mimic the real ones in the way that I just mentioned.
So, we'll go inside of our file explorer here, and inside of components, I'm going to make a new folder that I will just call skeletons. And then in here, we want to make a component for each card. So, for example, for the current
card. So, for example, for the current weather, we'll make a new component that we'll just call it current skeleton.tsx,
just like this. And we'll do the same for the daily forecast, the hourly forecast, and the additional info. So,
we'll go ahead and make those. We'll say
this one is hourly skeleton.tsx,
this one is daily skeleton.tsx,
and then this one will be additional info skeleton.tsx.
info skeleton.tsx.
And then for each of these, we're just going to run ts RFC to get the base component out.
Do that for all four of these.
And what we'll do here is actually pretty simple. So for each of these
pretty simple. So for each of these cards, how we're going to make the skeleton is we're going to go into whatever card you want to make. So we'll
start with the current weather. So we'll
go current weather card. And we
basically want to take literally all of this JSX exactly because we want these components to look pretty much the same.
We're going to take it exactly, copy it, and we're going to paste that in here to our skeleton. We'll make sure to import
our skeleton. We'll make sure to import the card and then also the weather icon.
And now we should have an exact copy of the current weather. However, we know that in the skeleton, we're not going to have access to this data yet. But what
we're going to do to create the skeleton loaders is everywhere in this component now where we see data, we're going to replace that with a skeleton loader. for
the skeleton loader. Just like the dropdown, we're going to get that component from shad CN instead of having to write our own custom version. So
it'll be this component exactly. So to
add it to our project, we can run this command right here. Same as the select, we'll take it, go ahead and add it to our terminal here. And now once this
runs, we should now if we close this, look in our file explorer in the UI folder, now we have this skeleton.tsx
component. So we can use that in each of our skeleton loaders. Now, we'll use this component and go through every single spot in this component that has data and replace it with a skeleton loader. So, for example, this first
loader. So, for example, this first header two, we're going to replace this, just delete it, and replace it with a skeleton component. And we'll want to
skeleton component. And we'll want to make sure that it's an accurate size as to what our temperature text would be normally. So, we'll try adding a class
normally. So, we'll try adding a class name on it. And let's throw width 50, height 4 on here. And we'll just rinse and repeat this for each thing that has data. So, for example, this weather icon
data. So, for example, this weather icon also has data. So, let's take this.
Let's copy and replace the weather icon here. The weather icon was already
here. The weather icon was already hard-coded to be size 14. So, we'll say size 14 for this. And then we'll also give it rounded full to make it appear circular. This header 3 also consumes
circular. This header 3 also consumes data. So, let's replace this with a
data. So, let's replace this with a skeleton. We'll make this one slightly
skeleton. We'll make this one slightly smaller in width than the top one. We'll
do with 30 here instead of 50. We'll
take this skeleton right here. We'll
replace this H3 with that. And then
we'll do the same for each of our P tags down here. We'll just replace all of
down here. We'll just replace all of them as such.
And now we're not consuming data anymore in this component. Everywhere we had data, we replaced it with a skeleton loader. Now what we'll do so we can see
loader. Now what we'll do so we can see this is go back to our app over here.
Let's go ahead and save this file. And
now we'll go back to app. And before we wrap this in a suspense and get the actual fallback stuff working correctly, let's render them out side by side just so we can see a visual comparison and you can kind of get an idea of what I'm doing. I'm going to render it out right
doing. I'm going to render it out right under here. We're just going to call it
under here. We're just going to call it current skeleton like this. And again,
it's a dump component. It doesn't need any sort of data. So, if we go ahead and save this now and look down, this is roughly what our skeleton loader would look like. And if you notice looking
look like. And if you notice looking between the two of them that this actual weather card that has the real information looks to be a bit taller than the skeleton card, just because some of the stuff that's filled in just appears to be bigger. So, let's actually
go through each of these fields here and get the correct height and width so that we can go into our skeleton and adjust the width and height of each one. We can
do that super easily if we just go into our dev tools here. Let's inspect and let's look through each thing here. So,
no matter what, even if we're kind of shrunk down like this, they should all still be the same width and height. So,
let's look at, for example, this header 2 says that it's about 120 pixels in width and about 60 pixels in height. So,
the general rule of thumb with setting up widths and height in Tailwind is that if we take any sort of given pixel size like 60, we can divide it by four and that's whatever the actual number should be for here. We want this to be 60
pixels tall. So for our skeleton, we
pixels tall. So for our skeleton, we would say height 15 because we divide by four. For the width, it's about, let's
four. For the width, it's about, let's just say it's 120. So 120 divided by 4 is 30. So we can say width 30. And then
is 30. So we can say width 30. And then
we'll just go through real quick and do this for each one. So it looks like that's fine. We know the weather icon is
that's fine. We know the weather icon is the correct size. For the sky clouds here, looks like we have 28. So divided
by 4 is 7. We'll say this is height 7.
And the width here is 150. So, we'll say that's roughly something like 36. Now,
if we go ahead and save this, these should look a bit more accurate to what the actual size of them should be. If we
go down and look at where the time is.
Let's open this up and see that the time here is about 150 in width and 40 in height. So, that means we can give that
height. So, that means we can give that a height of 10. And we said, what was the width there? 150. We'll do the same thing. We'll say width 36 there. Get
thing. We'll say width 36 there. Get
that looking a bit more accurate. And
now these look way closer in their actual size. The last thing would be
actual size. The last thing would be these few fields down here at the bottom. So let's look at these real
bottom. So let's look at these real quick. We can see that each of these
quick. We can see that each of these ones has 24 pixels in height, which that means 24 divided 4 is six. So each of these should be height six. So we'll
adjust all those to be six there. And
then each of these will not make super wide. Let's just say we'll say 64. Makes
wide. Let's just say we'll say 64. Makes
it kind of easy. So we'll say width 16 for all these.
And now I think this should be good to go. If we go ahead and save it, close
go. If we go ahead and save it, close this. Now, now our skeleton card
this. Now, now our skeleton card approximates the actual card much closer. In fact, if we look in our dev
closer. In fact, if we look in our dev tools and hover over this actual card here, it says that the height of it is 420. We hover over this card, it is also
420. We hover over this card, it is also 420. So, they're the exact same height.
420. So, they're the exact same height.
So, I think we've got these sizes looking perfect. Now that we know this
looking perfect. Now that we know this looks good, what we can do is we can go back to our app here and instead of rendering out current skeleton like this, obviously we don't want to actually do this for the real thing.
Let's get rid of this. And the way we do this is because current weather uses a suspense query. What we can actually do
suspense query. What we can actually do is highlight this whole thing. Control
shiftp wrap. We're going to wrap this in a suspense component. And suspense is a component that is built in in React.
What this suspense component allows us to do is give it a prop called fallback.
And what it means is that while this query is in flight, so whatever is using the suspense query, while it's in flight, meaning we don't have the data yet, we'll render whatever component we
put inside fallback. So we can say for this one, we will just render out current skeleton. Essentially meaning
current skeleton. Essentially meaning while this use suspense query is running, we're going to render current skeleton. Once it's done running, we'll
skeleton. Once it's done running, we'll render the actual current weather card.
If you're not super familiar with this suspense paradigm, that's understandable. I didn't start using it
understandable. I didn't start using it until more recently, but I think it is a really nice way of avoiding loading spinners without having to do like an is loading or is fetching state and simply relying on the suspense query built in
from Tanstack. It makes it really
from Tanstack. It makes it really convenient to do because without using suspense, what you'd probably have to do if you had your query inside of it like this is you would go in here, you would pull out something like is pending or is
loading or whatever. And then in your JSX you would conditionally say okay if we're pending your loading or whatever then render a loading spinner otherwise render the actual content. And that's I mean that's fine that works but I do
just think this suspense paradigm is pretty neat. It's a nice way of avoiding
pretty neat. It's a nice way of avoiding that and it makes the code cleaner. So
we'll go ahead and now save this file.
And now one thing you're going to notice is that whenever I go somewhere on our page like let's say I jump around on over here. Notice how we still don't
over here. Notice how we still don't render out the skeleton yet. It's still
just having the current weather and then it's flashing to the new card. The
reason for that is because each of these cards that we're using right now, like the current weather, hourly forecast, etc. are all subscribed to the exact same query. And if one of them is
same query. And if one of them is wrapped in a suspense, but the others aren't, this suspense boundary is actually never going to trigger because it'll see the other ones that already
have a query and it uses their cache data instead. But basically, before this
data instead. But basically, before this is able to work like this, we actually need all of these cards to also be wrapped in suspense. So, it's an all or nothing kind of thing, at least in this particular use case. What I can do to at
least show you this though is if I wrap all of these components in a suspense as well, like we have for the current weather. Let's go here, wrap all of
weather. Let's go here, wrap all of these, say wrap suspense, and do the exact same here.
Obviously, these ones right now need their own skeleton fallback. But for
right now, let's just all render the exact same card just so I can kind of show you what I'm talking about here.
But if I now go ahead and save this, let's just refresh the page here. And I
now go somewhere. Notice how now it goes to the skeleton loaders. If I kind of scroll down here so I can make you see this more. Click somewhere on the map.
this more. Click somewhere on the map.
Boom. That's what it would look like. So
each time the query is in flight, it would go to the skeleton loader. Once
it's done, it would load the actual card. Now, obviously, this looks kind of
card. Now, obviously, this looks kind of bad right now because they're all using the exact same skeleton loader, and they shouldn't be, but I think you get the idea. We have data on the page. Then, we
idea. We have data on the page. Then, we
click somewhere on the map to trigger a query again. While it's in flight, we
query again. While it's in flight, we render the skeleton loader. Once the
data is in, we render the actual card.
Skeleton loaders are becoming more and more common in apps I've seen recently, instead of just displaying a loading spinner. And in my opinion, they look
spinner. And in my opinion, they look really clean if they're done properly.
The reason I made sure the skeleton loaders are the exact same size and layout and everything in the current weather card is because I think the instance where skeleton loaders don't look all that good is if the sizes are
way off. If the sizes are way off
way off. If the sizes are way off between the actual card and the skeleton card, then every time I go and query for something, you're going to see a weird readjusting of sizes and a weird flickering on the page, which makes it
look way less smooth. That's why it's important if you're going to use skeleton loaders, you need to make sure that your sizes for your skeleton components are all pretty similar to what the real thing is going to be. That
way, it's just a better UI experience.
Now that we have the proper skeleton loader for the current weather card, let's do the exact same thing, but for the other cards. So, we'll go ahead and close out of current weather and current skeleton, and we'll follow the exact
same process. So, let's go into daily
same process. So, let's go into daily forecast. Here we have all this JSX here
forecast. Here we have all this JSX here that we're going to copy. Exactly. We're
going to go into our daily skeleton and we'll paste this in here. Making
sure to import card and we'll import weather icon. And again, same thing in
weather icon. And again, same thing in this component. We don't have access to
this component. We don't have access to data cuz it's a dumb skeleton component.
So instead of mapping over data.d
is we can map over an array of fixed length. The way I typically do that is
length. The way I typically do that is by saying array.fr,
you can write object like this and say length however long you want. We know
it's an 8day forecast. So we'll say length 8. And we're not going to have
length 8. And we're not going to have access to this day here. Obviously it's
going to be of type unknown here. So we
can just completely get rid of it. We're
not going to need that. And now
essentially everywhere where we use day, we're going to replace with the skeleton loader. Let's go back to our current
loader. Let's go back to our current skeleton real quick. Let's take this skeleton right here. We'll go ahead and just copy it and let's paste it into the code here. We see that this P tag is
code here. We see that this P tag is already hardcoded to width 9. So we know that's easy to use. We can get the skeleton here. hardcode this one to
skeleton here. hardcode this one to width nine. And then if we go ahead and
width nine. And then if we go ahead and go to our dev tools here and we look at each of these items here to see kind of how tall and wide each of them are. This
one is height 32. So divided by four that gives us eight. So we can say it's width 9 and height 8. The weather icon is also hardcoded to size eight. So we
can replace this with a skeleton as well. Just give this one size eight
well. Just give this one size eight rounded fold to make it appear to be a complete circle. And then if we look at
complete circle. And then if we look at our remaining three P tags down here, let's go here, inspect this, look at it, and these widths are going to vary slightly depending on what we have in
here. But it looks like all of them are
here. But it looks like all of them are going to be also 32 pixels. So that's 32 divided by 8 or sorry, 32 divided 4, which is 8. So these are all going to be
skeletons with height 8. Let's actually
just replace all these height eight.
replace this here and oops and we will also replace both of these.
Again, each of these has a slightly variable width, but it does look like for each of them the width is around 32 pixels. So, we'll say the same thing for
pixels. So, we'll say the same thing for each of these. So, instead of saying a separate width and height, we'll just hardcode size eight for each one of these things. So, we'll do that for each
these things. So, we'll do that for each one of these what were P tags before.
And the last thing to fix is we still need a key because we're mapping over something. Uh, we don't have day
something. Uh, we don't have day anymore. So, let's actually just get the
anymore. So, let's actually just get the index out of here. So, we'll just get the index just like this. And we'll make the key be the index. Normally, I'd
advise against using index as keys in React. It's kind of a frowned upon
React. It's kind of a frowned upon thing, but if you're mapping over an array of fixed length like this, you don't have any sort of ID to use, then index should be fine. And honestly,
that's it. This component was super simple. So now if we go ahead and save
simple. So now if we go ahead and save this, let's go back to our app and we can replace the fallback year of current skeleton, which is just a placeholder with daily skeleton. Now for our daily forecast while we're loading, we'll be
rendering out the daily skeleton instead. What I'll do real quick just to
instead. What I'll do real quick just to show you how this looks because it's a bit hard since it's kind of at the bottom here. I'm actually going to move
bottom here. I'm actually going to move this up a little bit. Move it up directly below our map. So we go in here, click on somewhere. Boom. You can
see, I mean, it loads in pretty quick, so it doesn't last very long, but whenever we click somewhere, it loads, flashes in. It's the exact same size, so
flashes in. It's the exact same size, so there's no weird layout shifting, which is exactly what we want. So, now let's go ahead and move this back down below the hourly forecast. And let's move on to the next card. We'll take care of the
hourly forecast. So, we'll do the exact
hourly forecast. So, we'll do the exact same process we've been doing. Close out
of these. Let's close out of this.
Actually, we'll keep it open for now.
Let's uh go into hourly forecast. Take
this JSX. Exactly. Again, same process.
Just got to do it a few more times. Go
here, replace it. It will get our card here. And now we'll just go through
here. And now we'll just go through again. Same exact thing. Anywhere that
again. Same exact thing. Anywhere that
consumes data, we want to replace it with a skeleton loader. We know for this data hourly, we don't have it. So, let's
do the same thing we did as the daily skeleton of mapping over an array of fixed length. So, we'll say array from
fixed length. So, we'll say array from it'll be length. We know this is a 48 hour forecast. So, it'll be length 48.
hour forecast. So, it'll be length 48.
And we'll do the same thing here where we will say we'll get the index out. And
then for each thing, we'll make sure that's the index there. And actually,
I'm realizing because of this, this doesn't actually have a key on it, but it should. Don't forget that. Uh, this
it should. Don't forget that. Uh, this
should just be Yeah, we can use this here. Say hour.dt. That's a little bit
here. Say hour.dt. That's a little bit of an oopsies. Should have had that earlier. Okay, but now that we have this
earlier. Okay, but now that we have this hourly skeleton, let's get this fleshed out. We know the weather icons are
out. We know the weather icons are always size eight by default. So, let's
render out a skeleton here from our UI folder and just oops, give this one a class name of size eight. And then we'll
replace this P tag and this P tag. If we
look at our dev tools here, open up this to get these sizes correct. Looks like
they are height 24. The width's going to be a bit variable. Again, let's just assume that's 60. So, we'll say it's a width 15. And then we'll say the height
width 15. And then we'll say the height is 24. So divide that by four and that
is 24. So divide that by four and that is uh six. So we'll say width 15, height six for each of these. We'll take this.
This will be width 15 and h6. And then
for the temperatures down here, looks like it's going to be roughly 32 pixels in width and 24 in height. So that would mean uh 32 in width that would be a
width of 8. And then what' I say for the the height there? See the height is 24.
So yeah, that is a height of six. And
now this card is done. As you can see, kind of getting the hang of it. It's not
too bad to create the skeleton components. Again, you pretty much just
components. Again, you pretty much just want the exact same JSX as the actual card itself or the actual element itself that you're mimicking with the fallback.
And then just make sure you calculate the sizes correctly. Now that we have this hourly skeleton, let's again go back to app here. We can replace current skeleton with hourly skeleton. Oops.
And now we should have just one more card left. the additional info card. So,
card left. the additional info card. So,
let's get this last one knocked out of the way. If I open up additional info,
the way. If I open up additional info, we're going to take the JSX, exact same thing as before, and we'll make a actually we already have the
component. We'll go here. We'll paste it
component. We'll go here. We'll paste it in. Making sure to import. And same
in. Making sure to import. And same
thing, we want to map over an array of fixed length. We look at our additional
fixed length. We look at our additional info card. We have what is it? Six
info card. We have what is it? Six
fields. So, we'll say array.fr. from and
this is going to be length six. We're
not mapping over anything, so we can't really dstructure anything, but we will get the index out of here just to make React happy. So, we'll say he is the
React happy. So, we'll say he is the index. Place this here. And now,
index. Place this here. And now,
anywhere where we have some data, we're going to replace it with the skeletons.
Same as the other three cards. Well, we
can see this icon here is clearly going to be size eight. And we know it's going to be circular. Let's go ahead and just replace this with a skeleton.
that is size eight and also rounded full. If we open up our dev tools to see how big the label is for each one of these, obviously again like the other text fields, the width's going to be kind of variable, but the
height should remain the same. So it
looks like in this case the height is going to be 32 pixels, which means it is H8 in Tailwind. So we will replace this span here with another skeleton, and we will say class name is going to be H8.
And then for each of these, let's just decide on a width that's maybe kind of the average of all of them. It looks
like. So, this one's like 103. The next
one here is going to be shorter. So, 64.
See, what's this one here? 105. Okay,
let's just say I don't know. Let's say
it's 80 pixels on average. So, for
these, um, that's 80 divided 4 is 20.
So, we can say width 20. And that'll be for our label. And then lastly, this format component that we have down here is just for the right side of each of our things here, which again, we'll just
say for each of these is also a height 32. So let's replace
32. So let's replace say replace this another skeleton, this is going to be height 8, which is already 32 pixels, so we're good. And
then for the width here, let's see, the width on these is much shorter. Let's
just say this is also 32 pixels. So
we'll say size eight for it. Just like
this. And now this component should be good. And now that we have this, let's
good. And now that we have this, let's go ahead and close out of our dev tools.
Let's go back to app. And now we should have a skeleton card for every single card on our dashboard. Replace this with the additional infoskeleton. And now
they're all unique and all ready to go.
Four separate cards, each with their own unique data and unique skeleton loaders.
That should look pretty much identical with some pulsing loader blocks. Let's
test it. So we can look at these top two. We have the current weather and the
two. We have the current weather and the hourly forecast. If I go click somewhere
hourly forecast. If I go click somewhere on our map, you can see that it does exactly what we want it to.
If I go somewhere here, it flashes for a second with the pulsating animation and then cuts to the real data. And this is exactly what we want. Just so we can see the daily forecast and the additional
weather info. Let's actually move both
weather info. Let's actually move both of these up in the DOM just so we can see it a bit easier. Go ahead and save it. Scroll up here. And the same thing
it. Scroll up here. And the same thing applies for these cards. Whenever I
click somewhere, they go to their skeleton variant and then go back to being the actual data once the data is loaded in. Super super cool. So now
loaded in. Super super cool. So now
we'll go ahead and move these back down to where they were. And I think it's safe to say we did a pretty good job with these skeleton loaders. And it
looks like they're all working the way that we want them to here. In just a bit, we're going to do responsive design, so we'll be able to see all this stuff at once and not, you know, just have this ugly looking single column.
But for right now, these appear to be all working exactly the way that we want them to. One final thing to add to these
them to. One final thing to add to these skeleton loaders that I think would make these look super super clean is if once the suspense finishes, the actual cards just fade in instead of just appearing there. It doesn't look super bad right
there. It doesn't look super bad right now, but it's a little harsh whenever we go from the card to the skeleton loader.
It kind of just flashes in. But one
thing that I think looks really clean, especially with skeleton loaders, is if the information once it's loaded kind of just fades in. There's no built-in fade animation in Tailwind to my knowledge.
So something I can do super quickly is just create a super basic fade in animation in our index.css file. So if
we go over to it here, what we can do is we can just scroll to the very bottom of the file and let's define an animation.
We will say at key frames, normally how you make animations in Tailwind and we will say fade in simple animation name.
All we have to do is just say something like from opacity zero to opacity 1. And it's really that simple.
opacity 1. And it's really that simple.
We're just defining a super simple animation where we go from being opacity zero, so invisible, to opacity one, which is visible. So, we'll go ahead and save this. And now we can add this to
save this. And now we can add this to each of our cards. So, because our skeleton loaders and our actual cards use the shared card component, we can just add it in here. Now, one thing I want to be careful of is I don't want
the entire card to fade in. So, I don't want this outer div to be the one that fades. I think that would be a bit too
fades. I think that would be a bit too over the top. if we had let's say data on the page and then we click somewhere to get the skeleton loader and the data loads in and then the entire card just fades back in. I think that would look
really clunky. So ideally instead we
really clunky. So ideally instead we have this divy the one that fades in. So
the whole card still stays on the page but just the content of the card fades in slowly. So essentially I want to add
in slowly. So essentially I want to add this anime fade in to this class name. A
simple way we can do this while still maintaining this children class name in here and not messing that up is to actually just wrap this whole thing here in a clsx and that way we can add more
stuff to it. So we'll say clsx imported from here. We'll say comma after
from here. We'll say comma after children class name so we still include it and then what we can do is we can say animate. This intellisense is kind of
animate. This intellisense is kind of annoying. We'll say animate and then
annoying. We'll say animate and then we'll do brackets for an arbitrary and we'll just do this in line. We will say fade-ash in because that's the name of our animation right here as we defined it in index.css. And then we want to
separate these out by underscores because you can't have spaces in an arbitrary. In CSS you can have spaces
arbitrary. In CSS you can have spaces but in arbitrary you can't add spaces like this. So you usually use
like this. So you usually use underscores to separate them out. So
we'll say 0.6 0.6 seconds. Another underscore
0.6 seconds. Another underscore ease out forwards. I was testing earlier and I think 1 second's a bit too long.
So that's why I'm doing 0.6. And then
this will just make it ease out. But
essentially, this just compiles to this CSS class right here. So, it's nothing crazy. It's just inline inside of an
crazy. It's just inline inside of an arbitrary. Now, if we go ahead and save
arbitrary. Now, if we go ahead and save this and we go to our page to test, click on here. These cards should fade in. Let's refresh the page here. It
in. Let's refresh the page here. It
looks like they're not quite. So, we
need to fix something. Let's actually
test to make sure it's not happening too fast. Let's set this like 5 seconds just
fast. Let's set this like 5 seconds just so it's super obvious. Go here. And
yeah, it's definitely not fading in. I
think I may have mistyped this on the next time. I think this should be an
next time. I think this should be an underscore because they should be two separate things. So we go ease out and
separate things. So we go ease out and then forwards. Let's actually save this
then forwards. Let's actually save this now. And there we go. The phase working
now. And there we go. The phase working in. So as you can see, this is like
in. So as you can see, this is like obviously super slow cuz we have this 5 seconds. Let's change it back to 0.6
seconds. Let's change it back to 0.6 seconds. Go and save this now. Refresh
seconds. Go and save this now. Refresh
this page. And now when we click somewhere, I think this fade in looks a lot cleaner than having the data just kind of flash there. We could draw it out a bit longer. I mean, if we did 1 second, I take it back. back. I don't
think it looks that bad. I think having 1 second actually looks a bit cleaner than having the the 0.6 seconds. So,
let's go ahead and keep this. Um, but
yeah, you get the gist here.
Essentially, with these, if I want to go anywhere on the map, click somewhere, skeleton loaders come in, fade out, fade in, and it looks way less harsh and quite a bit smoother. Before we get to the responsive design part of our app
and getting the light mode and dark mode toggle set up, the last major piece of functionality would be adding a side panel here on the right side where we can display some additional information about wherever in the world that we are.
The free tier of open weather only comes with so much stuff. And one of the other free APIs we could use without paying anything is the air pollution API. So I
thought it would be cool if we could use this API to fill up a side panel for our app. because we just have this onepage
app. because we just have this onepage dashboard like view. I think having a side panel would make the UI look really nice and clean. If we go back to the API for the air pollution and go to the API documentation, you can see that we have
a couple of different types of air pollution. So, different pollutants that
pollution. So, different pollutants that we have and they each have their own respective value ranges. An easy way to fill out a side panel for our app would be to have a card for each one of these pollutants. So, a card for SO2, one for
pollutants. So, a card for SO2, one for NO2, one for PM10, and you get the idea.
And then maybe for each card, we could have some sort of slider in it to show how low or high the value is. And then
something saying whether it's in the good range, fair range, moderate range, and so forth. If we're going to use a slider to show how good or bad the pollutant is, we can actually use a slider component from Shhatzien. So, if
we go back to their components here, they do have a slider, believe it is right here, that just looks something like this that I think could be a nice little visual indicator of where the pollutant is and how it falls on the
spectrum of being, you know, good, moderate, whatever. So, without wasting
moderate, whatever. So, without wasting any more time, let's get started on this side panel. The first question is, how
side panel. The first question is, how do we functionally make a side panel in an app like this? Well, typically at large enough screen sizes, the dashboard and the side panel would both be visible at the same time. But with all side
panels, at some point at small enough screen sizes, we need to make it collapse. And when the user optionally
collapse. And when the user optionally opens it up, it opens over the dashboard content. And that's because if we're on
content. And that's because if we're on a small screen size, we can't just cram the dashboard and the side panel together because we won't have enough room to work with. For now, let's just start working on the side panel under the assumption that it'll be displaying
over the dashboard content just so it's easy for us to see as we develop it. So,
if we go to our file explorer, go back to our components folder that we've been using this whole time. Let's make a new file in here. I'm going to call it side panel.tsx
panel.tsx tsrfc to make the component and we'll get this component made. Now let's think about how a side panel should be styled.
Side panels are usually pretty similar to headers and that they need to be positioned absolutely most of the time.
Whether it's a header or a side panel, they are positioned either fixed or sticky or something like that. For this
particular side panel, we want it to always be in the same spot in the screen no matter what. So let's make it fixed.
So let's throw a class name on this div here. We will say fixed top zero to make
here. We will say fixed top zero to make it aligned to the top of the screen.
Right zero to make it aligned to the right side of the screen. And we'll also give it height screen to make sure it takes up 100% of the viewport. And we're
going to change this. But for now, let's just set it to with width 50 by default.
And also to make it stand out a little bit, shadow MD. In terms of how we want the side panel to stand out from the dashboard, we don't want it to be the exact same color. However, one thing you will notice is if we go into our index
CSS, one of the variables that Shadian actually made was this sidebar color right here. So, we can actually just use
right here. So, we can actually just use that. So, for the background, for the
that. So, for the background, for the side panel, we will just say bg sidebar.
Now that we have these few things, let's quickly just save this and I'm going to go into our app and we'll actually render it out in here. Let's actually
wrap everything we have so far.
I'll wrap it in a div and then just remove div to make it a fragment. So
this way all this code right here is going to be our dashboard. But then in that same component. So in our uppermost app here, we're just going to render out side panel just like this. Go ahead and
save it. You can see this side panel
save it. You can see this side panel does now pop up and it overlays a lot of our content. Like I said before, for
our content. Like I said before, for some reason the map we have has a bunch of Z-index stuff set in there that's around Z,000. So, let's actually just go
around Z,000. So, let's actually just go into our side panel here and just hijack that and say Z1. That way, it goes over both the legend and the map itself. And
instead of doing width 50, let's actually up this to, I don't know, let's say width 80. Now that we have this outer div set up, what should be inside of here? Well, just like the rest of the
of here? Well, just like the rest of the cars that we have on the dashboard, I actually want to wrap this whole side panel in a suspense because I think it would look good to have skeleton loaders for these little side panel cards that we're going to make as well. So, what
we'll do in our side panel, if we're following that exact same pattern, we will just have suspense like this imported from React. And we'll get the side panel content set up inside of here. And we'll set up our fallback once
here. And we'll set up our fallback once we get there. And the reason I'm putting this suspense in here and not, for example, at this level with the side panel right here is because I don't want the entire side panel to disappear. I
just want the inner content of it to be suspended. So, that's why I'm moving in
suspended. So, that's why I'm moving in here and not on the outside like the others are. However, to take advantage
others are. However, to take advantage of the suspense in here, we'll still need to have our suspense query in a component that's inside of these. So,
what we'll do here is make another function. We'll just call this air
function. We'll just call this air pollution. And this will be kind of the
pollution. And this will be kind of the component that actually renders the content of the side panel, which we can just render up here, just like this. Now, in this component, here comes the query part.
We've got the suspense already wrapped around this component and we want to take advantage of that by calling a use suspense query inside of air pollution where we can actually get the air pollution data. However, we don't have a
pollution data. However, we don't have a function for that yet. So, let's go ahead and set that up. If we take a look back at our API.ts file, we already have a function for the weather data and we already have a function for the
geocoding. Let's make one last function
geocoding. Let's make one last function in here to get the information for the air pollution. So, what I'll do is just
air pollution. So, what I'll do is just like the rest of them, I will actually I'll just copy this to make it a little easier. Write this. Instead of calling
easier. Write this. Instead of calling this get geocode, we're going to say um I don't know, get air pollution. And
it'll actually take an identical arguments to get weather. We'll want
latitude and longitude. And I'll show you that in just a second in terms of why we're doing that.
Let's do it like this. And the reason we're letting it take in latitude and longitude just like get weather is because if we go back here and we look at the air pollution API, we can scroll down to where it gives us the link how
we can actually fetch this information.
And as you can see, just like the weather one, all it takes in is latitude, longitude, and an API key. We
already have all the logic on our page to get the latitude and longitude coordinates of wherever we click on on the map or select from from the drop down. Since we already have that, this
down. Since we already have that, this will be super easy. we can just pass it through to this get air pollution function. So what we'll do is just like
function. So what we'll do is just like the other ones, we'll take this link right here and instead of calling the weather link in here, we will just call this link instead for air pollution.
Much like the others, we'll replace this with API key and this dollar sign here for the expression. And then just like the other ones, we'll want to pass through latitude and longitude into this
expression. And now this should be good
expression. And now this should be good to go. And of course for the geocoding
to go. And of course for the geocoding and for the weather, we have our own unique schemas. So let's now generate
unique schemas. So let's now generate one for the air pollution. If we look here, scroll down. It should give us an example of the response object. So it's
just these couple of fields here. We
have just the coordinates and then we have a list that has a bunch of the information about the air pollution components. Just like the exact same way
components. Just like the exact same way I did with the other two. I'm going to plug this response into chat GBT and ask it to generate me a Zod schema. Let's go
ahead and in our schemas folder, we'll make our last and final schema and we would just call it air pollution schema.
I'll make a lowercase pollution schema.ts and we'll go ahead and paste in exactly what chatbt generated for me which is this right here. Making sure of
course to import Z actually just to make sure. I'll just do the import like this
sure. I'll just do the import like this just so we get it from Zod. And now we have a schema for our air pollution.
Said it before and I'll say it again.
Chad GBT or AI in general is really good at generating schemas like this. So, if
you want to save time and save a lot of monotonous stuff of just copying stuff over, this is something I definitely think you could use LLMs for. Now that
we have this, all we have to do is go back to our APIs file. And instead of parsing through any other schema, we'll just parse through air pollution schema.
And now everything should be fine and dandy. If we save this file, now we have
dandy. If we save this file, now we have a perfectly fine query function to call that will get us the air pollution information at any given coordinates.
Now that we have this, let's go back to our side panel here and construct our suspense query. So we'll say const data
suspense query. So we'll say const data equals use suspense query just like this. And then for here we'll need again
this. And then for here we'll need again a query key and a query function. For
the query key let's just say pollution for right now.
And then for the query function we're just going to make this a function we just wrote which is get air pollution making sure to pass in the latitude and
longitude. So we'll say like this
longitude. So we'll say like this get air pollution. And here we need to pass those coordinates in. And ideally
we pass them into both the query key and the query function. Well, luckily we already have the coordinates in the parent of the side panel which is app here. But the exact same way we're
here. But the exact same way we're passing it down to our children components, we can pass it down to the side panel. So let's give it the exact
side panel. So let's give it the exact same props. Actually, we'll pass
same props. Actually, we'll pass coordinates down just like this. We'll
save it and we'll make sure now that side panel actually expects to have coordinates. So we'll get chords out of
coordinates. So we'll get chords out of here and we will say chords is going to be of type chords. and we'll have to get coordinates from this component down to the air pollution. Now, we could just
pass down chords like this and that'll be perfectly fine. However, one thing that I typically like to do is if I'm drilling it down through two layers and I'm not actually using the coordinates in this component, I usually actually
don't destructure it. What I will do instead normally is just call props like this and then I will just spread it down. That way it spreads all the exact
down. That way it spreads all the exact same props down. So, if we decide to add more in the future, we don't have to be explicit about it. The way you do that is just by curly brackets and you do dot dot.rops. And now this air pollution
dot.rops. And now this air pollution component will have access to all of the same props that side panel does. We'll
just have to make sure they share the exact same prop type. So let's make sure this is expecting props. And for this one, we actually will destructure the latitude and longitude out of the coordinates. And actually, my bad, we
coordinates. And actually, my bad, we can't do that. That should be coordinates. We're destructuring not
coordinates. We're destructuring not latitude and longitude. And now that we have this in this component, what we can do, we can just say chords just like this. And then get air pollution is
this. And then get air pollution is expecting an argument that is just an object of latitude longitude which is the exact shape of chords. So we can just pass in chords like this and make
sure we import this function. And of
course, as always, when using tanstack query with Zod, now if we hover over data, it'll give us the exact type that we need from this air pollution API, which should match the exact type over here. So now we know that Typescript has
here. So now we know that Typescript has context on what we can and can't use.
The next question is how do we actually want to use this data? Well, one of the most important numbers that we get here is this AQI number which is just the air quality index. And at least from this
quality index. And at least from this API, this particular number is the most generic and most applicable number I guess for air pollution that we can use in our side panel. So I think it would make sense to put this particular number
in some big bold font. That way it's very easy to see. And then after we do that, what we could do is then map over components. If we look at components
components. If we look at components here, of course, components is just an object for each one. But essentially
what we could do is we could just map over each component like CO N O N O2.
And for each component, make one of those little cards I was talking about earlier. That would just show the
earlier. That would just show the information for that particular component. And when I say components in
component. And when I say components in this context, I'm referring to, of course, air pollution components, not React components. So let's go ahead and
React components. So let's go ahead and start setting up that styling. So, of
course, it's a React component. So, we
need to return some JSX. Let's first
return this div here. And for this outermost div, I'm going to give it a class name of flex flex call gap 4 just to give it that vertical layout. And
then, like I said, the first thing we'll do is do the AQI, the air quality index.
But let's because it's a header one here, let's give it a thing of text 5XL and we'll say font semibold. So, this
should really stick out quite a bit. And
then in here we want to actually get the AQI number. Again we can just look at
AQI number. Again we can just look at this type here and you can see it's inside list and then it is inside main and there is this AQI. However, this
whole thing here this list is an array technically. But like for other things
technically. But like for other things that have returned an array, we're just going to be worried about the first item in the array, not all the other ones. So
what we can do to access the AQI here is do essentially list.main.QI
but the first index of list. So that'll
just be data.list
at index0ero main. AQI. And now that we have this
main. AQI. And now that we have this underneath this, let's map out each one of our components. And honestly, this is going to look a little bit ugly, but here's how we're going to do this. We
are just going to say data. And then
let's look back here to make sure we're getting the right thing. We know we need to get list.components
and then get over each one of these. So
what we'll do is we can say data.list
list again at index0 dot components and here's where we can map over each component. So we'll say map just like
component. So we'll say map just like this. However, because components right
this. However, because components right here is actually an object and it's not an array. One thing we can usually do
an array. One thing we can usually do for that is to do object.entries
and we can actually wrap if I can spell entries right we can actually wrap this entire thing right here everything before the map and object. entries. That
way we can basically loop over the object and for each one we can get the key and we can get the value. The way we do that inside the map is you just make
this brackets array like this and we can extract the key and the value out of here. And then for each one that we're
here. And then for each one that we're mapping over, we're going to return a function mostly because we'll have to put some logic in here. Let's specify a return here. And for now, we'll just
return here. And for now, we'll just make this just return a fragment. And
actually, I lied. We're not going to do a fragment for each one. We're going to render out one of our card components because I want these cards to match how the cards actually look on our dashboard, at least in terms of the color and like the rounding and whatnot.
And then because we're doing a map, let's make sure we make React happy. And
we give each card a key. So for this one, we already have the key that we're getting out of here. That's just the key from the object. So we'll say key equals key. And one thing I'm realizing now is
key. And one thing I'm realizing now is that if we're going to use our card, we're going to be missing the title from the card because right now TypeScript expects an actual required title here to
put in. However, these cards are about
put in. However, these cards are about to make are not going to actually have a title on them. So to silence that, we'll just make sure title is optional. It'll
still be passed in, of course, for our dashboard cards, but not for our side panel cards. In terms of what to
panel cards. In terms of what to actually put inside these cards, I think the first thing that would make sense would be to be some indicator of what air pollution component that we're talking about. So for example, when we
talking about. So for example, when we do this key and this value here, the key is going to give us of course the actual component name like CO NO2 and the value is going to be the number. So I think
the first thing we should actually do is just display the key and the number separated with justify between. So what
we can do is we can just make two spans.
We'll have one for the key and then we'll also have one for the value. So
I'll say key in here and then value in here. And then we'll go ahead and wrap
here. And then we'll go ahead and wrap both of these in a div. And we'll give this the class name of flex justify between. Put them on opposite sides of
between. Put them on opposite sides of the card. In terms of the key and the
the card. In terms of the key and the value, we want both of them to be readable. Let's go ahead and give the
readable. Let's go ahead and give the key a class name of let's say text LG font bold. And then for the value here,
font bold. And then for the value here, we'll do a similar thing, but instead of being bold, we'll make it semi-bold just so it's slightly less apparent.
Shouldn't have two bolds there. And then
actually for the component name, I'm thinking because these are just lowercase, let's actually throw capitalize on your two to capitalize these. So we'll just throw capitalize
these. So we'll just throw capitalize like this. And now that we've done quite
like this. And now that we've done quite a bit with this, let's actually save it and see how it looks. It looks like we have a whole lot of nothing because it just crashed out the page. So, let's
actually refresh this. Maybe click
somewhere else and see what happens. And
it looks like we're still I think about to get an error here. Let's double check this real quick. We're probably going to get a network error if I had to guess.
It looks like we are getting the actual information back. Uh, okay. Yeah. So,
information back. Uh, okay. Yeah. So,
we're getting the information back.
Fine. Uh, let's see what this error is.
Might be a Zod error. Oh, it is. Okay.
So it's saying it's saying the coordinate expected tupil received object. So let's go back to our air
object. So let's go back to our air pollution schema real quick. See if we can fix this. Go here air pollution schema. And it's saying for coordinate.
schema. And it's saying for coordinate.
So it's expecting it to be of this shape here. This c.tuple of the number and
here. This c.tuple of the number and number. I wonder if chad gbt sold us out
number. I wonder if chad gbt sold us out on this. Let's double check the uh the
on this. Let's double check the uh the network. And it looks like cord is just
network. And it looks like cord is just an object with longitude and latitude.
So yeah, this is uh definitely not quite right. Makes me wonder if tbt was wrong
right. Makes me wonder if tbt was wrong or if the docs are wrong. Uh looks like it is. Oh yeah, it just says
it is. Oh yeah, it just says coordinates. So we didn't specify. Okay,
coordinates. So we didn't specify. Okay,
so let's fix that real quick. What we
can do to fix this is just replace this thing here. We'll say it's a Z dobject.
thing here. We'll say it's a Z dobject.
It's just going to be lat that is a Z dot number and lawn which is also a Z dot number.
And then we'll go ahead and save it. And
that should fix it because this actually matches the format that we want. Let's
go ahead and save that here. Refresh the
page. And now you can see we actually have the cards showing up on our page.
Now that we're seeing it for the first time, let's go back to our side panel real quick and adjust some of this stuff in here. So it has pretty much no
in here. So it has pretty much no padding on it. So let's throw a let's do py8 to create some separation between this AQI number and the top of the side panel. Let's also give px4 just to give
panel. Let's also give px4 just to give some more horizontal spacing. Go ahead
and save that. And that makes quite a bit more room. I also want to make it even a bit wider. So instead of doing width 80, let's just go ahead and do width 90. And these two changes alone
width 90. And these two changes alone immediately make it look quite a bit sharper. Right now, there's no context
sharper. Right now, there's no context to what this number here is doing. It's
kind of just floating there. So, let's
actually make a label for it. So, right
above where we're doing this, let's actually copy this. And we're just going to paste in AQI just like this. And
actually, let's make it say air pollution. And I want to have a separate
pollution. And I want to have a separate label for AQI. And this particular air pollution title should not be quite as big as AQI is. So, let's make it text 2XL just like this. So we can say air
pollution. This is the AQI number of
pollution. This is the AQI number of two. Let's add a label for that. Let's
two. Let's add a label for that. Let's
put actually this exact same class name here. We'll take that and we'll put it
here. We'll take that and we'll put it right underneath AQI and just say AQI here. Now if we go ahead and save this
here. Now if we go ahead and save this should have air pollution, the AQI number followed by AQI. Now we should address the fact that these cards over here in the side panel are going to be slightly different than the cards that
we have on the dashboard. We're
obviously wanting to share things like the rounding, the shadow, and the way it looks in general. However, because
they're going to look slightly different, I think what we should do in this case is allow the cars to take in an optional class name. So, we can pass in some conditional styling. So, just
like we're passing in this optional children class name, let's also add in class name as one of the props, which will just be an optional string in that same vein. So, we'll say this class name
same vein. So, we'll say this class name here. And now it takes in both an
here. And now it takes in both an optional children class name and class name. And of course, just like the way
name. And of course, just like the way we're doing clsx to get this children class name on here, let's do the exact same thing for the class name. So this
whole class name here, let's wrap this whole thing in brackets. And then in here, we can now call clsx. Make sure
this is all wrapped in clsx. And we'll
throw on class name. Oops, not inside the quotes. Throw on class name inside
the quotes. Throw on class name inside of here. That way, if we want to pass in
of here. That way, if we want to pass in a custom class name to our card, now it gets passed through to this outermost div. So, let's go ahead and save it. And
div. So, let's go ahead and save it. And
now, if we head back to our side panel, now where we're rendering out our cards, we can add some custom styling. Just for
the sake of adding a cool little hover effect to these cards, I'm going to give it a class name of hover. I'm going to say scale 105 so they grow slightly. And
to make that hover smooth, we'll give it transition. We'll say transform. And
transition. We'll say transform. And
then we will also say duration 300. We
give an explicit duration. And now if we go ahead and save this, the cards over here on the dashboard don't do anything.
But now if we hover over these cards, you can see with each mouse over, they just grow slightly and it's a nice little smooth animation to make it look pretty clean. However, now we can't just
pretty clean. However, now we can't just ignore the fact that these cards are pretty much the exact same color as the background of the sidebar. I mean,
they're actually really hard to see if you're not looking closely. The only
thing that's really differentiating it is the shadow that we have around each card. Unfortunately, for whatever
card. Unfortunately, for whatever reason, I don't really know the logic behind this. If we go into index CSS and
behind this. If we go into index CSS and look at the variables for whenever we are in dark mode that Chad CN made, we look at card here, it is this 021006
number. And if we actually go to BG
number. And if we actually go to BG sidebar, it's the exact same number. I'm
not sure why sidebar and card are the exact same because it makes it just a little bit awkward. So what we could do is we're passing in the custom class name for side panel. We can pass in our own custom color to make it stand out
more. If we go into our card component
more. If we go into our card component in here, we're doing the gradient where we're doing from the card color to card color at 60% opacity. So, let's do a similar thing in here. We're going to
say from and the question is what color should we use? Well, there's another variable in here called sidebar accent that I think would be nice to use. It's
a little bit different than the actual sidebar itself, so it would stand out way better. So, let's say we want it to
way better. So, let's say we want it to go from sidebar accent and we'll do the same exact logic where we'll say to
sidebar accent at 60% opacity. And if we go ahead and save this now our cards appear a lot more visible than they were a second ago. It's nice when they're not the exact same color as their background. We have a proper color and
background. We have a proper color and now it's probably a bit easier to see that hover effect I was doing earlier.
As you can see, just a nice little smooth animation here on some cards that look pretty clean. Okay, so right now these cards are super plain. All we're
doing is writing the pollutant name right here and then the pollutant value.
Firstly, this just isn't very visually appealing to only have this. But
secondly, you really have no context to what this value actually means. Is
126.72 good? Is it bad? Is it is it all right? Who knows what this actually
right? Who knows what this actually means? Obviously, we want it to be clear
means? Obviously, we want it to be clear to the user that these numbers actually mean something. So, let's now talk about
mean something. So, let's now talk about the slider component that we mentioned using from Shad CNN. I think this component here would work great. We want
to disable being able to actually move this because there's really no point in doing that. But what I was thinking is
doing that. But what I was thinking is for the slider, we could have on the left side of it some unit right here for, you know, the low end. On the right side, it' be a unit for the high end.
And the slider would just show how either bad or good the pollutant is. I
would imagine in most cases, if the pollutant is good, the slider is probably on the low end. And if the pollutant is really bad, like a really high number, then it's on the high end.
Let's go ahead and get the slider added to our project. If we scroll down here, we're going to take the same command the same way we've been installing our other components. Go ahead and copy it, open
components. Go ahead and copy it, open up our terminal, and we'll go ahead and add it to our app. Just like the other ones, it should add it to the components folder inside this UI folder here. So
now we have this nice slider component.
Now that we have this underneath this div where we're doing our pollutant name and its value, let's actually just render out a slider. So I'll render out a slider like this. Make sure to import it. Let's see what it looks like. It's a
it. Let's see what it looks like. It's a
bit hard to see. So, let's go ahead and go to this card here. Let's throw a children class name on it. Let's give it a vertical layout. So, flex flex call.
And we will make gap three on each of these just to create some separation between the div and the slider. I
mentioned not wanting to allow the user to go and just readjust the slider like this because in the context of our app, it doesn't really make sense for the user to be able to change this. So what
we can do is on our slider here and just throw it disabled which is a prop that should if we save it now disable it now disable it from actually doing anything.
Also it's kind of easy to see the actual slider when it's taking up color here but the actual track behind the slider is again kind of just blending in with the background of the card. So let's go
to let's go actually to that slider component real quick. And you can see that by default we're going to have this BG muted on here for the track. Let's
actually replace this with BG sidebar.
And now if we save it, I think the track will look a bit nicer being the same color as the sidebar. So it's still dark. It's not popping out, but it's
dark. It's not popping out, but it's also not the same color as the cards, which makes it easier to see. And I'm
noticing too on these cars, we have this awkward little gap at the top that we shouldn't be having. And that most likely stems from the fact that even though the title prop is optional, we still in the class name here have a gap
of four for the outer thing. So, we're
still creating a gap between the title and the children, even if there is no title. So, what I'll actually do to get
title. So, what I'll actually do to get rid of this gap four here on only these particular side panel cards is we can go back to side panel and we can actually
throw a gap zero on here. And if we save it, it won't quite get rid of it because in this case, the gap 4 is going to win out over the gap zero. However, if I
throw an important on it like this, it should actually Oh, yeah. Move it to the end. tail 1v4 thing. Now, if I do that,
end. tail 1v4 thing. Now, if I do that, it should get rid of that awkward space at the top. Okay, so now that we fixed a few things, let's get back to our slider. Right now, it's cool having the
slider. Right now, it's cool having the slider, but the slider has no context to the actual value itself. Like, this
slider doesn't have anything to do with 126.72. It's only like that because I
126.72. It's only like that because I changed it before I disabled it. So, if
I really just refresh, all these are starting at zero. So, how do I actually pass this value into the slider? Well,
the thing is this slider in order to work properly and look correct needs a min prop and a max prop. The reason
being is let's say on a slider I passed in 126.72 and let's say that's all I pass it into the slider just that number. How does the slider have any
number. How does the slider have any context of knowing where on the slider that is, right? Should 162 be at the beginning here? Should it be kind of in
beginning here? Should it be kind of in the middle? Should it be at the end? Who
the middle? Should it be at the end? Who
knows? The only way it knows how to actually position itself is if we have a min and a max. So if the min is zero and the max is 200, we know that 126 is somewhere kind of in the middle. But
without a min and a max, it has no context to that. So for all of our pollutants here, if we look at these, there should be this chart here kind of telling us what each one is. And for all of these, zero is actually the minimum.
You can see it here in this good chart.
So we can assume that for the slider, we can pass in min. And min is always going to be zero. Now, however, in terms of max, how do we know what the max one for each pollutant is? And I think if you've
been watching long enough, you can probably guess exactly what I'm about to say. We need to use this chart here. But
say. We need to use this chart here. But
actually transcribing this chart into a data structure by hand is going to be a little bit tedious if we're being honest. So, like many other things, I'm
honest. So, like many other things, I'm going to ask Chatbt to generate me an object that represents this entire chart here. And here is what it actually
here. And here is what it actually generated. We'll just go ahead and paste
generated. We'll just go ahead and paste this whole thing in. So, it actually looks like quite a lot. And because it's a lot, I'm not just going to blindly trust it. Let's look through it for a
trust it. Let's look through it for a second and make sure this all looks legit. I don't just want to be a
legit. I don't just want to be a certified vibe coder here. It looks like it generated a few types here at the top. So, we have the air quality level,
top. So, we have the air quality level, which we can see from open weather is good, fair, moderate, poor, and very poor. So, this type looks to be good. Uh
poor. So, this type looks to be good. Uh
range, we'll just have a minute and a max for each. Yep. And then we just have a pollutant type for each different pollutant on here. Seems legit. And then
we have air quality ranges which is just a record of records which is this massive thing right here. All this
object is essentially doing is mapping each pollutant to whatever ranges each keyword is. So like good we know is this
keyword is. So like good we know is this range. Fair we know is this range and
range. Fair we know is this range and you get the picture. This object
actually looks quite a bit more complicated than it really is. It's not
actually super crazy. If I want to know for example what is the moderate range for PM10? I would come to PM10 see
for PM10? I would come to PM10 see moderate and I'd say that it is between 50 and 100. If I want to know the very poor range for 03, I know that it is 180 and the max can be anything just
uncapped. So I think this AI generated
uncapped. So I think this AI generated data structure is actually going to do just fine. I know if I was doing this by
just fine. I know if I was doing this by hand, this would have taken me at least probably 10 minutes to make. So going
back to our slider up here, we know that we need to set both a max and also a value. If we go ahead and hover value
value. If we go ahead and hover value here, we can see that it expects an array of numbers. However, in our case, we only have one number and that's just going to be the value from right here.
We will just say value equals and we'll make it a array of just one item and that'll be value. Now, the real tough question is what do we do for our max?
So, for example, if we're looking at 03, how do we know what the max value is?
Because knowing 78 is nice, but I don't know if 78's near the top or near the bottom or what that even means. So to
figure out what the max is to get our ranges set up, all we have to do is go down here and look at each one of these objects. Luckily, all of these start
objects. Luckily, all of these start from the lowest number. The lowest
number meaning, I guess, the best air quality. And the higher the number is,
quality. And the higher the number is, the worse it gets. So because these are sorted by that, it makes our job a little bit easier. That would mean that the max value for each of these would essentially be the max value in all of
these very poor categories. However, if
we look, all of these have a max of null. That is because technically if we
null. That is because technically if we look at the open weather documentation here, it actually mentions that these values are any number greater than these. Meaning technically it could be
these. Meaning technically it could be infinitely high. That's why it's
infinitely high. That's why it's uncapped and it's just set to null like this because the max for these very poor categories are uncapped. But we'll
actually use for the max is the min value of the worst quality. So
essentially the highest value could possibly go for any one of these given pollutants is the minimum value of very poor. So the maximum value for PM10
poor. So the maximum value for PM10 would be 200. The maximum value for PM2_5 would be 75. And you get the picture. So
what we can do to actually get that, we can go back up to our JSX here where we're getting the um pollutant information. But before we return, let's
information. But before we return, let's go up here and we'll say const pollutant equals. We're going to access this
equals. We're going to access this object air quality ranges, which is just this big thing down here. and we're
going to say air quality ranges at key.
What this will do is if we have a key that is you know SO2 and we try and get air quality ranges at SO2 it will return this object. So that is what this will
this object. So that is what this will do. And I'm pretty sure that the keys we
do. And I'm pretty sure that the keys we have right here are going to be lowercase not uppercase. So let's
actually do key dot to uppercase just like this. And now TypeScript is going
like this. And now TypeScript is going to get mad saying that this has any type because technically key can be any string, but this object here is specific strings. So if you're ever in a case
strings. So if you're ever in a case where you know for a fact that these keys are going to be of the correct type, what you can do is you can just type cast it and you can say as key of
type of air quality ranges like this and that will silence the type error. It's
safe to assume in this case that the pollutants that are actually being returned from open weather are always going to match the exact same pollutants that it gives to us in the type here because this air quality ranges was based off that response in the first
place. So they should always match one
place. So they should always match one one which is why it's fine to type cast this like this. And then to get the max value for each pollutant like I said we're going to use the min value for the very poor category. So what we can do is
we can just say we can say con max equals pollutant and pollutant is of course going to be of the record that we saw. So we'll say pollutant at the very
saw. So we'll say pollutant at the very poor category domin. So for example if this pollutant
domin. So for example if this pollutant gets set to let's say this NO2 object and then we want to get the max here we're just saying pollutant at very
poor.m min. So we would get very poor
poor.m min. So we would get very poor min which is 200. So we assume the max for NO2 is going to be 200 and that's just what this logic is doing right here. However, we have to acknowledge
here. However, we have to acknowledge that there can be cases where the quality is very poor and the value that we actually end up getting will be higher than the max because if for
example we say the max is 200 what if our value is you know 370 or something for NO2 then technically max is not correct because then the max is going to be under what it actually should be. So
what we can do to actually fix this is where we're setting the max here. We'll
assume this is still the default value, but what we can do is we can say math domax here and we're going to take the maximum value between this value and the
actual value itself. So if the value is 370, but the max is 200, then the real max technically is 370. That's what this line here will do. Now that we have this, we can plug it into our slider. So
we'll just say max equals max. Now, if
we go back to our app here, go ahead and save it. And boom. Now, we have sliders
save it. And boom. Now, we have sliders that actually correspond to the real value in the correct ranges. This is for Tokyo. Let's go select, I don't know,
Tokyo. Let's go select, I don't know, London. And you can see now we have new
London. And you can see now we have new sliders. If I go to Madrid, same thing.
sliders. If I go to Madrid, same thing.
Sliders are a bit different. And
basically everywhere in the world that I go, they will all look a bit different, which is exactly what we want. I think
right below these sliders, we should put in actual legends so we know what the real min and max are just for the user to see. So below our slider here, let's
to see. So below our slider here, let's make another div to make some sort of scale. We'll go ahead and give this div
scale. We'll go ahead and give this div a class name of flex justify between and we'll make the text ss. And inside
of here, we'll just put two p tags.
We'll put the min and the max here. So
the min's always going to be zero and the max we'll just say is the max. So go
ahead and save it. And now we have at least some sort of scale for each one.
And now one thing I think we might run into in a second because we're going to be adding more stuff to these side panel cards is this side panel might need to scroll. We're already getting close to
scroll. We're already getting close to touching the bottom of the side panel.
Let's go up to our outermost side panel div here. And let's just throw overflow
div here. And let's just throw overflow y scroll on it just in case. That way if we do get to overflow, we can actually start scrolling it. The very last thing now that I want to add to each of these
side panel cards is some sort of visual indicator of what range each value falls under. For example, right now on this
under. For example, right now on this card 03 is 123.42.
Is that good? Is that fair? Moderate?
Very poor? We don't have that actual information yet. Well, we do. We're just
information yet. Well, we do. We're just
not actually displaying it. So 123.42,
if we look at the docs here, 03 would fall in the moderate range. So, in this example, we should display to the user that we're in the moderate range, not just have the slider because having the slider itself is cool, but I think
having the actual string of saying what condition we're in would be even better as well on top of it. So, underneath
this slider plus the labels that we have for the min and the max, let's just add some tags for each different range, and the one that is active will be highlighted. For example, we'll have all
highlighted. For example, we'll have all five ranges under each slider from good, moderate, very poor, whatever. But if 03 for example falls under moderate only the moderate tag would be highlighted
right under this div here. Let's make
one last one. We'll make a div here.
We'll give it a class name of flex justify between to separate out each of these tags. And then in here we want to
these tags. And then in here we want to map out each of the different range strings. What I mean by that is for each
strings. What I mean by that is for each pollutant that we have, we have the different ranges here. We have good, fair, moderate, poor, and very poor. So
they are the keys of this pollutant object. To get that we can access
object. To get that we can access exactly that. We can say object.keys
exactly that. We can say object.keys
and we're going to get the keys of the pollutant which will give us the actual range names. And then for each one we're
range names. And then for each one we're going to map each quality to a span. And
for this span here we'll throw a couple of things on the class name. First we'll
say px2 py1. I want these to be kind of small. So we're not going to be taking a
small. So we're not going to be taking a lot of space. We want to have a bit more horizontal spacing than we do vertical.
We'll also throw rounded MD text SS and font medium. I'm doing most of these
font medium. I'm doing most of these just to make sure these tags are pretty small so they can all five fit on the card. And then for each one of these
card. And then for each one of these spans here, what we want to display is just the quality. So if we save it now, now you'll see under each one we have the five different qualities. Having
this rounded medium on here doesn't really do anything when there's no sort of background color or borders cuz we can't really obviously, you know, see any rounding or anything. So, what I'll do around this whole span actually is
like our other things, I'll wrap it in a CLSX. And like I said before, I
CLSX. And like I said before, I essentially want the tag that is active to be highlighted a color. The other
ones are just going to be like a, you know, a dullish gray color. So, we'll
say CLSX here. And then in here, we're going to say if the quality is equal to whatever the current level is, then we're just going to give it, I
don't know, let's say BG yellow 500 just to see it being active. Otherwise, if
it's not that, let's make it BG muted.
So, it's kind of like a disabled color.
And then we'll also do text muted foreground just like this. So,
essentially, if our quality is the active one, just make each of these tags yellow. If it's not the active one, just
yellow. If it's not the active one, just make it this kind of gray doish color and make the text muted foreground, which would be kind of a white color.
But what is current level? I'm using
this check right here, but this current level doesn't actually exist yet. We
haven't written any logic to actually get the current range that we're in. So,
let's do that real quick. The way that we can do this isn't too complicated.
Essentially in here, what we can do is we can just say, and actually, sorry, it wouldn't be in here. It'd be up higher.
Let's go up here and let's say const current level. And the way that we're
current level. And the way that we're going to do this is we're going to write an immediately invoked function. So, a
nameless function here. We're just going to do parenthesis just like this. and then we're going to call it immediately. In here, all we really want to do is just loop through the different ranges and see if our
value falls in them. So, for example, let's say let's say we're looking at NO2 for example, and our value is, I don't know, 110. What we're going to do to
know, 110. What we're going to do to figure out what range we're actually in is we're just going to loop through each one. We're going to say, okay, good 0 to
one. We're going to say, okay, good 0 to 40. Does the whatever number I just said
40. Does the whatever number I just said that, like 110. Does 110 fall between 0 and 40? Nope. So, let's go to the next.
and 40? Nope. So, let's go to the next.
Is it 40 and 70? Nope. Let's go to the next. Is it between 70 and 150? Yes. So,
next. Is it between 70 and 150? Yes. So,
we know we're in moderate. That's what
we're going to do to get our current level here. So, let's write a for loop
level here. So, let's write a for loop in here. I'm just going to say for const
in here. I'm just going to say for const level and range of object entries pollutant. And for this, we're going to
pollutant. And for this, we're going to do something. All we're going to do is
do something. All we're going to do is check to make sure we're within the bounds of that particular level. So
we're going to say if the value that we have for this pollutant is greater than or equal to the minimum
and it's also less than or equal to the maximum then we can just return oops not null then we can return the level. So what
this is doing is for NO2 example the level would be NO2 and the range is going to be the 0 to 40 example for the first category and all we're doing is checking is our computed value actually
within that range. If it is we know we're in that range so return that because you want current level to be that level. It looks kind of complicated
that level. It looks kind of complicated but if you think about it for a second it's nothing too crazy. It is the exact logic that I just explained to you step by step. However, we're going to get
by step. However, we're going to get this type error on here saying that max is possibly null, which is true because if we look at our types here, max can be number or null because some of these are
uncapped. But what we'll do in here in
uncapped. But what we'll do in here in this check where we're saying if it is less than or equal to the max, we will just explicitly add a check in here and
say is range domax equal to null or this. And
that should silence that error. However,
what this means is that if we hover over current level now, it'll say it can be string or undefined, and we don't want that. So, let's say if for whatever
that. So, let's say if for whatever reason we can't compute it, we're just going to default to very poor. So, we're
just going to do this. That way, current level is always going to be a string.
So, we'll go ahead and save this. And
now, this should be our current level logic. What's cool is if you just notice
logic. What's cool is if you just notice because we already hardcoded the current level in here before we even had it. Now
if the quality is equal to current level then it will highlight it. So CO is in good. So now this good tag is
good. So now this good tag is highlighted. 03 is in moderate. So
highlighted. 03 is in moderate. So
moderates highlighted which is really cool cuz that's exactly what we want.
However, this yellow was more of a placeholder color. I don't actually want
placeholder color. I don't actually want each highlighted thing to be yellow cuz yellow for everything looks a little bit ugly and doesn't really, you know, make a lot of sense. I think what would make more sense is if moderate was always yellow, but good could be green and then
very poor could be red. So, real quick up here, let's make one last variable in addition to current level. Let's make
some space here actually so we can have this format a little nicer. And then
let's go up here. We will say const quality color. And we're going to do the
quality color. And we're going to do the exact same thing we actually did up here where we use an immediately invoked function to write out some logic. So I
say const quality color equals this parenthesis make it a function and then immediately invoke it. And then
all we're going to do inside of here is we're just going to make a switch statement. We're going to say switch and
statement. We're going to say switch and we're going to switch off whatever the current level is. So whether we're very poor, poor, moderate, whatever. And then
inside of here we'll just make some cases for each one to actually get the color. So we'll say case if it is equal
color. So we'll say case if it is equal to good then what we'll do oops should not be the syntax case good colon here
and then for good let's just say I don't know bg green 500 which we will return that so in that case if we're on good quality color can be bg green 500 and
we'll do this actually for each one so we'll actually going to copy this paste it a couple of times here this next one
going to be fair. This one will be moderate. This one will be poor. This
moderate. This one will be poor. This
one will be very poor. And then let's just make a default return case. So if
we don't have any for whatever reason any current level, which you know we should, but let's just say we don't then we are going to return BG just gray 500 just something neutral. And actually
instead of that we'll do zinc 500. And
then here we'll go ahead and save this just to get some formatting. And let's
change these colors around a little bit.
We'll assume that if we are fair, we'll use yellow.
If we are moderate, we'll I don't know, we use orange because that's a bit more severe. We'll say poor is actually red.
severe. We'll say poor is actually red.
And then very poor is going to be purple. Just like this. And now we have
purple. Just like this. And now we have a color for each different level of quality. So what we can do is go down
quality. So what we can do is go down here and instead of just hard coding in this BG yellow 500, what we can do is just instead
just make it quality color like this.
Now if we go ahead and save it, boom, they're all going to switch to their appropriate colors. So now the goods are
appropriate colors. So now the goods are green, moderates are oranges, and very poor are purple. And we of course have fair and poor. But now this at least gives some color association with each level. Let's click around this map a
level. Let's click around this map a little bit just to experiment. So right
now we are near I think Dubai here which usually has some kind of rougher air pollution at least for some of these numbers. If we go to I don't know
numbers. If we go to I don't know somewhere in Romania we can see here is the air pollution information there. I
don't know let's go Ireland this looks like let's do uh I don't know New York set up here. Yeah, we can see for each different place we go to, we have some of our own unique information. That's
whether it's a custom one, you know, whether we go to a known city, it will always have its own unique weather information. And the fact that we're
information. And the fact that we're able to get it like this is honestly pretty cool. This air pollution
pretty cool. This air pollution information isn't super essential, but it's some extra information just to take up on our dashboard. And honestly, I think it looks pretty clean having these little cards. Awesome. Now, there's just
little cards. Awesome. Now, there's just a few finishing touches I want to put on this side panel before we're fully done with it. First, I think it would be
with it. First, I think it would be great to add a tool tip next to each of these different pollutants describing what each of them actually means. For
example, I have no idea what SO2 means or what the heck PM2_5 is. So, having a tool tip next to these with a little descriptive field might be nice. Just
like we've been doing for our other components, we can use Shadz Cen to help us get a tool tip. So, if we go and look at the Shaden components and we head to the tool tip component, you can see it just looks something like this. you have
a little button or something and you hover over it and this tool tip appears and I think this would work perfectly in our example. So let's go ahead and add
our example. So let's go ahead and add this to our app. I'll go down here with the install command and I think this might actually be the last Shadian component we install. Cannot copy that.
Let's get that in here. Go ahead and add it into our terminal. For some reason copying weird still, so let's get rid of this. Go ahead and run it. And now we
this. Go ahead and run it. And now we should have the Shad CN tool tip. And
what we'll do is we'll take the exact code in this example here. We'll just
take everything in this JSX and we'll go ahead and copy it. And then what we'll do is going back to our app here. Let's
add a tool tip first next to the AQI right here. Because if I didn't have any
right here. Because if I didn't have any context to open weather or I didn't know that it was already air quality index, I'd have no idea what this number actually means. So where we find our AQI
actually means. So where we find our AQI here, which should be up here, let's go up a little bit. We have the AQI. Let's
put the AQI and the tool tip in the same row. So let's actually wrap this here
row. So let's actually wrap this here with a div just like this. We're going
to say class name is flex item center gap 2. And here right next to our aqi
gap 2. And here right next to our aqi we're going to add the tool tip. So
we'll just go ahead and paste in the exact code that we copied from shaden.
And let's go ahead and import each of these things. So we'll import the tool
these things. So we'll import the tool tip here. This should be from UI. Same
tip here. This should be from UI. Same
with the tooltip trigger and also the tool tip content. Instead of this tool tip just saying add to library.
Obviously we want to have it say our own thing. So let's actually go down here
thing. So let's actually go down here and look at where it describes the AQI or the air quality index. It has this pretty simple description here saying what it is. So let's actually just take
this verbatim here. Go ahead and copy it and let's paste this into our code. So
it says that instead of the add to library. And then lastly, we need a
library. And then lastly, we need a valid trigger for our tool tip. In this
example with the button, what it's using is a pre-built button component. And the
trigger for the tool tip is essentially whatever you hover that actually activates the tool tip. So in this example, it is actually just a shad CN button. But in our case, we're not going
button. But in our case, we're not going to need that. What I think would be cool in our app, which is what a lot of apps do for a tool tip, is to have a little information icon you can hover over that will have the tool tip. I went on SVG
repo real quick and just found this cool little information icon. So let's use this for our trigger. What we'll do is go into another one of our components where we import these SVGs as components. Like for example, um let's
components. Like for example, um let's do hourly forecast here. Actually, no, this is not a good example. Let's go to additional info. Let's take this import
additional info. Let's take this import here. Same thing. We're just using SVGR.
here. Same thing. We're just using SVGR.
So, let's get this. Now, we can go back into our side panel and let's import this at the top. We will say import, we'll call it information. And the SVG was just called information.svg,
but we're going to go ahead and use that. And now we can use this instead of
that. And now we can use this instead of this button for the trigger. So right
here we will just render out this information SVG and make it something pretty small. We'll just say class name
pretty small. We'll just say class name is size 4. And now if we go ahead and save this, let's see what it looks like.
Save it here. And also just so you can see a little easier, I'll add invert to make sure it is white. Go ahead and save it again. And here you can see our tool
it again. And here you can see our tool tip. And if I hover over it, looks like
tip. And if I hover over it, looks like nothing actually happens. So let's
remove this as child here. I don't think we actually need this. Go ahead and save that. and still looks like it's not
that. and still looks like it's not quite working correctly. If I had to guess, it's because our side panel has a z-index and this has no z-index. So,
it's probably just showing up behind it.
So, let's actually go on to our tool tip content here. And let's give it a high
content here. And let's give it a high zindex. Tool tips should generally show
zindex. Tool tips should generally show over everything on the page. And we have a lot of content around the thousand z-index area. So, I don't like doing
z-index area. So, I don't like doing this all the time, but let's just throw like a Z2000 on this. And now it should be guaranteed to show over everything.
So now if I hover it, we can see this is what the tool tip looks like. Just some
white text. And I don't like that it's like super long here. I'd rather have it just wrap. So we can actually do here is
just wrap. So we can actually do here is on our P tag. Let's just throw something like a max width XS. We hover this.
That's just setting the max width to 300 pixels. That way it'll wrap. So if I go
pixels. That way it'll wrap. So if I go ahead and save it now and hover over it, that looks a little bit better. Also, I
don't know how this random slash got here. Let's remove that. And now it
here. Let's remove that. And now it should be looking good. This tool tip is exactly what we want. just a little descriptive field saying exactly what AQI actually is. And it gives context to what this number actually means. Let's
now get this exact same tool tip with just different text inside of it on each one of these cards to explain what each one of these pollutants actually means.
So, what we'll actually do is take this tool tip exactly. Let's go ahead and copy it. Let's go down to where we're
copy it. Let's go down to where we're rendering out our cards down here. We're
going to find the div where we have the key cuz that's the actual pollutant name. So, this right here. And just like
name. So, this right here. And just like we did with the AQI, let's wrap this in a div. And we'll give it the exact same
a div. And we'll give it the exact same thing. We'll say it's going to be a
thing. We'll say it's going to be a class name of I think it was just flex item center and gap 2. Make some space
for this here. And then paste in our tool tip. And if we go ahead and save it
tool tip. And if we go ahead and save it now, we should have a tool tip on each one of the cards. If I hover it, however, we're still going to see the exact same tool tip content as the AQI, which is not what we want. We want to have each one be unique to what it
actually means. I'm not familiar with
actually means. I'm not familiar with what each one of these things is. So I
went to chat GBT and I asked it to basically just generate the names of each of these from the abbreviation. So
for example, for CO we have carbon monoxide. For 03 we have ozone. And you
monoxide. For 03 we have ozone. And you
get the picture. So what we can do now is we can actually use this mapping to display something in our tool tip. If I
scroll up to where our tool tip is, it's right here. Let's go ahead and get rid
right here. Let's go ahead and get rid of this AQI text right here because we don't obviously need that. And let's
just say concentration of. And then
here's where we'll enter in whatever the name is that this mapping gives us right here. So we can say it's called
here. So we can say it's called pollutant name mapping. So in here we can say pollutant name mapping key just like this. And now if we go
ahead and save it each of these tool tips should say exactly what they are.
CO says concentration of nothing. That's
not quite right. Probably again because the key is lowercase and these down here are actually uppercase. So let's go ahead and change that. Let's just say to
uppercase for this key here if I can spell it correctly. And then now if we save it and hover these should say the exact thing. So this says concentration
exact thing. So this says concentration of carbon monoxide, concentration of nitrogen monoxide, concentration of nitrogen dioxide and so forth. We know
the key that we're getting here is technically just a string, but it's always going to map this uh pollutant type right here. So, what we can do is because we're already typ casting it to uppercase, we know it's always going to
be that. So, let's just say as
be that. So, let's just say as pollutant. And that should silence our
pollutant. And that should silence our type error. And now this side panel
type error. And now this side panel should be good to go. We have quite a bit of information here. We have the air pollution. We have the AQI number. We
pollution. We have the AQI number. We
have these cards that have a nice little hover effect with sliders on them that change depending on where in the world that we go. And we have tool tips to explain what each one of these things mean. The actual content of the side
mean. The actual content of the side panel is done. But the very very last thing I want to do is add the ability for it to open and close. That way it can just flow better with the rest of our page. Cuz right now it's just
our page. Cuz right now it's just sitting over the dashboard, but that might change depending on the screen size that we're on. So let's take care of that really quickly. It's not going to be too difficult. Let's close out of all these tabs here so we can kind of
start fresh. And in our app component,
start fresh. And in our app component, let's actually make one last piece of state that just tells us whether or not our side panel is open. So we'll say
const is side panel open. set is side panel open equals use state and we're going to default it to true and then what we'll do is we'll pass both these
pieces of state to our side panel. So
we'll go down here and we'll say is side panel open equals that exact state and then we'll do the same for the setter function. So set is side panel open and
function. So set is side panel open and also pass that in as well. And once we do that we'll have to update our props to make sure it expects those exact
props. So we'll say is side panel open
props. So we'll say is side panel open is going to be a boolean and then set is side panel open will just be a dispatch
of set state action that's also going to be a boolean and we will import dispatch and the reason that I'm defining the state up here in app and then passing it down to side panel is because we're
about to tackle the responsive design aspect of our app and the rest of our dashboard meaning all this stuff is going to have to know whether or not our side panel is open to position itself properly. So that's why I have this
properly. So that's why I have this state up here and then I'm just passing it down to the side panel. So what we'll do now is we'll go ahead and save this.
Go back to our side panel. We'll go
ahead and dstructure these things from our props, but still keeping this props thing here so we can still spread it down. And we will just say const
down. And we will just say const is side panel open set is side panel open equals props so we can get them out of here. What we'll do for our class
of here. What we'll do for our class name here is we'll wrap this all in a CLSX because I want to add a condition to it. So we'll say clsx here. Obviously
to it. So we'll say clsx here. Obviously
it goes to the very end. And what we're going to add here is we're going to say if the side panel is open then what I want to do is I want to translate X0 meaning it should stay exactly where it
is. Otherwise if it's not I want to
is. Otherwise if it's not I want to translate X full. That way it shifts it off the entire screen. And then what we'll do is at the very top of our panel here, let's actually make a button that
will just call the set is side panel open and set it to true or false depending on whether we are opening or closing it. So what we'll do is we'll
closing it. So what we'll do is we'll just say button and then in the on click here we will just say set is side panel
open and we will just set it to false.
And it makes sense to only set it to false because if the side panel isn't open at all, then well, this button won't even be clickable in the first place. So, the only way this button can
place. So, the only way this button can actually be here is if it's already open. Meaning, if we click the button
open. Meaning, if we click the button again, it should set it to false now to close it. Now, I could just make this a
close it. Now, I could just make this a super simple button that just, you know, says something like hide in here. But I
don't want to do that. It's not very clean from a UI perspective. Ideally,
it's something like a chevron or visual indicator that we're going to be closing the side panel. I found this simple chevron left. SVG on SVG repo that I
chevron left. SVG on SVG repo that I think we can use at the top of our side panel. That'll be a good little
panel. That'll be a good little indicator that we're going to be closing it. Let's go ahead and import this
it. Let's go ahead and import this chevron left as a component. We'll go up to our side panel here. And the same way that we're doing our information, we will just say I don't know, we'll just call this component chevron. And it was
I believe chevron left.svg. So now
instead of saying hide, let's just render out chevron like an actual component. and we will say class name uh I don't know let's try
size 8 and if we go ahead and save it here now we should have the chevron up here it still is black so I'll go ahead and invert it to make it white so we can see it and now this chevron should be
our button it looks like this SVG has a little bit of extra space on it because it's not quite left aligned with our A here if we actually go into it I think we can verify that let's make a bit more
room here I believe yeah so it looks like there's some awkward space there on the left side. So, let's actually go into here. I don't like doing this cuz
into here. I don't like doing this cuz it's a little bit hacky, but I'm going to add negative two for the left margin here. That way, it just scoots it over a
here. That way, it just scoots it over a little bit. And now, if we go ahead and
little bit. And now, if we go ahead and click it, you can see our panel completely closes. We don't actually
completely closes. We don't actually have a way to get it back yet, so I'll have to refresh. But I can show you again, we have our panel here. I click
the chevron, and now it closes it. Let's
add a super simple animation so it doesn't just immediately disappear, but actually slides out. All we have to do is we have to go up here to our uppermost class name which yeah is this
one right here. And we'll just add transition transform and we will say duration 300. And now if we go ahead and
duration 300. And now if we go ahead and save it and now close. Now it slides out as opposed to just flashing out. So this
looks really smooth. And this is what most side panels at least on most modern apps are going to do. They're going to slide in and out like this because it just looks cleaner than it just teleporting. The very last thing we'll
teleporting. The very last thing we'll need to do with this is make a button to actually reopen the panel. That way, we can go between open and close. Just like
I found the chevron SVG to close it, I found this hamburger SVG. That's a
pretty common sign in most apps that we're going to open up some sort of menu like a side panel. But I think this icon would work well for our open button. So,
what we'll actually do here is copy this exact same button and let's head over to our app component and let's put it in the same div that has these two dropdowns right here. So, they're all in a row together. So if I go to the div
down here, you can see we have this div that is flex flex call gap 8, which I think is not the right one. It should
actually be this one here. This flex gap 8 is going to hold both of our drop downs. So let's here after the map type
downs. So let's here after the map type dropdown, go ahead and just paste in this button. I'll take this negative
this button. I'll take this negative left margin off and instead just do ML auto. That way it aligns itself against
auto. That way it aligns itself against the far right side. And of course we'll have to replace this chevron with the hamburger. So let's go ahead and take
hamburger. So let's go ahead and take this image import. We'll just copy it verbatim up here and just change this to say hamburger
instead and imports. I think it's capital Hamburger. No, lowercase. Okay,
capital Hamburger. No, lowercase. Okay,
so it's just hamburger. SVG just like this. And now we'll replace our chevron
this. And now we'll replace our chevron with hamburger still being size eight though with the invert. Go ahead and save it now. And now we should have this hamburger at the very top that if we click it won't do anything cuz it's
setting it to false still. So, let's set it to true whenever we click the hamburger here. Let's go ahead and save
hamburger here. Let's go ahead and save it. Side panel starts open. We have our
it. Side panel starts open. We have our chevron to close it. And we have our hamburger to open it back up. So, now we can safely alternate between the two states, open and close. This is super
smooth. Now, we might come back in a
smooth. Now, we might come back in a minute and customize how these buttons look, but the functionality is completely there, and I think we're in a great spot. So, we now have a fully
great spot. So, we now have a fully functioning side panel that displays a bunch of information that is useful for us to see with a bunch of cool logic that we can just open and close however we want. However, one thing I don't want
we want. However, one thing I don't want to forget about before we move on from the side panel is the skeleton loaders.
We already have the skeleton loaders for the rest of our cards whenever we're fetching new data. However, we don't have any for the side panel. If I click somewhere, the entire panel just blanks out before it reappears. Because the
cards fade in, it actually doesn't look horrible that way. But I think to keep things consistent, we should make skeleton loaders for each of these cards as well. Making skeleton loaders now is
as well. Making skeleton loaders now is super super easy because we've done it a couple of times. Let's go ahead and go to our file explorer. Let's go to our skeletons folder and we'll make another
skeleton card in here. We will just say sidecard skeleton.tsx
sidecard skeleton.tsx just like this. We'll do tsrfc to create the component. And what I'll do is the
the component. And what I'll do is the exact same thing we did for our other cards. I will go to the JSX for the card
cards. I will go to the JSX for the card itself. Take all of this exactly.
itself. Take all of this exactly.
Go ahead and copy it and we'll paste it in here. Being sure to import the card
in here. Being sure to import the card and we'll remove a couple of things from here. First of all, we don't need the
here. First of all, we don't need the key. Next, this tool tip is not needed
key. Next, this tool tip is not needed on any of this. So, I'll go ahead and just remove that. And if you recall with our other skeleton guards, I'll go to the current skeleton for example.
Everywhere that we were consuming some sort of API data or anything on the actual component, we just replaced it with this skeleton component from shaden. So let's do the exact same thing
shaden. So let's do the exact same thing here. We'll go ahead and copy one of
here. We'll go ahead and copy one of these over back to our sidecard skeleton and we'll start replacing them. If we
first look at this uh right here, this text with the tool tip, we can see that with the tool tip plus the text itself, it's about 50 in width. So it's I mean 46 here. We'll just say 50 to round up.
46 here. We'll just say 50 to round up.
And then 28 in height. So we'll actually replace this whole thing here. This div
that has the key. We will say it's a skeleton that has what' I say it was uh 28 in height. So divided by four that is height 7. So that's already good. And
height 7. So that's already good. And
then the width is 50. So divided by 4.
That's about 12. So we'll just say 12 here. And of course we'll make sure to
here. And of course we'll make sure to import this skeleton here. Now for this next one for the actual value. This can
also be a bit variable, but we'll say this is going to be the exact same height and actually pretty much the same width, too. So, we'll give it the exact
width, too. So, we'll give it the exact same stats for this one. We will say width 12 and height 7. Next, if we go to look at our slider here, we can see that this slider is just hardcoded to be 6
pixels tall, which makes our job super easy. We'll make a skeleton here to
easy. We'll make a skeleton here to replace the slider. We will make sure it is it is width full to take up the entire space of the card. and six pixels divided by four is going to be a height
of 1.5. That should be our sliders taken
of 1.5. That should be our sliders taken care of. Now, if we just look at these
care of. Now, if we just look at these two numbers down here that represent our scale, we can see that 0 is always going to be, let's see, 16 pixels and about,
let's just say 8 wide. So, we'll replace this with 8 / 4 is with two. And then 16 tallid 4 should be height four. So,
we'll replace both of these with that.
And now we're almost done. We don't need to map over the actual pollutants here.
We already know that there's only five possible qualities. That's good, fair,
possible qualities. That's good, fair, moderate, poor, and very poor. So
instead of mapping over this, all we have to do instead is do an array of fixed length because after all, it's a skeleton. So we don't need the actual
skeleton. So we don't need the actual data. And we'll just say length is five.
data. And we'll just say length is five.
We don't need this quality here, but we will go ahead and get the index out of here just to throw it on this. And what
we'll do is we'll just put a skeleton here. Let's throw a key of index. And
here. Let's throw a key of index. And
then for each of these, each of these little tags we have down here, it looks like they're going to be so 24 in height. So divided by four will be a
height. So divided by four will be a height of six. And then the widths are going to vary a little bit. Let's just
say for the sake of these, let's see, we have 40 70. Let's just say 60. So 60 / 4 will be a width of 15. Now if we go ahead and save it, this skeleton should
be fully done. It should just mimic our sidecards exactly. Let's go ahead and
sidecards exactly. Let's go ahead and close our side panel here. And now if we actually head back to the side panel, we can finally make use of the suspense that we wrote at the very beginning when we started this. Up here we have a
suspense that is wrapping around air pollution. But hold on a second. We just
pollution. But hold on a second. We just
made a skeleton component for each of the cards. But this suspense wraps the
the cards. But this suspense wraps the entire air pollution component. Meaning
essentially this entire air pollution component also needs to be a skeleton.
At least the component that we use in the fallback of the suspense. So what we can do will actually be super super simple. We'll make one final component
simple. We'll make one final component here in our skeletons folder that we're going to call uh let's say side panel skeleton.tsx
skeleton.tsx just like this tsrfc to make the component. And what we're going to do is
component. And what we're going to do is we're going to go into the side panel take all the JSX here that we have for the air pollution. So literally all of this. It should be the opening and
this. It should be the opening and closing div. Yep. We're going to take
closing div. Yep. We're going to take all of it and we're going to go ahead and paste it in here. But we can obviously remove almost all of this.
First of all, this tool tip, we don't need it. So, let's go ahead and delete
need it. So, let's go ahead and delete this. And because we're doing with the
this. And because we're doing with the skeleton, we don't need any of this logic in here. So, let's completely purge all of this logic out. We only
have the return. And we just finished working on our skeleton cards for the side panel. So, instead of mapping over
side panel. So, instead of mapping over the regular cards here, let's map over those. So, we'll just say sidecard
those. So, we'll just say sidecard skeleton just like this. And of course, we won't have actual data to map over.
Let's just map over an array of fixed length. We know there's always going to
length. We know there's always going to be looks like four, five, six, seven, eight pollutants. So we'll say an array
eight pollutants. So we'll say an array of length 8. So array from going to be length 8 like this. And we obviously won't have any key and value to map over. So we don't need to worry about
over. So we don't need to worry about that. But let's still go ahead and get
that. But let's still go ahead and get the index out just to oops just to continue good practice here. And we
don't actually need this curly brace or this return anymore. So we can go ahead and get rid of this and just map this directly. So, let's uh go down here. Get
directly. So, let's uh go down here. Get
rid of I think all of these except not quite all of them. Just these two.
Actually, let's get rid of this, too.
Might need to fix this formatting in a second. I don't think it's quite right.
second. I don't think it's quite right.
Make sure we have an opening parenthesis here to get this correct. For the key, we'll just pass in the index. And this
sidecard skeleton can actually be self-closing because we literally don't need any of this content down here. So,
what I can do is just pretty much close all of this. Let's literally delete all of this. Don't need it. And we can
of this. Don't need it. And we can simplify this code down way more. We
don't need either of these class names.
So, we'll go ahead and remove them. And
then let's just fix this styling here.
Looks like this div is the closing for here. But I think we're just missing a
here. But I think we're just missing a curly bracket. So, let's go ahead and
curly bracket. So, let's go ahead and add this. And this parenthesis should
add this. And this parenthesis should not be needed. Actually, that's a lie.
It should. That's our return. Let's
track these real quick. So, that's the opening and closing. There we have this.
Maybe we're missing this. Looks like
we're missing a parenthesis here. So
yeah, now that we have that, this should be good. We'll make sure to import the
be good. We'll make sure to import the sidecard skeleton so we have the right reference. And now minus these few
reference. And now minus these few fields up here, this is all that our actual side panel skeleton is going to be. We're going to replace these with
be. We're going to replace these with skeletons in just a second, but all it is is just a mapping of our sidecard skeletons to make it look onetoone how our actual panel is. Now the very last thing is just this number right here.
This data.list.eqi
is obviously not going to be in our skeleton. So, let's go ahead and inspect
skeleton. So, let's go ahead and inspect the actual number just to see kind of how big it's going to be. Looks like
it's always going to be height 48. So,
48 / 12. We can just get one more of our skeletons here. 48 / 12 should be four.
skeletons here. 48 / 12 should be four.
So, let's replace this a skeleton of height four. And then the width, this is
height four. And then the width, this is going to be the width of the whole entire thing. But the skeleton shouldn't
entire thing. But the skeleton shouldn't be that whole width. So, we'll just say for the actual number itself, let's also do a uh width of 48. So, 48. Oops, this
should be not four. It should be 12. And
since they'll both be 12, we can just say size 12. And we'll go ahead and import skeleton. And now we should have
import skeleton. And now we should have a fully functional skeleton for the entire side panel. So, I can finally show you what this looks like. Let's
close out of here. And let's go ahead and go back to our side panel. And now
after all that work, we can finally plug in our fallback component for this suspense, which you will just say is side panel skeleton. Just like this.
Now, if we go ahead and save it, this should be a fully functional suspense.
If I go over to our map and I click somewhere, you can see all these cards go into suspense mode into their skeletons and they come back with the data on them. One thing, however, that's
immediately obvious is that we can't really see our skeletons inside of each card because they're the exact same color as the card itself. So, what we can do is we can go into the sidecard
skeleton here. And what I'll do is just
skeleton here. And what I'll do is just add BG sidebar on each of these. So, we
make our color a little bit different than the actual card itself. I'll throw
that on each one of our skeletons here.
And we'll go ahead and refresh this page. I'm not sure why it crashed there.
page. I'm not sure why it crashed there.
Looks like we forgot the BG sidebar on the slider here. So, let's add it to this one. And now, if we save it and we
this one. And now, if we save it and we go somewhere else, you can see our skeleton does look pretty nice. It's
still a pretty subtle dark. It's not
really super noticeable. However, I
don't think that's a big deal. I think
if it was too dark, I think it would look a little bit out of place. And now
everything appears to be working like clockwork. I click, we suspend the whole
clockwork. I click, we suspend the whole panel, show the skeleton cards, and as soon as the data is ready, it comes in, and it looks great. Now, pretty much everything in our app, including the side panel and also all of our cards
down here, are all going to have skeleton loaders, so it looks super crisp whenever we load any new data in.
All right, so what's the next step for this app? Well, we're getting very close
this app? Well, we're getting very close to the end here. For almost this whole video, we've kind of just sat these cards in this super ugly looking column.
I we just have a single column on the page here. And let's be honest, it looks
page here. And let's be honest, it looks kind of bad cuz we've essentially just been ignoring this container formatting here the entire video to make the actual content. But now I think we're at a
content. But now I think we're at a great point in the video to finally address it. Especially because we just
address it. Especially because we just added the side panel and now if we hover over this or I mean we open it up now it just looks even more awkward because it looks like we're trying to just cram a ton of stuff in one page and it doesn't really look super professional. So with
that being said, let's finally address the issue of styling these cards and this dashboard in general to look good and work in conjunction with this side panel here along with responsive design.
As part of that, we'll make sure the dashboard and the side panel both look great together on all screen sizes and just make sure that they can look good on any possible device. The dashboard
structuring plus responsive design isn't going to be super challenging. We'll
just have to be careful and pay extra attention to the layout of all the divs and whatnot that we have. So, without
wasting any more time talking, let's get started on this. Now, we're going to go ahead and shrink the VS Code window down to make more room for us on this page.
This entire video that we've done so far, we've had the entire website over here just kind of in that little narrow view. But now let's make it actually
view. But now let's make it actually wide so we can see what it really looks like. And it's obvious that right away
like. And it's obvious that right away with this bigger view, it looks pretty bad. So what we can do and how I
bad. So what we can do and how I normally do responsive design is if you open up the dev tools here, you can actually let's shrink this down a little bit. You can open this button right here
bit. You can open this button right here in the Chrome dev tools that puts it into responsive mode. And that looks absolutely horrendous, but we'll ignore that for a second. Essentially, I use this responsive mode most of the time
where you can kind of just take the width and you can resize it. And you can see exactly how it looks at every possible screen size. Now, in terms of the side panel here, every single app that we have is going to be slightly
different in terms of when and where we actually show the side panel, but generally if you're using Tailwind breakpoints, the sidebar should collapse around the MD or LG breakpoint. So LG is
1024 and MD is 768. So, usually around that part, if you're shrinking down from, you know, big to small, around those numbers and width is generally where you would close the sidebar to make more room for the actual content
since it's getting skinnier and skinnier. For our use case, since we
skinnier. For our use case, since we have a decent amount of stuff to show on this dashboard, let's air on the side of caution and say that if we are below the large breakpoint, the side panel will be automatically collapsed with the option
of opening it and closing it like we already have. And then if it's above LG,
already have. And then if it's above LG, it will always be on the screen and it won't be collapsible. Meaning the side panel in the dashboard will be showing at the exact same time. And again, like I said, the large breakpoint or LG is
1,024 pixels in width. So if you look at our dev tools here, when I'm in this responsive mode, it actually gives me the width in this number up here. So
right now, we're at 1241. And if I just start shrinking it down, you can see that number adjust. So 1024 is going to be at roughly this width here. And at
this width is where we're going to change how the side panel behaves.
Because the way that we originally coded the side panel, how it already behaves is that it opens up over the dashboard.
So we already have the case where it's under 1,024 pixels taken care of. But
now we need to fix whenever it's over 1,024 and we want to show both at the same time. So usually what this means,
same time. So usually what this means, the way I go about doing this is having some sort of CSS variable that controls the width of the actual side panel. And
then when we're calculating the width of the dashboard, we just subtract the width of the side panel from it. That
way they can fit on the screen together.
I'll show you what I mean by that. So
let's actually close out of all these files here. I'm going to zoom out a
files here. I'm going to zoom out a little bit. I know it might be a bit
little bit. I know it might be a bit harder to see, but since we have a smaller window here, I think it'll definitely be helpful not be super zoomed in. And let's go to our index.css
zoomed in. And let's go to our index.css
file here. And let's go to the very top here to the root. And let's go above all these CSS variables. And let's add one for the sidebar width. So we'll do dash
sidebar-width and we will say it is 23 rim wide. And now what we'll do with
rim wide. And now what we'll do with that is we will save it. Let's go to our side panel. And instead of having it
side panel. And instead of having it hardcoded to be width 90, we will just hardcode it to instead be width of sidebar width just like this. And now if
we save it, it should be 23 rem. If we
go ahead and go into the inspect here and we look at it, it looks like that's not quite right. I think because this should be square brackets and not parenthesis. So, let's change that
parenthesis. So, let's change that square brackets here. I think that should be good. Says we have let's see.
Okay, my bad. I keep forgetting this new uh tail 4 syntax. We can just do it like this. And now, there we go. If we save
this. And now, there we go. If we save it, this should be 23 rim right here.
Now that we have a defined width for the side panel that is based on a CSS variable that is global, let's now go back to app here. And right now if we scroll to the outermost div this
dashboard, it should be this div right here. So we have essentially this div
here. So we have essentially this div wrapping the whole entire dashboard and then the side panel. So this div right now is going to be width full.
Technically it's with auto but it has no constraint parent. So by default it's
constraint parent. So by default it's always going to be the width of the entire screen. So what we can do on here
entire screen. So what we can do on here just to be explicit is we'll say width full by default because on small screens we still want it to be width full. But
once we're above the LG breakpoint, so we can do it like this. We want our width to actually be a calc of let's say 100 dvw which is basically the screen.
It's 100% of the device width. We do
that minus the sidebar width.
And now if you do it like this we're saying on small screen sizes we're still width full but as soon as we get above 1,024 pixels then we want to have the entire dashboard be 100% of the screen
minus however wide the sidebar is. And
now if I go ahead and save it, you can see that everything shifts over. It
looks a little busted because we don't have any padding yet for this entire dashboard. So everything is kind of just
dashboard. So everything is kind of just crammed together. So let's go in here.
crammed together. So let's go in here.
And we already have gap 8 for these individual items. But let's throw a PA on here. Now if we go ahead and save it,
on here. Now if we go ahead and save it, it should look quite a bit cleaner. This
map right here is still going to be weird because we have the hard-coded width value for the map. So let's
actually go into our map component. And
right now we have just width hardcoded to a,000 pixels and height as 500 pixels. However, for now, let's keep the
pixels. However, for now, let's keep the height on here hardcoded how it is. And
we will just say the width instead of being 1,000 pixels will just be 100%.
And now if we go ahead and save it and refresh this, the map should now also be constrained to this same dashboard div.
And now we can clearly see that everything fits on this page. None of it is going underneath or over the side panel. Each section has its own space,
panel. Each section has its own space, which is exactly what we want for larger screens. However, once we go smaller,
screens. However, once we go smaller, like we keep getting smaller and smaller until we eventually drop below the 1024 break point. Now, we can close our side
break point. Now, we can close our side panel and this width can still be the full width of the screen, which is how almost all modern web apps are going to behave to some capacity. And ideally,
what should happen is whenever we're on larger screens like this, the side panel should open by default. And then once we get below the point of a large screen,
so below 1024, then this side panel should also close automatically. So
let's add that real quick. How we can do that is we can go back to our side panel here. And on this very top outermost
here. And on this very top outermost class name, let's throw one more thing on it. Let's say when we're on large
on it. Let's say when we're on large screen, so we're at 1,024 pixels or above, then we're going to translate X0, meaning no matter what, if we are above
LG, translate the side panel onto the screen. And we'll likely need to throw
screen. And we'll likely need to throw an important on here so that our side panel open state that we have right here doesn't try and override that. If we're
on large screens, we don't care about this state literally at all. We're
always going to have it on the screen.
So, we make sure it's important. And
now, if we go ahead and save this, let's start on a small screen size. And if we close the side panel, and now we start expanding, we're eventually going to go over 1024. And now, the side panel will
over 1024. And now, the side panel will just automatically open back up. And
then what we can notice here is if I refresh it here, we're on a big screen.
We start shrinking down.
Notice how it's not going to close automatically. The way that we can get
automatically. The way that we can get that to happen is we can actually just go to our app here. And right now we're defaulting this is side panel open to true. Let's just change it to false and
true. Let's just change it to false and see what happens. If I start it as false, do this. Now I go make our screen big. Refresh just to make sure we're
big. Refresh just to make sure we're starting completely fresh. And now I start shrinking down. Slide panel's
open. Slide panel's open. And as soon as I hit 1024, boom, it closes. And the
dashboard expands. If I go back out, does the same thing. And now I can go back and forth between them. And it
works exactly the way that we want it to to ensure responsive design. Now, one
thing to keep in mind is that if we're on big screens here, we shouldn't be able to click this chevron at all. And
in fact, if I try to, nothing's going to happen. And that's again because when
happen. And that's again because when we're on large or bigger screen sizes, we're always showing the side panel. So,
it doesn't make sense to have this chevron here when we're on large screens because obviously we don't want to show it if the user can't even click it. A
super easy fix for that is to just go to where our chevron is here, which is right here. And all we have to do is add
right here. And all we have to do is add LG hidden. So, by default, it won't be
LG hidden. So, by default, it won't be hidden, but as soon as we're at 1,024 pixels or wider, it's going to be gone.
So, now save it. And now we have no more chevron. In that same vein, it doesn't
chevron. In that same vein, it doesn't make sense to show this hamburger icon either. Normally, this should only
either. Normally, this should only really show if we have the ability to open the sidebar. But again, on this screen size, we're big enough where it's always going to be open. So, this
hamburger icon doesn't do anything. So,
we'll just go to app and we'll add the same exact thing to the hamburger icon.
We'll go here. And right after ML auto, let's just throw LG hidden, save it, and now when we're on this screen size, they'll be hidden. So, now the chevron and the hamburger are both gone in the screen size because we can't open or
close the side panel. It's just stuck right here. And now if we go small
right here. And now if we go small enough and we fall below 1024, now the hamburger is going to reappear. Our
styling is messed up. So it's actually behind the side panel, which we'll fix in a second. However, I can close out of this here. And boom. You can see the
this here. And boom. You can see the hamburger is right here. So once we're small enough screen size, we can still open and close this at will. But once we get big enough, those disappear, and we're just stuck with the side panel.
Now that we have this, let's actually figure out what we're trying to do for the dashboard. After all, it's the most
the dashboard. After all, it's the most important part of our app, and we pretty much haven't styled it at all. I mean,
the cards themselves and the data is styled, but the formatting of the cards and how they're positioned is just hard-coded to a single column, which, you know, is completely fine if we're on like a mobile screen like this because obviously we can't really have much more
room to have more than a single column.
However, when we have like more regular size larger computer screens, having everything just be the width of the entire page just does not look very professional. And this is going to stem
professional. And this is going to stem purely from the fact that if we look at this outermost div here, it's just set up to be a flex flex call. Instead of
hard coding this to be flex call, I think we should do what most dashboards do to ensure they're responsive at all sizes, and that's using a grid. With a
grid, we'll be able to change how many rows or columns each item takes up at different screen sizes and get way more flexibility and options instead of just being pigeonholed into a single column.
So, let's go ahead and get this div set up. First, we don't want this div that
up. First, we don't want this div that holds all of our drop downs and our hamburger icon to actually be included in this grid cuz they're kind of off on their own thing. So, I think that div is this div right here. But essentially
everything from the map all the way down. So, from here, not including this,
down. So, from here, not including this, but essentially here to here, all of this should be inside the grid. So,
essentially just the map and all these different cards that we have down here.
But what I'll actually do is go back over here, control shiftp wrap, and we're going to wrap this all in a div.
and we're going to give it a class name of grid. The next question to ask when
of grid. The next question to ask when setting up a grid is how many columns should this grid actually be? Well, when
you're working with Tailwind responsiveness, sometimes it's easier to start with the small screen sizes and work your way up from there because technically that's the way that Tailwind is built. The default styles that you
is built. The default styles that you want at the smallest screen sizes, you don't put any breakpoint on them. But as
soon as you want to start getting bigger, that's when you specify the SM, MD, LG, and so forth. So, let's actually do that. And we'll start from the
do that. And we'll start from the smallest screen size. Let's set it to around, I don't know, 6 or 700 in terms of width here. Let's just say, I don't know, 650. At this screen size, we don't
know, 650. At this screen size, we don't really have any more room to allow more than one column in this grid. I mean,
essentially, we're going to have each car take up the max width because we don't have a whole lot of width real estate to work with. So, on this grid here, we'll say by default, this will be grid calls one. If we go ahead and save
the file like this, we are still in our column layout, although we lost all of our gap. So, let's go ahead and just add
our gap. So, let's go ahead and just add gap four back on here to ensure we have proper spacing between our grid items. And this grid now looks perfectly fine for this screen size because it's super basic just being a single column. So,
now let's start widening our window a little bit. Once we start to widen and
little bit. Once we start to widen and we get closer to the I don't know 900 or a,000 width starts to be the point where I think realistically we can fit more than just one column. We can keep the
map as being the whole width cuz the map is kind of the focal point of the app.
Plus, we have some cards that are still pretty wide. But what I'm willing to bet
pretty wide. But what I'm willing to bet is that the daily forecast here and also the current weather could most likely fit in the same row at this point instead of each having their own row, especially this current weather card
because right now there's so much empty kind of like white space here on the card since it's all centered. And I
think it looks a bit bad at this screen size. So, I think what we can do here is
size. So, I think what we can do here is just make the current weather fit on the left side of the column and the daily forecast fit on the right side of the column. And then aside from those two
column. And then aside from those two cards, everything else will still stay at the full width. And the way that we can specify how much space we want each grid item to take up is by specifying
call span and row span on each of the items. And unfortunately, I can't just go in here and throw a class name on each of these suspense items and do like call span 4 in here because suspenses don't take class names. So what I'll do
here actually for each of these divs or sorry for each of these sections is I will actually wrap them in a div. So
I'll wrap each one of these suspenses in a div here. That way we can get more specific with the styling because we can give each one of these their own class name. So we'll do this one wrapped in
name. So we'll do this one wrapped in div and we will wrap this last one in a div right here. And this div here for each one is essentially just going to represent the actual grid item for that
card. So this is the grid item for our
card. So this is the grid item for our current weather. This is the grid item
current weather. This is the grid item for hourly forecast and so forth. Now
what we can do is we can say by default every single one of these is going to be call span one. Now technically you don't need a class name to specify that but to be explicit and to show you what I'm trying to do here I'm going to write
this on every single one. So we'll just add this call span one on all these by default with no other conditions.
Every single one of these will take up one whole column and that'll also be added to our map. So we'll add it to this div right here. Oops. We'll just
add call span one. throw it onto this already existing class name. And now
we're just being explicit. The H grid item is always going to be one column.
However, I just mentioned that once we're around this point of being, I don't know, 900 or so pixels in width, we're starting to have a bit more room.
And I know that the medium break point for Tailwind is 768 pixels in width. So,
I actually want to try that one first just to see how it looks. So what we'll do is on our grid here, we'll go up and we will say at medium or higher screen sizes, we want it to be grid calls two.
That way our entire grid is going to be two columns wide. Now if we go ahead and save this, everything is going to get super super out of whack. And that's
happening because all these grid items are still only spanning one column. So
essentially what we need to do here is for all of these we need to say when we're on medium or bigger screen sizes and we have two columns we still want some of these to span the entire width.
So we'll say call span 2 for the map for example. And now if you do that the map
example. And now if you do that the map still goes the entire width. Let's take
this and we'll add it to the map. We'll
also add it to the hourly card and we'll also add it to the additional info card.
So these three will still take up the full width. Now, the only ones that
full width. Now, the only ones that aren't taking up the full width are going to be the current weather and the daily forecast. Because this hourly
daily forecast. Because this hourly forecast card is now sitting between the current and the daily forecast, that means that these aren't ever going to connect. So, to fix that for right now,
connect. So, to fix that for right now, let's take this daily forecast div.
And now, if we save it, we're getting somewhere. If I go ahead and shrink to
somewhere. If I go ahead and shrink to around 800 width where this break point starts, right around hereish. I think
this still looks pretty good. Everything
has plenty of breathing room. If I go to 900, still looks great. Nothing is
stretched too crazy. So, I actually think this format would work quite well, keeping everything mostly in one row, minus the current weather and the daily forecast. The only thing that's a little
forecast. The only thing that's a little bit awkward right now is that this current weather card and the daily forecast are clearly not the same height. One way to fix this would be
height. One way to fix this would be doing kind of a band-aid fix, but we could just go into the current weather card here and we can add some extra spacing at the bottom. That way, they're not off on the height. Meaning we could
go on this card, could add a class name on this, and we could add something like PB8. Oops. PB8 like this. And it should
PB8. Oops. PB8 like this. And it should create some extra spacing on the bottom.
It looks like that isn't quite enough.
So, let's actually change this to, I don't know, PB12. Too much 11 maybe.
Yeah, let's do PB11. And that looks to be good. However, one thing is we want
be good. However, one thing is we want to make sure we don't mess it up on the other screen sizes cuz we already got it looking good on the small screens. So,
let's add MD to this. And we're saying only on medium and above screens do we want this extra padding to be there. So
it's kind of a banded fix. I don't
really love doing this, but this problem I think is only going to be happening on this particular screen size. So I don't think it's that huge of a problem. And
now I think we're actually in the clear with this screen size. Let's go to the starting point again. Our starting point is 768. So once we're below that, we're
is 768. So once we're below that, we're going to have this single column layout.
As soon as we get to 768, boom. mostly
the same thing minus the fact that now the current card and the daily forecast are side by side. And if we keep scaling up 800 looks great, 900 looks great,
1,000 looks great, and everything is still looking good. And just to double check, let's just make sure it's all smooth here. It looks like there's no
smooth here. It looks like there's no points where it gets really weird or looks super ugly. In fact, I think it looks super nice all the way throughout all these transitions. It's just growing and shifting and everything seems to be
moving the way we want it to. Now that
we know it looks good at this screen size, I'm going to shrink my VS Code to be even smaller. And I'm actually going to take these dev tools here and I'm going to pop them out. So, I'm going to go Oops, that is not the right thing there. What I'll do is I'll go here and
there. What I'll do is I'll go here and just pop the dev tools out. That way,
it's not actually taking up space. And
now, if we expand this, we should be able to get as big as almost 1,700 pixels. I think I went a bit overboard
pixels. I think I went a bit overboard with shrinking VS Code down. So, now I just hardcoded this to 1,600 pixels, and we'll have a lot more real estate to work with. So going back to the size we
work with. So going back to the size we had, we just working around the roughly the 800 to,000 pixel range. And now
let's keep working our way up. I think
that once we cross the 1,024 pixel point, I think it's definitely still smart to have current weather and daily forecast side by side and the other one still in their own column. Because at
this point, because the side panel just came back in, it doesn't make sense to switch it up because now we're again forced into a smaller size for our dashboard. So, as soon as we get over
dashboard. So, as soon as we get over 1024, we're actually losing quite a bit of real estate for our dashboard, at least in terms of the width that we have to work with. So, this screen size should be good as is. Now, if we keep
scaling up here, we can notice we're obviously getting more and more real estate. And once we're at the I don't
estate. And once we're at the I don't know, 1 12200 to 1300 mark here, I still don't really think there's a ton more we could do. Again, having the side panel
could do. Again, having the side panel here essentially collapses the dashboard real estate to be almost what it was before 1024. And actually, yeah, this
before 1024. And actually, yeah, this size looks to be a little bit bigger than the size that we have here because this side panel is now taking up actual space. But I would say once we keep
space. But I would say once we keep stretching and we get to the point where where we're at about I don't know, probably at this point here, so around the 1500 pixel point in width, now we're starting to have quite a bit more real
estate to work with. Might be able to fit more things into our grid rather than just a mostly single column layout.
And funny enough, 1536 is the last built-in tail and breakpoint, and that's 2XL. But I think 2XL would actually be a
2XL. But I think 2XL would actually be a great breakpoint for us to use for full-size computer screens. And let's be honest, nobody's really using 1280 x 720 screens anymore. Pretty much every
screens anymore. Pretty much every computer device is going to be at least 1920x 1080, meaning the width on all genuine computer screens will be at least 1920 pixels. So, it will be
included in this 2x breakpoint. In other
words, the view that we already have here is what most mobile devices are going to see or tablets. The view that we're about to make at this 2x breakpoint once we get above 1536 is what most computers are going to see.
So, how should we now design this dashboard to look on computer screens?
If we go back to the drawing board here, here's kind of what I was thinking.
We'll assume this white space here is pretty much just our dashboard. I was
thinking we would still have the map essentially take up the entire width of the dashboard. Again, I think that's
the dashboard. Again, I think that's fine because the map is kind of the focal point of our entire app. And then
what I think we can do is we can have the current and daily weather cards go below it but on opposite ends of the dashboard. So we'll have let's say the
dashboard. So we'll have let's say the current weather card here and then we will have the daily weather card right here just like this. And then
the last two cards that we have are the hourly forecast and the additional info.
And we could put these two here kind of more long ways. We could say the hourly forecast is this one. And then the additional info could be this one. Just
like this. I don't think this will get super complicated, but we'll have to watch super carefully to make sure that we're setting up our grid properly. So,
looking at our sketch here, I think this can logically be broken down into four separate columns. Essentially, I'll draw
separate columns. Essentially, I'll draw red lines here for each of the columns.
These aren't actually proportional really. Actually, got a bit of a
really. Actually, got a bit of a brighter color. Still a brighter red
brighter color. Still a brighter red here. So, our first column is going to
here. So, our first column is going to be kind of this line right here. These
middle cards here are going to be two in width. So, we'll say these are each two
width. So, we'll say these are each two columns. And then we'll have kind of
columns. And then we'll have kind of this layout. We're going to have
this layout. We're going to have essentially section one, section two, section three, and section four. With
that being said, let's make this a reality. So, it's four columns. So, on
reality. So, it's four columns. So, on
our grid here, we're going to go let's see where's it at. Oh, wrong component.
Let's go back to our app. Here we have the grid. Grid calls one by default. on
the grid. Grid calls one by default. on
medium or higher where grid calls two.
And I just said on 2 XL, so 1536 or higher, we want it to be four columns.
And if I save it, it's of course going to screw up our layout pretty bad because we haven't told each of the grid items how many columns each of them should span. And also because of the way
should span. And also because of the way that I drew out the sketch, we're not only going to want to control the horizontal spacing, so the calls, I also want to control the vertical spacing as
well. So we're going to also throw 2XL
well. So we're going to also throw 2XL grid rows for. That way, we can think about this grid like a 4x4. And that's
going to make things easier if we have an actual defined height and width for each one. Let's start with the map. I
each one. Let's start with the map. I
mentioned in our sketch that the map should really still just be the whole width of the entire dashboard. So, let's
first throw a 2XL. We're going to say it should be call span 4 to take up the whole width of the grid. Go ahead and save it. And we've already achieved
save it. And we've already achieved that. I also think not only should the
that. I also think not only should the map take up the entire width of the dashboard, I think it would make sense to take up half the height as well. That
way the first half of the dashboard height is just the map and the second half is the rest of our cards. So with
that being said, we know that our grid is going to be four rows tall. So we'll
say this at 2XL is going to be a row span of two because two is half of four.
So it'll take up 50% of the height. Go
ahead and save it. Now everything is going to look a bit whack. Part of the reason for that is because our grid doesn't have a defined height. So it
doesn't really know what to base it off of. However, it's logical to assume that
of. However, it's logical to assume that this dashboard is going to take up the entire height of the screen at this size. So, let's go to the outermost
size. So, let's go to the outermost dashboard div here, which is this div.
And let's just throw a h full on it.
That way, it's the height of the screen.
And actually, sorry, not h full. It
should be H screen. And it should only be confined to the height of the screen on 2 XL or higher. So, we'll say 2XL hide screen just like this. We also need to fix the fact that if we go back to our map here, this height is still
hardcoded to 500 pixels. So, now that we have a defined height for the grid, let's set this to 100% as well. That
way, we shouldn't have to worry about a hard-coded size messing things up. Go
ahead and save it. And let's refresh this. And now, our map is going to look
this. And now, our map is going to look a bit weird still, but we're getting better. The reason it's so big right now
better. The reason it's so big right now is because these cards aren't spaced properly. So, right now, this map is
properly. So, right now, this map is technically half the height of the rest of this height, but all this is overflowing still. So, it looks really
overflowing still. So, it looks really bad. So, now let's fix the rest of these
bad. So, now let's fix the rest of these cards. Remember in the design I said
cards. Remember in the design I said that the map should be half the height of the entire dashboard, meaning the rest of these four cards here have essentially two columns more of height to work with for all four of them. So
let's now look at our current weather card. Right now it's only spanning one
card. Right now it's only spanning one column, which actually works out cuz that's exactly what we want on the screen size. However, we want it to take
screen size. However, we want it to take up half the height of the grid. So we'll
also just like the map say this should be 2x row span 2 just like this. And for
a second real quick, I'm going to skip daily forecast. If we look at hourly
daily forecast. If we look at hourly forecast, I mentioned that both the hourly and the additional info should both span two columns to make room for the other cards on the side. So each of
these is actually going to take a 2XL call span two. So they each take up two columns of horizontal width. We'll take
it on this one. And we'll also throw on the additional info one like this to make sure they are spaced properly in terms of their width. But actually,
funny enough, I just realized we already have that for the MD. This actually is not needed at all because it'll still apply to the 2x breakpoint. So, we're
actually perfectly fine as is. However,
looking back at our design, we want both the hourly forecast and the additional info to look pretty short. So, that when you add the height of the hourly forecast and the additional info card together, you get the height of the current weather card. Current weather
card is two, which should mean that each of these little smaller cards, the hourly forecast and additional info should be a height of one. So what we'll do for that is we'll say on 2 Excel we're going to say it should be a row
span of one to give us some more vertical height. We'll throw it on here
vertical height. We'll throw it on here and we'll also throw it on here both the hourly and the additional info. Go ahead
and save it and that should adjust the height of those cards. However, it's
still going to look really bad for one particular reason. These cards are now
particular reason. These cards are now super out of order in our layout.
Everything just reads from left to right. Meaning if we look back to our
right. Meaning if we look back to our sketch, the actual ordering of the elements should be the map first, then the current weather card, and then the hourly forecast, then the daily forecast, and then the additional info
at the very end. What that essentially means is that our hourly forecast right here and our daily forecast would have to swap positions in the JSX. However,
if we simply do that, we're going to mess it up how it looks at the smaller screen sizes because we already made the ordering work for this. Though lucky for us, grids have a pretty convenient way to handle the ordering of things,
especially at different screen sizes.
Till one has a property called order that you can use that specifies the actual order of things. So let's smack order on each of these in the default order that they're in. So matching the
JSX for the map up here, which is this div, we will just give this order of one. We'll take this essentially for
one. We'll take this essentially for each one. We'll say map is one. This one
each one. We'll say map is one. This one
is going to be two. the current weather card. Daily forecast is going to be
card. Daily forecast is going to be order three. Hourly is going to be four
order three. Hourly is going to be four and additional info is going to be five.
Now, this is self-explanatory and this by itself doesn't actually do anything because they're already in this exact order. However, on 2XL, we need the
order. However, on 2XL, we need the order to switch slightly. So, what we can do to make sure the daily and the hourly swap positions, we can say normally this is order three, but once
we're on 2XL screen size, we're going to make this order four. And we'll do the exact opposite for the hourly forecast.
So we'll say normally it's order four, but on 2XL or higher screen sizes, we'll make it order three. And now if we go ahead and save it, our dashboard is almost going to look normal. Not quite,
but it's getting there. Go ahead and refresh just to make sure. And yeah, so we clearly have some spacing issues still, but the overall kind of grid layout is actually how we want it.
However, we might have an actual problem now. So, we made our map essentially be
now. So, we made our map essentially be height auto because we did the height and width of 100%. However, this is going to cause problems if we're on a really small screen size because now
there's no actual defined width or height. The map has no idea essentially
height. The map has no idea essentially how it needs to size itself. So, what
we'll do is we'll go to the grid item that's actually holding the map here, which should be this right here, this div. What we're going to do is we're
div. What we're going to do is we're going to say let's give it height 120 when we're on small screen sizes and then only at 2xL or higher. Then we'll
do height auto. That way once we're on this screen size, it still looks fine.
However, once we go bigger now, it can do the auto sizing. Let's keep expanding and get to our bigger screen size here.
And again, we still have a really weird spacing issue because this stuff down here is way too tall and out of place.
So, it's making our map also look super tall. I think part of the reason this is
tall. I think part of the reason this is happening is because this additional weather info card is a bit too tall. So
it's causing some weird spacing issues.
So let's actually go into that component real quick. Close our other components.
real quick. Close our other components.
We will go into additional info. Right
now this additional info card is just hardcoded to be a flex flex call. So
everything is very vertical in our design. We have this additional weather
design. We have this additional weather info card being quite a bit wider than it is tall. So I think in that case we should make it not a singular column but maybe like a 3x two. So we'll have for example the cloudiness, UV index and
wind direction in one column and then the pressure, sunrise and sunset in the next column. The easiest way that we can
next column. The easiest way that we can do that is by still maintaining this on smaller screen sizes by just making this a grid but just giving it grid calls one. So by default it is still a
one. So by default it is still a singular column but on let's say medium screens or higher then it's going to be grid calls 2. And the reason I'm doing MD and not 2XL is because I think there
would still be plenty of space on medium as well. So, if I go ahead and save it,
as well. So, if I go ahead and save it, I think it looks decent here being two columns instead of one. And if I for a second shrink down to a smaller size, I also think this still looks completely
fine being two columns and not just one.
If we go smaller though, below 768, now it'll go into the vertical layout. Let's
go back to our super big screen size for a second. And even though now it's two
a second. And even though now it's two columns, we still have this really weird spacing issue going on. The thing that's immediately obvious to me is that our grid is overflowing the page. As you can see here, where the background of the
page cuts off is right roughly in the middle of this hourly forecast card. And
that's where the grid should actually cut off as well. But the grid content is just way overflowing it. We need to make sure our grid takes up all of the space in this dashboard that is given. So
basically the entire dashboard and no more. The way that we can actually do
more. The way that we can actually do that is we can go to our app here. We
can go to the div that has our grid on it and we can actually throw a flex one on it. The grid's immediate parent is
on it. The grid's immediate parent is already flex flex call. So by throwing flex one on this div, the grid, we're giving this grid permission to grow to fill the rest of the space that it has.
If we save it, however, it's still going to be messed up. Nothing changed. But I
want to show you some magic here. If for
whatever reason in a flex call layout, flex one by itself is not working, go on the div that has flex one and throw a minz0 on it and that will actually fix your spacing issues. I'm going to be
honest with you. I don't know why min height zero works for flex one like this, but I've used this trick a bunch of times and it saved my life quite a bit. In the same vein, if you have a
bit. In the same vein, if you have a flex row, you can also use min width zero to achieve the same effect.
Sometimes having that min height or that min width actually saves the flex one and makes it behave properly. Having
this flex one and min here shouldn't affect the styles in other screen sizes, but just to be sure, let's make sure they only apply when we're on 2x or higher. So, we'll add 2 Excel to both of
higher. So, we'll add 2 Excel to both of these because we've already got the smaller screen sizes looking good. I
wouldn't want to regress and mess these ones up. And now, this is going to fix
ones up. And now, this is going to fix our grid sizing problem. It looks like our grid now is the actual height that it should be. However, we still have this kind of awkward gap between the hourly forecast and the additional
weather info. For some reason, there's
weather info. For some reason, there's just the spacing here that makes it look really awkward. I think what the issue
really awkward. I think what the issue is, we're noticing we have that gap between those. And also, if you look
between those. And also, if you look below the current weather and the daily forecast cards, we also have some gap where the cards aren't even getting close to touching the bottom of the screens. And I suspect the reason for
screens. And I suspect the reason for that is is if we go into our shared card component, these cards don't actually have a fixed height. So, they're only going to be as tall as their content is.
However, on this screen size, I want these cards to take up the entire height of each of their grid cells. So, what I can do is throw one more class name on here on these cards and say at 2XL I
want them to be height full. Now, if I go ahead and save it, they'll take up the entire height that they are supposed to. It looks like that fixed them except
to. It looks like that fixed them except for the fact that the daily forecast card is now only one tall. So, actually,
let's close this. Look back at our daily forecast here. And we forgot to throw a
forecast here. And we forgot to throw a row span on here. So, let's go ahead and let's go ahead and do that. We'll say
2XL row span 2. the same styles that are on our current weather column. Go ahead
and save it. And now it should be looking good. The only issue that adding
looking good. The only issue that adding HFold inadvertently caused is that now on some of our cards, we have some awkward space at the bottom that just looks a bit out of place. Like for
example, this current weather card here has way too much spacing at the bottom.
That doesn't quite look right. One thing
that we could do is we could go again actually back to our card components here. Let's go to card.tsx.
here. Let's go to card.tsx.
And we have this children class name.
Let's go ahead and add one more utility on here. Let's say that on 2XL screen
on here. Let's say that on 2XL screen size, so on this screen size, we're going to give these flex one. Meaning
that at 1536 or above, the children of the card need to fill the rest of the space that they're given. So we can go ahead and save this. And now what we could do is go to each of our individual cards, like for example, let's say the
daily forecast and go on to the children class name here, and we can say something like 2XL justify between. And
now these will evenly space out and fill the remaining content of the card in terms of the height. And we can do the exact same thing for current weather. So
let's go to current weather on the children class name here. We will throw 2XL justify between like this. And it
again spaces them out. And this works because these cards are both flex flex call. So if we do justify between it'll
call. So if we do justify between it'll space them out to fill all the remaining height. And then the very last thing
height. And then the very last thing would just be to fix some of the awkward spacing on the hourly forecast card because there's just this empty space kind of chilling at the bottom which I don't think looks very good. What we
could do is we could close both these files here. We could go into our hourly
files here. We could go into our hourly forecast and go to the div that's mapped out for each hour and we could throw a 2XL justify between which would stretch the
items to actually match the height.
However, now since we added some more space between each of the items, let's scale everything up slightly in size.
So, what we'll do is we'll go to both these P tags right here, and we're just going to throw a 2XL scale 110. Again, I
want to reiterate the fact that I'm using 2XL here a lot just to be sure that I don't mess it up as smaller screen sizes. I only want these changes
screen sizes. I only want these changes to apply on this big screen size, hence why I'm using the break points. So,
we'll do 2XL scale 110 right here. We'll
also add the exact same thing on this P tag down here. So, we'll add that. And
now, if we go ahead and save it, now the text is scaled up to look slightly more appropriate. And lastly, we'll just
appropriate. And lastly, we'll just scale up our weather icon. We know the weather icon takes in a custom class name if we want to control the size. So,
what we could do is just give it a class name here. And we will say at 2XL,
name here. And we will say at 2XL, instead of being size 8 by default, we want it to be, I don't know, size 10.
Now, if we save it, the icons are going to get a bit bigger. And now with these changes, our dashboard actually looks great. Before we call it good on the
great. Before we call it good on the responsive design, let's test every breakpoint and make sure we're looking good at every single screen size. On
this computer screen size that we've been at for a while, our side panel looks great. If we try and scroll down,
looks great. If we try and scroll down, we can't because this all fits on the dashboard. Let's start now scaling down.
dashboard. Let's start now scaling down.
The second we drop below 1536, so obviously here 1537, so we're at 2 XL.
The second we drop below it, now we go to this format where current weather card and daily forecast are side by side, but the rest is still width full.
Now, if we keep going down, let's keep shrinking. And let's see how this
shrinking. And let's see how this adjusts. Still everything's looking
adjusts. Still everything's looking great. Looking great. And eventually, if
great. Looking great. And eventually, if we keep going to 1024, the sidebar is going to drop out. And now we have plenty of real estate left for our dashboard. And we can open and close the
dashboard. And we can open and close the sidebar at will if we keep getting smaller. So eventually we'll hit below
smaller. So eventually we'll hit below 768. Eventually we go to just a single
768. Eventually we go to just a single column layout for everything, which is standard for most small screens. And
we're going to notice actually if we start to get too small, it starts to look really horrendous. Phone screen
sizes are going to be about I think 400ish pixels. So if I go on like an
400ish pixels. So if I go on like an iPhone right now and look at this, this looks completely unusable. This looks
horrible. Go back to our responsive mode here and go slightly. It looks like around the I don't know 700ish mark it starts to get to this point where the actual sizing of the dashboard starts to
disconnect from the rest of the screen.
And what I think is happening is we haven't really touched the styling of the header at all. This header has kind of just been chilling here. And my guess is is that the CSS is trying to make the page account for the header width. So we
get this horrible looking space start to just scale between our cards and the side of the screen. Let's go ahead and fix this real quick. Let's head back over to our dashboard component and let's just go ahead and collapse this
entire grid here just so it's easier to see what's going on with just the header alone. So, we'll go ahead and reopen
alone. So, we'll go ahead and reopen that and we'll focus on these couple of divs right here. First of all, this has been bothering me for a little bit. I
actually want to change this hamburger to size six instead of size eight just to make it a bit smaller. One of the issues we're having with this header that you can see already right here is this map type is wrapping and it's not
something we want. Ideally, it always just stays like this where it's in one row. It doesn't just wrap down like this
row. It doesn't just wrap down like this cuz then it starts to get a bit awkward.
So, let's go ahead and go to the H1 that has mapsite. And we will just throw a
has mapsite. And we will just throw a wide space no wrap on it. That way, that is never going to be a problem. Now, no
matter how much we go big or small, it always just stays in one line. Now, the
recurring trend you're going to notice is whenever things get small on a screen, generally we go to a single column layout. So having both these drop
column layout. So having both these drop downs in a row together gets a little bit weird when you want to get really small because if I get to I don't know say 500 pixels both the location and the
map type dropdowns are just super super cramped and to me it doesn't make sense to actually have them like this. I think
that once we're at a small enough screen size these drop downs should go to a flex call format so they're lined up vertically instead of being in a row. If
we go ahead and start bigger let's see at what point they start to become bad.
It looks like it's mostly okay right here. And then roughly around this
here. And then roughly around this point, around the 768 break point here is when it starts to kind of all fall apart. We lose the hamburger and the
apart. We lose the hamburger and the sizing starts getting weird. What we can do in this case is go to this div right here that has flex gap 8 that holds both the drop downs and the labels. What we
can do is we can say by default we want it to be flex call. However, once we're on medium screens or higher, then we want it to be flex row. And because
we'll be vertical on smaller screens, let's take away this gap 8, let's actually make it gap 2 when we're vertical. And then once we are
vertical. And then once we are horizontal, so we're on MD, let's say it is just gap 4. And now if we save it, it's still fine on this screen. However,
once we get small enough, then it goes to the column layout instead of being rows. So, it gives us way more room to
rows. So, it gives us way more room to work with and we don't have to worry about it getting super cramped.
Obviously, you can see the hamburger is now completely busted because it's just kind of sitting off screen here on the right. Let's address that in a second.
right. Let's address that in a second.
And for now, keep scaling down. If we go farther and farther down, this header is still going to be overflowing. It looks
like only once we get to the point of I don't know. I mean, it's about like 500ish, you can see the hamburger starts to kind of come back into the screen here. So like I don't know right
here. So like I don't know right hereish. But before then the header is
hereish. But before then the header is still overflowing because this hamburger you can see it's getting cut off.
However, the problem is right now with the size that we're working at, there's no break point below 640. 640 is
actually SM. So essentially if we're working below 640, we can't make any more break points under here. However, I
lied because we can actually use our own custom breakpoints and we can define them ourselves. So how we can do that,
them ourselves. So how we can do that, for example, is by going into our index.css CSS and finding the block here
index.css CSS and finding the block here should be at the bottom that is just this uh right here that is just at theme. We can add our own custom
theme. We can add our own custom breakpoint by going into here and saying dash breakpoint dash we'll call it excss for extra small and set it to 500
pixels. Just like we add colors by
pixels. Just like we add colors by specifying the color like for these or for radius, we can do the exact same thing with break points. And we're just setting this particular breakpoint to
500 pixels. and we can now use it like
500 pixels. and we can now use it like any other breakpoint. So if we go ahead and close this and head back to our app, now what we'll actually do is for this div we have right here that we added our
stuff to, let's actually take this and let's apply this only on the inner divs. So on this one and on this one and I'll show you what we're doing here in just a second. So let's take this third on
second. So let's take this third on these two. And now that we made that
these two. And now that we made that custom breakpoint being XS, let's actually use XS on this div. and we'll
go ahead and keep it flex flex call, but we're going to say once we're at XS or higher, then it should go flex row.
We'll also say we can maintain the gap 8 that we already had once we're at that big enough screen size. If we go ahead and save it, we can see this is the change that it just made. It's still in a row because we're above 500. But if we
get below it now here, now it drops down to a column. And we lost the gap here between these two things. Let's make
sure to keep a I don't know, gap four right here just to create that spacing.
So, it goes to the columns here, but once we're here, it's still in the row format. And I think this way right here
format. And I think this way right here might be one of the better ways to go about it. So, as you can see already,
about it. So, as you can see already, our screen isn't shrinking nearly as soon as it was before. We're not getting that weird thing where the size is actually disconnecting from the rest of the page. There's still some problems,
the page. There's still some problems, obviously, but we have that main problem fixed now. Just to make sure it looks
fixed now. Just to make sure it looks fine on bigger screens. If I go out here, it still looks the way that we want it. Now it just shrinks down once
want it. Now it just shrinks down once we run out of space and then shrinks down again. Let's fix these last couple
down again. Let's fix these last couple of things. First of all, when we're at
of things. First of all, when we're at this small of a screen size, it doesn't make sense for these drop downs to take up just a little bit of the width. I
think when we're at this small a screen, we need to make these drop downs right here take up the entire width of the screen. So, what we can do, for example,
screen. So, what we can do, for example, is go to location dropown. And right
now, it's just hardcoded to be 180 pixels. But what we can do for that is
pixels. But what we can do for that is we can say we still want to maintain that exact size. if we're at or above 500 pixels. However, if we're below
500 pixels. However, if we're below that, then let's just make it width full. And now this will be the full
full. And now this will be the full width of the screen. And we'll do the exact same thing for the other drop down, which is the map type dropdown. So
go in here, 500 or above, it's width 180 pixels. Otherwise, we want it to be
pixels. Otherwise, we want it to be width full. Go ahead and save it. And
width full. Go ahead and save it. And
now they're both the full width of the screen. And now the last obvious problem
screen. And now the last obvious problem that we have to fix is this hamburger right here. just looks awful sitting
right here. just looks awful sitting here. It's just kind of chilling in a
here. It's just kind of chilling in a super bad spot. And that's because this overall div layout right here is still flex call. And also the hamburger is ML
flex call. And also the hamburger is ML auto, which means it's going to go in the third column below the other two and be right aligned. So it just looks horrible. What really needs to happen at
horrible. What really needs to happen at this point, which most apps actually should do, is once we're small enough, we should probably have a separate header component and not try to make everything just fit. Because at a
certain screen size in most apps, trying to cram header stuff into the body gets really weird. So, for example, this
really weird. So, for example, this hamburger button is normally something that would be on the header. When we're
at big enough screen sizes, it is. It's
just here kind of sitting at the top.
But once we get smaller and we start getting more specific with how we're wrapping the stuff, having the hamburger there just doesn't make any sense at all. Cuz right here, it's just super out
all. Cuz right here, it's just super out of place. So, what I can actually do is
of place. So, what I can actually do is go to this button that has the hamburger. I'm going to give it a class
hamburger. I'm going to give it a class name of hidden by default. So for our small enough screens, it won't even be there, but a excess. So once we're at 500 pixels or higher, then we will make
it block. That way, it's going to appear
it block. That way, it's going to appear visible. So if we go ahead and save it,
visible. So if we go ahead and save it, that already makes our page look a lot better with that hamburger not being there. Now that we have this, let's make
there. Now that we have this, let's make a completely separate mobile header component where we can put the hamburger in. So I'll go to our file explorer over
in. So I'll go to our file explorer over here, go to our components, and make a new one that I will just call mobile header.tsx.
header.tsx.
Go ahead and close this. TSRFC to make the component. And let's make some basic
the component. And let's make some basic styles here. Let's set up this div.
styles here. Let's set up this div.
We're going to say class name is going to be width full to take up the full screen space. And let's just hardcode it
screen space. And let's just hardcode it to something like H16. E4 to give it some nice padding. And we'll also give it BG background. Normally, I wouldn't want the background of my header to be
the exact same as my dashboard, but in this case, I think it may actually look better this way. Lastly, we'll just throw sticky top zero on this. That way,
it'll always be stuck to the top of the page. The reason why I'm using sticky
page. The reason why I'm using sticky and not fixed is that sticky will make this header take up space in the DOM.
So, I don't have to worry about shifting everything else down. If I were to do fix, for example, I'd have to shift stuff around to make it look good.
Sticky is just easier in this case. Now,
let's go ahead and put some content in here instead of just having mobile header. So what we'll do is we'll go
header. So what we'll do is we'll go into here and we will actually copy and paste our hamburger button into this mobile header. We don't have access to
mobile header. We don't have access to hamburger in here. So let's go into app.
Let's find the import for hamburger.
Should be right here. Go ahead and copy it and paste it. That way we have the hamburger. And it's important to
hamburger. And it's important to remember that the reason that we made this whole component was to only show it on small screens. So essentially if we're over 500 pixels in size, we actually don't want to show it at all.
So let's just do excess hidden just like this. So anything above 500 pixels will
this. So anything above 500 pixels will be hiding this component entirely. I
also now just don't have the set is side panel open. So let's go ahead and
panel open. So let's go ahead and comment out this on click for just a second. Now let's go ahead and save
second. Now let's go ahead and save this. Let's go to our app here and let's
this. Let's go to our app here and let's render out mobile header up here and just see what it looks like. Save it
like this. And here's what we have. Just
so we can see it. Let's actually go in here. Let's make sure or remove BG
here. Let's make sure or remove BG background for a second. Make sure it is bg red 500. We can see it up here. It
looks like Oh, yeah. We don't care for these other styles now, like hidden and xs block. So, let's go ahead and let's
xs block. So, let's go ahead and let's get rid of this. And we can also get rid of this LG hidden. And let's change this back to BG background. Go ahead and save it. And here is what this actually looks
it. And here is what this actually looks like. Now, you can see that as we
like. Now, you can see that as we scroll, this should stay at the top of our screen. Looks like it's not
our screen. Looks like it's not actually. Yeah, looks like it's uh not
actually. Yeah, looks like it's uh not there.
It stays fixed for a little bit.
Actually, before we do that, let's fix a few other things. First of all, this hamburger should be on the far right of the header, not the very beginning.
Instead of relying on ML auto to do that, we could actually remove that. We
can just say flex justify end. And that
should make it align to the right side.
And just to make sure this header goes over our map, let's give it a z-index of 1,01.
However, if we start scrolling, we'll still notice the header is not quite staying fixed the way that we want it to or sorry, staying sticky. And we've seen it for a little while now, but we have this weird little side scrolling issue
where it seems like the width of the page is just more than what we actually have. We go ahead and refresh this just
have. We go ahead and refresh this just to make sure. Looks like yeah, it's still there. But let's try and fix that.
still there. But let's try and fix that.
I think the issue may be that in our app here, we have this W full. Let's go
ahead and just get rid of that. And I
think this actually might fix the problem. Not 100% confident. It did not
problem. Not 100% confident. It did not fix it. Okay. Well, I closed and
fix it. Okay. Well, I closed and reopened the responsive dev tools and now it's working perfectly fine. So, I
guess that's a non-issue. I'm not really sure why it was doing that. Let's fix
the sticky thing real quick. Actually,
that one also appears to be fixed. Might
have just been the responsive dev tools.
Sometimes these dev tools when you're in responsive mode, stuff just kind of bugs out and doesn't quite work properly. But
now, whether we go up or down, the header appears to be sticky cuz it's not moving at all. It's always staying at the very top of the screen because you can see the hamburger staying fixed, which is exactly what we want. Okay, so
now the last thing that we should actually need in here is to make this hamburger button actually functional. We
just commented out the on click. Let's
actually uncomment this. And we know this is a piece of state that is in the parent of this component. So let's just pass it down as props. We'll say that it is expecting type. I don't like typing
that out. We're going to say this, which
that out. We're going to say this, which we know is just going to be a dispatch of set state action and that's going to be a boolean.
Dispatch is of course imported from React and then we will extract this out of here. Now save this. It should be
of here. Now save this. It should be good to go. But we'll go ahead and pass it down now. That is side panel open to set is side panel open. And now if we go
ahead and save it, go back here. This
hamburger should work the exact way we expect it to. We can now open and close the side panel at this small screen size. So now what's really cool with
size. So now what's really cool with this mobile header is that once we're on big enough screen sizes, it doesn't even show up. So now we're at 550. So we're
show up. So now we're at 550. So we're
above the excess break point. So this
entire component is just hidden. We
still have the hamburger right here that works the exact way that we want it to.
But now when we go below spawn of sizes, we actually render out a separate header component that has a hamburger in it that works in the exact same way. Pretty
cool. One thing we should quickly fix is there's a bit too much spacing, I think, here between the bottom of the header and where this first map type drop down is. It looks like there's just some
is. It looks like there's just some awkward kind of space there because if we go to our app and look at this, it is using P8 all the way around. Now, I
think that's normally fine except when we're on this screen size, it just looks like it's too much. So, let's actually change this. Let's say um the others can
change this. Let's say um the others can be P8. So the left, right, and bottom,
be P8. So the left, right, and bottom, but the top should actually be we'll say PT4, but then on big screen sizes, we'll still be fine with PT8. So excess PT8
just like this. And if I go and save it, I think that looks a bit better not having so much padding at the top. So
essentially PT right here, padding top is more specific than just padding. So
PT4 is going to override P8 normally.
However, once we get to XS, we default back to PT8. Let's now go through a couple of different small sizes just to see how everything works, especially on mobile. So, right now we're on
mobile. So, right now we're on responsive. Let's go to iPhone SE. It
responsive. Let's go to iPhone SE. It
looks like right here, everything looks almost great except for the fact that especially this uh map legend right here is just completely out of place. It's
just way too big. So, what we'll do actually is go to that map component, which should be the map legend. Right
now it's hardcoded at width 96. Let's
say only do that if we are on XS or higher. Otherwise, let's just do it as
higher. Otherwise, let's just do it as half that width. So, we'll do width 48.
Now, go ahead and save it. And that
looks quite a bit more manageable. If we
go back to testing this, we know that scrolling this still works perfectly fine. If I go click on this, it opens
fine. If I go click on this, it opens the side panel just the way that we want it to. And it looks like all this is
it to. And it looks like all this is working beautifully. Just out of
working beautifully. Just out of curiosity, let's go to landscape. And
here we can see it also looks good. I
can scroll down here, see all of our cars that are width full, and everything appears to be in order. I'm not a graphic designer, so I'm sure there's ways to make this look even better. And
maybe there's certain cases or things that I'm not thinking about, but that's something that could be added if you wanted to make improvements to this app.
If we just go back to portrait mode, let's go ahead and change this to, I don't know, Pixel 7. Should be a bit bigger, but same kind of idea. Our drop
down still work perfectly. Our side
panel opens the way that we expect it to. Our map works. We can click on it,
to. Our map works. We can click on it, load some data in. Go over here. Let's
say I want to know somewhere near Vietnam. Go here. All that works
Vietnam. Go here. All that works perfectly. Then we have our cars down
perfectly. Then we have our cars down here. Same thing. And they all look
here. Same thing. And they all look great. This still scrolls horizontally,
great. This still scrolls horizontally, so it appears to be working right. And
everything else just looks super tidy.
Now, let's put this back in responsive mode and go through these break points one last time. So, we're small here. We
know what this looks like. We just been messing with this mobile header. All
this works very smoothly. If we start expanding it, we get over 500. Mobile
header goes away. And now the hamburger plus the drop downs are all in the same div. And everything looks good all in a
div. And everything looks good all in a single column layout. We eventually hit 768. Here we're now still single column
768. Here we're now still single column for the most part, but these go to double column. Keep going. Keep going.
double column. Keep going. Keep going.
And it looks to be good. Side panel
reappears. We hit the large break point.
and everything is just as we intended to be. Now, there's just one final thing I
be. Now, there's just one final thing I want to do in terms of responsive design. That's going to be for our
design. That's going to be for our desktop view. If we go back to having
desktop view. If we go back to having this desktop view, the view still looks great. However, one thing that we're
great. However, one thing that we're going to notice is if we start collapsing the height, this view is going to start looking kind of bad pretty quick. As you can see already
pretty quick. As you can see already right here, the stuff is already overflowing out of the grid. So, it's
getting like this stuff's getting cramped together and suddenly it starts looking real bad. The reason why the other screen sizes are fine and don't have the same problem is because this screen size at 2 XL is the only one that
has an actual defined height. It's
supposed to be the height of the screen.
So on other screen sizes, it's expecting it to overflow. That's fine. But on
here, it's not, and that's why it's causing this weird visual shift. Now,
it's worth noting that this is probably not going to be a very common occurrence. I mean, this aspect ratio is
occurrence. I mean, this aspect ratio is pretty out of whack. It's quite a bit wider than it is tall, but certain laptop screens I've noticed especially can kind of have these weird dimensions where the aspect ratio is like a 21 by9
or something that actually does get quite a bit wider than it is tall. So,
you end up having this really weird layout shifting stuff that happens because you're at an aspect ratio that you're not probably testing all that often. Because, like I said, this
often. Because, like I said, this situation is likely an edge case. I
don't think we need to start reordering grid items and making this more complex than we have to. It looks like at about roughly, if we keep going down, roughly
1,080 pixels in height here, it starts to get a little bad. Actually, not even that. It's more like I would say like
that. It's more like I would say like 1120 pixels. Starts to look kind of
1120 pixels. Starts to look kind of rough. What we can do as a quick
rough. What we can do as a quick band-aid to that is we can go to our app here. We can go to the div that has this
here. We can go to the div that has this 2XL height screen. And on here, we're just going to add a min height. We're
going to say 2xl. It's going to be min height, 1100 pixels. Actually, we'll do 1120. Just like that. Now, we go ahead
1120. Just like that. Now, we go ahead and save it. And this will be the min height. So that if we go below this,
height. So that if we go below this, we'll just have to overflow. No more
trying to cram everything all together in the screen height. If it gets too small for whatever reason, we can just start scrolling. The big thing with
start scrolling. The big thing with responsive design is the width. That's
why all the break points are width based. You know, SM, MD, LG, they're all
based. You know, SM, MD, LG, they're all based in the width of the viewport, not the height. When you start worrying
the height. When you start worrying about the height, things get a little bit weirder. You can use height break
bit weirder. You can use height break points or you can just do something like this where things scroll if the width to height ratio gets out of whack. And
that, ladies and gentlemen, should wrap it up for responsive design. Now that
we've tested practically every screen size, it is safe to say our app is pretty responsive. Maybe not 110%
pretty responsive. Maybe not 110% perfect, maybe not completely flawless, but it does look great on each screen size. And we've verified we have no
size. And we've verified we have no major issues with it. Again, I'm sure there's improvements to be made here and there if you'd like to on your own time.
But now that we have this looking great, let's move on. Before we move on to the very last thing, we made some changes to our cards like daily forecast and current weather. But we didn't update
current weather. But we didn't update our skeleton cars with those changes. If
we don't make these components match one to one in terms of how their size and how they look, we can get some weird mismatch problems. So, let's add those real quick. Let's open both the current
real quick. Let's open both the current weather card and the current weather skeleton. So, we'll open this here and
skeleton. So, we'll open this here and the current skeleton. And you'll notice because we added stuff to the children class name of current weather, it's going to be different from this children class name. So let's actually take this
class name. So let's actually take this whole thing and just copy it over to make sure it still matches up perfectly one to one. We wouldn't want that to get out of whack. And we'll do the exact same thing for the daily forecast. So
we'll close these here. Go to daily skeleton and we will also go to daily forecast. You can see this class names
forecast. You can see this class names obviously been changed. So let's take this, copy it, and then just copy it over to the children class name here to make sure they are perfect one to one.
We'll also do the exact same thing for hourly forecast and hourly skeleton, except we shouldn't have to change the children class name. We open this here.
We also open this. The only things that we actually added was first of all this 2XL justify between. So let's take this and add it to this div right here. And
then we also just need to take the scales that we changed. So, we made this 2XL scale 110 on both the P tags. So,
we'll make sure to throw that on these two. And then this skeleton went from
two. And then this skeleton went from size 8 to being size 10 for 2XL. So,
we'll take this and we will go ahead and throw that just on here. And now these should also match up one to one. And it
seems like I might have forgot. But
while we're here, let's make sure this is rounded full because it should be circular since it's an icon. And lastly,
let's look at the additional info card.
So, we'll go into this and we will also go into the additional infos skeleton.
Looks like all we changed in here was this children class name. So, let's take this, copy it, and we'll go ahead and paste it over. And now, taking into account our responsive changes, our skeleton loaders should match with their
actual components one to one. No matter
what screen size we're on, whether it's SM, MD, LG, 2XL, the skeleton loaders and their actual cards are going to match up perfectly. All right, so we've come a super super long way and we have
one final thing I want to do on this app before we call it done. And that's going to be making a toggle for light and dark mode. This is something that is becoming
mode. This is something that is becoming more and more common in modern apps. So
I want to show you how it's done using Tailwind plus Shad CN. We're going to have some help from Shad CN on this one, especially with the styles that it gave us in the index.css file. But I'll
explain exactly how this works and I will show you every single step. First
things first, let's get a switcher component set up that will actually toggle us between light mode and dark mode. If we head over to Shad CN, they
mode. If we head over to Shad CN, they have a really nice switch component that I think we could use. It basically just works like this. You just toggle it back and forth. And in our case, because it's
and forth. And in our case, because it's binary, we can just go between light mode maybe on the left and then dark mode on the right or vice versa. Doesn't
really matter. We'll go ahead and this should be the last actual Shadian component we install. We'll take this command. Go ahead and open our terminal
command. Go ahead and open our terminal here, make a new one, and we will add this switch. And now we have the switch
this switch. And now we have the switch in our app. So now that we have this, let's actually make the final component that we're going to make in this video.
What we'll do is we'll open up our file explorer, go to components, and we will make one called light dark toggle.tsx.
PS RFC to create the component. And in
the JSX here, let's just render out the switch component just like this. That
comes from UI on SVG repo. I found two good SVGs that we can use. I have this sun.svg that we can use for light mode.
sun.svg that we can use for light mode.
And then I have this moon.svg that we can use for dark mode. Just to get the sun and the moon imported into this component here, I'll go to where we have our app. Let's take this hamburger for
our app. Let's take this hamburger for example. I don't feel like retyping this
example. I don't feel like retyping this all cuz I'm kind of lazy. Get this in here. We'll call this sun. Call this one
here. We'll call this sun. Call this one moon. And I'll make sure we name them
moon. And I'll make sure we name them accordingly. I think it was just sun.svg
accordingly. I think it was just sun.svg
and moon.svg. Yep. So sun.svg
and moon.svg right here. What we can then do is wrap this switch right here in its own div.
And now around it on either side of it, what we're going to do is render the sun on one side and we will render the moon on the other. And to make this switcher look like a switch with two SVGs on each
side in a row format. What we'll do is on this div, just give it a class name of flex. We'll say item center and gap
of flex. We'll say item center and gap 2. And then for each of these SVGs,
2. And then for each of these SVGs, let's give them the class name. We'll
just say, I don't know, we'll try size five. It shouldn't be too big, but just
five. It shouldn't be too big, but just big enough to see. Let's go ahead and add that. Now that we have this, let's
add that. Now that we have this, let's go ahead and save it and let's render it out in our app. Let's go back here.
Let's go to app here. And if we go to where we have our drop downs at we have this div right here that has the hamburgers. Let's actually render it
hamburgers. Let's actually render it right here between the hamburger and the last dropdown. We will just say light
last dropdown. We will just say light dark toggle just like this. Making sure
to import it. And if we go ahead and save it, here it is on our page. Oh, and
I forgot because we're on dark mode now, let's actually add invert to both of these right here. That way they turn white. Now if we go ahead and save it,
white. Now if we go ahead and save it, here is our actual light mode, dark mode toggle. So we have a sun on the left and
toggle. So we have a sun on the left and a moon on the right. And now we can just toggle between them. I want to make sure that whenever this hamburger is actually on the screen that this light and dark mode toggle is still aligned at the far
right side. Right now it happens to look
right side. Right now it happens to look like it cuz the spacing is just perfect.
However, let's make sure that by wrapping both the hamburger button and the light dark toggle in a div here.
Just go ahead and give this div a class name of ML auto so it's spaced to the far right side. and we'll say flex gap for item center just like this. And
because we have ML auto here, we won't need it on this hamburger. Let's go
ahead and just get rid of that here. And
now that should make sure that styling looks good on all screen sizes. Again,
still looks the exact same here. But
that'll just ensure it stays good when we get bigger. Okay, so that's cool and all. We have the switcher that actually
all. We have the switcher that actually looks functional, but it does literally nothing right now. So, let's actually do what we came here to do, and that's making the switcher toggle between light mode and dark mode. How we can do this
is kind of up to us because ultimately there are a few ways we could approach this. Typically, the way I like to
this. Typically, the way I like to handle light mode and dark mode, is to wrap my entire app in a provider, which I usually call theme provider. This
provider will have the theme, which is only two options, light or dark, and also a function we can use that will toggle these between each other. The
reason why I do it in a provider normally is because sometimes, especially on bigger apps, you might need to know in really deeply nested components, whether we're on dark mode or not. Styling and dark mode can get a
or not. Styling and dark mode can get a bit tedious. So, I like to make sure
bit tedious. So, I like to make sure every component in my app has the ability to know whether we're in dark mode or not. So, I actually lied a second ago when I said we're making the last component, but now we're actually making the very last component, which I
will just go ahead and call theme provider. So, I'll go up here,
provider. So, I'll go up here, components, theme provider.tsx. tsx go
ahead and close out of this tsrfc to make the component at the very top of this file let's to be explicit here make a type that I'm going to say is theme
that is either just light or dark dark as a string in terms of the context this provider is actually going to pass down let's type it right here as type
theme context type equals this and like I said it'll have two things theme which is of type theme and then it'll also have toggle theme which we can just
assume is a function that returns void.
Now that we have these types we can just say const theme context equals create context and we'll make sure this is of theme context type or
undefined and it will default to undefined making sure to import create context from react. This is the standard way of defining a context always outside
of a component. Now that we have this, like all providers, we need to wrap the JSX with a context. So I'll remove this div here. We need to wrap it with theme
div here. We need to wrap it with theme context.provider
context.provider like this. And we can put the children
like this. And we can put the children inside of here. Children like in all providers are just a prop. So we can add to our props here. We can add children
which is always just of type react node.
Then we can get children out of here.
Again, children's a reserve keyword, of course, and render out children just like this. This theme context provider
like this. This theme context provider is going to wrap the entire app, which means that children is literally the entire app. So, the entire app is going
entire app. So, the entire app is going to have context to whether we're on light mode or dark mode because of this.
How we pass that down is actually by giving this a value prop. Right now,
it's going to say it's missing because we don't have it. We basically need to give it a prop just like this. Now for
this particular value, how I normally do it in a provider and how you normally should do it is just to make a state for it. So we'll say const theme and this
it. So we'll say const theme and this will be set theme use state is going to be of type theme and we're going to default it actually to dark mode to
start. And then we'll make a easy
start. And then we'll make a easy function to toggle between the themes.
We'll just say const toggle theme. It's
going to be a function and it's going to be super super simple. We're just going to call set theme. We're going to get the previous value out of here. And
we're going to say if previous equals dark, then when we toggle it, we're going to go to light. Otherwise, we're
going to dark. And now that we have this, we have both our theme and our toggle theme, which is exactly what this type expects. So, all we have to do is
type expects. So, all we have to do is pass in an object for our value and just give it theme and toggle theme like that. And now this provider is all good
that. And now this provider is all good to go. Again, just to reiterate, this
to go. Again, just to reiterate, this theme provider is going to wrap the entire app. And all it's doing, it looks
entire app. And all it's doing, it looks kind of complicated, but all it's doing really is giving our entire app context to whether we're on light mode or dark mode because of this theme here. And
then we give every component technically the ability to change whether we're run light or dark mode with this toggle theme. Now that we have this, we just
theme. Now that we have this, we just have to go to main.tsx, which is the entry point for our app. And all we need to do here is just wrap oops just wrap
this in a theme provider just like this.
And now our entire app has context to whether we're in light mode or dark mode depending on what this state is. What we
should do now is make a hook to consume the value of this context. Technically
because I'm doing this as just a one-off, I don't have to do this, but I think this is a good pattern to get into. Basically, in whatever file I made
into. Basically, in whatever file I made my provider, I'll usually export a hook.
So in our case, I'll say export const use theme just like this. It will be a function like all hooks. And all it's going to do is say const context equals
use context and it's going to use the theme context. Use context should be
theme context. Use context should be imported from React. And then we'll completely eliminate the possibility of undefined by just saying if for whatever reason context doesn't exist. Then we're
going to throw a new error saying use theme must be used within a theme provider. Then all we're going to do is
provider. Then all we're going to do is return context just like this. And
voila, we're good to go. This is
actually a pattern that I was taught that I think makes using context really safe and exposes them in a convenient hook. If you try and call use theme now
hook. If you try and call use theme now in somewhere where you shouldn't, it'll throw an error. Otherwise, you know you're good. This provider wraps the
you're good. This provider wraps the entire app. So, everywhere should be
entire app. So, everywhere should be safe. But in general, I think it's a
safe. But in general, I think it's a good thing to do. It just makes it so that in every component where I want to use a theme, I don't have to call use context. I can just call this hook and
context. I can just call this hook and it's already there for me. So,
basically, wherever I call this now, wherever I call use theme, I can get the theme and the toggle theme out of this context. Now that we have this set up,
context. Now that we have this set up, let's take this and let's actually head back to the light dark toggle because I wrote that convenient hook. All we have to do to get these out of here is say
const theme toggle theme just like this equals use theme. And now we have access to the light mode or dark mode. And we
also have the ability to change it between light and dark. This switch
right here is going to take in two primary props checked and unchecked change. For checked, we'll just say it's
change. For checked, we'll just say it's going to be checked if the theme is equal to dark because the moon icon is on the right side of the switch. And for
uncheck change, meaning whenever we actually click the switcher, all we're going to do is just give it toggle theme. And it's actually really that
theme. And it's actually really that simple. And now technically this switch
simple. And now technically this switch is fully functional. Whenever I'm
switching it to the light mode here, we're actually setting this theme and our provider to be light. Whenever it
goes to the moon, this is going to switch to being dark. So, we have our light and dark mode working correctly in our actual React state. But what good does this actually do? Well, let me show you the magic now and the reason why we
did this. If we open up our dev tools
did this. If we open up our dev tools here, you can see that from the beginning of the video, we've had this class equals dark on the main body of our HTML. This is what's making our app
our HTML. This is what's making our app dark mode right now. If I just completely get rid of this. So I go here, get rid of it. Now our app goes
into light mode. Pretty cool, right?
What this means is that what we need to do is listen for our theme changing. If
it changes to dark, we need to add class dark to the root div. If it goes light, we need to remove dark from the root div. So again, if this theme becomes
div. So again, if this theme becomes dark, we want it to look like this. The
second that it becomes light, then it should go light. and remove it. So, we
just alternate between these two states.
And this is actually very easy to do.
Let me show you how. We go to our theme provider here. All we have to do in here
provider here. All we have to do in here is just make a use effect just like this. Importing from React that has theme as a dependency. So,
we're listening for theme changing. What
we'll do is get the root div of our app by just saying const root equals document.cumentelement
document.cumentelement just like this. Then all we're going to do here is we're going to say if the theme is equal to dark, then what do we
do? Then we just want to say root.class
do? Then we just want to say root.class
list dot add dark else root.class list.reove
root.class list.reove
dark. And it's really that simple. And
just so it's not hardcoded anymore, let's go to index.html.
Let's remove this class dark. Completely
get rid of it here. Start like this. Now
it defaults to light mode here. Let's go
ahead and save this div. Now we're on dark mode. Click this. Now we go to
dark mode. Click this. Now we go to light mode. Boom. Magic. Every time this
light mode. Boom. Magic. Every time this toggle is clicked, we're alternating between light mode and dark mode. Now
you're most likely wondering how this even works. Because I haven't added a
even works. Because I haven't added a single style in this entire app that is dark mode specific. I didn't spend any time using Tailwind going into components and doing something like the dark selector where I can go and do dark
like this. because you actually can use
like this. because you actually can use this if you're on dark mode. It works
perfectly fine in Tailwind, but I didn't do that anywhere. So, how is this working perfectly? How does my app know
working perfectly? How does my app know to just change every single color on the page based on what the dark class is?
Well, let's go to index.css.
Go to index.css here. And I can actually show you. If we look in here, something
show you. If we look in here, something that happened a long time ago is we initialized shad CNN in our project and it asked what color palette we wanted to use. After I told it zinc, what it did
use. After I told it zinc, what it did and what it always does is defines a ton of CSS variables that we've seen time and time again. And these are all the CSS variables that we've been using. So,
we've got a color for background, foreground, card, card, foreground. You
get the idea. What you might not have noticed is that under all these CSS variables, but in this same file, we also have at the bottom this dark selector right here. And this is the
key. This dark selector right here is a
key. This dark selector right here is a class selector, meaning any element that has dark on it will define all of these CSS variables. If you look right now
CSS variables. If you look right now over here, we're on dark mode. And
because of that, we're going to use these CSS variables. And these variables right here are literally all the variables, all the same exact ones that are defined here up top. So essentially,
these are going to be the default variables that are used on light mode.
However, once we have the dark selector, we use these variables instead. And
these are all ones again that shad CN set up. So, for example, on our cards,
set up. So, for example, on our cards, when we're on light mode, our cards look like this. That is because the card is
like this. That is because the card is using this CSS variable. However, as
soon as we go into dark mode like this, because dark is a class selector that is more specific, this card is going to overwrite the other card. So, now we're using this color instead. And that is how literally all these other CSS
variables work as well. So basically in summary, we're in light mode. So we
default to just using this route here.
So all these CSS variables are relative to light mode. Now when I click dark mode, we now have the dark selector on.
So this class is triggered. And now our CSS variables are redefined. And that is what causes all these color changes when I go between light mode and dark mode.
Because after all, our card for example, our card component is using background card. And of course, like I just showed
card. And of course, like I just showed you, background card changes between light mode and dark mode, hence why the cards change color. I am going to 100% recommend this pattern over spamming the
dark selector in Tailwind. This pattern
here where you have variables defined up here that just get overridden by a dark selector is so much cleaner than going in every single component and saying, "Oh, I want it to look like this on dark
mode and saying dark, you know, BG Red 500 like this." It's actually kind of a cool gradient right there. But but no, you shouldn't be just spamming dark mode like this. It's fine to use it in some
like this. It's fine to use it in some spots if you need to, but if you want to handle the majority of the dark mode styling, do it with CSS variables. Now
that we have this understanding, let's clean up a few things between this light mode and dark mode because clearly not everything is quite perfect yet. So,
let's go ahead and head over to light mode. So, we go here. And now, if we
mode. So, we go here. And now, if we look, our SVGs aren't really visible.
For example, let's look at our additional weather info down here. If we
go to where that card actually is, so additional info, and we look at it, remember we have this invert on here, that just inverts the colors. If I go in here and I add the dark invert, so we're
only inverting when we're on dark mode, then they appear perfectly fine.
However, then I have to go and do this in every single SVG that's in dark mode, and I don't want to do this. So, what
I'll instead do is I'm actually going to remove every single reference in our app to this invert here. So, I'm going to search up invert.
Let's just see where it's used. I'm
going to get rid of it here. Save this
file. And basically everywhere it is, we're going to get rid of it because we're going to solve it all with one style instead of spamming it everywhere.
This will make things quite a bit simpler. Move these here. Take this. Get
simpler. Move these here. Take this. Get
rid of this one.
Got a couple in here. Let's uh see right here. Right here. And right here.
right here. Right here. And right here.
Get rid of all those. Don't need them.
And we'll get rid of this one. And now
instead of going individually in every single component and just putting invert wherever we can, what I can instead do is just do it one time. I go to index.css.
index.css.
Close all these extra tabs here. And all
I have to do is just go down here somewhere and I can actually write a style for this. I can just say dark SVG.
So any SVGs that we see that are in dark mode, all we have to do is say filter invert one. And now that's going to do
invert one. And now that's going to do the exact same thing. So if you notice, these icons still look good on light mode because they're black. If I go to dark mode here, they now look good because they're white. So it's the same
thing we were just doing, but I'm doing it in one spot instead of doing it in like 10 different files. Honestly, with
where it is right now, I think dark mode looks great. Everything looks crisp. The
looks great. Everything looks crisp. The
cars look great with their slight gradient, and everything seems to just kind of pop well. However, light mode is a bit of a different beast. It doesn't
look horrible, but there are some improvements to be made. First of all, these cards that are on the dashboard and the background color are clashing big time. The cards aren't distinguished
big time. The cards aren't distinguished from the background pretty much at all.
So, it almost just looks like one just massive white mesh. And I'll show you exactly why this is. For some reason, I don't know the logic behind this. If you
go up here or the top, you can see we have a problem here. Card is one 0 0, so pure white. However, background is also
pure white. However, background is also one 0 0, so pure white. So the card and the background are quite literally the exact same color. They're different on dark mode as you can see here. Card is
this 02106 and then background is 0141. So clearly
here they're different on dark mode but on light mode they're the same which I think looks pretty bad. Generally when
we're in light mode we want the cards to be slightly darker than the background.
This is the opposite of dark mode where we want the cars to be slightly lighter than the background. So instead of changing the background let's look at card here. Instead of this being one,
card here. Instead of this being one, let's make it slightly darker. Let's do,
I don't know, 0.97. So, it just very slightly offsets the white. Save it. And
now that's a little bit better. It
doesn't completely change it, but it makes it at least offset enough to somewhat tell the difference. One more
thing that we should add to make it help even more is throwing a border class on this, especially if we're on light mode.
So, let's go into our card here. Here,
and we have no border on it right now.
So, let's just throw a border like this.
Go ahead and save it. And now that makes it look even better, just more distinguishable from the background.
Having the border helps create some separation between each of the cards. So
you can see where each one kind of begins and ends. I think it's fair to assume that we don't need this border when we're on dark mode. I think it's a little bit overkill. I mean, it does look kind of okay. Uh but I think we
liked it before. So let's just say border like this. And we're on dark mode. We'll just say border none to go
mode. We'll just say border none to go without it. There's really no reason to
without it. There's really no reason to have it on dark mode cuz there's plenty of separation. It's just more needed on
of separation. It's just more needed on light mode to create that more shadowy effect. If we go ahead and open up our
effect. If we go ahead and open up our side panel, we're going to notice a couple of problems. Actually, only one main problem really. That being that the slider, not the actual slider itself, but the track for the slider is pretty
much invisible. I mean, it's super white
much invisible. I mean, it's super white against that pretty white background.
So, it's hard to see. If we head over to the side panel component, and we look to see where our slider is, should be actually down here. Our slider is right here. Let's go ahead and control-click
here. Let's go ahead and control-click into it. And if we view it, you'll
into it. And if we view it, you'll notice that we have this BG sidebar right here. However, clearly that BG
right here. However, clearly that BG sidebar does not look very good in light mode. So, let's keep this BG sidebar if
mode. So, let's keep this BG sidebar if we're in dark mode. Let's make us something different when we're on light mode. We'll instead try BG foreground.
mode. We'll instead try BG foreground.
So, when we're in light mode, use BG foreground. When we're in dark mode, use
foreground. When we're in dark mode, use BG sidebar. Now, if we go and save it,
BG sidebar. Now, if we go and save it, the track is actually visible now.
However, I can tell right away this looks way out of place and looks way too strong against the background. So, let's
make this, I don't know, 25% opacity there. And now, I think that is much
there. And now, I think that is much better. And one thing I want to take a
better. And one thing I want to take a look at right now, especially, is our skeleton loaders. If you just notice
skeleton loaders. If you just notice right here, our skeleton loaders literally aren't showing up like at all against the white. That's because the color of the skeleton loaders is pretty much white just like the cards in the
background. So, it looks really off.
background. So, it looks really off.
That's not a big problem at all, though.
If we go to our skeleton component here, we can see that normally it is BG accent because it looks great on dark mode.
Still, we don't want to change it. Let's
say it's still BG accent on dark mode, but on light mode, we're going to do the same thing as our sliders. We're going
to say BG foreground/25%.
And now, if we go ahead and save it, and we go and test our skeleton loaders now, they should all be visible, at least the ones that are on the dashboard. Let's
look at these real quick. Go here. Looks
like all these skeleton loaders are showing up exactly the way that they should. It also looks like real quick
should. It also looks like real quick this current weather card at this particular screen size is not quite the same size for the skeleton loader. Real
quick, let me open those. Must have been something we missed. Go to the current skeleton, I believe. Oh, yeah. It's
coming from this MD PB11 here. So, we
need that in here. Go ahead and copy this.
Paste it over. Just fix that real quick.
And now they should be the same size. So
now these skeleton loaders look and work great. However, if we go back to the
great. However, if we go back to the side panel here, it looks like they are not quite visible on this card. And I
believe that is because if we go to our side card skeleton, we're actually hard coding this background on here. We only
want to do that if we're on dark mode.
So let's take dark the selector here and let's add it on all of these skeletons just so only like that on dark mode but on light mode we use the regular color.
So here and boom now the skeleton loaders are great on light mode and if we go to dark mode like this boom boom boom they also look great. And
the very last thing I want to do to make sure this all is perfect is go in responsive mode real quick. shrink down
super tiny. And we'll notice right here, we now have this problem of the same thing we have with the hamburger. Let's
actually do the same thing we did with the hamburger. Head to our app here and
the hamburger. Head to our app here and this light dark toggle. And what I'll do here is I'll actually wrap this light dark toggle in a div. And I'm just going to say same as the hamburger. It's going
to be hidden, but above excess. We can make sure it is block. Now, if I go ahead and save this,
block. Now, if I go ahead and save this, it's not going to show up right there.
And we'll go into our mobile header component. We will take the light dark
component. We will take the light dark toggle just like this.
And let's just go ahead and copy and paste it into here just like this. Import it. Save it. And now we
this. Import it. Save it. And now we have our light mode dark toggle up here when we're on mobile. Just to create a bit more separation, let's just do a gap eight here. Now they're separated. We
eight here. Now they're separated. We
have light mode and dark mode on mobile plus the hamburger mobile. We go bigger.
And of course, it looks the way we want them to. Add a responsive mode on a big
them to. Add a responsive mode on a big screen. And there they are, still
screen. And there they are, still working perfectly. And that, my friends,
working perfectly. And that, my friends, is going to wrap up this video. It took
a long time, but here we are. We started
completely from scratch with literally an empty VS Code terminal. And we've
built an entire app from nothing. An app
that interacts with an API, uses modern tech stacks like Tailwind and Tanstack Query, and has just about every major thing you need to consider when developing front-end React apps. Of
course, it's not 100% without flaw. Like
I said, I'm not a graphic designer, so there are certainly improvements that could be made to this. If we wanted to spend 40 hours together on this video, we could have hyperoptimized everything and made it 100% flawless. But I think we've got everything we need to consider
this app a done deal. I'll leave it in your hands now if you want to mess around with this code on your own time and make any improvements or changes you want. This entire codebase is going to
want. This entire codebase is going to be linked in the description to GitHub, so please feel free to mess around with it or revisit the code if there's parts of it you don't fully understand. This
video took me a fat second to make, so I really appreciate all the support for it, and it would mean the world to me if you would consider liking the video and subscribing, as it really helps my channel engagement and helps me grow my audience by helping me out in the
YouTube algorithm. Well, with that being
YouTube algorithm. Well, with that being said, it's been a fun ride. I hope you guys all found the video helpful. If
there's any questions, concerns, criticisms, or anything else that you have, please feel free to leave them in the comments, and I'd be more than happy to respond. Take care everyone and I'll
to respond. Take care everyone and I'll see you in the next
Loading video analysis...