Loading video player...
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
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
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
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
command, it'll ask us a couple of
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
weather- project. Something super
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
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
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
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
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
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
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
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
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
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,
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
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
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
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.
We'll close it and let's go back to
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
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
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
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
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
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
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.
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
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.
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
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
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
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
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
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
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
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
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
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
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,
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
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
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
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
start the name with vit and then v will
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
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.
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
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
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
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
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.
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
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
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
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
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
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,
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
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
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
say const API key
equals to properly use vit environment
variable system. All we have to do is
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
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
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
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
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
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
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, 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
data. That way we can at least see it on
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
parse through everything in this massive
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
this page and you can see now we have
lat 10 and lawn 25 which is somewhere in
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,
inspect, and at least open up our
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
is an array of all the hourly forecast
data. So this would be the forecast for
the first hour, forecast for the second
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
represents one day. This is day one of
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
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
typically do is in our source folder, I
will create another folder that I will
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
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,
Zarray string Z dotstring. You get the
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,
for example, latitude, which is a
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
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,
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.
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
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
want to map to the Z type or the Zod
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
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
this, we're going to parse through our
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
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
can access current, daily, hourly, or
any of these things because data is
typed properly. That's something that's
going to be really helpful going
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
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
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
right here, we will just render out
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 div right here, control shiftp and
type in wrap. I'm going to wrap it in a
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.
Instead of rendering out data just being
stringified, let's actually do something
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
like this.
And then let's copy and paste this
twice. So, we have three cards on our
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
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
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
outermost div, let's give it a class
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
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
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
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
some separation. So we'll say flex flex
column
layout. and we'll give gap eight to
create even more separation between the
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
also just throw a shadow. We'll try MD
for now. Save it. And that creates just
a slight separation against the
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
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
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
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
here. And to make TypeScript happy,
we'll say that title is going to be a
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
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,
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
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
getting the specific card set up. So,
the daily card, hourly card, and the
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
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
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
this component. Make sure to import
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
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
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.
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
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
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,
we're going to add skeleton loaders. So,
I want suspense boundaries around each
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,
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
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
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
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
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
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.
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
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
flexbox for each day. But basically for
each day we want to render something
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
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
flex row layout and space everything out
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
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,
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
exactly what we're looking at in our
schema. Right here, it says how to get
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,
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
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
icon that we're getting from the
response. Like I just showed you a
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
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.
Go ahead and go back to our app and now
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
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
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
three numbers right here. So, let's
actually make P tags for all three of
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
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
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
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
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
which is why it's a number and not a
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
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
this first argument because we don't
want to pass in any local time zone
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
of having the day of the month, we just
have the day of the week, but
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.
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
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
then I'm using Fahrenheit, so we'll do
degrees Fahrenheit. We'll take this and
we'll add this to the other temperatures
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
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
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
should now be aligned because we're
ensuring that each of these has a fixed
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
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
and some stuff like roundedness and
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
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
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
it out of here.
And then on this div, let's give it a
class name. And this class name is going
to be, you guessed it, children class
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
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
forecast done, let's move on to the
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
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
could have something like the actual
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
going to be during the hour. So whether
that's, you know, sunny, cloudy,
whatever. And at the bottom, we'll just
put the actual temperature we expect it
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
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
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
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
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,
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
say in our card here, we're going to do
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
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
layout, we can give this div a class
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
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
right here. And similar to daily, we
will make this new date calling the date
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,
just so you can see how this looks,
let's actually save this file real
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
the idea. Back to our hourly forecast
component. right underneath this timer
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
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
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.
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
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.
Inside of that, we have this weather
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
in here is our actual temperature, which
should just go right underneath the
weather icon. Let's make another P tag
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
daily information, I want to have a unit
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
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
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,
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
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
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,
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
right here, flex flex call, and we give
item center. That'll make sure
everything is centered within the
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
honestly don't like this time format
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
two digit and then we'll give hour 12
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
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.
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
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
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
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
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
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
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
it is at the actual location that 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
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
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
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
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?
Well, if we look at data, we can see in
the current object here, we have this
temp number. That is the current
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
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
make sure it's centered. So now that
looks quite a bit better, way easier to
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
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
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
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
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
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
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
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
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
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
eight, let's actually say, I don't know,
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
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
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
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
in big letters saying local time and
then right underneath it have the actual
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
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
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
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
stored in data.curren.dt
that is the current time. So we'll do
that data.curren.dt dt* 1,000 like
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
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
text is quite a bit more readable.
However, now this time just got way
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
do on this current weather card would be
to add the three quick sections at the
very bottom. getting the feels like
temperature, the humidity, and the wind
speed. These will all share the same
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
give it a class name of flex flex call
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
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
consistency with the units. So, we'll do
degrees Fahrenheit. And we will also
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
good because there's only still one item
there. So, let's actually take this div
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,
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
a percentage. So we'll use the percent
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
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
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
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
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
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
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
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
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
sunrise and sunset time. We have, I
don't know, the clouds. We have the wind
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
have some additional weather
information. So, it's not going to be
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
to do? Well, in terms of design, I was
thinking about just making it something
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
cloudiness, whatever, and then have some
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
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
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
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
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
something like cloudiness and value
would be clouds. So it maps to this
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
clouds we have UVI. So I think we can
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
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
it for me. And here is what it cooked
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
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
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
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
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
for example to get that particular
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
row value, we can just use them directly
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.
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
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
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
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
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
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
before. Undefined for the initial
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,
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
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
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
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,
at least when compared to the other
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
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
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
this. This will be imported from the
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
just like this and then add question
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
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
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
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
this super nice thing we can use right
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
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
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
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
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
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
icon here. Select all of them. Capital I
for component. just so we have uh proper
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
just render out icon as a component just
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
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
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
doesn't really make a whole lot of
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
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
called up arrow that just looks like
this that we can use to point the proper
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
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
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
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
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,
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
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
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,
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
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
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
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
should give us an npm command right
here. So, we'll go ahead and install
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
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
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
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
and I'm going to paste it into our map
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
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
something like five, something way less
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
recognized. So I'm going to take uh this
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
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.
And at the very top here, we're
importing everything else. We're just
going to add import
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
have a little bit more real estate to
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
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
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
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
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
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
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
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
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
is where we would actually use the use
map hook. So we'd call it down here. We
would get this imported from React
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
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
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
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
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
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
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
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. I'll open up our console right
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
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
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
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
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
easier to see that it's centering where
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
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
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
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
because it happens to be the direct
parent of the map but also all of our
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
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
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
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
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
this type structure right here a lot of
the object that has the lat longitude
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.
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
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
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
type error because they don't expect any
actual props. They don't expect the
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
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
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
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
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
just, you know, close out of these cards
here. Let's go back to our app real
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,
let me save this. Refresh it. You can
see it says we're at 76°, you know,
1246, whatever. So, let's change this
1025 to something else. Let's make it
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 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
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
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
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
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
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
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
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
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
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,
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
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
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
actually just call it in here right
below our panoo. All we'll do is call on
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
have those two. We'll just pass in lat,
lawn to get latitude and longitude
passed in. And now we've actually done
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
to be sure, and click it, we're going to
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
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
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
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
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
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
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
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
static anymore. Go ahead and get this
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
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
already have a huge, huge chunk of
functionality of this app knocked out of
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
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
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
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
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.
And it wants us to add these compiler
options right here, right underneath the
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
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
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,
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
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
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
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
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
before we add the select in our index
CSS here. Notice how we have this card
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
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
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
/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.
And now our cards I think look a little
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
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
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
existing, it'll make a new folder called
UI. And this is where all the shad CN
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
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
here that I'm just going to call
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.
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
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
to this this button you're seeing that
says select a fruit over here is the
select trigger. Now the select content
is what actually gets dropped down right
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
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
you can see it's opening up over the
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
because we're mapping that we have a key
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.
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,
London, whatever I want to click on. And
our drop down is actually looking pretty
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
exactly something that Open Weather has
support for. It's called the geocoding
API, which you can just get to at / API-
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
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
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
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
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
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
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
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
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
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
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.
Luckily, the schema that we're going to
have for this is actually super simple
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
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
don't know, geocode schema.ts, just like
this. And then we'll need a zod schema
that matches this return shape from Open
Weather. I'm again trusting my good
friend Chad GBT here to generate me the
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.
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
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,
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
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
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
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 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
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
right here direct. And this is the
response that we're getting from the
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
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
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
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
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
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
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
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
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
everything happy. So if we save it and
refresh, that should make our map
return. And it looks like we keep
getting this error kind of crashing our
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
schema real quick. We go to weather
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
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
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
to add. We're setting location to Tokyo
by default. And we're setting it to
custom whenever we click anywhere on the
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
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
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
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
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
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
coordinates change, the map component is
going to rerender to reflect the
coordinate changes. So why isn't this
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
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
changes, React is 100% going to rerender
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
chords.lat,
chords.lon, just like this. Just so it's
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
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
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.
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
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
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
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
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
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
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
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 exact behavior you want. No more
weird flickering thing happening on this
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
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
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
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
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
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 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
layer would look something like this.
Precipitation layer, sea level, wind
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
characters and go ahead and paste this
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
another dropdown, this is what I was
meaning. We'll make a new dropdown. We
will just call this map type
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
going to paste it in here. And we'll
just make sure to rename this one to map
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
obviously in here, we're not calling
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
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
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
I'm going to go ahead and save this
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
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
we'll take kind of this same format
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
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
same thing. And now our dropdown is
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
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
really that simple. So I'll go ahead and
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
clouds, boom, here's what the clouds
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
single one of these gives me a new
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 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
the format of the strings I saw, I
definitely think it's a bit weird
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
class name on select item here. We'll
throw a class name and we'll add
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
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
should just say pressure. Basically on
each one, just get rid of the underscore
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
split it into an array. So for each
thing if we have for example clouds new
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
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
save it, look at here. It just says
clouds precipitation pressure wind
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
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
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
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
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
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
this. It'll say custom
and then the value itself will actually
be custom. And this doesn't need to be
an expression. We can just have it be a
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
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
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
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
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.
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
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
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
back to a thousand and see if it looks
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,
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
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
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,
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.
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
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
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
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
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
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
in a second, but we'll use this for our
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
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
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,
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
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
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
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
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.
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
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
this with some layering generally have a
legend to show what numbers each color
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
actual quantitative way to make a
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
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
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
temperature, clouds, and you get the
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
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
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
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
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. 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
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
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
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
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
actually get the percentage is by doing
the current value divided by the max
value. That will give us exactly where
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
this expression is in here we can simply
say stop.value value divided by max
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
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
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.
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 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
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
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.
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
class name, let's give it just a couple
of things. First, we'll make it be
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
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
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
space for the uh the formatting. And
then let's give this div a class name of
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
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
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
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.
Now, you can see now we have this little
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
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
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
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
want it to stick out. We'll say text SM
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,
we have already theta. We're going to
say
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
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
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.
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
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,
same thing temperature. This one's
probably the best looking gradient in my
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
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
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
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
just be a very similar thing, but we'll
get the very last one. So it's data at
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.
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
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
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
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
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
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
and transitioning from a skeleton loader
to the actual data should look way
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
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
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
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
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.
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
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
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
header two, we're going to replace this,
just delete it, and replace it with a
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
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
also has data. So, let's take this.
Let's copy and replace the weather icon
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
data. So, let's replace this with a
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
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
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
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
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
would say height 15 because we divide by
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
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
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
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
that looking a bit more accurate. And
now these look way closer in their
actual size. The last thing would be
these few fields down here at the
bottom. So let's look at these real
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
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
this. Now, now our skeleton card
approximates the actual card much
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. So, they're the exact same height.
So, I think we've got these sizes
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
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
while this use suspense query is
running, we're going to render current
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
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
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
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
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
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
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
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.
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
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
click somewhere on the map to trigger a
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
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
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
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
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
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
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
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
already hardcoded to width 9. So we know
that's easy to use. We can get the
skeleton here. hardcode this one to
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
rounded fold to make it appear to be a
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
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
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
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
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
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
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
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
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
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
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
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
it'll be length. We know this is a 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
should just be Yeah, we can use this
here. Say hour.dt. That's a little bit
of an oopsies. Should have had that
earlier. Okay, but now that we have this
hourly skeleton, let's get this fleshed
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
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
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,
let's get this last one knocked out of
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
in. Making sure to import. And same
thing, we want to map over an array of
fixed length. We look at our additional
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
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
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
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
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
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
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
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
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
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.
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
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
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
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
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
children class name so we still include
it and then what we can do is we can say
animate. This intellisense is kind of
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
but in arbitrary you can't add spaces
like this. So you usually use
underscores to separate them out. So
we'll say 0.6
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
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
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
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
underscore because they should be two
separate things. So we go ease out and
then forwards. Let's actually save this
now. And there we go. The phase working
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. 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
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
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
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
any more time, let's get started on this
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
opens it up, it opens over the dashboard
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
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
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
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
does now pop up and it overlays a lot of
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
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
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
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
here and not on the outside like the
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
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
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
in here to get the information for the
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
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
will be super easy. we can just pass it
through to this get air pollution
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
to go. And of course for the geocoding
and for the weather, we have our own
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
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
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
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
equals use suspense query just like
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
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
passing it down to our children
components, we can pass it down to the
side panel. So let's give it the exact
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
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
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
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
can't do that. That should be
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
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
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
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
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
information for that particular
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
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
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
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
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
this. However, because components right
here is actually an object and it's not
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
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
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
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
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
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
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
both of these in a div. And we'll give
this the class name of flex justify
between. Put them on opposite sides of
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
key a class name of let's say text LG
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
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,
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
pollution schema real quick. See if we
can fix this. Go here air pollution
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
number. I wonder if chad gbt sold us out
on this. Let's double check the uh the
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
or if the docs are wrong. Uh looks like
it is. Oh yeah, it just says
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.
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
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
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
immediately make it look quite a bit
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
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
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
right underneath AQI and just say AQI
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
here. And now it takes in both an
optional children class name and class
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
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
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
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
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
reason, I don't really know the logic
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
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
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
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
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
means? Obviously, we want it to be clear
to the user that these numbers actually
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
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
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
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
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
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,
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, 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
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
context of knowing where on the slider
that is, right? Should 162 be at the
beginning here? Should it be kind of in
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
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
going to ask Chatbt to generate me an
object that represents this entire chart
here. And here is what it actually
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
second and make sure this all looks
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,
which we can see from open weather is
good, fair, moderate, poor, and very
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
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
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
data structure is actually going to do
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
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
from the lowest number. The lowest
number meaning, I guess, the best air
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
look at the open weather documentation
here, it actually mentions that these
values are any number greater than
these. Meaning technically it could be
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
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
go up here and we'll say const pollutant
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
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
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
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
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
poor category
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
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
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
that actually correspond to the real
value in the correct ranges. This is for
Tokyo. Let's go select, I don't know,
London. And you can see now we have new
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
make another div to make some sort of
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
touching the bottom of the side panel.
Let's go up to our outermost side panel
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
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
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
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
map out each of the different range
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
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
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
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
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
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
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
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
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
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
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.
Is it 40 and 70? Nope. Let's go to the
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
in here. I'm just going to say for const
level and range of object entries
pollutant. And for this, we're going to
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
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
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
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
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
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
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
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
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
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
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
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.
And then very poor is going to be
purple. Just like this. And now we have
a color for each different level of
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
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
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
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
able to get it like this is honestly
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
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
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
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
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
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
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
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
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
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
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
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
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
do
hourly forecast here. Actually, no, this
is not a good example. Let's go to
additional info. Let's take this import
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
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
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
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
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
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
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
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
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
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
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
we did with the AQI, let's wrap this in
a div. And we'll give it the exact same
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
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
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
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
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
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
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
pollutant. And that should silence our
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
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
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
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,
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
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
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
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
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
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
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
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
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
actually be here is if it's already
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
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
think we can use at the top of our side
panel. That'll be a good little
indicator that we're going to be closing
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
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
little bit. And now, if we go ahead and
click it, you can see our panel
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
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
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
dropdown, go ahead and just paste in
this button. I'll take this negative
left margin off and instead just do ML
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
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,
so it's just hamburger. SVG just like
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
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
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
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
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
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
just like this. We'll do tsrfc to create
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
itself. Take all of this exactly.
Go ahead and copy it and we'll paste it
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
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
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.
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
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
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
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
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
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,
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
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
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 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
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
made a skeleton component for each of
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
here in our skeletons folder that we're
going to call uh let's say side panel
skeleton.tsx
just like this tsrfc to make the
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
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
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
the regular cards here, let's map over
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
be looks like four, five, six, seven,
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
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
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.
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
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
curly bracket. So, let's go ahead and
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
sidecard skeleton so we have the right
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
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
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.
So, let's replace this a skeleton of
height four. And then the width, this is
going to be the width of the whole
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
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
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.
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
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
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
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
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
great point in the video to finally
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
wide so we can see what it really looks
like. And it's obvious that right away
with this bigger view, it looks pretty
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
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
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,
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,
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
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
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
that is we will save it. Let's go to our
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
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
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
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
always going to be the width of the
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
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,
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
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,
which is exactly what we want for larger
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
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
class name, let's throw one more thing
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
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
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
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
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
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
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
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
make sense to show this hamburger icon
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
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
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
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
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
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,
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
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
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
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
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
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
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
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
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
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
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
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
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
aren't taking up the full width are
going to be the current weather and the
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,
let's take this daily forecast div.
And now, if we save it, we're getting
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
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
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
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
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
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
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
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
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
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
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
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
here essentially collapses the dashboard
real estate to be almost what it was
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
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
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
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
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
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
red lines here for each of the columns.
These aren't actually proportional
really. Actually, got a bit of a
brighter color. Still a brighter red
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
columns. And then we'll have kind of
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
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
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
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
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
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
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
this dashboard is going to take up the
entire height of the screen at this
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
a bit weird still, but we're getting
better. The reason it's so big right now
is because these cards aren't spaced
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
bad. So, now let's fix the rest of these
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
column, which actually works out cuz
that's exactly what we want on the
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
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
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
super out of order in our layout.
Everything just reads from left to
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
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
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 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
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
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
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
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.
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
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
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
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,
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
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
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
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
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
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
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
just the spacing here that makes it look
really awkward. I think what the issue
is, we're noticing we have that gap
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
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
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
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
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.
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
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
space them out to fill all the remaining
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
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
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
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,
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
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,
we can't because this all fits on the
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
adjusts. Still everything's looking
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
sidebar at will if we keep getting
smaller. So eventually we'll hit below
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
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
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
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
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
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
point, around the 768 break point here
is when it starts to kind of all fall
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
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
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.
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
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,
for example, is by going into our
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
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
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
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
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
might be one of the better ways to go
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,
obviously, but we have that main problem
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
we run out of space and then shrinks
down again. Let's fix these last couple
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,
is go to location dropown. And right
now, it's just hardcoded to be 180
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
that, then let's just make it width
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
width full. Go ahead and save it. And
now they're both the full width of the
screen. And now the last obvious problem
that we have to fix is this hamburger
right here. just looks awful sitting
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
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
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
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
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
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
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
a completely separate mobile header
component where we can put the hamburger
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.
Go ahead and close this. TSRFC to make
the component. And let's make some basic
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
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
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
into here and we will actually copy and
paste our hamburger button into this
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
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
be hiding this component entirely. I
also now just don't have the set is side
panel open. So let's go ahead and
comment out this on click for just a
second. Now let's go ahead and save
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
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
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
like. Now, you can see that as we
scroll, this should stay at the top of
our screen. Looks like it's 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
to make sure. Looks like yeah, it's
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
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
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
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
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
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
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
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
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
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
the side panel just the way that we want
it to. And it looks like all this is
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,
load some data in. Go over here. Let's
say I want to know somewhere near
Vietnam. Go here. All that works
perfectly. Then we have our cars down
here. Same thing. And they all look
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
single column layout. We eventually hit
768. Here we're now still single column
for the most part, but these go to
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
want to do in terms of responsive
design. That's going to be for our
desktop view. If we go back to having
this desktop view, the view still looks
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
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
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
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
1120 pixels. Starts to look kind of
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
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
and save it. And this will be the min
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
responsive design is the width. That's
why all the break points are width
based. You know, SM, MD, LG, they're all
based in the width of the viewport, not
the height. When you start worrying
about the height, things get a little
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%
perfect, maybe not completely flawless,
but it does look great on each screen
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
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
weather card and the current weather
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
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
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
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
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
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
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
here, make a new one, and we will add
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.
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
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
moon. And I'll make sure we name them
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
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
big enough to see. Let's go ahead and
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
right here between the hamburger and the
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,
here is our actual light mode, dark mode
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
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
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
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
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,
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
context.provider
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
is going to wrap the entire app, which
means that children is literally the
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
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
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
pass in an object for our value and just
give it theme and toggle theme like
that. And now this provider is all good
to go. Again, just to reiterate, this
theme provider is going to wrap the
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
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
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
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
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
in somewhere where you shouldn't, it'll
throw an error. Otherwise, you know
you're good. This provider wraps the
entire app. So, everywhere should be
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
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,
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
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
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
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
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
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
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
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
list dot add dark else
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
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
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
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
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
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
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
class selector, meaning any element that
has dark on it will define all of these
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,
when we're on light mode, our cards look
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
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
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
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
rid of this one.
Got a couple in here. Let's uh see
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.
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
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
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
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
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,
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
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
light mode to create that more shadowy
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
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
into it. And if we view it, you'll
notice that we have this BG sidebar
right here. However, clearly that BG
sidebar does not look very good in light
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.
So, when we're in light mode, use BG
foreground. When we're in dark mode, use
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
better. And one thing I want to take a
look at right now, especially, is our
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.
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
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
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
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,
it's not going to show up right there.
And we'll go into our mobile header
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
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
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
screen. And there they are, still
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
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
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
see you in the next
Stop copying tutorials. Today, we’re building a real, production-ready React app completely from scratch — no shortcuts, no BS. This is a full, end-to-end React project walkthrough that shows you how to think, structure, and build apps like a professional. This is not a todo app. This is not a 10 minute tutorial about how to use components. This is how real React devs build real apps. Technologies used 🧑💻 : • TanStack Query (React Query) • Tailwind • Shadcn • OpenWeather • Zod • and more Key Concepts 🔥: • API fetching • Responsve design • Suspense queries • Skeleton loaders • State management • Light mode & Dark mode • and more Github: https://github.com/AustinDavisTech/WeatherApp Openweather: https://openweathermap.org/api Timestamps: 0:00 - Intro 1:10 - Project setup 5:29 - Installing Tailwind 7:35 - Installing TanStack Query 9:22 - API Fetching 20:10 - useQuery 23:18 - TypeScript Support 28:33 - Dashboard Cards 1:27:51 - svgr (svg support) 1:33:05 - Card Refinements 1:36:23 - Interactive Map 1:59:54 - Shadcn 2:09:20 - Location dropdown 2:12:48 - Geocoding 2:26:15 - Interactive Map 2:31:43 - Weather map layering 2:44:52 - Custom Map (MapTiler) 2:50:49 - Map Legend 3:04:43 - Skeleton Loaders (and Suspense) 3:31:04 - Side Panel 4:14:34 - Tooltip 4:28:37 - Side panel skeletons 4:38:22 - Responsive Design 5:36:01 - Light/Dark mode