In the last lesson, we created the user interface for minting NFTs and programmatically creating these
new containers which contain all unique non-fungible tokens.
And when we minted it, we got our loader to work,
we got our frontend to be formatted. And once the canisters are created in the backend, then we can pull the
data in and show the user what they had just created.
Now if we keep minting these tokens, we want a way to be able to view all of them,
right?
And we want to do that inside this tab, which is called My NFTs.
So I want to be able to click on it and show me a gallery of all the NFTs that I own or that I've minted.
Now, at this point in time, the NFTs that you own will be the same as the ones you mint, because that's
the only way that where you get hold of NFTs at the moment.
How do we get this to actually work?
Well, firstly, we have to get this navigation to work.
And to do that, we're going to use something called React Router to route us through to these different
routes and display a different page depending on where we're going.
So we're going to now comment out this Minter and take our homepage back online,
so this last one. And we don't need this NFTID hardcoded here anymore.
So I'm going to comment that out as well.
And now if we go back, you should see our home page like this
and these links are still yet inactive.
So let's go ahead and fix that.
Now we're rendering the Header, and in the Header, we want to add some code to be able to take the user
to a different route depending on which of these buttons they pressed on.
When I created the package.json, I added in a dependency which is called, react-router-dom.
And in addition to the react-hook-form, these are all React utilities that are not from the main React
module, but they provide a lot of really useful functionality that you will find frequently needing
when you're creating websites.
Coming in to our Header.jsx,
we're going to import something called a BrowserRouter from the react-router-dom.
So let's go ahead and import that.
And the location will be, "react-router-dom" . And if that is all spelled correctly, then we should be able
to use it inside here.
So just above our very first div, we're going to add a BrowserRouter and we're going to wrap it around
the entire div. Coming down to the very bottom div
we're going to wrap it all inside this BrowserRouter.
And what this is going to do is it's going to allow us to define what are called links, and the links
or rather Link as you'll import
it, also comes from the react-router-dom, and we can put those around some pieces of text like what we've
got here and it'll allow us to turn this text into an actual link.
And each of these links have an attribute called to, where you can define the actual route that you want
to render.
So we're going to have a route which is Discover, and we're going to have a route called Minter which
wraps around this piece of text here.
And we're also going to have a route down here which is going to be called collection because it's my
collection of NFTs, right?
Just check and make sure that you've got a different route set for each of these three buttons and that
your link wraps the piece of text and not the button.
I've added a little bit of styling to the text just so that it shows up nicely purple and looking clickable.
Now that we've got our three links in here, the final piece to this navigation is we need to put in
what's called a switch.
And basically the switch is going to look at which of these links were triggered and to render the appropriate
React component depending on which path.
And we of course have to import it also from the react-router-dom.
So make sure that you spelt it correctly,
it's got a capital S and then it's Switch, and then we're going to add it in here, right here. And inside
this Switch component, we're going to add some routes.
Route is yet another class that comes from the react-router-dom, and once we import it, we can create
a route component.
We need a total of four of them because we have three of these links here and we want one for the home
page as well.
So when we click here, it shows us the image and we click here
it shows us the various different pages and routes.
Now the route takes an attribute called path, and the path allows us to match to one of these links.
So basically it's going to look at what the path is.
So for example, if I had /collection, then this would be the path.
And once it can match the path, then it will render whatever it is we tell it to
inside here. The first thing we want to match is the root route.
And in this case, what we want to do is we want to render this image.
So I'm going to cut this out from here and paste it in between this Route component.
And in addition, I'll need to import it from here,
so I'm going to cut that line as well
and paste it up here.
So now the root path is going to render this image and I'm going to add the path for all the other routes
as well.
We've got the /discover and remember that these routes have to match to the to attribute
of your links.
So in the case of preventing typos, I would recommend just copying and pasting it into there.
Now I'm going to copy and paste this a few times and I'm going to add minter path and also the collection
path as well.
So we've now got four routes and with different paths. For the Discover route,
I'm just going to add an h1where it says Discover. We're going to come back to this a little
bit later on because this is going to be the page where we see all the NFTs that are listed for sale.
And until we manage to get our NFTs listed for sale, we can't actually work on this route yet.
But the next one though, the minter route, we can definitely work on. Again in between the open and closing
route component tags, we're going to add the component that's going to be rendered.
And in this case, it's going to be the Minter component.
And if you select it, then hopefully it will auto import.
And finally, our very last route, which is the collection.
We're going to render the Gallery component and again, make sure that got imported.
And if you take a look at the Gallery component, it's really, really simple.
It's basically a bit of grid formatting and it's going to render a bunch of items.
At the moment,
we've just got one item in there and it's not even going to display properly because we haven't given it
an ID,
but we're going to solve that very quickly in this current lesson when we create the gallery of all
the NFTs that our current logged in user owns.
Coming back over here, we've got pretty much all of these routes set out.
And there's just one last thing I want to add.
When you use this route component and you've got these paths, the / will match pretty much
every path.
So whenever it sees anything with a forward slash, it'll go here first and it will get really confused.
So one thing we can do is, as you know, with switch statements, they're going to check from top to
bottom.
So you could move it all the way down here so that it's the very last one being checked so that it doesn't
match any of our previous routes, not just because it sees the / and it already immediately
thinks it should be the first one.
Alternatively, and I think this is a little bit better, we can add the keyword, "exact" in front of the path
and this tells the router that unless you see this exact string, then don't match this route.
Now when do we want this to show up? Well, we want it
when the user first loads up the homepage,
but also we want the user to be able to click on this OpenD and to be able to render it.
So let's go ahead and add another link and it's going to link to the root route.
And I'm going to wrap this link around this h5 right here. Coming back to our frontend website,
you can see that when we go to our home page, our root route, even though it's no longer in here,
that main image, it's still being rendered from our router.
Now when we go to discover, we see an h1 for Discover.
When we go to Minter, we see our usual minting page.
And finally, when we go to My NFT, we've got blank
and that's because we've got some errors in our gallery. Because as I mentioned earlier on, the item
doesn't render without an ID. We can fix that quite easily by simply passing in the id prop and setting
it equal to our previous canister id that we used.
Like so.
And now when you click on the My NFTs page, you can see it no longer is an empty screen.
This top text says Discover and it's because it's hardcoded in here.
Let's go ahead and make that something that we can pass in as some props.
So we'll add the props there and we'll call this props.title.
Now we have to go to the previous section in order to pass in that title. Right here where we've got
our Gallery,
let's go ahead and say title equals...and we can name that page.
I'm going to call My NFTs, like
so. Now you can see that update dynamically.
And at the moment, this NFT is actually hardcoded to display here because remember inside gallery we
provided it's id
hard-coded like this and it's only showing one item.
We want a whole bunch of items, as many items as the owner owns, to go in to the Gallery.
To do that, we need to update our code a little bit in our main.mo, because at the moment
when we mint a new NFT, all we do is return the canister id.
And in fact, I've got a typo right here and hopefully you don't.
But it should say newNFTPrincipal, although in this case it matched so it didn't actually create
an error.
In addition to minting the NFT, returning the Principal ID, we also want to set up some form of data
store inside our OpenD canister that keeps track of which NFTs were minted and who the owners are.
And to do that, we're going to use our trusty HashMap.
So let's go ahead and import our HashMap.
And we're going to create a map of NFTs.
This is going to be a variable because we're going to be modifying it.
And I'm going to call it a mapOfNFTs.
And I'm going to set it equal to HashMap.HashMap.
And then I can add the data types that will be stored,
and it's going to be Principals as keys,
and these are going to be the Principals of each new canister that gets created for the NFT.
And then it's going to be linked to an NFT canister stored here.
So we have to import the NFTActorClass,
and this is going to come from the ../nft
/nft.
And we should now be able to tap into the NFTActorClass inside here.
And that is going to be the NFTActorClass
.NFT data type.
And now finally, we can go ahead and initialize our HashMap in the same way that we've done before.
So we're going to be using, Principal.equal to check the equality of any Principal IDs that's passed
in to be checked against the key in our HashMap.
And we're going to be using, Principal.hash to hash those keys.
Once we've created our map of NFTs, we want to add to it every single time we create a new NFT.
So after this line, let newNFTPrincipal where we get hold of the id of the canister
that our new NFT is hosted on,
then we can get hold of our map of NFTs and we can use the put method to put a new item into that HashMap
.
The key is going to be the newNFTPrincipal, and the value is going to be the newNFT that was minted
right here. In addition to having a map of NFTs,
it will be really handy if we had a mapOfOwners,
because this way we can have a map of the Principal IDs of the owners of NFTs who have minted on our
website, and we can match them to the NFTs that they own.
And we can bring it up in the Gallery when they click on my NFTs.
So this again, is going to be created using HashMap.HashMap.
The key is going to be the Principal IDs of the owners.
Now, here comes the tricky part, because it's not going to be the same as our map of NFTs, where
each NFT principal is mapped to one single NFT.
But because each owner can own several NFTs, we actually want to create a list of NFTs here.
So we need to import the List type.
And we're going to specify the data type as List.List, and then the list is going to consist
of Principals.
And then finally, after the weird-looking two double angle brackets, we can initialize this HashMap
in the same way we did before because the keys are also made of Principals.
So now that we've got our map of owners, we again can't simply just use a put here.
Instead, we actually have to go through a few steps to get hold of the existing list that's stored
for a particular user and then update that list and then push it back into the HashMap.
So I'm going to split out a separate function. Underneath this first mint function,
I'm going to close it off with some semicolons, and I'm going to create a new private function, which
is called addToOwnershipMap(),
and it's a really long-winded way of describing what it is that this function is going to do.
It's a private function because we don't want any other class or any other code from outside our main.mo
to be able to access it.
And the addToOwnershipMap() is pretty simple,
the whole purpose is just to figure out how to add the newly created NFT to this mapOfOwners.
So it's going to take some inputs.
First is going to be the owner, which is the Principal of the owner of the NFT.
And then I'm also going to need the nftId, which is going to be yet another Principal.
And this is the newly minted NFT that I want to add to our ownership map.
As I mentioned, the first thing we have to do is to actually get hold of the NFTs that the user currently
owns.
So I'm going to call this ownedNFTs,
and whenever you try to get hold of a value from a HashMap, because the Principal that you supplied
could actually be a brand new Principal, it could be a new owner who's just minted their very first NFT
in which case they don't actually exist in our mapOfOwners,
so when you provide the key of the owner's Principal, it might actually not exist in HashMap, in
which case there will also be no value.
So this is why if we call mapOfOwners.get and provide the owner Principal, what actually gets returned
here,
the data type of this, is a Motoko option.
So that's the one with the question mark on it,
and it means that it could be nil and it might not contain any data.
So the way to work with this is to use a switch statement in order to reduce that uncertainty.
You've seen this before.
We first have to provide the data type that we're expecting,
so it's a List of Principles,
and immediately we get this error because on the left-hand side, we're saying that it has to be a List
of Principals,
but on the right-hand side, what we're actually getting is an Optional List.
So it might be nil.
Now, what we have to do is add in that switch statement,
wrap this expression inside a set of parentheses, and then we open up a set of curly braces to define
what should we say the ownedNFTs should be equal to in the case where this returns null?
And what should it be in the case where it actually does return a result?
In the first case, when it's null, we're going to return an empty list.
The way to write an empty list in Motoko is a little bit cumbersome.
I'm kind of used to writing empty arrays like this in most other languages, but in Motoko it's going
to be List.nil.
And then we provide the data type of the list, which is Principal, and then we add an empty set of
parentheses to initialize this empty list.
In the second case where we've actually got a result, well then we're going to unwrap this optional
result and set the ownedNFTs to the unwrapped version of that result.
So this is the safest way of dealing with options and the easiest ways at the moment anyways, when working
with Motoko.
Now that we finally got a solid version of the List, then what we can do is we can push our newly minted
NFT onto this list. So we can set the ownedNFTs to use the List.push in order to push the
nftId that's passed in to the ownedNFTs list.
And if I add my semicolon here, then that error should go away.
So what we've done here is we've safely gotten hold of the list of canister ids that are owned by this
owner, and we've taken the previous version of that list, added the new nftId, and then set that list
to equal the new updated version of the list.
So a little bit roundabout, but it is necessary in order to make it work.
Now finally, we can do our put into our mapOfOwners where the key is going to be the owner, which
is the owner's Principal ID, and the value is going to be this newly updated list of nftIds that that
owner owns,
so from here. And back up here, we actually have to call this method addToOwnershipMap(), in order to
trigger it when a new minting happens.
And we're going to pass in the owner as the first input and we're going to pass in the newNFTPrincipal
as the second input.
And once we add our semi-colon, then all our errors should go away
and we are now able to keep track of all the NFTs that are minted on our website, and all the owners
and which NFTs they own.
And that's going to be really useful later on when we set up our buying and selling on the website as
well.
The final thing we need to do is to be able to bring this from the backend over to the frontend. In
our main.mo,
I'm going to create another method and this method is basically going to fetch that list of IDs and
turn it into an array that can be used on the frontend.
So this is going to be a public query function, func, and it's going to be called getOwnedNFTs(), and we're
going to pass in the Principal ID of a user and hopefully, we'll be able to return all of the NFTs that
that particular user owns.
So we're going to return that asynchronously
and it's going to be an array of Principals, which the syntax will look like this, square brackets and
then Principal inside.
Now let's add our semicolon to make this error go away.
And we've got some more errors because we haven't yet returned what we promised to,
but we're going to very soon.
Now, what we want to do is we want to return what is contained as the value of the key that we're matching
with.
We're going to be sending over the user's ID, and using this mapOfOwners,
we're going to match the Principal, get hold of the list of the NFT canister ids and then return that
as an array.
Having done this just now, I want you to give this a go and try to get a hold of the list of NFT canister
ids that belongs to this user that's coming in through the input.
Pause the video and give this a go.
All right.
So this is going to be extremely similar to what we did before to the extent where you could almost
just copy and paste it,
but I think at this stage when the syntax is really unfamiliar, it's a really good idea to just write
everything out and wrap your head around it a second time.
So I'm going to call this variable userNFTs and I'm going to set its data type to List.List
<Principal>.
So a list containing Principal data types.
And then I'm going to use the switch statement to call my mapOfOwners.get, and here I'm going
to pass in the user that we got in the input and then I'm going to open up a set of curly braces. In
the case where this list is null, then I'm going to return an empty list of Principal data types and
initialize it with my parentheses.
And in the case where it's not null, then I'm going to unwrap the result and return the unwrapped version.
So now with our new userNFT, we can actually return it by using that method that we saw a little
while ago,
List.toArray() which converts this userNFT list into an array.
And if I add the semicolon here then all my errors should go away and we should now be able to test
this on the frontend.
So I'm going to go to my Header.jsx and here I want to, first of all, import the main.mo.
I'm going to import opend, all lowercase because as I mentioned before, that's the name of our canister
as defined in dfx.json and then the from is going to be the file path where I can find my declarations
folder/opend.
So now we can use this canister definition inside our Header.
And what I'm going to do here is create a new async function which is called getNFTs() and this is
going to allow us to tap into our await opend.
and then the name of our function down here that we just created is called, getOwnedNFTs(), and we
have to pass in the Principal of the user whose NFTs we are interested in.
As I mentioned before in our index.jsx in order to avoid dealing with the log in flow, because
this project is complicated enough as it is, for simplicity's sake, we're simply just using the anonymous
user's Principal, which I've defined as a constant in the index.jsx
.
And you should have a copy of these two lines of code in your index.jsx as well.
And it's exported as this constant called CURRENT_USER_ID which we can tap into. If I find it in the dropdown and hit Enter,
it'll get imported.
So now we want to set this equal to a new constant, which is the userNFTIds.
Now we want to be able to console.log() this just to confirm that it actually works.
So let's put the userNFTIds into a console log,
and finally we have to make sure that we actually trigger this function and it's going to be triggered
the first time that this component is rendered.
So we're going to use the useEffect Hook. And as usual, our useEffect Hook is going to take a fat arrow
function,
and then the second parameter is going to be an empty array to make sure that this function only gets
triggered once. And the function is going to get triggered to call our getNFTs.
And now if I hit Save and also make sure that my main.mo is saved as well, then I can go ahead
and redeploy my canister to make these changes available on the frontend.
So going back over to our README and copying our deploy code right here.
So let's redeploy our canisters and then we can check out the changes on the frontend.
Now if I go ahead and mint something new from my list of CryptoDunks and once this is minted, I'm going
to pull up my console.
You can see that when we're printing our console.log(userNFTIds), it's giving us this array.
And because I've been testing this a few times, there's currently three items that have been minted
that belong to this current user.
Now you might just have one and that's totally normal.
So now,
if we go into our My NFT IDs, we want to be able to pass this userNFTIds to that gallery component,
and to render all of those IDs each as an individual item.
And I'm going to use the useState Hook to hold this Gallery component.
So let's create a new constant called the userOwnedGallery,
and the setOwnedGallery,
and it's going to start off with absolutely nothing,
so our useState will be empty.
And then once we've gotten a hold of our useNFTIds, then we're going to set the owned gallery to
what we have down here.
So I'm going to cut this out and replace it with that userOwnedGallery in here.
And I'm going to paste what I copied over into here.
Our title is going to be My NFTs, and the other prop I'm going to pass in is something called ids.
These will be the ids of the NFTs that need to be rendered as Item components, and we're going to pass
in the userNFTIds here.
And once we've done that, then we can go into our Gallery and we'll be able to catch it inside the
props.
So now, instead of rendering each item individually, I want to be able to add something called items,
and these items are going to be rendered programmatically.
Let's create a const called items and a setter could setItems, and we're going to useState.
And just make sure that it gets imported.
Now, next, we're going to create a new function, which I'm going to call fetchNFTs.
And this function is going to look through our props and make sure that the props.ids is not equal to
undefined,
so hopefully we've passed in some IDs. Then we're going to call setItems in order to create some item
components.
Now the way we're going to do this is through the map function.
You've seen this before when we did Keeper, but we're going to do props.ids
and we're going to call the map function. In this map function,
it expects a callback where each of the items inside this array will be mapped to a new constant.
So let's create another set of parentheses inside the parentheses.
And here we'll be able to get the NFTid, which is each of the items inside this array as it loops
through it,
and then we can use the fat arrow to define what it is we want rendered.
And that requires opening up another set of parentheses, not curly braces,
so be careful here with the syntax, and we're going to render the Item component.
And if you remember, the Item component expects a prop called id.
So we're going to set its id to the NFTId that we're looping through, like so.
And then we can close off that component.
You might also remember this from the Keeper project when we did our React module,
but when we're mapping through an array and rendering components like this,
React likes us to have a key prop set.
It doesn't really care what exactly this key is, as long as it uniquely identifies each of the components,
so they're different.
In our case, we're going to use this NFTId that we've got and
we're going to call, toText on it, which of course comes from the Principal module,
so we have to import that up here.
And that will basically turn each of the NFTIds to a text value that we can keep track of and uniquely
identify each Item component.
Now finally, we're ready to use our useEffect and go ahead and add our callback,
and also our empty parentheses, and inside the callback we'll call our fetchNFTs() method.
The very last thing we have to change is our Item.jsx,
but just a little bit, because remember that what we're getting hold of in the props.ids are actual
Principal ids to loop through each one of them and pass it over as the id.
These are not text ids, they're actually the Principal data type, which is why for the key, we actually
had to convert it to text.
So over here we're expecting that to be text instead of actual Principal ids,
all we have to do is delete those parts and simply leave it as props.id.
Now let's hit Save and let's go back and you should be able to see the NFTs rendered.
Now they're looking a little bit weird on different lines because I think this final div actually has
its closing div right here and our items need to go inside that grid
in order for this to be rendered correctly.
You might have made this mistake or you might not have, but just check if you have it looking weird
like that.
But now I've got all my nfts showing up in here, and if I go into the Minter and mint a brand new NFT,
once that NFT is minted, I should be able to go into My NFTs, and if I hit Refresh, then you'll see that
NFT show up.
Now it's a bit weird having to click refresh,
so what we can do is in our Header, in our BrowserRouter here, in the opening component tag, we can
add an attribute called forceRefresh,
and we can set it to true, like this.
And what this is going to do is it's going to refresh our Header, which means our useEffect gets called,
which means it's going to fetch the NFTs again and it's going to push it into that collection route.
So let's test this out again.
Let's try and mint something we haven't got on here.
Let's do this guy with the weird stripy cap.
And you can see now, when I head over to My NFTs, it forces a refresh and that guy shows up in
here exactly like how we would expect it to.
Hopefully this has worked out alright for you,
if not, as always, the completed project up to here will be downloadable,
but in order to continue with our project, the next step is setting up our NFTs for sale and listing
them into the Discover page.
So for all of that and more,
I'll see you in the next lessons.